cardbuddy 0.22.0 → 0.24.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 (76) 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-CPpLZZyi.js → index-BumNBo-n.js} +81 -80
  7. package/dist/preview-editor/assets/{index-CU8zVA2P.css → index-MWKPP9nd.css} +1 -1
  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/cardCompile.js +202 -0
  18. package/dist/vendored/server/core/cardDigest.js +324 -0
  19. package/dist/vendored/server/core/cardExport.js +5 -5
  20. package/dist/vendored/server/core/cardFiles.js +1 -1
  21. package/dist/vendored/server/core/cardHistoryRemote.js +1 -1
  22. package/dist/vendored/server/core/cardImportMeta.js +1 -1
  23. package/dist/vendored/server/core/cardListCache.js +92 -10
  24. package/dist/vendored/server/core/cardPublish.js +11 -6
  25. package/dist/vendored/server/core/cardPublishVersions.js +8 -8
  26. package/dist/vendored/server/core/cardSave.js +14 -10
  27. package/dist/vendored/server/core/cardSidecar.js +3 -3
  28. package/dist/vendored/server/core/cardSync.js +35 -23
  29. package/dist/vendored/server/core/cardVersion.js +1 -1
  30. package/dist/vendored/server/core/flameCompile/client.js +466 -0
  31. package/dist/vendored/server/core/flameCompile/clientElements.js +372 -0
  32. package/dist/vendored/server/core/flameCompile/common.js +253 -0
  33. package/dist/vendored/server/core/flameCompile/h5.js +326 -0
  34. package/dist/vendored/server/core/flameCompile/index.js +126 -0
  35. package/dist/vendored/server/core/flameCompile/model.js +162 -0
  36. package/dist/vendored/server/core/index.js +12 -6
  37. package/dist/vendored/server/core/logger.js +1 -1
  38. package/dist/vendored/server/core/remoteCard.js +5 -5
  39. package/dist/vendored/server/core/skillPaths.js +1 -1
  40. package/dist/vendored/server/core/sliceUpload.js +4 -4
  41. package/dist/vendored/skills/README.md +13 -13
  42. package/dist/vendored/skills/card-doctor/SKILL.md +2 -2
  43. package/dist/vendored/skills/dsl-card-ai/SKILL.md +4 -4
  44. package/dist/vendored/skills/dsl-card-generator/SKILL.md +2 -2
  45. package/dist/vendored/skills/dsl-card-generator/references/amount_formatting.md +1 -1
  46. package/dist/vendored/skills/dsl-card-generator/references/build_protocol.md +54 -0
  47. package/dist/vendored/skills/dsl-card-generator/references/component_rules.md +2 -2
  48. package/dist/vendored/skills/dsl-card-generator/references/converge_protocol.md +36 -0
  49. package/dist/vendored/skills/dsl-card-generator/references/event_patterns.md +1 -1
  50. package/dist/vendored/skills/dsl-card-generator/references/generation_pitfalls.md +1 -1
  51. package/dist/vendored/skills/dsl-card-generator/references/image_to_card.md +1 -1
  52. package/dist/vendored/skills/dsl-card-generator/references/quickref.md +1 -1
  53. package/dist/vendored/skills/dsl-card-generator/references/recipes.md +4 -4
  54. package/dist/vendored/skills/dsl-card-generator/references/rules.md +1 -1
  55. package/dist/vendored/skills/dsl-card-generator/references/slices_convention.md +6 -6
  56. package/dist/vendored/skills/dsl-card-generator/scripts/inventory_schemas.py +1 -1
  57. package/dist/vendored/skills/dsl-card-generator/scripts/lib/scaffold.mjs +1 -1
  58. package/dist/vendored/skills/dsl-card-generator/scripts/lib/slice_dedupe.mjs +2 -2
  59. package/dist/vendored/skills/dsl-card-generator/vendor/preview/assets/standalone-CeVX_y-M.js +137 -0
  60. package/dist/vendored/skills/dsl-card-generator/vendor/preview/assets/standalone-DiEiY59H.css +1 -0
  61. package/dist/vendored/skills/dsl-card-generator/vendor/preview/standalone.html +2 -2
  62. package/dist/vendored/skills/{finmall-card-sync → platform-card-sync}/SKILL.md +26 -26
  63. package/dist/vendored/skills/{finmall-card-sync → platform-card-sync}/scripts/api_batch_sync.mjs +184 -162
  64. package/dist/vendored/skills/{finmall-card-sync → platform-card-sync}/scripts/slice_upload.mjs +48 -29
  65. package/dist/vendored/skills/{finmall-session-navigator → platform-session-navigator}/SKILL.md +13 -13
  66. package/dist/vendored/skills/{finmall-session-navigator → platform-session-navigator}/scripts/package-lock.json +2 -2
  67. package/dist/vendored/skills/{finmall-session-navigator → platform-session-navigator}/scripts/package.json +2 -2
  68. package/dist/vendored/skills/{finmall-session-navigator/scripts/finmall_login.mjs → platform-session-navigator/scripts/platform_login.mjs} +41 -31
  69. package/dist/vendored/skills/{finmall-session-navigator → platform-session-navigator}/scripts/session-manager.mjs +17 -17
  70. package/dist/vendored/skills/shared/README.md +21 -21
  71. package/dist/vendored/skills/shared/auth-model.md +8 -10
  72. package/dist/vendored/skills/shared/{finmall-config.mjs → platform-config.mjs} +10 -10
  73. package/dist/vendored/tools/{finmall-env.mjs → platform-env.mjs} +13 -13
  74. package/package.json +1 -1
  75. package/dist/vendored/skills/dsl-card-generator/vendor/preview/assets/standalone-BCLshIzC.css +0 -1
  76. package/dist/vendored/skills/dsl-card-generator/vendor/preview/assets/standalone-pP5kTKiI.js +0 -88
@@ -4,7 +4,7 @@ import path from 'node:path'
4
4
 
5
5
  /**
6
6
  * 远端资源代理:把预览里引用的站内资源路径(@filestore / /minio / /fastdfs …)
7
- * 回源到 FinMall 上游站点并落盘缓存。所有跨网络回源逻辑集中在本模块维护。
7
+ * 回源到远端上游站点并落盘缓存。所有跨网络回源逻辑集中在本模块维护。
8
8
  */
9
9
 
10
10
  /**
@@ -1,4 +1,4 @@
1
- import { devApiBase } from '../../tools/finmall-env.mjs'
1
+ import { devApiBase } from '../../tools/platform-env.mjs'
2
2
 
3
3
  /**
4
4
  * 共享浏览器单例 + 卡片页面加载(screenshot.js / bbox.js 共用)。
@@ -0,0 +1,202 @@
1
+ /**
2
+ * 卡片编译端点:把本地 schema.json 编译成两套可投放产物
3
+ *
4
+ * POST /__compile-cards body { cardIds: string[], targetDir?: string }
5
+ * → { ok, outDir, cards: [{ id, name, ok, version, templates, nodes, clientBytes, h5Bytes, warnings, error }] }
6
+ *
7
+ * 产物布局(客户端 / H5 分两个目录,同属一个父目录):
8
+ * <targetDir>/ ← 默认 <工程根>/flame-build
9
+ * client/<cardCode>/<templateCode>/index.{json,css,databinding,data,js,style}
10
+ * h5/<cardCode>.js
11
+ * manifest.json ← 目录内产物清单(累计)+ 本次编译批次(lastBatch)
12
+ *
13
+ * 编译逻辑本身在 flameCompile/ 下(1:1 迁移自 flame-release-mange 的 card/exporter),
14
+ * 本文件只负责「读盘 → 编译 → 落盘 → 汇报」,不含任何渲染规则。
15
+ *
16
+ * 覆盖语义:产物是构建缓存,允许直接覆盖——
17
+ * - 逐文件 writeFileSync 截断重写,同卡重复编译结果字节一致;
18
+ * - 写之前先删该卡的整个模板目录,模板数量变少(如删掉列表)时不会残留 _sub_/_item;
19
+ * - 编译失败不删旧产物(先编译成功再落盘),上一次的好产物不会被半途失败搞坏;
20
+ * - manifest 同理不丢信息:按 id 并入已有记录(本次为准),只保留磁盘上仍有产物的卡。
21
+ */
22
+
23
+ import fs from 'node:fs'
24
+ import path from 'node:path'
25
+ import { compileCard } from './flameCompile/index.js'
26
+ import { log } from './logger.js'
27
+
28
+ /** 单次编译上限(与导出的 500 同口径) */
29
+ const MAX_COMPILE_CARDS = 500
30
+
31
+ /** 卡片 id 白名单:只允许字母数字下划线中划线,杜绝目录穿越 */
32
+ const CARD_ID_RE = /^[A-Za-z0-9_-]+$/
33
+
34
+ /**
35
+ * 读已有 manifest 的 cards 段。缺失 / 损坏 / 结构不对一律当空数组 ——
36
+ * 一份旧清单坏掉不该让整批编译失败(它是构建产物,不是输入)。
37
+ */
38
+ function readPrevManifestCards(manifestPath) {
39
+ try {
40
+ const mf = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
41
+ return Array.isArray(mf.cards) ? mf.cards : []
42
+ } catch {
43
+ return []
44
+ }
45
+ }
46
+
47
+ export function createCompileMiddleware({ root }) {
48
+ const readJsonBody = (req, res, cb) => {
49
+ let body = ''
50
+ req.setEncoding('utf8')
51
+ req.on('data', (c) => {
52
+ body += c
53
+ if (body.length > 1024 * 1024) req.destroy()
54
+ })
55
+ req.on('end', () => {
56
+ try {
57
+ cb(JSON.parse(body || '{}'))
58
+ } catch (e) {
59
+ res.statusCode = 400
60
+ res.setHeader('Content-Type', 'application/json')
61
+ res.end(JSON.stringify({ ok: false, error: 'invalid json', message: String(e.message) }))
62
+ }
63
+ })
64
+ }
65
+
66
+ const sendJson = (res, status, obj) => {
67
+ res.statusCode = status
68
+ res.setHeader('Content-Type', 'application/json')
69
+ res.setHeader('Cache-Control', 'no-store')
70
+ res.end(JSON.stringify(obj))
71
+ }
72
+
73
+ return function compileMiddleware(req, res, next) {
74
+ const pathname = decodeURIComponent((req.url || '').split('?')[0])
75
+ const method = (req.method || 'GET').toUpperCase()
76
+ if (pathname !== '/__compile-cards' || method !== 'POST') return next()
77
+
78
+ readJsonBody(req, res, (body) => {
79
+ const rawIds = Array.isArray(body.cardIds)
80
+ ? body.cardIds.filter((x) => typeof x === 'string')
81
+ : []
82
+ // 去重:同一张卡在同一批里传两次没有意义(第二次会把第一次刚写的产物删掉再写一遍),只留首个
83
+ const ids = [...new Set(rawIds)]
84
+ if (!ids.length) return sendJson(res, 400, { ok: false, error: 'cardIds 不能为空' })
85
+ if (ids.length > MAX_COMPILE_CARDS) {
86
+ return sendJson(res, 400, { ok: false, error: `单次最多编译 ${MAX_COMPILE_CARDS} 张卡片` })
87
+ }
88
+
89
+ const bad = ids.filter((id) => !CARD_ID_RE.test(id))
90
+ if (bad.length) return sendJson(res, 400, { ok: false, error: `非法卡片 id: ${bad.join(', ')}` })
91
+
92
+ // 目标目录:默认工程根下 flame-build;自定义时限制在工程根内,避免误写到系统目录
93
+ const outDir = body.targetDir
94
+ ? path.resolve(root, String(body.targetDir))
95
+ : path.join(root, 'flame-build')
96
+ const rootResolved = path.resolve(root)
97
+ if (!outDir.startsWith(rootResolved + path.sep)) {
98
+ return sendJson(res, 400, { ok: false, error: 'targetDir 必须位于工程目录内' })
99
+ }
100
+
101
+ const clientDir = path.join(outDir, 'client')
102
+ const h5Dir = path.join(outDir, 'h5')
103
+ fs.mkdirSync(clientDir, { recursive: true })
104
+ fs.mkdirSync(h5Dir, { recursive: true })
105
+
106
+ const results = []
107
+ const manifestCards = []
108
+
109
+ for (const id of ids) {
110
+ const schemaPath = path.join(root, 'src', 'cards', id, 'schema.json')
111
+ if (!fs.existsSync(schemaPath)) {
112
+ results.push({ id, ok: false, error: 'schema.json 不存在' })
113
+ continue
114
+ }
115
+
116
+ try {
117
+ const schema = JSON.parse(fs.readFileSync(schemaPath, 'utf8'))
118
+ const compiled = compileCard(schema, { cardCode: id })
119
+
120
+ // 清掉该卡的旧产物(模板数量可能变化)
121
+ const cardClientDir = path.join(clientDir, id)
122
+ if (fs.existsSync(cardClientDir)) fs.rmSync(cardClientDir, { recursive: true, force: true })
123
+
124
+ for (const template of compiled.client) {
125
+ const dir = path.join(cardClientDir, template.code)
126
+ fs.mkdirSync(dir, { recursive: true })
127
+ for (const [file, content] of Object.entries(template.files)) {
128
+ // index.style 是 SM4 密文(Buffer),必须按字节原样落盘;
129
+ // 其余是 utf8 文本,显式声明编码便于排查乱码。
130
+ if (Buffer.isBuffer(content)) fs.writeFileSync(path.join(dir, file), content)
131
+ else fs.writeFileSync(path.join(dir, file), content, 'utf8')
132
+ }
133
+ }
134
+
135
+ fs.writeFileSync(path.join(h5Dir, `${id}.js`), compiled.h5, 'utf8')
136
+
137
+ results.push({
138
+ id,
139
+ ok: true,
140
+ version: compiled.version,
141
+ templates: compiled.client.length,
142
+ nodes: compiled.stats.nodes,
143
+ clientBytes: compiled.stats.clientBytes,
144
+ h5Bytes: compiled.stats.h5Bytes,
145
+ warnings: compiled.warnings,
146
+ })
147
+ manifestCards.push({
148
+ id,
149
+ name: schema.name || '',
150
+ version: compiled.version,
151
+ templates: compiled.client.map((t) => t.code),
152
+ nodes: compiled.stats.nodes,
153
+ clientBytes: compiled.stats.clientBytes,
154
+ h5Bytes: compiled.stats.h5Bytes,
155
+ warnings: compiled.warnings,
156
+ })
157
+ } catch (e) {
158
+ log.warn(`[compile] ${id} 编译失败: ${e.message}`)
159
+ results.push({ id, ok: false, error: String(e.message || e) })
160
+ }
161
+ }
162
+
163
+ const okCount = results.filter((r) => r.ok).length
164
+ // manifest 描述的是「这个目录里有什么」,因此跨批次累计:按 id 合并,本次记录覆盖同 id 旧记录。
165
+ // 只按本次批次重写会得到一份自相矛盾的清单——磁盘上 10 张卡,manifest 里只有最后勾的 2 张。
166
+ // 旧记录还要过一道「磁盘上仍有产物」的存在性检查:换过 targetDir、手工删过文件时它会失真。
167
+ const manifestPath = path.join(outDir, 'manifest.json')
168
+ const merged = new Map()
169
+ for (const c of readPrevManifestCards(manifestPath)) {
170
+ if (!c || typeof c.id !== 'string' || !c.id) continue
171
+ const hasArtifacts =
172
+ fs.existsSync(path.join(clientDir, c.id)) || fs.existsSync(path.join(h5Dir, `${c.id}.js`))
173
+ if (hasArtifacts) merged.set(c.id, c)
174
+ }
175
+ for (const c of manifestCards) merged.set(c.id, c)
176
+ const manifest = {
177
+ generatedAt: new Date().toISOString(),
178
+ outDir,
179
+ // 目录内卡片总数(累计),不是本批张数
180
+ count: merged.size,
181
+ cards: [...merged.values()].sort((a, b) => String(a.id).localeCompare(String(b.id))),
182
+ // 本次请求的批次结果,便于回溯「这一次又编了什么」
183
+ lastBatch: {
184
+ requested: ids.length,
185
+ ok: okCount,
186
+ failed: ids.length - okCount,
187
+ ids,
188
+ },
189
+ }
190
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf8')
191
+
192
+ log.info(`[compile] 编译完成 ${okCount}/${ids.length} → ${outDir}`)
193
+ return sendJson(res, 200, {
194
+ ok: okCount > 0,
195
+ outDir,
196
+ clientDir,
197
+ h5Dir,
198
+ cards: results,
199
+ })
200
+ })
201
+ }
202
+ }
@@ -0,0 +1,324 @@
1
+ /**
2
+ * 大卡 schema 的**读方向折叠**(structure digest)。
3
+ *
4
+ * 为什么需要:`schema.json` 是编辑器持久化格式,单卡紧凑 JSON 实测最大 239KB / 167 节点。
5
+ * 读大卡时旧行为是「JSON 字符串取前 12000 字符」,而 JSON 的键序是
6
+ * `version / pageId / name / desc / platform / dictionary / view` —— `dictionary` 排在 view 前面,
7
+ * 30 条 inputParams 就吃掉 10.5KB。实测 overseaTransfer(239KB / 167 节点)的 12k 切片里
8
+ * **componentId 出现 0 次**:模型花 12k 字符只拿到参数声明 + 根节点的半张样式表,
9
+ * 一个组件都没看到,于是转头用 card_grep 反复试路径,白走 2~5 步(每步 20~40k 输入)。
10
+ *
11
+ * 同一个 12k 预算下,全节点骨架只要 4.7k:167 行 `componentId(ClassName) 「主文本」`。
12
+ * 所以折叠的目标不是「少给」,而是**把预算从「前 N 个字节」换成「N 个节点的地图」**:
13
+ * - 骨架:全部节点的身份 + 主文本 + 层级(模型据此能点到任何一处)
14
+ * - 参数:inputParams 压缩成一行 key:type=default
15
+ * - 不给样式与属性全量 —— 要就 `card_read {id, node:"<componentId>"}` 定向取子树
16
+ * - 同时给出该子树的真实 JSON 路径,`card_patch` 可直接用,省掉 card_grep 摸索
17
+ */
18
+
19
+ /** 骨架里主文本的截断长度:够认人即可,不搬正文 */
20
+ const TEXT_MAX = 24
21
+ /** 层级缩进最多叠到第几层(更深的保持同缩进,防止 30 层缩进把行宽吃光) */
22
+ const DEPTH_INDENT_MAX = 8
23
+ /** 骨架占预算的比例,其余留给参数表 / 元信息 / 下一步指引 */
24
+ const TREE_BUDGET_RATIO = 0.66
25
+ /** 参数表预算上限 */
26
+ const PARAM_BUDGET = 2400
27
+
28
+ /**
29
+ * 取节点主值(等价于渲染出来的那段字)。
30
+ * `itemValueFrom`: 0=常量 / 1=变量 / 2=图片(与 ir-format.md 的 p / v / pi 对应)。
31
+ * @returns {string} 空串表示该节点没有主值
32
+ */
33
+ function mainValue(node) {
34
+ const props = Array.isArray(node.properties) ? node.properties : []
35
+ for (const p of props) {
36
+ if (!p || p.itemKey !== 'value') continue
37
+ const from = String(p.itemValueFrom ?? '0')
38
+ if (from === '2') return '[图片]'
39
+ const raw = p.itemValue
40
+ if (from === '1') {
41
+ const name = typeof raw === 'string' ? raw : raw && typeof raw === 'object' ? (raw.value ?? raw.key) : ''
42
+ const s = name ? String(name) : ''
43
+ if (!s) return '[变量]'
44
+ // itemValue 本身就形如 `$remark`,不要再补一个 $(曾出现 `$$remark`)
45
+ return s.startsWith('$') ? s : `$${s}`
46
+ }
47
+ if (raw == null || typeof raw === 'object') return ''
48
+ return String(raw)
49
+ }
50
+ return ''
51
+ }
52
+
53
+ /**
54
+ * 事件摘要:把 `eventactions[]` 压成 `eventKey:actionType(N字)`,多个逗号分隔。
55
+ *
56
+ * 为什么必须进骨架:全库 366 个节点有事件、代码合计 324KB,单卡最多 27.5KB。
57
+ * 事件是卡片里真正的业务逻辑,却藏在 8 层路径下
58
+ * (`eventactions[0].action.properties[0].itemValue.code`)。骨架不给这个标记,
59
+ * 模型拿到 167 行结构图也不知道哪个节点有事件,只能 `jq` 手数路径去扫 ——
60
+ * 历史日志里同型命令跨 3 张卡出现 12 次以上,每次一整步(20~40k 输入)。
61
+ *
62
+ * 只标「有事件 + 什么事件 + 代码多长」,不搬代码本身:读代码走
63
+ * `card_read {id, node:"<componentId>"}`,那一棵子树才几千字符。
64
+ * @returns {string} 空串表示该节点无事件
65
+ */
66
+ function eventSummary(node) {
67
+ const ea = node && node.eventactions
68
+ if (!Array.isArray(ea) || !ea.length) return ''
69
+ const parts = []
70
+ for (const a of ea) {
71
+ if (!a || typeof a !== 'object') continue
72
+ const key = (a.event && a.event.eventKey) || '?'
73
+ const type = (a.action && a.action.actionType) || '?'
74
+ const props = Array.isArray(a.action && a.action.properties) ? a.action.properties : []
75
+ const fn = props.find((p) => p && p.itemKey === 'function')
76
+ const iv = fn && fn.itemValue
77
+ // 运行时取 `code || compiled`(见 preview 的 actionCode),长度按同口径算
78
+ const code = iv && typeof iv === 'object' ? String(iv.code || iv.compiled || '') : ''
79
+ parts.push(`${key}:${type}${code ? `(${code.length}字)` : ''}`)
80
+ }
81
+ return parts.join(',')
82
+ }
83
+
84
+ /**
85
+ * 全卡事件代码总字符数(运行时口径 `code || compiled`)。
86
+ * 只用于 meta 报规模,帮模型判断「要不要一次读完」。
87
+ */
88
+ function countEventChars(view) {
89
+ let total = 0
90
+ const walk = (n) => {
91
+ if (!n || typeof n !== 'object') return
92
+ const ea = n.eventactions
93
+ if (Array.isArray(ea)) {
94
+ for (const a of ea) {
95
+ const props = Array.isArray(a && a.action && a.action.properties) ? a.action.properties : []
96
+ const fn = props.find((p) => p && p.itemKey === 'function')
97
+ const iv = fn && fn.itemValue
98
+ if (iv && typeof iv === 'object') total += String(iv.code || iv.compiled || '').length
99
+ }
100
+ }
101
+ for (const c of n.components || []) walk(c)
102
+ for (const sl of n.slots || []) for (const c of (sl && sl.components) || []) walk(c)
103
+ }
104
+ walk(view)
105
+ return total
106
+ }
107
+
108
+ /**
109
+ * 子节点列表,带**可直接用于 card_patch 的真实 JSON 点路径**。
110
+ * 节点树是 `view.components[]` 与 `view.slots[].components[]` 两种容器,不是 `view.children`。
111
+ * @returns {Array<{node: object, path: string}>}
112
+ */
113
+ function childEntries(node, nodePath) {
114
+ const out = []
115
+ if (Array.isArray(node.components)) {
116
+ node.components.forEach((c, i) => {
117
+ if (c && typeof c === 'object') out.push({ node: c, path: `${nodePath}.components.${i}` })
118
+ })
119
+ }
120
+ if (Array.isArray(node.slots)) {
121
+ node.slots.forEach((sl, j) => {
122
+ const inner = Array.isArray(sl && sl.components) ? sl.components : []
123
+ inner.forEach((c, k) => {
124
+ if (c && typeof c === 'object') out.push({ node: c, path: `${nodePath}.slots.${j}.components.${k}` })
125
+ })
126
+ })
127
+ }
128
+ return out
129
+ }
130
+
131
+ /** 深度优先遍历,产出行列表(带真实路径) */
132
+ function flatten(view) {
133
+ const rows = []
134
+ const walk = (node, nodePath, depth) => {
135
+ const v = mainValue(node)
136
+ rows.push({
137
+ depth,
138
+ cid: typeof node.componentId === 'string' ? node.componentId : '',
139
+ cls: typeof node.className === 'string' ? node.className : '',
140
+ text: v ? v.slice(0, TEXT_MAX) : '',
141
+ events: eventSummary(node),
142
+ path: nodePath,
143
+ })
144
+ for (const e of childEntries(node, nodePath)) walk(e.node, e.path, depth + 1)
145
+ }
146
+ walk(view, 'view', 0)
147
+ return rows
148
+ }
149
+
150
+ /** 骨架单行渲染(缩进=层级;componentId(className) 「主文本」 ⚡事件) */
151
+ function renderRow(r) {
152
+ const indent = ' '.repeat(Math.min(r.depth, DEPTH_INDENT_MAX))
153
+ // 根节点通常没有 componentId,别渲染成 `(无id)(Root)` 这种噪音
154
+ const head = r.cid ? `${r.cid}(${r.cls})` : r.cls || '(无类名)'
155
+ const ev = r.events ? ` ⚡${r.events}` : ''
156
+ return `${indent}${head}${r.text ? ` 「${r.text}」` : ''}${ev}`
157
+ }
158
+
159
+ /**
160
+ * inputParams 压成一行 `key:type=default`。
161
+ * 富字段(rule / children / desc)不进 —— 需要结构化细节时读 node 或 fs_read data.json。
162
+ * 只给**标量**默认值:对象/数组默认值是 mock 数据(实测 `myCardList` 一条就 400 字符),
163
+ * 它要的是「存在」而不是「内容」,全搬进来等于把 dictionary 换个地方重新塞满预算。
164
+ */
165
+ function renderParams(inputParams, budget = PARAM_BUDGET) {
166
+ const list = Array.isArray(inputParams) ? inputParams : []
167
+ const parts = []
168
+ let used = 0
169
+ let listed = 0
170
+ for (const p of list) {
171
+ if (!p || typeof p !== 'object') continue
172
+ const key = String(p.key ?? '')
173
+ if (!key) continue
174
+ const type = String(p.type ?? 'any')
175
+ const def = p.defaultValue
176
+ const isScalar = typeof def === 'string' || typeof def === 'number' || typeof def === 'boolean'
177
+ let suffix = ''
178
+ if (isScalar && def !== '') {
179
+ const shown = typeof def === 'string' && def.length > 24 ? `${def.slice(0, 24)}…` : def
180
+ suffix = `=${JSON.stringify(shown)}`
181
+ }
182
+ const seg = `${key}:${type}${suffix}`
183
+ if (used + seg.length + 2 > budget) break
184
+ parts.push(seg)
185
+ used += seg.length + 2
186
+ listed++
187
+ }
188
+ const more = list.length - listed
189
+ return {
190
+ text: parts.join(', ') + (more > 0 ? `, …(另有 ${more} 个未列出,用 fs_read data.json 看全量)` : ''),
191
+ listed,
192
+ total: list.length,
193
+ }
194
+ }
195
+
196
+ /**
197
+ * 生成大卡结构摘要。
198
+ * @param {object} schema 完整 schema 对象
199
+ * @param {object} data 派生 data(用于参数表兜底;通常与 inputParams.defaultValue 一致)
200
+ * @param {{id?: string, budget?: number}} opts budget ≈ 该工具结果的字符档位
201
+ * @returns {{meta: object, tree: string, treeTruncated: boolean, params: string, next: string[]}|null}
202
+ */
203
+ export function digestCard(schema, data, { id = '', budget = 12000 } = {}) {
204
+ const view = schema && typeof schema.view === 'object' ? schema.view : null
205
+ if (!view) return null
206
+ const rows = flatten(view)
207
+ const compactBytes = JSON.stringify(schema).length
208
+ const treeBudget = Math.max(1200, Math.floor(budget * TREE_BUDGET_RATIO))
209
+
210
+ const lines = []
211
+ let used = 0
212
+ let listed = 0
213
+ for (const r of rows) {
214
+ const line = renderRow(r)
215
+ // 留 1 字节给换行;超预算即停(宁可少列,也不让骨架本身被 clipToolResult 二次腰斩)
216
+ if (used + line.length + 1 > treeBudget) break
217
+ lines.push(line)
218
+ used += line.length + 1
219
+ listed++
220
+ }
221
+ const truncated = listed < rows.length
222
+
223
+ // 截断时补一段「被截掉的区块分布」:不列全文,但让模型知道还有哪些区域、各多少节点,
224
+ // 从而能直接 node 定向读,而不是重新猜。
225
+ let restNote = ''
226
+ if (truncated) {
227
+ const rest = rows.slice(listed)
228
+ const byCls = new Map()
229
+ for (const r of rest) {
230
+ const k = `${r.cls}`
231
+ byCls.set(k, (byCls.get(k) || 0) + 1)
232
+ }
233
+ const dist = [...byCls].sort((a, b) => b[1] - a[1]).slice(0, 12).map(([k, n]) => `${k}×${n}`).join(' ')
234
+ // 事件节点尤其要点名:截断区里的事件是「看不见但一定要改」的那部分
235
+ const restEv = rest.filter((r) => r.events)
236
+ const evNote = restEv.length
237
+ ? `\n其中 ${restEv.length} 个节点带事件:${restEv.slice(0, 10).map((r) => `${r.cid || '(根)'}[${r.events}]`).join(' ')}${restEv.length > 10 ? ' …' : ''}`
238
+ : ''
239
+ restNote =
240
+ `\n… 还有 ${rest.length} 个节点未列出(已达骨架预算)。它们的组件分布:${dist}${evNote}` +
241
+ `\n用 card_read {id:"${id}", node:"<componentId>"} 定向读某棵子树;` +
242
+ `未列出的组件 id 可用 card_grep 按文案/类名查,或 fs_read schema.json 按 offset 续读。`
243
+ }
244
+
245
+ const params = renderParams(schema?.dictionary?.inputParams)
246
+ const evRows = rows.filter((r) => r.events)
247
+ const evChars = countEventChars(view)
248
+ const next = [
249
+ `**带 ⚡ 标记的节点有事件**(click / onReady / onMeasured…),圆括号里是代码字符数;读它 → card_read {id:"${id}", node:"<componentId>"},返回该子树含完整 JS(比整卡小一到两个数量级)`,
250
+ `改值/改样式/改事件 → card_patch {id:"${id}", patches:[{path:"@<componentId>/eventactions.0.action.properties.0.itemValue.code", value:"…"}]};@ 前缀自动解析成真实 JSON 路径,不用先 card_grep;写 code 时 compiled 会自动同步,不必手写第二份`,
251
+ `看某节点的样式/属性 → card_read {id:"${id}", node:"<componentId>"}`,
252
+ `要看原文 → fs_read {"path":"src/cards/${id}/schema.json","offset":<字节偏移>}`,
253
+ ]
254
+
255
+ return {
256
+ meta: {
257
+ id,
258
+ pageId: schema?.pageId ?? '',
259
+ platform: schema?.platform ?? '',
260
+ root: view.className || '',
261
+ nodes: rows.length,
262
+ nodesListed: listed,
263
+ compactBytes,
264
+ /** 有事件的节点数 / 事件代码总字符(不含在白给的 tree 里,要按 node 定向读) */
265
+ eventNodes: evRows.length,
266
+ eventChars: evChars,
267
+ },
268
+ tree: lines.join('\n') + restNote,
269
+ treeTruncated: truncated,
270
+ params: params.text,
271
+ next,
272
+ hasData: !!(data && typeof data === 'object' && Object.keys(data).length),
273
+ }
274
+ }
275
+
276
+ /**
277
+ * 按 componentId 找节点,返回它的真实 JSON 路径与祖先链。
278
+ * @returns {{path: string, node: object, depth: number, ancestors: Array<{cid: string, cls: string, path: string}>}|null}
279
+ */
280
+ export function findNodeByComponentId(schema, componentId) {
281
+ const view = schema && typeof schema.view === 'object' ? schema.view : null
282
+ const want = String(componentId || '')
283
+ if (!view || !want) return null
284
+ let hit = null
285
+ const walk = (node, nodePath, depth, ancestors) => {
286
+ if (hit) return
287
+ if (node.componentId === want) {
288
+ hit = { path: nodePath, node, depth, ancestors: ancestors.slice() }
289
+ return
290
+ }
291
+ const chain = depth === 0 ? [] : ancestors
292
+ for (const e of childEntries(node, nodePath)) {
293
+ walk(e.node, e.path, depth + 1, [...chain, { cid: node.componentId || '', cls: node.className || '', path: nodePath }])
294
+ if (hit) return
295
+ }
296
+ }
297
+ walk(view, 'view', 0, [])
298
+ return hit
299
+ }
300
+
301
+ /**
302
+ * 把 `@<componentId>` 开头的路径解析成真实 JSON 点路径。
303
+ * 支持三种写法:
304
+ * `@text_dw` → 节点自身路径
305
+ * `@text_dw/s.properties` → 用 `/` 分隔相对段(推荐:componentId 本身可能含 `.`?实际不会,
306
+ * 但相对段里全是数字下标与字段名,用 `/` 更不易歧义)
307
+ * `@text_dw.properties.0...` → 用 `.` 续写相对段
308
+ * 找不到组件时抛错(响亮失败,不静默新建分支)。
309
+ * @returns {string} 真实点路径
310
+ */
311
+ export function resolveComponentPath(schema, atPath) {
312
+ const s = String(atPath || '')
313
+ if (!s.startsWith('@')) return s
314
+ const body = s.slice(1)
315
+ // componentId 的合法字符集是字母数字 _ -,遇到首个 `/` 或 `.` 就断开
316
+ const m = body.match(/^([A-Za-z0-9_-]+)(.*)$/)
317
+ if (!m) throw new Error(`@ 路径缺少 componentId:${s}`)
318
+ const cid = m[1]
319
+ const rest = m[2].replace(/^\//, '').replace(/^\./, '')
320
+ const found = findNodeByComponentId(schema, cid)
321
+ if (!found) throw new Error(`找不到 componentId=${cid};先用 card_read 看结构图,或 card_grep 按文案定位`)
322
+ if (!rest) return found.path
323
+ return `${found.path}.${rest}`
324
+ }
@@ -10,7 +10,7 @@ import { resolveCardDir } from './cardOpenFolder.js'
10
10
  /**
11
11
  * 卡片导出端点(编辑器「卡片网格预览 → 勾选 → 导出」的服务端实现)。
12
12
  *
13
- * 产物契约(对桌面两份 FinMall 导出样本的逆向结论,详见
13
+ * 产物契约(对桌面两份远端导出样本的逆向结论,详见
14
14
  * docs/卡片导出产物逆向分析与导出功能.md):
15
15
  * <targetDir>/<yyMMddHHmmss+4位序号>/ ← 临时文件夹(完成后删除)
16
16
  * cards.json ← 卡片清单(cardPO 数组,殿后写入)
@@ -49,7 +49,7 @@ const EXPORT_JOB_TIMEOUT_MS = Number(process.env.CARDBUDDY_EXPORT_JOB_TIMEOUT_MS
49
49
  /** 图片下载超时(与资源代理回源超时同口径) */
50
50
  const IMAGE_FETCH_TIMEOUT_MS = 12000
51
51
 
52
- /** 封面候选扩展名(与 finmall-card-sync 的 findExistingCover 同集) */
52
+ /** 封面候选扩展名(与 platform-card-sync 的 findExistingCover 同集) */
53
53
  const IMAGE_EXTS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg'])
54
54
 
55
55
  /**
@@ -60,7 +60,7 @@ const IMAGE_EXTS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.
60
60
  */
61
61
  const COVER_FILESTORE_PREFIX = '@filestore/dzqd-download'
62
62
 
63
- /** 平台状态常量:两份样本恒为三端全平台(与 FinMall 发布接口默认平台集一致) */
63
+ /** 平台状态常量:两份样本恒为三端全平台(与远端发布接口默认平台集一致) */
64
64
  const PLATFORM_STATUS = ['iOS', 'Android', 'HarmonyOS']
65
65
 
66
66
  /** 最小合法骨架屏 SVG:样本为线上编辑器按布局生成,本地不做布局推测,占位保证字段非空 */
@@ -255,7 +255,7 @@ function stampFolderName(targetDir, now = new Date()) {
255
255
  return base + Math.random().toString(36).slice(2, 6).padStart(4, '0')
256
256
  }
257
257
 
258
- /** 找本地封面预览图(<cardId>.<ext>);没有返回 null(与 finmall-card-sync 同规则) */
258
+ /** 找本地封面预览图(<cardId>.<ext>);没有返回 null(与 platform-card-sync 同规则) */
259
259
  function findExistingCover(cardDir, id) {
260
260
  let entries = []
261
261
  try {
@@ -639,7 +639,7 @@ async function exportOneCard({ root, id, outDir, upstream, buildPreview }) {
639
639
  fs.copyFileSync(src, dest)
640
640
  } else {
641
641
  const url = filestoreUrlFor(ref, upstream)
642
- if (!url) throw new Error('未配置 FinMall 上游地址,无法下载 @filestore 图片')
642
+ if (!url) throw new Error('未配置远端上游地址,无法下载 @filestore 图片')
643
643
  const res = await fetch(url, {
644
644
  headers: { 'User-Agent': 'Mozilla/5.0', Referer: upstream ? `${upstream}/` : '' },
645
645
  signal: AbortSignal.timeout(IMAGE_FETCH_TIMEOUT_MS),
@@ -14,7 +14,7 @@ import { buildDataFromSchema } from './buildDataFromSchema.js'
14
14
  * 空白卡必须显式归零压掉注入,否则新卡自带隐性间距且属性面板显示「未设置」。
15
15
  * 形状取自存量卡根节点(见 style-meta.json:marginType / paddingType,'0' = 全边统一值)。
16
16
  * 同一形状还有两份内联副本(禁跨包 import):skills/dsl-card-generator lib/scaffold.mjs、
17
- * skills/finmall-card-sync api_batch_sync.mjs 的 buildBlankSchema —— 改这里必须同步改那两处。
17
+ * skills/platform-card-sync api_batch_sync.mjs 的 buildBlankSchema —— 改这里必须同步改那两处。
18
18
  */
19
19
  function blankResetStyle() {
20
20
  return {
@@ -5,7 +5,7 @@ import { writeCardFiles } from './cardFiles.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
  /** 读取 JSON 请求体(1MB 上限防失控) */
11
11
  function readJsonBody(req) {
@@ -45,7 +45,7 @@ export function buildImportMeta({ baseUrl, appId, workspaceId, cardId, cardCode,
45
45
  // 推文件名去读卡目录根的封面,picUrl 为空会退化成目录路径、直接抛异常。真值只在同步时可获得。
46
46
  remote: remote || null,
47
47
  syncedAt: new Date().toISOString(),
48
- tool: 'finmall-card-sync',
48
+ tool: 'platform-card-sync',
49
49
  dirty: false, // 本地编辑后偏离导入镜像为 true(重置为导入镜像后回 false)
50
50
  lastExport: null, // 远端保存(/__save)后写入校验结果
51
51
  editedAt: null, // 最近一次本地编辑时间(每次写盘都更新),供「按修改时间排序」