cardbuddy 0.23.0 → 0.25.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 (23) hide show
  1. package/README.md +2 -2
  2. package/dist/init.js +9 -2
  3. package/dist/preview-editor/assets/{index-C7N5n_qW.js → index-DL4ZCJCA.js} +77 -76
  4. package/dist/preview-editor/assets/{index-7RzNd7tf.css → index-MWKPP9nd.css} +1 -1
  5. package/dist/preview-editor/index.html +2 -2
  6. package/dist/skills-upgrade.js +59 -0
  7. package/dist/vendored/server/core/browser.js +7 -4
  8. package/dist/vendored/server/core/cardCompile.js +202 -0
  9. package/dist/vendored/server/core/flameCompile/client.js +466 -0
  10. package/dist/vendored/server/core/flameCompile/clientElements.js +372 -0
  11. package/dist/vendored/server/core/flameCompile/common.js +253 -0
  12. package/dist/vendored/server/core/flameCompile/h5.js +326 -0
  13. package/dist/vendored/server/core/flameCompile/index.js +126 -0
  14. package/dist/vendored/server/core/flameCompile/model.js +162 -0
  15. package/dist/vendored/server/core/index.js +6 -0
  16. package/dist/vendored/skills/dsl-card-generator/references/generation_pitfalls.md +1 -1
  17. package/dist/vendored/skills/mbank-design-gen/tools/screenshot.mjs +15 -7
  18. package/dist/vendored/skills/platform-card-sync/scripts/api_batch_sync.mjs +12 -4
  19. package/dist/vendored/skills/platform-session-navigator/scripts/package-lock.json +1 -16
  20. package/dist/vendored/skills/platform-session-navigator/scripts/package.json +2 -2
  21. package/dist/vendored/skills/platform-session-navigator/scripts/platform_login.mjs +12 -1
  22. package/dist/vendored/skills/platform-session-navigator/scripts/session-manager.mjs +1 -1
  23. package/package.json +2 -4
@@ -8,8 +8,8 @@
8
8
  <link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon-16-Cg6Nrosk.png" />
9
9
  <link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon-32-B_1744sp.png" />
10
10
  <link rel="apple-touch-icon" sizes="180x180" href="/assets/apple-touch-icon-BQcES9QK.png" />
11
- <script type="module" crossorigin src="/assets/index-C7N5n_qW.js"></script>
12
- <link rel="stylesheet" crossorigin href="/assets/index-7RzNd7tf.css">
11
+ <script type="module" crossorigin src="/assets/index-DL4ZCJCA.js"></script>
12
+ <link rel="stylesheet" crossorigin href="/assets/index-MWKPP9nd.css">
13
13
  </head>
14
14
  <body>
15
15
  <div id="app"></div>
@@ -18,12 +18,16 @@
18
18
  * 交互环境外(非 TTY)无法确认时,必须带 --force 才能升级,否则明确拒绝并提示(不再
19
19
  * 静默跳过、假装升级成功)。
20
20
  * 标志:--force/-y 跳过确认直接覆盖;--prune 额外删除 ORPHAN;--dry-run 只报告不落盘。
21
+ * 依赖检查:升级后探测 skills/ 内 package.json 声明的依赖能否从工程解析,缺失的
22
+ * 显式给出 `npm install <pkg>` 补装指引(升级不自动装依赖——是否动工程 node_modules
23
+ * 由用户决定;解析探测与脚本运行时的 Node 解析链一致)。
21
24
  * 同步后写 skills/.synced-from = 当前装包版本,供下次 drift 检测与启动提示。
22
25
  */
23
26
  import fs from 'node:fs'
24
27
  import path from 'node:path'
25
28
  import readline from 'node:readline/promises'
26
29
  import { fileURLToPath } from 'node:url'
30
+ import { createRequire } from 'node:module'
27
31
  import chalk from 'chalk'
28
32
  import { loadConfig } from './config.js'
29
33
 
@@ -75,6 +79,38 @@ function walkFiles(dir, base = dir, out = []) {
75
79
  return out
76
80
  }
77
81
 
82
+ /**
83
+ * 收集 skills/ 内各 package.json 声明的依赖中「从其所在目录无法解析」的包名。
84
+ * 技能脚本运行时按 Node 规则从脚本位置向上找 node_modules(先 skill 自身,再工程根),
85
+ * 这里用 createRequire 从该 package.json 所在目录模拟同一解析链;解析不到的即缺口——
86
+ * 升级只拷文件不装依赖,缺口必须显式提示用户补装(0.83.x 事故的教训:
87
+ * playwright 曾是 optional 依赖静默缺失,脚本运行时才报 Cannot find package)。
88
+ * @param {string} destSkills 工程内 skills/ 目录
89
+ * @returns {string[]} 形如 "playwright-core(platform-session-navigator/scripts/package.json)"
90
+ */
91
+ function collectMissingSkillDeps(destSkills) {
92
+ const missing = []
93
+ for (const rel of walkFiles(destSkills)) {
94
+ if (path.posix.basename(rel) !== 'package.json') continue
95
+ const pkgPath = path.join(destSkills, rel)
96
+ let deps
97
+ try {
98
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
99
+ deps = { ...pkg.dependencies, ...pkg.devDependencies }
100
+ } catch {
101
+ continue
102
+ }
103
+ for (const name of Object.keys(deps)) {
104
+ try {
105
+ createRequire(path.join(pkgPath, 'index.js')).resolve(name)
106
+ } catch {
107
+ missing.push(`${name}(${rel})`)
108
+ }
109
+ }
110
+ }
111
+ return missing
112
+ }
113
+
78
114
  /**
79
115
  * 比对源/目标技能树(内容比对)。
80
116
  * @returns {{ added:string[], same:string[], modified:string[], orphan:string[] }}
@@ -153,6 +189,25 @@ export async function runSkillsUpgrade({
153
189
  const warn = (m) => say(chalk.yellow(`• ${m}`))
154
190
  const dim = (m) => say(chalk.dim(`• ${m}`))
155
191
 
192
+ // 依赖在位检查(升级只同步文件、不装依赖,但必须把缺口显式暴露出来):
193
+ // 逐个解析 skills/ 内 package.json 声明的依赖,解析不到的给出补装命令。
194
+ const reportDeps = () => {
195
+ let missing
196
+ try {
197
+ missing = collectMissingSkillDeps(destSkills)
198
+ } catch {
199
+ return
200
+ }
201
+ if (!missing.length) {
202
+ dim('技能依赖在位:skills/ 内 package.json 声明的依赖均可从工程解析')
203
+ return
204
+ }
205
+ const names = [...new Set(missing.map((m) => m.slice(0, m.indexOf('('))))]
206
+ warn(`⚠ 检测到 ${missing.length} 个技能依赖无法解析(新版本技能可能带来新依赖):`)
207
+ for (const m of missing) say(chalk.yellow(` ${m}`))
208
+ say(chalk.yellow(` 请在工程根执行:npm install ${names.join(' ')}`))
209
+ }
210
+
156
211
  if (dryRun) say(chalk.yellow('(dry-run:只报告将要做的事,不落盘)'))
157
212
 
158
213
  if (!src || !fs.existsSync(src)) {
@@ -169,6 +224,7 @@ export async function runSkillsUpgrade({
169
224
 
170
225
  if (!diff.added.length && !diff.modified.length && !diff.orphan.length && fs.existsSync(destSkills)) {
171
226
  dim(`skills 已是最新(cardbuddy@${version},无新增 / 修改 / 孤儿)`)
227
+ reportDeps()
172
228
  return 0
173
229
  }
174
230
 
@@ -240,6 +296,9 @@ export async function runSkillsUpgrade({
240
296
  }
241
297
  }
242
298
 
299
+ // 依赖在位检查(dry-run 也报,作为升级后的预期状态)
300
+ reportDeps()
301
+
243
302
  // 汇总
244
303
  log('')
245
304
  say(chalk.green(dryRun ? 'dry-run 完成(未落盘)' : 'skills 升级完成'))
@@ -35,8 +35,11 @@ let chromiumMod = null
35
35
 
36
36
  /** 取(惰性启动的)单例浏览器;启动失败会把 browserPromise 复位并抛出 */
37
37
  async function getBrowser() {
38
- // 动态 import playwright:避免其进入 vite.config 加载图(见文件头注释)
39
- if (!chromiumMod) chromiumMod = await import('playwright')
38
+ // 动态 import playwright-core:避免其进入 vite.config 加载图(见文件头注释)。
39
+ // 用 playwright-core 而非 playwright:浏览器走系统 Chrome(channel: 'chrome'),
40
+ // 只需 JS API;playwright 的 postinstall 浏览器下载作为 optional 依赖失败时会被
41
+ // npm 静默跳过,导致 vendored 发布态运行时报 Cannot find package(0.83.x 事故)。
42
+ if (!chromiumMod) chromiumMod = await import('playwright-core')
40
43
  if (!browserPromise) {
41
44
  browserPromise = chromiumMod.chromium
42
45
  .launch({ channel: 'chrome', headless: true })
@@ -98,9 +101,9 @@ function serialize(task) {
98
101
 
99
102
  /**
100
103
  * 打开卡片渲染页(chrome=off 纯净模式)并等待首帧稳定。
101
- * @param {import('playwright').BrowserContext} context
104
+ * @param {import('playwright-core').BrowserContext} context
102
105
  * @param {string} id 卡片 id
103
- * @returns {Promise<{ page: import('playwright').Page, screen: import('playwright').ElementHandle | null }>}
106
+ * @returns {Promise<{ page: import('playwright-core').Page, screen: import('playwright-core').ElementHandle | null }>}
104
107
  */
105
108
  async function openCardScreen(context, id) {
106
109
  const page = await context.newPage()
@@ -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
+ }