cardbuddy 0.22.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +11 -11
  2. package/dist/cli.js +2 -2
  3. package/dist/config.js +3 -3
  4. package/dist/init.js +13 -13
  5. package/dist/mcp.js +1 -1
  6. package/dist/preview-editor/assets/{index-CU8zVA2P.css → index-7RzNd7tf.css} +1 -1
  7. package/dist/preview-editor/assets/{index-CPpLZZyi.js → index-C7N5n_qW.js} +80 -80
  8. package/dist/preview-editor/index.html +2 -2
  9. package/dist/skills-upgrade.js +1 -1
  10. package/dist/standalone.js +2 -2
  11. package/dist/vendored/card-editor/runtime/adapter.js +10 -11
  12. package/dist/vendored/mcp/client.mjs +50 -13
  13. package/dist/vendored/mcp/tools.mjs +13 -7
  14. package/dist/vendored/server/core/ai-chat.js +540 -183
  15. package/dist/vendored/server/core/assetProxy.js +1 -1
  16. package/dist/vendored/server/core/browser.js +1 -1
  17. package/dist/vendored/server/core/cardDigest.js +324 -0
  18. package/dist/vendored/server/core/cardExport.js +5 -5
  19. package/dist/vendored/server/core/cardFiles.js +1 -1
  20. package/dist/vendored/server/core/cardHistoryRemote.js +1 -1
  21. package/dist/vendored/server/core/cardImportMeta.js +1 -1
  22. package/dist/vendored/server/core/cardListCache.js +92 -10
  23. package/dist/vendored/server/core/cardPublish.js +11 -6
  24. package/dist/vendored/server/core/cardPublishVersions.js +8 -8
  25. package/dist/vendored/server/core/cardSave.js +14 -10
  26. package/dist/vendored/server/core/cardSidecar.js +3 -3
  27. package/dist/vendored/server/core/cardSync.js +35 -23
  28. package/dist/vendored/server/core/cardVersion.js +1 -1
  29. package/dist/vendored/server/core/index.js +6 -6
  30. package/dist/vendored/server/core/logger.js +1 -1
  31. package/dist/vendored/server/core/remoteCard.js +5 -5
  32. package/dist/vendored/server/core/skillPaths.js +1 -1
  33. package/dist/vendored/server/core/sliceUpload.js +4 -4
  34. package/dist/vendored/skills/README.md +13 -13
  35. package/dist/vendored/skills/card-doctor/SKILL.md +2 -2
  36. package/dist/vendored/skills/dsl-card-ai/SKILL.md +4 -4
  37. package/dist/vendored/skills/dsl-card-generator/SKILL.md +2 -2
  38. package/dist/vendored/skills/dsl-card-generator/references/amount_formatting.md +1 -1
  39. package/dist/vendored/skills/dsl-card-generator/references/build_protocol.md +54 -0
  40. package/dist/vendored/skills/dsl-card-generator/references/component_rules.md +2 -2
  41. package/dist/vendored/skills/dsl-card-generator/references/converge_protocol.md +36 -0
  42. package/dist/vendored/skills/dsl-card-generator/references/event_patterns.md +1 -1
  43. package/dist/vendored/skills/dsl-card-generator/references/generation_pitfalls.md +1 -1
  44. package/dist/vendored/skills/dsl-card-generator/references/image_to_card.md +1 -1
  45. package/dist/vendored/skills/dsl-card-generator/references/quickref.md +1 -1
  46. package/dist/vendored/skills/dsl-card-generator/references/recipes.md +4 -4
  47. package/dist/vendored/skills/dsl-card-generator/references/rules.md +1 -1
  48. package/dist/vendored/skills/dsl-card-generator/references/slices_convention.md +6 -6
  49. package/dist/vendored/skills/dsl-card-generator/scripts/inventory_schemas.py +1 -1
  50. package/dist/vendored/skills/dsl-card-generator/scripts/lib/scaffold.mjs +1 -1
  51. package/dist/vendored/skills/dsl-card-generator/scripts/lib/slice_dedupe.mjs +2 -2
  52. package/dist/vendored/skills/dsl-card-generator/vendor/preview/assets/standalone-CeVX_y-M.js +137 -0
  53. package/dist/vendored/skills/dsl-card-generator/vendor/preview/assets/standalone-DiEiY59H.css +1 -0
  54. package/dist/vendored/skills/dsl-card-generator/vendor/preview/standalone.html +2 -2
  55. package/dist/vendored/skills/{finmall-card-sync → platform-card-sync}/SKILL.md +26 -26
  56. package/dist/vendored/skills/{finmall-card-sync → platform-card-sync}/scripts/api_batch_sync.mjs +184 -162
  57. package/dist/vendored/skills/{finmall-card-sync → platform-card-sync}/scripts/slice_upload.mjs +48 -29
  58. package/dist/vendored/skills/{finmall-session-navigator → platform-session-navigator}/SKILL.md +13 -13
  59. package/dist/vendored/skills/{finmall-session-navigator → platform-session-navigator}/scripts/package-lock.json +2 -2
  60. package/dist/vendored/skills/{finmall-session-navigator → platform-session-navigator}/scripts/package.json +2 -2
  61. package/dist/vendored/skills/{finmall-session-navigator/scripts/finmall_login.mjs → platform-session-navigator/scripts/platform_login.mjs} +41 -31
  62. package/dist/vendored/skills/{finmall-session-navigator → platform-session-navigator}/scripts/session-manager.mjs +17 -17
  63. package/dist/vendored/skills/shared/README.md +21 -21
  64. package/dist/vendored/skills/shared/auth-model.md +8 -10
  65. package/dist/vendored/skills/shared/{finmall-config.mjs → platform-config.mjs} +10 -10
  66. package/dist/vendored/tools/{finmall-env.mjs → platform-env.mjs} +13 -13
  67. package/package.json +1 -1
  68. package/dist/vendored/skills/dsl-card-generator/vendor/preview/assets/standalone-BCLshIzC.css +0 -1
  69. package/dist/vendored/skills/dsl-card-generator/vendor/preview/assets/standalone-pP5kTKiI.js +0 -88
@@ -5,15 +5,15 @@ import { fileURLToPath } from 'node:url'
5
5
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
6
6
 
7
7
  import { recordExport, markDirty } from './cardImportMeta.js'
8
- import { checkPushAllowed } from './cardListCache.js'
8
+ import { checkPushAllowed, currentAppContext } from './cardListCache.js'
9
9
  import { assertRemoteSource, findCardDir } from './cardSidecar.js'
10
10
  import { log } from './logger.js'
11
11
  import { resolveSkillPathOrProject } from './skillPaths.js'
12
12
  import { generateCardCover } from './screenshot.js'
13
13
  import { serialize } from './browser.js'
14
14
 
15
- /** 远端保存脚本相对 skills/ 的路径(与「同步」共用 finmall-card-sync) */
16
- const SYNC_SCRIPT_REL = 'finmall-card-sync/scripts/api_batch_sync.mjs'
15
+ /** 远端保存脚本相对 skills/ 的路径(与「同步」共用 platform-card-sync) */
16
+ const SYNC_SCRIPT_REL = 'platform-card-sync/scripts/api_batch_sync.mjs'
17
17
 
18
18
  /**
19
19
  * 推送成功后「截图 + 上传为远端预览图」的整段预算。
@@ -47,7 +47,7 @@ function withDeadline(promise, ms, label) {
47
47
  *
48
48
  * 为什么放在服务端而不是前端:①截图走本地无头渲染(screenshot.js 单例 Chrome),
49
49
  * 与「预览图」按钮同一条链路,产出透明底、与卡片实际渲染尺寸 1:1 的 PNG;
50
- * ②上传必须带 FinMall token,只有已登录的后台会话拿得到,前端不接触凭据。
50
+ * ②上传必须带远端 token,只有已登录的后台会话拿得到,前端不接触凭据。
51
51
  *
52
52
  * 三条纪律:
53
53
  * - **best-effort**:任何失败都返回结构化结果,绝不向上抛——预览图是推送的附加产物,
@@ -108,14 +108,14 @@ export async function uploadPreviewAfterPush({
108
108
  }
109
109
 
110
110
  /**
111
- * 远端保存(写回 FinMall)服务端处理。
111
+ * 远端保存(写回远端)服务端处理。
112
112
  * 前端「保存」按钮把当前预览的 schema(含实时数据绑定)POST 过来,
113
113
  * 本中间件校验 pageId 后,经已登录浏览器中转调用 IDC10010.do 写回远端,
114
- * 与「同步」共用 finmall-card-sync 的 saveCardToRemote(带会话自愈)。
114
+ * 与「同步」共用 platform-card-sync 的 saveCardToRemote(带会话自愈)。
115
115
  *
116
116
  * @param {{ root: string, previewAfterPush?: typeof uploadPreviewAfterPush }} opts
117
117
  * - previewAfterPush:推送成功后的「截图 + 上传预览图」实现,默认即上面的
118
- * uploadPreviewAfterPush;单测注入替身以便在无 Chrome / 无 FinMall 会话的环境断言接线。
118
+ * uploadPreviewAfterPush;单测注入替身以便在无 Chrome / 无远端会话的环境断言接线。
119
119
  */
120
120
  export function createSaveMiddleware({ root, previewAfterPush = uploadPreviewAfterPush }) {
121
121
  return async function saveMiddleware(req, res, next) {
@@ -150,7 +150,7 @@ export function createSaveMiddleware({ root, previewAfterPush = uploadPreviewAft
150
150
  return res.end(JSON.stringify({ ok: false, error: 'schema.pageId 缺失,无法确定要保存的卡片' }))
151
151
  }
152
152
  // 远端来源硬闸:本地自建、无 .import-meta.json 侧车的卡一律禁止推送,
153
- // 避免把测试数据误提交到 FinMall 服务器(比「是否在远端列表」更根本)。
153
+ // 避免把测试数据误提交到远端服务器(比「是否在远端列表」更根本)。
154
154
  {
155
155
  const src = assertRemoteSource(root, { code, pageId: schema.pageId })
156
156
  if (!src.ok) {
@@ -183,9 +183,13 @@ export function createSaveMiddleware({ root, previewAfterPush = uploadPreviewAft
183
183
  }
184
184
  }
185
185
  // 存在性闸门:推送前确认目标卡片在远端卡片列表缓存中(全量同步时刷新)。
186
- // 缓存非空才校验(为空说明尚未同步,放行);不存在则拒绝推送,防打错/已删卡片。
186
+ // 缓存非空才校验(为空说明尚未同步,放行);缓存属于其他 appId(环境切换)也放行。
187
187
  {
188
- const allow = checkPushAllowed(schema.pageId)
188
+ const ctx = await currentAppContext(root)
189
+ const allow = checkPushAllowed(schema.pageId, ctx)
190
+ if (allow.staleContext || allow.notFullSynced) {
191
+ log.warn('远端列表缓存对当前 appId 不可判(环境已切换或未全量同步),存在性闸门放行;建议先「远程同步」刷新当前环境列表', { ...ctx, staleContext: !!allow.staleContext })
192
+ }
189
193
  if (!allow.allowed) {
190
194
  res.statusCode = 409
191
195
  res.setHeader('Content-Type', 'application/json')
@@ -2,9 +2,9 @@
2
2
  * 卡片「远端来源侧车」判定。
3
3
  *
4
4
  * 本地卡片目录 src/cards/<code>/ 下的 .import-meta.json 由「远程同步」写入,
5
- * 记录该卡来自 FinMall 远端的 cardId 等来源信息。只有存在侧车的卡才被视为
5
+ * 记录该卡来自远端的 cardId 等来源信息。只有存在侧车的卡才被视为
6
6
  * 「有合法远端来源」,允许推送(保存 / 预览)到服务器;本地自建、从未同步过的
7
- * 测试卡没有侧车,一律禁止推送,避免把测试数据误提交到 FinMall。
7
+ * 测试卡没有侧车,一律禁止推送,避免把测试数据误提交到远端。
8
8
  */
9
9
 
10
10
  import fs from 'node:fs'
@@ -66,7 +66,7 @@ export function assertRemoteSource(root, { code, pageId } = {}) {
66
66
  ok: false,
67
67
  reason:
68
68
  '该卡片为本地自建、无远端同步来源(缺少 .import-meta.json 侧车)。' +
69
- '为防止将测试数据误提交到 FinMall 服务器,已禁止保存 / 推送。' +
69
+ '为防止将测试数据误提交到远端服务器,已禁止保存 / 推送。' +
70
70
  '请先通过「远程同步」把卡片来源刷新到本地后再操作。',
71
71
  }
72
72
  }
@@ -1,14 +1,14 @@
1
1
  import path from 'node:path'
2
2
  import { pathToFileURL } from 'node:url'
3
3
  import { createHash } from 'node:crypto'
4
- import { checkPushAllowed, refreshCardList, hasCardId } from './cardListCache.js'
4
+ import { checkPushAllowed, refreshCardList, hasCardId, currentAppContext } from './cardListCache.js'
5
5
  import { assertRemoteSource } from './cardSidecar.js'
6
6
  import { log, fmtBytes } from './logger.js'
7
7
  import { buildImportMeta, writeImportMeta } from './cardImportMeta.js'
8
8
  import { resolveSkillPathOrProject, projectSkillsDir } from './skillPaths.js'
9
9
 
10
- /** finmall-card-sync 的批量同步脚本(同步 / 远端预览共用),相对 skills/ */
11
- const SYNC_SCRIPT_REL = 'finmall-card-sync/scripts/api_batch_sync.mjs'
10
+ /** platform-card-sync 的批量同步脚本(同步 / 远端预览共用),相对 skills/ */
11
+ const SYNC_SCRIPT_REL = 'platform-card-sync/scripts/api_batch_sync.mjs'
12
12
 
13
13
  /** 卡片 id 合法形态(与 normalizeCard / 新建卡同源正则) */
14
14
  const CARD_CODE_RE = /^[A-Za-z0-9_-]+$/
@@ -96,8 +96,8 @@ function logSyncSummary(summary, requestLabel) {
96
96
  }
97
97
 
98
98
  /**
99
- * 预览「同步远程数据」按钮的服务端处理:经已认证的 FinMall 浏览器会话拉取最新卡片写回 src/cards。
100
- * 复用 finmall-card-sync 的 syncCards(connectOverCDP 连用户已开的 Chrome,disconnect 不杀进程)。
99
+ * 预览「同步远程数据」按钮的服务端处理:经已认证的远端浏览器会话拉取最新卡片写回 src/cards。
100
+ * 复用 platform-card-sync 的 syncCards(connectOverCDP 连用户已开的 Chrome,disconnect 不杀进程)。
101
101
  *
102
102
  * 任务制端点(编辑器「远端 → 查看全部 → 下载选中 / 全部下载」的进度弹窗依赖):
103
103
  * - POST /__sync body { cardCode? , cardCodes? } → 立即返回 { ok, jobId, total }
@@ -282,7 +282,7 @@ export function createSyncMiddleware({ root }) {
282
282
  job.result = {
283
283
  success: false,
284
284
  timedOut: true,
285
- error: `同步任务超过 ${Math.round(SYNC_JOB_TIMEOUT_MS / 1000)} 秒未完成,已自动收尾(上游无响应)。请重试,或检查 FinMall 登录态 / 网络。`,
285
+ error: `同步任务超过 ${Math.round(SYNC_JOB_TIMEOUT_MS / 1000)} 秒未完成,已自动收尾(上游无响应)。请重试,或检查远端登录态 / 网络。`,
286
286
  }
287
287
  log.error('远端同步看门狗触发:任务超时,已代其收尾并释放同步锁', {
288
288
  jobId: job.id,
@@ -352,9 +352,9 @@ export function createSyncMiddleware({ root }) {
352
352
 
353
353
  /**
354
354
  * 预览「远端预览」按钮的服务端处理:复刻 ubomp 的 PreviewPopover 全链路——
355
- * 先把当前卡片保存到 FinMall(IDC10010),再取卡片 code(IDC10015)、
355
+ * 先把当前卡片保存到远端(IDC10010),再取卡片 code(IDC10015)、
356
356
  * 发布预览测试版(IDC10020)、通知真机(IDC10021),最终返回 cardCode 供前端生成二维码。
357
- * 复用 finmall-card-sync 的 previewCardRemote(与「同步」「保存远端」共用同一后台 FinMall 会话)。
357
+ * 复用 platform-card-sync 的 previewCardRemote(与「同步」「保存远端」共用同一后台远端会话)。
358
358
  *
359
359
  * @param {{ root: string }} opts
360
360
  * @returns {(req: import('http').IncomingMessage, res: import('http').ServerResponse, next: Function) => void}
@@ -386,7 +386,7 @@ export function createPreviewMiddleware({ root }) {
386
386
  return res.end(JSON.stringify({ error: 'missing schema.pageId', message: '需传入当前卡片 schema(含 pageId)' }))
387
387
  }
388
388
  // 远端来源硬闸:本地自建、无 .import-meta.json 侧车的卡一律禁止推送,
389
- // 避免把测试数据误提交到 FinMall 服务器(比「是否在远端列表」更根本)。
389
+ // 避免把测试数据误提交到远端服务器(比「是否在远端列表」更根本)。
390
390
  {
391
391
  const src = assertRemoteSource(root, { pageId: payload.schema.pageId })
392
392
  if (!src.ok) {
@@ -396,9 +396,13 @@ export function createPreviewMiddleware({ root }) {
396
396
  }
397
397
  }
398
398
  // 存在性闸门:推送前确认目标卡片在远端卡片列表缓存中(全量同步时刷新)。
399
- // 缓存非空才校验(为空说明尚未同步,放行);不存在则拒绝推送,防打错/已删卡片。
399
+ // 缓存非空才校验(为空说明尚未同步,放行);缓存属于其他 appId(环境切换)也放行。
400
400
  {
401
- const allow = checkPushAllowed(payload.schema.pageId)
401
+ const ctx = await currentAppContext(root)
402
+ const allow = checkPushAllowed(payload.schema.pageId, ctx)
403
+ if (allow.staleContext || allow.notFullSynced) {
404
+ log.warn('远端列表缓存对当前 appId 不可判(环境已切换或未全量同步),存在性闸门放行;建议先「远程同步」刷新当前环境列表', { ...ctx, staleContext: !!allow.staleContext })
405
+ }
402
406
  if (!allow.allowed) {
403
407
  res.statusCode = 409
404
408
  res.setHeader('Content-Type', 'application/json')
@@ -428,22 +432,22 @@ export function createPreviewMiddleware({ root }) {
428
432
  }
429
433
  }
430
434
 
431
- /** finmall-session-navigator 登录脚本(凭据读写 + 页面内登录验证),相对 skills/ */
432
- const SESSION_LOGIN_REL = 'finmall-session-navigator/scripts/finmall_login.mjs'
433
- /** finmall-session-navigator 后台会话管理(同步 / 登录验证共用同一 headless Chrome) */
434
- const SESSION_MANAGER_REL = 'finmall-session-navigator/scripts/session-manager.mjs'
435
+ /** platform-session-navigator 登录脚本(凭据读写 + 页面内登录验证),相对 skills/ */
436
+ const SESSION_LOGIN_REL = 'platform-session-navigator/scripts/platform_login.mjs'
437
+ /** platform-session-navigator 后台会话管理(同步 / 登录验证共用同一 headless Chrome) */
438
+ const SESSION_MANAGER_REL = 'platform-session-navigator/scripts/session-manager.mjs'
435
439
 
436
- /** 凭据落盘路径:与 finmall_login.mjs 默认路径同址,但经 projectSkillsDir 永远写项目根 skills/,不进 node_modules */
440
+ /** 凭据落盘路径:与 platform_login.mjs 默认路径同址,但经 projectSkillsDir 永远写项目根 skills/,不进 node_modules */
437
441
  function credentialsFilePath(root) {
438
442
  return path.join(
439
443
  projectSkillsDir(root),
440
- 'finmall-session-navigator',
444
+ 'platform-session-navigator',
441
445
  '.local',
442
446
  'credentials.json',
443
447
  )
444
448
  }
445
449
 
446
- /** 动态加载 finmall-session-navigator 登录脚本(仓库内 / 消费者工程 / 包内快照三态通用) */
450
+ /** 动态加载 platform-session-navigator 登录脚本(仓库内 / 消费者工程 / 包内快照三态通用) */
447
451
  async function loadLoginModule(root) {
448
452
  const p = resolveSkillPathOrProject(root, SESSION_LOGIN_REL)
449
453
  return import(pathToFileURL(p).href)
@@ -462,14 +466,14 @@ async function readSavedLoginName(root) {
462
466
 
463
467
  /**
464
468
  * 用给定账号密码做一次真实登录验证(不落盘)。
465
- * 与「同步」共用同一后台 headless Chrome 会话池:连上 / 拉起后在 FinMall 同源页面内
469
+ * 与「同步」共用同一后台 headless Chrome 会话池:连上 / 拉起后在远端同源页面内
466
470
  * 直调登录接口(performLogin),以是否建立会话(sessionCreated)判定凭据是否正确——
467
471
  * 已登录会话不会蒙混过关,因为performLogin 每次都真实提交一遍账号密码。
468
472
  * 只 disconnect 不关闭浏览器:后台 Chrome 以 detached 常驻,供后续同步复用。
469
473
  *
470
474
  * @returns {Promise<{ sessionCreated: boolean, businessFailure: boolean, status?: number }>}
471
475
  */
472
- async function verifyFinmallLogin(root, loginName, rawPassword) {
476
+ async function verifyPlatformLogin(root, loginName, rawPassword) {
473
477
  const {
474
478
  performLogin,
475
479
  tryConnectBrowser,
@@ -485,10 +489,18 @@ async function verifyFinmallLogin(root, loginName, rawPassword) {
485
489
  const md5Hex = createHash('md5')
486
490
  .update(String(rawPassword), 'utf8')
487
491
  .digest('hex')
492
+ // 对端站点 host:从登录模块给出的目标 URL 派生(不写死域名),用于认出已登录的标签页
493
+ const targetHost = (() => {
494
+ try {
495
+ return new URL(TARGET_URL).host
496
+ } catch {
497
+ return ''
498
+ }
499
+ })()
488
500
 
489
501
  let browser = await tryConnectBrowser(SYNC_PORT)
490
502
  if (!browser) {
491
- // 端口未启动:拉起后台 headless Chrome(与 ensureFinMallSession 同端口同数据目录)
503
+ // 端口未启动:拉起后台 headless Chrome(与 ensurePlatformSession 同端口同数据目录)
492
504
  launchChromeDetached(findChromePath(), SYNC_BROWSER_DATA_DIR, SYNC_PORT, TARGET_URL, {
493
505
  headless: true,
494
506
  })
@@ -508,7 +520,7 @@ async function verifyFinmallLogin(root, loginName, rawPassword) {
508
520
  }
509
521
  try {
510
522
  const context = browser.contexts()[0]
511
- let page = context?.pages().find((p) => p.url().includes('finmall.com'))
523
+ let page = context?.pages().find((p) => targetHost && p.url().includes(targetHost))
512
524
  if (!page) {
513
525
  page = context ? await context.newPage() : await browser.newPage()
514
526
  await page
@@ -594,7 +606,7 @@ export function createLoginCredentialsMiddleware({ root }) {
594
606
  }
595
607
  try {
596
608
  // 1) 先做一次真实登录验证,通过才落盘
597
- const result = await verifyFinmallLogin(root, loginName, rawPassword)
609
+ const result = await verifyPlatformLogin(root, loginName, rawPassword)
598
610
  if (result.sessionCreated) {
599
611
  const { saveCredentials } = await loadLoginModule(root)
600
612
  await saveCredentials(loginName, rawPassword, credentialsFilePath(root))
@@ -5,7 +5,7 @@ import { readImportMeta, recordRemoteVersion } from './cardImportMeta.js'
5
5
  import { log } from './logger.js'
6
6
  import { resolveSkillPathOrProject } from './skillPaths.js'
7
7
 
8
- const SYNC_SCRIPT_REL = 'finmall-card-sync/scripts/api_batch_sync.mjs'
8
+ const SYNC_SCRIPT_REL = 'platform-card-sync/scripts/api_batch_sync.mjs'
9
9
 
10
10
  /**
11
11
  * 调试面板「卡片来源」tab 的远端最新版本持久化(POST,fetch + persist):POST /__card-version/:id
@@ -89,7 +89,7 @@ export {
89
89
  * 把全部卡片中间件挂到 Connect 风格容器(需实现 .use(mw))。
90
90
  * @param {{ use: (mw: Function) => void }} app
91
91
  * @param {{ root: string, viteServer?: import('vite').ViteDevServer | null, upstream?: string, notify?: (data: object, event?: string) => void, watchFile?: (file: string, cb: () => void) => { close: () => void }, allowDispatch?: boolean }} opts
92
- * - upstream:FinMall 远端站点基址(来自 FINMALL_BASE_URL),资源代理回源用;空串表示不回源。
92
+ * - upstream:远端站点基址(来自 PLATFORM_BASE_URL),资源代理回源用;空串表示不回源。
93
93
  * - notify:SSE 推送入口(cardbuddy 的 /__events hub);卡片改名的 card-renamed 广播、代码桥
94
94
  * bridge 事件都经它推送,缺省时静默降级。
95
95
  * - watchFile:代码桥单文件监听能力注入(chokidar 版);缺省代码桥用 fs.watch 兜底。
@@ -101,24 +101,24 @@ export function mountCoreMiddlewares(app, { root, viteServer = null, upstream =
101
101
  app.use(createPreviewMiddleware({ root }))
102
102
  app.use(createLoginCredentialsMiddleware({ root }))
103
103
  // 卡片发布(顶栏「远端 → 卡片发布」):取远端最大版本号 +1 后发布上线(IDC10004 → IDC10016),
104
- // 复用 finmall-card-sync 的 publishCard,与同步/保存/预览共用同一后台 FinMall 会话。
104
+ // 复用 platform-card-sync 的 publishCard,与同步/保存/预览共用同一后台远端会话。
105
105
  app.use(createPublishMiddleware({ root }))
106
106
  // 卡片来源 tab 的「远端最新版本」查询(只读):GET /__card-version/:id 取 IDC10004 的 maxVersion,
107
- // 复用 finmall-card-sync 的 fetchCardLatestVersion,与发布/同步/保存共用同一后台 FinMall 会话。
107
+ // 复用 platform-card-sync 的 fetchCardLatestVersion,与发布/同步/保存共用同一后台远端会话。
108
108
  app.use(createCardVersionMiddleware({ root }))
109
109
  // 「卡片历史记录」tab 的远端版本时间线(只读):GET /__card-history/:cardCode 取 IDC10013 的
110
- // schemaValueList(新→旧,含预览图 / 变更标签),复用 finmall-card-sync 的 fetchCardHistory。
110
+ // schemaValueList(新→旧,含预览图 / 变更标签),复用 platform-card-sync 的 fetchCardHistory。
111
111
  app.use(createCardHistoryMiddleware({ root }))
112
112
  // 「导入信息」tab「发布版本记录」弹框的数据源(只读):GET /__card-publish-versions/:cardCode
113
113
  // 取 IDC10004 的 versionList(新→旧,版本号 / 平台 / 发布状态 / 发布时间),滚动到底懒加载下一页。
114
114
  app.use(createCardPublishVersionsMiddleware({ root }))
115
- // 切图上传(顶栏「远端 → 切图上传」):把当前卡引用的 src/slices 切图传到 FinMall 图片库,
115
+ // 切图上传(顶栏「远端 → 切图上传」):把当前卡引用的 src/slices 切图传到远端图片库,
116
116
  // 写 src/slices/manifest.json 映射并把卡片里的 /slices/ 引用改写为 @filestore/ 线上地址。
117
117
  // 仅在用户点击时触发,没有任何自动上传路径。
118
118
  app.use(createSliceUploadMiddleware({ root }))
119
119
  // 远端卡片基本信息维护(「远端全部卡片」弹窗的「添加卡片 / 编辑」抽屉):
120
120
  // 类型字典 IDC10009、新建 IDC10002、编辑 IDC10008、封面上传 CC0000,
121
- // 复用 finmall-card-sync 的后台会话;只改 FinMall 侧元数据,不写本地 src/cards。
121
+ // 复用 platform-card-sync 的后台会话;只改远端侧元数据,不写本地 src/cards。
122
122
  app.use(createRemoteCardMiddleware({ root }))
123
123
  app.use(createCardFilesMiddleware({ root, notify }))
124
124
  app.use(createSaveMiddleware({ root }))
@@ -7,7 +7,7 @@
7
7
  * - 安全性:redact() 对 schema / data / 远端响应体等大段正文只给规模概览,
8
8
  * 绝不 dump 卡片正文;Error 只附 message + 简短 stack;
9
9
  * - 双消费方:① server/core 内兄弟引用 './logger.js';
10
- * ② skills/finmall-card-sync/scripts/*.mjs 经 '../../../server/core/logger.js' 借用;
10
+ * ② skills/platform-card-sync/scripts/*.mjs 经 '../../../server/core/logger.js' 借用;
11
11
  * build 把 server/core 整体复制到 dist/server-core,兄弟引用在发布态仍成立。
12
12
  *
13
13
  * 颜色原语基于 chalk(零配置、自动探测 TTY / FORCE_COLOR / CI / NO_COLOR /
@@ -3,9 +3,9 @@ import { log } from './logger.js'
3
3
  import { resolveSkillPathOrProject } from './skillPaths.js'
4
4
 
5
5
  /** 卡片元数据(列表字典 / 新建 / 编辑)所在脚本,相对 skills/ */
6
- const SYNC_SCRIPT_REL = 'finmall-card-sync/scripts/api_batch_sync.mjs'
6
+ const SYNC_SCRIPT_REL = 'platform-card-sync/scripts/api_batch_sync.mjs'
7
7
  /** 图片上传(CC0000.do)所在脚本,与切图上传同一条链路 */
8
- const SLICE_SCRIPT_REL = 'finmall-card-sync/scripts/slice_upload.mjs'
8
+ const SLICE_SCRIPT_REL = 'platform-card-sync/scripts/slice_upload.mjs'
9
9
 
10
10
  /** 封面 base64 请求体上限:一张 2x 卡片封面通常 < 1MB,留足余量又不至于吃满内存 */
11
11
  const MAX_BODY_BYTES = 12 * 1024 * 1024
@@ -71,11 +71,11 @@ function sendError(res, err, label) {
71
71
  * - POST /__remote-card/delete → 删除卡片(IDC10003.do),body { cardId }(不可逆,前端须二次确认)
72
72
  * - POST /__remote-card/cover → 上传封面图(CC0000.do),body { fileName, base64, mime? } → { url }
73
73
  *
74
- * 为什么必须经 Node 中转:FinMall 接口需要 `token` 头(值只在页面上下文内可读)、且跨域,
75
- * 纯前端发不出去。这里复用 finmall-card-sync 的后台会话(与同步/切图上传同一个 Chrome),
74
+ * 为什么必须经 Node 中转:远端接口需要 `token` 头(值只在页面上下文内可读)、且跨域,
75
+ * 纯前端发不出去。这里复用 platform-card-sync 的后台会话(与同步/切图上传同一个 Chrome),
76
76
  * 不额外引入登录态。
77
77
  *
78
- * 注意:这些接口只改 FinMall 侧的卡片元数据,不写本地 src/cards ——
78
+ * 注意:这些接口只改远端侧的卡片元数据,不写本地 src/cards ——
79
79
  * 本地目录由「下载选中 / 全部下载」按需拉取,避免远端建卡即污染工作区。
80
80
  *
81
81
  * @param {{ root: string }} opts
@@ -8,7 +8,7 @@
8
8
  * - 包内快照: <pkg>/dist/vendored/skills (构建期从仓库 skills/ 内联,随包发布)
9
9
  *
10
10
  * 解析优先级(只读用途):configureSkillPaths 显式指定 > DSL_SKILLS_ROOT > <root>/skills > 包内快照。
11
- * 写入用途(如 FinMall 凭据落盘)一律走 projectSkillsDir(root) —— 绝不写进 node_modules,
11
+ * 写入用途(如远端凭据落盘)一律走 projectSkillsDir(root) —— 绝不写进 node_modules,
12
12
  * 否则重装即丢、且多项目互相污染。
13
13
  */
14
14
  import fs from 'node:fs'
@@ -4,15 +4,15 @@ import { log, fmtBytes } from './logger.js'
4
4
  import { resolveSkillPathOrProject } from './skillPaths.js'
5
5
 
6
6
  /** 切图上传脚本相对 skills/ 的路径 */
7
- const UPLOAD_SCRIPT_REL = 'finmall-card-sync/scripts/slice_upload.mjs'
7
+ const UPLOAD_SCRIPT_REL = 'platform-card-sync/scripts/slice_upload.mjs'
8
8
 
9
9
  /**
10
- * 顶栏「远端 → 切图上传」按钮的服务端处理:把卡片引用的 src/slices 切图上传到 FinMall
10
+ * 顶栏「远端 → 切图上传」按钮的服务端处理:把卡片引用的 src/slices 切图上传到远端
11
11
  * 图片库(CC0000.do),写「本地文件 ↔ 线上地址」映射(src/slices/manifest.json),
12
12
  * 并把该卡 schema.json / data.json 里的 `/slices/<name>` 引用改写为返回的 `@filestore/...`。
13
13
  *
14
- * 复用 finmall-card-sync 的 uploadSlices(与「同步」「版本推送」共用同一后台 FinMall 会话)。
15
- * 纯前端无法直接调 FinMall:跨域 + 需 token(只在页面上下文内读取,不回传 Node),故必须经此中转。
14
+ * 复用 platform-card-sync 的 uploadSlices(与「同步」「版本推送」共用同一后台远端会话)。
15
+ * 纯前端无法直接调远端:跨域 + 需 token(只在页面上下文内读取,不回传 Node),故必须经此中转。
16
16
  *
17
17
  * **只由用户点击触发**——没有任何自动上传路径:保存 / 推送 / 同步都不会顺带上传切图。
18
18
  *
@@ -14,14 +14,14 @@
14
14
  card-skill-trainer ────┘ (训练闭环反哺 generator 与 sketch-spec-to-card)
15
15
  │
16
16
  dsl-card-generator
17
- │ 接受 finmall-card-sync 同步进来的卡作为"导出实证"
17
+ │ 接受 platform-card-sync 同步进来的卡作为"导出实证"
18
18
  ▼
19
- finmall-card-sync ──────► finmall-session-navigator
19
+ platform-card-sync ──────► platform-session-navigator
20
20
  │ │
21
21
  │ 复用 playwright 依赖 │ 启动 Chrome、维持登录态
22
22
  ▼ ▼
23
23
  shared/
24
- (finmall-config.mjs
24
+ (platform-config.mjs
25
25
  auth-model.md)
26
26
  ```
27
27
 
@@ -29,10 +29,10 @@
29
29
 
30
30
  | skill | 职责 | 依赖 |
31
31
  |---|---|---|
32
- | **shared/** | 跨 skill 共享代码与文档:环境配置加载器(`finmall-config.mjs`)、鉴权与 Playwright 工程集中陈述(`auth-model.md`) | 无(最底层) |
33
- | **finmall-session-navigator** | 用 Playwright 独立启动 Chrome、自动登录 FinMall UAT、维持会话。所有需要登录态的下游 skill 都先调它 | shared |
34
- | **finmall-card-sync** | 通过 API 把 FinMall 卡片同步进 `src/cards/<id>/`(schema/data/封面)。复用 finmall-session-navigator 的登录态与 playwright 依赖 | shared + finmall-session-navigator 的登录态 |
35
- | **dsl-card-generator** | 从零/图片生成卡片、组件/表达式/事件深度语义、校验脚本库、范式片段库、**整卡模板库与沉淀闭环**(resolve 检索 → register 登记 → build 重建保留 pin)、组件使用分析、**建卡输入路由**(`scripts/route_input.mjs` / MCP `card_route_input` + SKILL.md §8 Workflow 0:识别到 Sketch MeaXure 标注导出时自动先转 sketch-spec-to-card 前处理,再回本 skill 逐卡生成)。是"卡片生成与契约"的权威方 | sketch-spec-to-card(MeaXure 导出输入时的强制上游);接受 finmall-card-sync 同步进来的卡作为 export-proven 证据 |
32
+ | **shared/** | 跨 skill 共享代码与文档:环境配置加载器(`platform-config.mjs`)、鉴权与 Playwright 工程集中陈述(`auth-model.md`) | 无(最底层) |
33
+ | **platform-session-navigator** | 用 Playwright 独立启动 Chrome、自动登录远端 UAT、维持会话。所有需要登录态的下游 skill 都先调它 | shared |
34
+ | **platform-card-sync** | 通过 API 把远端卡片同步进 `src/cards/<id>/`(schema/data/封面)。复用 platform-session-navigator 的登录态与 playwright 依赖 | shared + platform-session-navigator 的登录态 |
35
+ | **dsl-card-generator** | 从零/图片生成卡片、组件/表达式/事件深度语义、校验脚本库、范式片段库、**整卡模板库与沉淀闭环**(resolve 检索 → register 登记 → build 重建保留 pin)、组件使用分析、**建卡输入路由**(`scripts/route_input.mjs` / MCP `card_route_input` + SKILL.md §8 Workflow 0:识别到 Sketch MeaXure 标注导出时自动先转 sketch-spec-to-card 前处理,再回本 skill 逐卡生成)。是"卡片生成与契约"的权威方 | sketch-spec-to-card(MeaXure 导出输入时的强制上游);接受 platform-card-sync 同步进来的卡作为 export-proven 证据 |
36
36
  | **dsl-card-ai** | 在已有卡片上用 AI 编辑(修绑定 / 批量改 / 解释卡)+ **建卡入口路由**(Workflow D:MCP `card_route_input` 判输入形态,MeaXure 导出强制先转 sketch-spec-to-card,再交 dsl-card-generator 生成)。纯编排层,所有写卡收口是 `card_write` | dsl-card-generator 的 references + MCP 工具;sketch-spec-to-card(MeaXure 输入时的上游) |
37
37
  | **sketch-spec-to-card** | 读取 Sketch MeaXure 标注导出,按画板聚焦提取卡片要素(prompt.md / spec.md / 截图 / 切图),产出到 UI/ 供 dsl-card-generator 消费。**用户把 MeaXure 导出丢给 dsl-card-generator 时会被自动路由到本 skill 前处理**(无需用户显式点名) | dsl-card-generator(下游消费方) |
38
38
  | **card-skill-trainer** | 无人值守训练闭环:抽样设计稿 → 生成 → 结构/事实/像素评分 → 失败归因 → 反哺 sketch-spec-to-card 与 dsl-card-generator;lessons 经 `training/tools/aggregate_lessons.mjs` 聚合成提案人工审批落盘 | 被训练的两个 skill + training/ 设施 |
@@ -44,7 +44,7 @@
44
44
 
45
45
  - **生成新卡 / 改组件结构 / 校验**:只读 `dsl-card-generator/SKILL.md` + `references/quickref.md`
46
46
  - **AI 编辑已有卡(修绑定、批量改)**:读 `dsl-card-ai/SKILL.md`,需要字段细节时跳到 `dsl-card-generator/references/`
47
- - **拉取/刷新 FinMall 卡**:先 `finmall-session-navigator` 拿登录态,再 `finmall-card-sync` 同步
47
+ - **拉取/刷新远端卡**:先 `platform-session-navigator` 拿登录态,再 `platform-card-sync` 同步
48
48
  - **跨 skill 排查会话/鉴权问题**:直接读 `shared/auth-model.md`(唯一权威方)
49
49
  - **存量卡体检 / 治理 lint 存量**:读 `card-doctor/SKILL.md`,先跑 `node tools/card-doctor.mjs` 再看报告
50
50
  - **建卡提速(模板复用/沉淀)**:先 `resolve_card_template.mjs <意图>` 查模板库;新模式交付后经 `register_card_template.mjs --check` 确认再登记
@@ -57,15 +57,15 @@
57
57
  skills/
58
58
  ├── README.md ← 本文件
59
59
  ├── shared/
60
- │ ├── finmall-config.mjs ← 环境配置加载器(baseUrl/appId/cdpPort/...)
60
+ │ ├── platform-config.mjs ← 环境配置加载器(baseUrl/appId/cdpPort/...)
61
61
  │ └── auth-model.md ← 鉴权模型 + Playwright 工程集中陈述
62
- ├── finmall-session-navigator/
63
- │ ├── SKILL.md ← 自动登录 FinMall
62
+ ├── platform-session-navigator/
63
+ │ ├── SKILL.md ← 自动登录远端
64
64
  │ └── scripts/
65
- │ ├── finmall_login.mjs ← 主脚本
65
+ │ ├── platform_login.mjs ← 主脚本
66
66
  │ ├── session-manager.mjs ← 会话管理辅助
67
67
  │ └── package.json ← playwright 依赖
68
- ├── finmall-card-sync/
68
+ ├── platform-card-sync/
69
69
  │ ├── SKILL.md ← API 同步卡片到 src/cards/
70
70
  │ └── scripts/
71
71
  │ └── api_batch_sync.mjs ← 批量同步脚本
@@ -32,7 +32,7 @@ node tools/card-doctor.mjs --out .codex-tmp/doctor.json # 落盘 JSON 报告(.
32
32
  |---|---|---|---|
33
33
  | `validate` | `validate_schema.py`(离线) | 硬契约 | 结构/绑定/data 一致性;error 必须清零 |
34
34
  | `lint` | `lint_card.mjs --all --json`(离线) | 体验软规 | error(如按钮缺 click、itemId 错配)应清零;warn/info 按存量分批治理 |
35
- | `files.cover` | 文件系统 | 完整性 | 缺 `<id>.png` 封面的卡无法走 FinMall 封面上传链路 |
35
+ | `files.cover` | 文件系统 | 完整性 | 缺 `<id>.png` 封面的卡无法走远端封面上传链路 |
36
36
  | `files.data` | 文件系统 | 完整性 | data.json 缺失或 JSON 解析失败 |
37
37
  | `render`(可选) | dev server `/__render-status/:id` | SSR 自检 | bindingErrors / runtimeErrors;离线标 `skipped` 不算失败 |
38
38
 
@@ -49,4 +49,4 @@ node tools/card-doctor.mjs --out .codex-tmp/doctor.json # 落盘 JSON 报告(.
49
49
 
50
50
  - **不检查视觉还原度**:与设计稿的像素级比对归 `dsl-card-generator` 的 fidelity 工具链(check_fidelity / render_diff)。
51
51
  - **不替代训练**:生成质量优化归 `card-skill-trainer`;doctor 管存量卡健康。
52
- - **不碰 FinMall 远端**:远端一致性归 `finmall-card-sync` / `card_sync`。
52
+ - **不碰远端**:远端一致性归 `platform-card-sync` / `card_sync`。
@@ -23,7 +23,7 @@ dsl-card 项目的 AI 技能层。它是**编排层**:把卡片编辑任务拆
23
23
  4. **读字段结构用 `card_node_models`**:不要去解析 raw DSL + component-meta。该工具返回每节点的可编辑字段树。
24
24
  5. **render_status.ok ≠ 画对了**(rules.md §七):视觉正确性以 `card_screenshot` 为准。**但视觉验证只对 UI 变更做**(rules.md §七.1 铁律):改 `style.*` / 节点增删移动 / 静态文案 / 图片资源 / v-show 条件才截图;改绑定表达式取值逻辑 / `inputParams` / 事件代码 / `data.json` 属非 UI 变更,用单元断言收口(`evalExpr` 比对返回值、`new Function(code).call(假 this)` 跑 customAction 断言 `state`),不截图。
25
25
  6. **改坏可回滚**:`card_write` 每次写卡前自动把当前版本存为历史快照(与编辑器改卡同语义);编辑失误时用 `card_rollback`(默认回滚到最新快照)撤销,用 `card_history` 挑选更早的回滚目标。
26
- 7. **dev server 前置**:`card_render_status` / `card_node_models` / `card_import_meta` 必须 dev server 已启动,未起时返回 `dev-server-required`;`card_sync` / `card_save` / `card_slice_upload` 除 dev server 外还需已登录 FinMall。(本仓库 `npm run dev` 默认 5180,由 Vite 把 `/__*` 代理到 8787;`cardbuddy init` 出来的工程 `npm run dev` 直接是 8787。)`card_list` / `card_read` / `card_write` / `card_validate` / `card_history` / `card_rollback` 会自动降级为直接读盘/写盘/跑 Python(render_status 标 skipped);`card_screenshot` 降级到包内独立离线渲染器(`source:'standalone'`)。
26
+ 7. **dev server 前置**:`card_render_status` / `card_node_models` / `card_import_meta` 必须 dev server 已启动,未起时返回 `dev-server-required`;`card_sync` / `card_save` / `card_slice_upload` 除 dev server 外还需已登录远端。(本仓库 `npm run dev` 默认 5180,由 Vite 把 `/__*` 代理到 8787;`cardbuddy init` 出来的工程 `npm run dev` 直接是 8787。)`card_list` / `card_read` / `card_write` / `card_validate` / `card_history` / `card_rollback` 会自动降级为直接读盘/写盘/跑 Python(render_status 标 skipped);`card_screenshot` 降级到包内独立离线渲染器(`source:'standalone'`)。
27
27
  8. **建卡先判输入形态**(Workflow D):收到设计稿输入先调 `card_route_input({path})`,按返回的 `kind` 分流,**不要凭感觉决定要不要跑上游**。判为 `meaxure-export`(Sketch MeaXure 标注导出)时必须先经 `sketch-spec-to-card` 前处理产出 `UI/<稿名>/` 要素包,**禁止直接读 `preview/` 整图建卡**;`preprocess.needed=false` 表示已前处理过,直接复用存量产物。`card_route_input` 纯离线,不需要 dev server。
28
28
 
29
29
  ## 前置:`card_*` 工具从哪来(agent 里看不到工具先看这里)
@@ -91,13 +91,13 @@ printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' \
91
91
  | `card_screenshot` | 无头截图(json `{base64}` / png 图片) | 优先 dev server;未起时降级到包内独立离线渲染器(返回 `source:'standalone'`),渲染器也失败才报 `dev-server-required` |
92
92
  | `card_history` | 编辑历史快照元数据(ts/时间/字节数/节点数),用于选择回滚目标 | 直接读 `.history/` |
93
93
  | `card_rollback` | 回滚到指定快照(默认最新);回滚前自动把当前版本另存为安全快照,回滚本身可撤销;写回走 card_write 同一收口 | 直接读写盘 + Python 硬校验 |
94
- | `card_sync` | 从 FinMall 远端拉最新卡写回 `src/cards/` | 不支持 |
95
- | `card_save` | 把卡片经已登录浏览器写回 FinMall(IDC10010) | 不支持 |
94
+ | `card_sync` | 从远端拉最新卡写回 `src/cards/` | 不支持 |
95
+ | `card_save` | 把卡片经已登录浏览器写回远端(IDC10010) | 不支持 |
96
96
  | `card_route_input` | **建卡输入形态判定**(Workflow D):`kind`(`meaxure-export` / `ui-bundle` / `card-dir` / `image` / `unknown`)+ `evidence` 判定证据 + `next` 下一步命令 + `preprocess`(是否需要先跑 sketch-spec-to-card、稿名、产物路径) | 纯离线(无需 dev server) |
97
97
  | `card_scaffold` | 新建卡骨架双模:`intent`(`blank`/`list`/`detail`/`form`/`dialog`/`tab`/`canvas-head` 的合法最小骨架,`data.json` 由 `inputParams` 反推自洽)或 `from`(整卡模板克隆,只重写 pageId/name,版式纪律与绑定范式原样继承);返回 `{ok, id, dir, intent 或 from, note?}` | 纯离线(无需 dev server) |
98
98
  | `card_add_component` | 向指定**容器**节点(有 slots,或 ListLayout/GridLayout)的默认插槽追加一个合法最小组件;返回 `{ok, inserted:{componentId, className, parentId, parentClassName}, setProps, dir, validate}` —— 插完即离线跑 Python 硬校验并带回。纪律:只增不改、不重排 componentId、不碰孤儿字段 | 纯离线(无需 dev server) |
99
99
  | `card_import_meta` | 单卡的「导入来源」侧车(Phase E 可追溯性):`{source, interface, baseUrl, appId, workspaceId, cardId, cardCode, cardName, syncedAt, tool, dirty, lastExport}`,据此判断这张卡是否从远端同步来、何时导入、上次导出读回校验是否通过;无侧车(本地新建 / 未同步)返回 `{error:"import-meta not found"}`。只读,不污染 DSL | 不支持(需 dev server) |
100
- | `card_slice_upload` | 把该卡引用的本地切图(`src/slices`)经已登录浏览器上传到 FinMall 图片库(CC0000.do),写「本地文件 ↔ 线上地址」映射 `src/slices/manifest.json`,并把 schema/data 里的 `"/slices/<name>"` 改写为 `@filestore/…`;按 sha256 判重,内容未变的切图复用已有地址不重复上传,本地切图真源保留不动 | 不支持(需 dev server **且**已登录 FinMall) |
100
+ | `card_slice_upload` | 把该卡引用的本地切图(`src/slices`)经已登录浏览器上传到远端图片库(CC0000.do),写「本地文件 ↔ 线上地址」映射 `src/slices/manifest.json`,并把 schema/data 里的 `"/slices/<name>"` 改写为 `@filestore/…`;按 sha256 判重,内容未变的切图复用已有地址不重复上传,本地切图真源保留不动 | 不支持(需 dev server **且**已登录远端) |
101
101
 
102
102
  > **分工提示**:`card_scaffold` / `card_add_component` 属**生成侧**(P5 脚手架),权威用法在
103
103
  > `dsl-card-generator`(其 SKILL.md §8 Workflow 0 是建卡入口路由的准绳)。本技能只在 Workflow D
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: dsl-card-generator
3
- description: 生成、编辑 FinMall/YNet 低代码卡片 schema.json + data.json(15 个可渲染组件、$ 表达式绑定、v-show、globalSet、alertCard、Canvas 自绘)。适用:从图片/设计稿/MeaXure 导出生成卡片、编辑已有卡片、逆向解释 schema。触发词:图片生成卡片、读图建卡、编辑卡片、改卡片。核心流程:route_input 判输入形态 →(MeaXure 导出先经 sketch-spec-to-card 前处理)→ 整树 IR 一次下发建卡 → validate+lint 双门禁 → iterate 视觉闭环(改卡用 add_component/ir_patch)。常规任务只读 references/quickref.md,避免加载全部 references。
3
+ description: 生成、编辑远端/YNet 低代码卡片 schema.json + data.json(15 个可渲染组件、$ 表达式绑定、v-show、globalSet、alertCard、Canvas 自绘)。适用:从图片/设计稿/MeaXure 导出生成卡片、编辑已有卡片、逆向解释 schema。触发词:图片生成卡片、读图建卡、编辑卡片、改卡片。核心流程:route_input 判输入形态 →(MeaXure 导出先经 sketch-spec-to-card 前处理)→ 整树 IR 一次下发建卡 → validate+lint 双门禁 → iterate 视觉闭环(改卡用 add_component/ir_patch)。常规任务只读 references/quickref.md,避免加载全部 references。
4
4
  ---
5
5
 
6
6
  # DSL 卡片 Schema 生成器(DSL Card Schema Generator)
7
7
 
8
- 为 FinMall/YNet 卡片设计器生成合规的 schema.json + data.json。本 skill 把平台当作一套**带版本号的 JSON DSL**:卡片 = 数据字典 + 组件树 + 属性元数据 + 样式分组 + 指令 + 插槽 + 事件动作。一切结构都有据可查,不臆造。
8
+ 为远端/YNet 卡片设计器生成合规的 schema.json + data.json。本 skill 把平台当作一套**带版本号的 JSON DSL**:卡片 = 数据字典 + 组件树 + 属性元数据 + 样式分组 + 指令 + 插槽 + 事件动作。一切结构都有据可查,不臆造。
9
9
 
10
10
  > **图片/AI 生成卡片 = 你(Agent)直接读图产出 schema,不接外部接口**:读截图/设计稿(多模态),按组件/样式/表达式/事件规则直接写出合规 schema.json + data.json,再用内置校验器自查。详见 §8 Workflow A 与 references/image_to_card.md。
11
11
 
@@ -21,7 +21,7 @@
21
21
 
22
22
  1. **参数类型默认 `number`。**
23
23
  金额字段在 `dictionary.inputParams` 中声明为 `type:"number"`,`defaultValue` 给数字字面量(如 `8900`、`12300.5`),`data.json` 同以数字写入(见 §5)。
24
- - **与上游的兼容**:部分 FinMall 接口(如 multiMonthTransitionList 的 IDC10012)以**字符串**投喂金额。表达式里的 `* 1` 会强制数值化,故无论上游给 number 还是 string 都能正确格式化——这种情况下该卡可保留 `type:"string"`,但表达式写法不变。
24
+ - **与上游的兼容**:部分远端接口(如 multiMonthTransitionList 的 IDC10012)以**字符串**投喂金额。表达式里的 `* 1` 会强制数值化,故无论上游给 number 还是 string 都能正确格式化——这种情况下该卡可保留 `type:"string"`,但表达式写法不变。
25
25
  2. **显示用表达式做千分号,不要预拼格式化字符串进 data。**
26
26
  即:Text 的 `value`(`itemValueFrom:"1"`)绑定一个 `$变量`,其 `itemValue` 是**格式化表达式**,而非已带逗号的字符串。这样一份数据既能展示(带千分号)又能参与计算(取原始数值)。
27
27
 
@@ -0,0 +1,54 @@
1
+ # 建卡协议(S0~S4.5):从输入到 IR 落盘
2
+
3
+ > 本文件是 system 提示词里「建卡流程骨架」的**全文**。判定为建卡流程时必须先读本文件再动手。
4
+ > 收敛阶段(S4.9~S6)在 converge_protocol.md,两段按阶段读,不要合并成一次读。
5
+
6
+ ## S0 路由
7
+ 输入形态已由服务端机械判定,写在「用户挂载的本地文件」段(kind / preprocess / next):照它执行,不要再调 card_route_input;该段缺失才自己调 card_route_input({ path })。
8
+
9
+ ## S1 要素包
10
+ kind=meaxure-export(Sketch MeaXure 标注导出)必须先有 sketch-spec-to-card 要素包,禁止直接读 preview/ 整图建卡。
11
+ - fs_read 判定里给出的 manifest(通常 UI/manifest.json)找目标画板条目:status=generated ⇒ 产物已在;status=pending ⇒ 先看 preprocess.alsoFound(同一导出的其它产物目录,平铺与按稿分片常并存),有就直接用;确实都没有才补跑:
12
+ ① skill_exec script=skills/sketch-spec-to-card/scripts/parse_meaxure.py,args=["--src","<导出目录>","--list"] 拿画板清单(**每行行尾自带该画板的 --pick,板号 1 起**);
13
+ ② args=["--src","<导出目录>","--out","UI","--draft","<稿名>","--pick","<照抄清单行尾>","--build"] 只产该画板(**照抄行尾的 --pick,禁止把序号换算成 0 起的 --indices**——实测换算错一位,白跑 34s 解析 + 一步 LLM 才发现;多张逗号分隔,连板写 7~9、同板连卡写 8.1~8.2);
14
+ ③ script=skills/sketch-spec-to-card/scripts/cluster_states.py,args=["--out","UI/<稿名>"]。
15
+ - 要素包落点记作 <bundle>:传了 --draft 在 UI/<稿名>/<画板名>/,否则 UI/<画板名>/;以 parse_meaxure.py 打印的落盘路径为准。仓库可能两种布局并存,manifest 说的画板在 UI/ 下找不到就用 fs_list UI 找同名目录,别急着判定产物缺失。
16
+
17
+ ## S1.4 批量建卡
18
+ (用户一次点了多张卡 / 回了「全部」):**一次** parse_meaxure --pick 出全部要素包(别每卡重跑一遍解析),然后**同一个回合内**按「S2→S5 走完一张,再走下一张」连续推进——每张卡各写各的 IR、各跑各的 expand + iterate。禁止每张卡各起一个回合:新回合的首步要把整段对话重发一遍(实测 1.3-3 万 token 一步),还要把同一个要素包重读一遍,纯浪费。
19
+
20
+ ## S1.5 选卡门禁
21
+ (多画板 / 多卡画板必需;SKILL.md §13 + rules.md §13.4):目标目录定下后先跑 skill_exec script=skills/dsl-card-generator/scripts/spec_tree.mjs,args=["--list-cards","<目标目录>"] 拿「画板序号.卡片序号」全量清单(一个画板含 N 张卡展开为 K.1~K.N,行下自带 "图: <card-N.png 绝对路径>");S0 挂载段已带同一份命令就直接复用,不要重复跑。
22
+ - 只有 1 张卡、或用户已明说目标(序号如 2.3 / 画板名 / 卡片名)⇒ 直接进 S2,不要为走流程停下来问。
23
+ - 多张卡且用户未指定 ⇒ **本轮到此为止**:把清单写进正文 + 明确提问,正文最后**单独一行输出 [需用户确认]**,然后停止输出、等用户回序号再继续。没有这行标记会被判定「任务未完成就停手」而自动续跑(实测多花一整轮重发清单)。聊天面板**没有**弹选项工具(不存在 request_user_input),不要去调用或寻找弹窗工具,也不要替用户挑。
24
+ 清单**逐行配图**:每个卡片行下紧跟一行 Markdown 图片,写成 !\[画板名\](该卡 card-N.png 的完整绝对路径),与序号一一对应——**必须是 !\[\](...) 包起来的图片语法**;parse_meaxure 输出里的 "图: <路径>" 行是给你看的提示,照抄成裸路径渲染端不显示(实测照抄后用户 0 张图可看);缺图显式标缺失,禁止把图堆到清单末尾。末尾追加一项「全部」,用户回「全部」才算显式批量授权(产了几张卡要在正文报)。
25
+ **清单必须以普通 Markdown 列表逐行输出,禁止用代码块围栏(``` 或 ~~~)或 4 空格缩进包裹清单的任何部分**——渲染端把代码块当纯文本展示,里面的图片语法一行都不会转成预览图,选卡协议整段失效(实测 8 张配图全灭)。parse_meaxure --list 的输出**照抄行内容即可,围栏与缩进不要照抄**。
26
+ **路径必须写完整绝对路径**(以 /Users/… 或 C:\… 打头,一字不改照抄),**禁止用省略号(… 或 ...)代替前缀**(实测「…/积分助手-4-xx.png」这种写法渲染端取不到图,用户那几行就只剩一串路径=配图全丢),**禁止把路径包进反引号或写进代码块**(反引号 / 代码块里的是给人抄的文本,不会渲染成图片)。清单长不是省前缀的理由:图显示不出来才是真的白写。
27
+ 只回画板序号默认取该画板第 1 张卡。
28
+ - **禁止**自行挑画板 / 挑卡、**禁止**「先建一张看看」、**禁止**未经同意批量产卡。
29
+
30
+ ## S2 审计
31
+ (一步代替反复探索):skill_exec script=skills/dsl-card-generator/scripts/spec_audit.mjs,args=["--spec","<bundle>/spec.json","--card","N","--slices-dir","<bundle>/slices","--project-slices","src/slices","--design","<bundle>/card-N.png","--save",".cache/ai-scratch/<id>.audit.json"]。
32
+ 切片引用名先查 <bundle>/slices/index.json(--build 已生成:每张切图的真实相对路径与大小),照它写引用、按 R31 命名规则消毒(禁用字符 # ? % & + 不进引用名),expand_ir 会递归命中嵌套子目录里的真身;**禁止 fs_list 逐层摸索**(实测 6 次才找到嵌套真名,纯浪费)。
33
+ 它交叉审计 spec + 聚焦截图 + 切图目录,给出:bands(水平行与行内元素序)、gaps(padLeft / padRight / padTop / padBottom / rowGaps)、texts(文案 / 字号 / 字重 / 颜色 / 行高 / 对齐)、shapes(装饰条)、slices(存在性 / 复用 / 是否烘焙背景与标签文字)、colors(背景采样点)、missing(缺哪张切图 + 建议裁剪矩形)。
34
+ 它交叉审计 spec + 聚焦截图 + 切图目录,另给 bgGradientHint(顶部渐变带的起止色/跨度 + IR 正确写法)与 slices[].crops(reused-differs 时「矩形 → 该用哪张 _alt 图」的映射,exists=true 直接引用、false 才需裁)。
35
+ IR 的几何与样式一律照抄审计值;**禁止 fs_read spec.json**(几百 KB 只会拿到截断片段,重复读纯浪费步数),禁止反复 fs_list 摸索结构。要看层级树才跑 spec_tree.mjs。
36
+ **审计 JSON 只认落盘文件**:--save 已把整份审计写到 .cache/ai-scratch/<id>.audit.json,下游脚本(crop_slices --from-audit)一律引用该文件。**禁止用 fs_write 手抄 / 节选审计内容**——实测手抄的 156 字节「摘要」让 crop_slices 裁出错名错位的切图,还自造了违反命名约定的新切片。
37
+
38
+ ## S3 补切图
39
+ S2 的 missing 非空 ⇒ skill_exec script=skills/dsl-card-generator/scripts/crop_slices.mjs,args=["--design","<bundle>/card-N.png","--card-width","<卡宽>","--out","src/slices","--from-audit",".cache/ai-scratch/<id>.audit.json"],从聚焦截图派生缺失切图(审计已落盘,直接引用)。切图唯一落点 src/slices/ 扁平命名,IR 里 Image.value 用 /slices/<名>。
40
+ 只缺一两张时用 --crop <名>@x,y,w,h(可多条),x/y/w/h 直接抄审计 missing[].crop 的**卡片本地 1x 坐标**,脚本自己按 design 宽 ÷ 卡宽换算像素。**禁止用 sips / convert / magick 等系统图像命令裁图**:它们不在 shell 白名单,且要手算缩放比与裁剪偏移,实测必错。
41
+ 切图用法看 slices[].baked:label=true ⇒ 切图已含标签文字,tile 用单个 Image(96×84 之类照 rect 抄);label=false ⇒ 需要 Image + Text 组合;verdict=reused-differs ⇒ 同一张切图被标到内容不同的多个位置,除第一个外必须用审计 slices[].crops 给出的 _alt 名(exists=true 直接引用,exists=false 由 crop_slices 派生)——**禁止自造新名字**(实测自造 icon_xx_左/右 让同一设计内容在 src/slices 下出现三套名字)。
42
+
43
+ ## S4 写 IR
44
+ (全文只写这一次;**单次输出上限截断过 ⇒ 必须分片写**,见 2c 与 ir-format.md §2c:骨架 fs_write → 每区块一片 pNN.json → skill_exec ir_merge.mjs --ir <ir> --part …(可多条) 合并一次 → 再 expand_ir):fs_write .cache/ai-scratch/<id>.ir.json。结构照 bands 一条一段;gaps.rowGaps 映射成 flex.gap / padding(paddingType:"1" + 四边值);texts 逐条转 Text(fontSize / fontWeight / color / lineHeight 全抄,**lineHeight 必写**,否则渲染器按 1.5 倍字号撑高、整卡累计漂移几个像素);标题 + 底部装饰条**整段照抄 ir-format.md §2d 的片段**(AbsoluteLayout(align:"BottomLeft") > [Row(条, s.position.top/left), Text(标题, s.position.top/left)],子节点顺序 [装饰条, 标题];几何读标注差值 position.top = 标注条.y − 标注标题.y,别套「行高 + gap」的公式;条子不得排在流式里、不得用 margin 顶位(正负都不行,lint r64 硬拦))。
45
+ **类型开关铁律(渲染器静默丢弃类缺陷的唯一防线)**:margin / padding 组写单边值(marginTop 等)必须同组写 marginType:"1" / paddingType:"1"(expand_ir 会自动补;手写 schema 漏了会被 lint 报 r44-*-no-type);背景三选一:纯色 backgroundType:"01"+backgroundColor、图片 "02"+backgroundImage、渐变 "03"+backgroundGradient。审计给了 bgGradientHint 就照抄它的 ir 字段;**禁止自造 gradientColor / gradientDirection,禁止把 linear-gradient(...) 塞进 backgroundColor**——这三者都会静默失效(实测模型连试 3 轮全无效后放弃并编造理由)。 **水平内缩禁止写成「子节点 width:100% + marginLeft/Right」**:width:100% 已等于父容器内容宽,水平 margin 只会把节点推出父容器右缘、被 overflow:hidden 裁掉(父容器按列排布时水平方向是交叉轴,flexShrink 救不回来)——间距要放**父容器** paddingLeft/Right,或删掉 width 靠 stretch 补宽(lint r63-width100-with-hmargin ERROR 拦截,pitfalls §25)。
46
+ 落点卡默认 = 当前正在编辑的卡片 ID(见「当前卡片上下文」):IR 的 id 与 iterate 的 --out 一律 src/cards/<当前卡片ID>/,即覆盖当前卡 / 空卡是默认行为。**「建卡」「生成卡片」「用这张画板做一张卡」都不是「新建一张」**——那是在说「用设计稿出一张卡」,落点仍是当前卡;只有用户明确说「新建一张 / 另存为新卡 / 换个新 id / 不要覆盖当前卡」才自拟 ID,且自拟前先用一句正文写明「将新建卡片 <新id>,不覆盖当前卡 <当前卡id>」,让用户当场看得见(实测:把「建卡」读成「新建」,界面里凭空多出一张卡而对话里没有任何交代)。当前卡已有内容时,动手前一句正文说明将覆盖、原内容留 .history 快照可回滚。
47
+
48
+ ## S4.5 变量抽取
49
+ (真实卡片不是截图:这步漏了,产物就是 dictionary / data.json 全空的死图):先 skill_read ir-format.md 的「2b. 变量绑定」看契约,再决定哪些文本绑变量——
50
+ 下限 = S2 审计的 bindHints[](amount / percent / date / masked / count / level),再按语义补正则认不出的动态值(用户名 / 客户层级 / 卡片名称 / 状态词);静态标签(区块标题 / 按钮 / 说明文字)才写字面量。
51
+ - 动态值:Text 主值用 v:"$key"(条件文案写表达式如 "$showAll ? '收起' : '查看更多'",金额展示照 recipe_snippets/money-format.json,别把「¥7,000.00」拆成字面量)。
52
+ - IR 顶层**显式写 dictionary.inputParams**:{key,name,type,defaultValue,desc,rule:{type:"",options:{}},children:[]},defaultValue 一律取设计稿字面量——expand_ir 用它生成 data.json,渲染即设计稿文案,S5 像素闭环才不受绑定影响。
53
+ - 重复行 / tile 组(审计 listCandidates[]、或用户明说要列表):数组变量 + listScope 插槽。item 的字段**已逐个展开到作用域** ⇒ 规范形三种:$<字段名>(字段名语义化自取)、$$(整个 item)、$_index(行索引)。写成 $item.<字段> 也不影响结果——expand_ir 在扩展开头会把它自动归一化成规范形(作用域里本来就没有 item 这个键,schema 只存规范形;schema 级另有 autofix_schema.py 兜底)。范式照 recipe_snippets/list-expand-collapse.json 或 src/cards/account 的 account_details。结构确实不规则时允许保持静态,但要在正文说明理由。
54
+ - 命名语义化(memberLevel / totalAmount / assetRows),禁 text1 / val2;写完在正文列一句绑定清单(key → 设计稿文案)。