cardbuddy
v0.16.0
Published
Local dev service for the CardBuddy card editor — decouples card CRUD (sync/save/validate/render-status/screenshot) from the static public editor, so the editor can be deployed to any static host while each developer runs a local service against their own
Readme
cardbuddy
本地开发服务(Local Dev Service)—— 把「卡片编辑器平台」的前后端彻底解耦。
编辑器前端是纯静态产物(可部署到任意公网静态托管),而卡片的本地增删查改(同步 / 保存 / 校验 / 渲染自检 / 截图 / 远端取卡)由本服务在开发者本机承接。
目标:编辑器平台统一维护、统一部署;每位开发者只在本地启动一个
cardbuddy,在编辑器「设置 → 本地开发服务」里填好地址,即可立即开发自己的卡片代码。
公共 npm 包:cardbuddy —— npm i -g cardbuddy(或 npx cardbuddy)即可安装使用。
改名说明:本包
0.6.0起由cardbuddy-dev改名为cardbuddy(可执行命令同名)。旧包cardbuddy-dev已 deprecated —— 仍可安装但不再更新,请改用npm i -g cardbuddy。
快速开始
# 任选其一安装(全局或 npx 均可)
npm i -g cardbuddy
# 或
npx cardbuddy
# 在「卡片代码仓库根目录」下启动:
cd /path/to/your/card-repo
cardbuddy
# → 监听 http://127.0.0.1:8787启动后,在公网编辑器里打开「设置 → 本地开发服务」,填入 http://127.0.0.1:8787,即可跨源调用本机的卡片 CRUD。
初始化一个新工程(cardbuddy init)
没有现成的卡片工程时,一条命令生成「立刻能开发调试」的最小实例:
npx cardbuddy init my-cards # 目录不存在会自动创建;省略目录名则在当前目录初始化
cd my-cards
npm install
npm run preview # 本地服务(8787) + 内置编辑器前端 + 自动开浏览器生成内容(增量、幂等:已存在的文件一律保留,加 --force 才覆盖):
| 产物 | 说明 |
| --- | --- |
| src/cards/ | 卡片目录骨架(后端与编辑器的约定落点) |
| src/cards/helloCard/ | 示例卡(schema.json + data.json,构建期已过校验)——起服务即有卡可看可改 |
| skills/ | 随包发布的全量技能库:校验 / 同步 / 保存 / 切图上传 / 建卡管线脚本与工作流文档 |
| tools/link-skills.mjs | npm run link:skills:把 skills/ 链到 Codex / Claude / WorkBuddy 等 AI 工具的项目级技能目录 |
| .env.cb | 共享配置模板(端口、FinMall 远端基址、AI 网关占位;密钥请写 .env.cb.local) |
| package.json | 声明 cardbuddy 依赖 + dev / start / preview / link:skills 脚本 |
| .gitignore | 只追加缺失条目:node_modules/、.env.cb.local、.cache/、skills/*/.local/ 等 |
| README.md | 上手说明 |
参数:--force 覆盖已存在文件 · --dry-run 只报告不落盘 · --no-skills 不释放技能库(项目已自带)· --no-sample 不生成示例卡。
本命令取代了 dsl-card 仓库里旧的
npm run gen:demo:脚手架随 npm 包发布,无需检出仓库, 且版本天然与所装cardbuddy一致(旧方案生成的demo/会随主干漂移)。
⚠️
npx cardbuddy init卡住不退、目录里只多了个.cache/? 本机装着 0.12.0 之前的 全局旧版:npx优先命中全局 bin,而旧版没有init子命令,会把init当无关参数忽略、 转而启动常驻开发服务(于是既不退、也不脚手架)。用npm ls -g cardbuddy核对版本, 再npm i -g cardbuddy@latest升级,或npm uninstall -g cardbuddy让 npx 每次自行拉取。 0.12.0 起 CLI 对未知子命令会直接报错退出,不再静默起服务。
Preview 模式(本地独立调试)
cardbuddy --preview # 等价短参 -p启动后,本地服务在同一个 8787 端口同时托管:
- 卡片 CRUD / 校验 / 截图等
/__xxxAPI - 打包后的编辑器前端(替代
https://poc-agent.finmall.com/cardBuddy/index.html) - 自动注入本地 API 地址并打开默认浏览器
命名说明:
--preview指「本地托管编辑器前端做预览」,与端点GET /__preview(远端取卡)无关。
前端产物来源优先级:
DSL_PREVIEW_ROOT显式指定目录- npm 包自带的
dist/preview-editor/(发布包内置;旧包布局dist/offline-editor/自动回退) - 当前目录下的
dist/(仓库内直接运行源码时)
包内已内置打包好的编辑器前端(dist/preview-editor/),因此无需自己构建前端即可使用 --preview。
已废弃别名(仍生效):--offline / -o、DSL_DEV_OFFLINE=1、DSL_OFFLINE_ROOT、注入全局
window.__CARD_DEV_OFFLINE_BASE__。请把手上的脚本 / CI / demo 换成 preview 名,旧名将在后续版本移除;
服务端目前会同时注入新旧两个全局名,因此已构建好的旧 dist/ 前端仍能被新服务托管。
MCP:把 card_* 工具挂给外部 AI agent
cardbuddy --mcp # stdio JSON-RPC 2.0;不启 HTTP 服务、不占端口编辑器内置的网页 AI 助手走 POST /__ai/chat;--mcp 把同一套 16 个 card_* 工具
(实现同为 mcp/tools.mjs + mcp/client.mjs,不存在第二份契约逻辑)以 stdio 暴露给
Codex / Claude 这类外部 agent。工具清单与用法见 skills/dsl-card-ai/SKILL.md。
agent 侧配置(cwd 必须是卡片工程根,读写才落在 src/cards/):
{
"mcpServers": {
"dsl-card": {
"command": "<工程绝对路径>/node_modules/.bin/cardbuddy",
"args": ["--mcp"]
}
}
}stdout 是协议信道。 经
npm run调用必须加--silent:npm run mcp会把> 包名 脚本名横幅打到 stdout,多一行客户端就解析失败;直接调node_modules/.bin/cardbuddy --mcp无此问题。诊断一律走 stderr 且默认静默,CARDBUDDY_MCP_VERBOSE=1打开(会打出注入后的root=/apiBase=)。
cardbuddy init 生成的工程已带 npm run mcp 脚本。回归见 tools/tests/cardbuddy-mcp.test.mjs。
第一次接 agent 的人建议先读 dsl-card 仓库的
docs/CardBuddy MCP 新手教程.md:角色拓扑、 Codex / Claude / Cursor 三种注册配置、实测的完整编辑闭环、四个最常见的坑与排查手册。 (该文件在仓库docs/下,不在本包files白名单内,故只给路径不给链接。)
工具对本地服务的依赖分档
--mcp 自己不启服务。启动时自动注入 CARD_ROOT=<工程根> 与
CARD_API_BASE=http(s)://127.0.0.1:<端口>(端口取 CARDBUDDY_CARD_PORT / DSL_DEV_PORT,默认
8787;已有同名环境变量时不覆盖),工具据此决定走 HTTP 还是本地降级:
| 档 | 工具 |
| --- | --- |
| 纯本地,从不需要服务 | card_route_input card_scaffold card_add_component |
| 优先服务,连不上自动降级为直接读写盘(validate 跑 Python 校验器) | card_list card_read card_write card_validate card_history card_rollback |
| 优先服务,降级到包内独立离线渲染器 | card_screenshot |
| 必须另起一个 cardbuddy | card_render_status card_node_models card_import_meta |
| 必须另起服务 且已登录 FinMall | card_sync card_save card_slice_upload |
别直接跑包内
dist/vendored/mcp/server.mjs。mcp/client.mjs在模块顶层就 resolveCARD_ROOT/CARD_API_BASE(不是每次调用时读),绕过 CLI 包装层时前者会落到包内快照目录 (card_list返回 0 张卡)、后者回退devApiBase()的 5180(消费者工程没有 Vite 把/__*代理到 8787,card_render_status直接dev-server-required)。仓库内跑源码时入口回退<repo>/mcp/server.mjs,无需先 build。
配置(环境变量 / .env)
在仓库根目录的 .env 中配置,或直接用环境变量:
| 变量 | 默认 | 说明 |
| --- | --- | --- |
| DSL_CARD_ROOT | 当前目录 (cwd) | 卡片代码仓库根(含 src/cards)。可用环境变量二次覆盖。 |
| DSL_DEV_PORT | 8787 | 本地服务监听端口。 |
| DSL_DEV_PORT_TAKEOVER | auto | 端口被占用时的接管策略:auto 自动结束占用该端口的旧 cardbuddy 实例后启动;force 连无关进程一并结束;off 关闭前置检查(退回原生 EADDRINUSE 报错)。 |
| EDITOR_URL | http://127.0.0.1:5180 | 截图自访问的编辑器实例地址(默认指向本地编辑器)。写入 .env 即可持久化。 |
| DSL_DEV_TOKEN | (空) | 可选鉴权令牌;开启后请求须带 x-dev-token 头。默认仅靠 127.0.0.1 绑定(不暴露局域网)。 |
| DSL_DEV_HTTPS | 0 | 设为 1 启用自签 HTTPS(兜底旧 Safari / 企业策略的混合内容拦截)。 |
| DSL_EDITOR_SRC | (空) | 内核开发用:指向本地编辑器 card-editor/ 源码,绕过 vendored 快照调试。 |
| DSL_DEV_PREVIEW | 0 | 设为 1 等价于 --preview:同端口托管打包后的编辑器前端并自动打开浏览器。 |
| DSL_PREVIEW_ROOT | (空) | 自定义 preview 前端构建产物目录(默认包内 dist/preview-editor/,回退 cwd/dist)。 |
| DSL_EDITOR_CMD | (空) | 代码桥外部编辑器命令(如 code / cursor / 带参数整串),按 shell 语义执行并追加临时文件路径;缺省按平台拉起 VSCode(mac open -a、win cmd /s /c "code"、linux code),CLI 缺失再回退 vscode:// 协议。 |
端口被占用时自动接管
启动前先探测 DSL_DEV_PORT:若被上一个 cardbuddy 实例占用(终端直接关掉、Ctrl+C 漏杀、后台残留等),
自动 SIGTERM(宽限 2.5s 后升级 SIGKILL)结束旧进程、等端口真正释放,再继续启动,免去手动
lsof -ti tcp:8787 | xargs kill。
判定「占用者是本服务」有两条依据:HTTP 探测 /__cards 返回 cardbuddy 响应体({ cards: [...] } 或 token 鉴权 403),
或占用进程命令行含 cardbuddy / dsl-card 字样。无关进程占着端口时不会动手,而是直接报错并给出手动处理命令;
确需强制接管设 DSL_DEV_PORT_TAKEOVER=force。
跨平台:macOS / Linux 用 lsof 定位 PID(缺失时回退 ss),Windows 用 netstat -ano + taskkill /T /F。
HTTPS 模式(混合内容兜底)
默认本服务跑在 http://127.0.0.1:8787。当公网编辑器是 HTTPS 时,浏览器可能因「混合内容(Mixed Content)」或「私有网络访问(PNA)」策略拦截对 http://127.0.0.1 的跨源请求。现代 Chrome / Firefox 对回环地址(127.0.0.1 / localhost)豁免混合内容,默认即可用;但 Safari 等更严格的环境会拦截,此时启用 HTTPS 模式:
DSL_DEV_HTTPS=1 cardbuddy
# → 监听 https://127.0.0.1:8787,证书首次自动生成并缓存到 <root>/.local/dsl-dev-tls/证书为自签(SAN 覆盖 localhost / 127.0.0.1 / ::1),不在系统信任链,首次使用需:
- 在浏览器手动打开
https://127.0.0.1:8787/__cards; - 接受「您的连接不是私密连接」警告(一次性,之后该主机被信任);
- 回到编辑器「设置 → 本地开发服务」,把 Base URL 改为
https://127.0.0.1:8787即可。
即便走 HTTP 模式,CORS 层也已回写
Access-Control-Allow-Private-Network: true,向前兼容 Chrome 的 PNA 预检。
端点(与编辑器约定的 /__xxx)
| 端点 | 来源 | 说明 |
| --- | --- | --- |
| GET /__cards | 自包含 | 卡片列表 |
| GET /__card/:id | 自包含 | 单卡 schema/data 读取 |
| POST /__sync | 本地服务代理 | 同步远端 FinMall(依赖 finmall-card-sync skill) |
| POST /__save | 本地服务代理 | 保存到远端 FinMall(依赖 finmall-card-sync skill) |
| GET /__preview | 本地服务代理 | 远端取卡(IDC10015,依赖 finmall-card-sync skill) |
| POST /__validate | 本地服务代理 | 契约校验(依赖 dsl-card-generator skill) |
| GET /__render-status/:id | 自包含 + 内嵌 Vite SSR | 渲染自检 |
| GET /__node-models/:id | 自包含 + 内嵌 Vite SSR | 节点 Property 树模型 |
| GET /__import-meta/:id | 自包含 | 导入侧车元数据 |
| GET /__history | 自包含 | 编辑历史 |
| GET /__screenshot/:id | 自包含(需 Playwright + 系统 Chrome)| 无头截图 |
| GET /__events (SSE) | 自包含 | cards-changed 实时推送 |
| POST /__code-bridge | 自包含 | 代码桥:代码写临时 js 并拉起外部编辑器,返回 { id, file } |
| GET /__code-bridge/:id | 自包含 | 代码桥:拉取外部保存后的最新代码(LF 归一) |
| POST /__code-bridge/:id/heartbeat | 自包含 | 代码桥:会话心跳(前端 5s 一次) |
| DELETE /__code-bridge/:id | 自包含 | 代码桥:结束会话并删除临时文件 |
FinMall 凭据只在本机:
/__sync/__save/__preview由本地服务代理 FinMall,凭据存于本机skills/finmall-card-sync与.env.cb,公网前端永不接触。
VSCode 代码桥(事件面板代码编辑区)
事件配置面板的代码编辑区底栏右侧有 VSCode 图标:点击后把当前代码片段写到
os.tmpdir()/cardbuddy-code-bridge/<session>.js 并用外部编辑器打开;编辑器内保存即经
文件监听 + SSE(code-bridge-changed)实时回写编辑区(桥接期间 Web 编辑区只读,避免双向冲突)。
- 清除语义:会话结束 = 再次点击图标断开 / 面板关闭 / 组件卸载(前端
DELETE), 或心跳 TTL 兜底(20s 无心跳即删,覆盖关浏览器 / 崩溃);服务启动时清扫 >24h 残留。 VSCode 标签页关闭事件不装扩展拿不到,故不以「关标签」为清除时机。 - 回声环防护:服务端写入哈希去重(忽略自写回声);前端回写用
updateCode(code, false)不触发 onUpdate,不会把收到的内容再写回临时文件。 - 换编辑器:
DSL_EDITOR_CMD=cursor或任意命令整串(按 shell 语义执行,开发者自配环境变量)。 - 拉起方式(任一级 spawn 失败或非 0 退出自动降下一级):
win
code "<file>"→vscode://协议;macopen -a "Visual Studio Code"→codeCLI →vscode://协议; linuxcode→xdg-open vscode://。文件监听 win/mac/linux 均走 chokidar(原子保存改名覆盖可捕)。
依赖:技能库(skills/)
远端读写与校验类端点(/__sync /__save /__preview /__validate)以及 AI 助手的
skill_read / skill_exec,都通过动态 import / 子进程调用技能脚本:
finmall-card-sync(sync / save / preview / 切图上传)dsl-card-generator(validate / autofix / 建卡管线)finmall-session-navigator(FinMall 登录会话;凭据落skills/finmall-session-navigator/.local/credentials.json)
解析顺序(见 server/core/skillPaths.js,全后端统一):
DSL_SKILLS_ROOT显式指定;- 项目根
skills/—— 跑过cardbuddy init即有,推荐:可写(凭据、审计、缓存都落在项目内)、随项目走; - 包内快照
dist/vendored/skills/—— 随 npm 包发布的全量技能库,没跑init也能只读兜底。
远端能力(同步 / 保存 / 切图上传)还需要
.env.cb里的FINMALL_BASE_URL等配置, 以及playwright(本包 optionalDependency,默认会装)与一次 FinMall 登录。 写入类文件(凭据等)永远落项目根,不会写进node_modules。
安全
- 仅绑定
127.0.0.1,不暴露局域网。 - CORS 反射请求
Origin(任意公网域名均可跨源调用,无需配置白名单)。 - 可选
x-dev-token鉴权。 - HTTPS 自签模式(
DSL_DEV_HTTPS=1)兜底混合内容拦截(Safari / 严格策略);Chrome / Firefox 默认 HTTP 即可用。
本地开发(编辑器 + 本地服务)
本仓库既是「卡片编辑器平台」的源码,也是 cardbuddy 的源码。下面说明怎么在本地开发编辑器本身(即 card-editor/ 前端与 server/core/ 后端)。
关键区别:同域(dev)vs 跨域(生产)
npm run dev 能"免费"工作,是因为 card-editor/api.js 的 resolveApiBase() 在 import.meta.env.DEV 为真时走同源(第 3 条回退),而这条同源路径在生产里不存在——build 产物的 DEV=false,生产永远依赖「编辑器配好 8787 地址 + 跨源调用本地服务」。
所以本地开发分两条互补的路径:
| 命令 | 编辑器来源 | 后端来源 | 用途 |
| --- | --- | --- | --- |
| npm run dev | card-editor/ 源码(Vite dev + HMR,5180) | cardbuddy 服务(8787,由 tools/dev.mjs 同起) | 日常改编辑器 / 卡片 |
| npm run dev:prod | dist/ 构建产物(vite preview) | cardbuddy 服务(8787) | 部署前权威跨域 smoke test(最贴近生产) |
| 部署公网 | dist/ 静态托管(CDN) | 用户本机起 8787 | 生产环境 |
已废弃
dev:full:它用 dev 源码 + 注入VITE_API_BASE仿真跨域,但既不贴近生产(非 build 产物),又不如「dev + 设置里启用本地服务」更真实。直接用dev:prod(build 产物 + 8787)做验证即可。
模式一:纯同域开发(默认,最常用)
npm run dev
# → Vite dev server 端口 5180(由 .env.cb 的 CARDBUDDY_DEV_PORT 控制)
# 浏览器打开 http://localhost:5180/__cards /__card/:id /__render-status /__node-models 等后端端点全部由 cardbuddy(8787)提供:server/core 的中间件经 mountCoreMiddlewares 挂到它的 connect 实例,Vite 不承载任何后端插件。编辑器前端在 DEV 下经 resolveApiBase() 默认直连 http://127.0.0.1:8787(用非回环地址打开时走 vite.config.mjs 的 cardDevProxy 同源兜底,规避浏览器私有网络访问限制)。
模式二:开发期顺手看跨域(可选)
不想切到 build 产物、又想在开发时验证跨源?不用专门脚本,直接:
npm run dev # 编辑器 5180(同域,HMR)
node packages/cardbuddy/src/cli.js # 另起 8787 服务(另开一个终端)然后在编辑器「设置 → 本地开发服务」里启用并填 http://127.0.0.1:8787。resolveApiBase() 第 2 条(设置启用)优先于 DEV 回退,编辑器立即改走 8787 跨源——这与生产用户操作完全一致,比旧 dev:full 注入还真实。
模式三:部署前生产级验证(npm run dev:prod)
npm run dev:prod
# = npm run build && npm run preview(静态托管 dist/,默认 4173)+ 并行起 8787打开 http://localhost:4173,在「设置 → 本地开发服务」启用并填 http://127.0.0.1:8787,即可对 build 产物 跑完整的跨源 smoke test(卡片列表 / 画布渲染 / 保存同步 / SSE 实时刷新)。这是部署前最贴近生产的验证;退出 Ctrl+C 同时关闭 preview 与 8787。
dev:prod不注入VITE_API_BASE,因此产出的dist/是干净的、可直接用于生产部署——跨源地址完全交由设置(与生产一致)。
改动即时性(哪些要重启)
| 你改的文件 | npm run dev | npm run dev:prod |
| --- | --- | --- |
| card-editor/**(Vue 组件 / runtime / 样式) | ✅ Vite HMR 即时 | ⚠️ 需重跑 npm run dev:prod(build 产物,无 HMR) |
| src/cards/*.json(卡片内容) | ✅ cards-changed watcher 自动重载 | ✅ 8787 SSE 推送自动重载 |
| server/core/**(后端中间件) | ⚠️ 需重启 Vite | ⚠️ 需重启 8787 服务 |
| vite.config.mjs / 插件配置 | ⚠️ 需重启 Vite | ⚠️ 需重跑 npm run dev:prod(重新 build) |
铁律:改
server/core/**必须重启对应进程(Node 不会热重载服务端模块),别白等热更新。
前置条件
.env.cb已存在(含FINMALL_BASE_URL远端基址、CARDBUDDY_DEV_PORT)。缺它编辑器仍能起,只是远端资源代理会告警。- 首次跑前先
npm install(仓库根)。
服务侧单独起(调试 8787 本身)
# 仓库内直接跑源码(无需先 build)
node packages/cardbuddy/src/cli.js
# 或带 HTTPS 兜底
DSL_DEV_HTTPS=1 node packages/cardbuddy/src/cli.js