cardbuddy 0.24.0 → 0.25.1

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.
@@ -8,7 +8,7 @@
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-BumNBo-n.js"></script>
11
+ <script type="module" crossorigin src="/assets/index-B6NhNlBx.js"></script>
12
12
  <link rel="stylesheet" crossorigin href="/assets/index-MWKPP9nd.css">
13
13
  </head>
14
14
  <body>
@@ -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()
@@ -238,17 +238,19 @@ function convertDirectives(component, template, warnings) {
238
238
  const extend = bucket.extend && typeof bucket.extend === 'object' ? bucket.extend : {}
239
239
  bucket.extend = extend
240
240
 
241
+ // 可见时写空串而非 'block':display 由样式类决定(.ce_row/.ce_column 为 flex),
242
+ // 内联 block 会盖掉 flex,导致 justify/align 失效(同 h5.js applyDirectives 注释)。
241
243
  const expression = conditionMap.expression
242
244
  if (hasText(expression)) {
243
- if (showType === '01') extend.display = `(${expression}) ? 'block' : 'none'`
244
- else if (showType === '02') extend.display = `(${expression}) ? 'none' : 'block'`
245
+ if (showType === '01') extend.display = `(${expression}) ? '' : 'none'`
246
+ else if (showType === '02') extend.display = `(${expression}) ? 'none' : ''`
245
247
  return
246
248
  }
247
249
 
248
250
  if (Array.isArray(conditionMap.group) && conditionMap.group.length > 0) {
249
251
  const expr = convertCondition(conditionMap.group)
250
- if (showType === '01') extend.display = `${expr} ? 'block' : 'none'`
251
- else if (showType === '02') extend.display = `${expr} ? 'none' : 'block'`
252
+ if (showType === '01') extend.display = `${expr} ? '' : 'none'`
253
+ else if (showType === '02') extend.display = `${expr} ? 'none' : ''`
252
254
  return
253
255
  }
254
256
 
@@ -116,6 +116,51 @@ export function collectProps(properties = []) {
116
116
 
117
117
  const IDENT_RE = /^[A-Za-z0-9_$]+$/
118
118
 
119
+ /* ------------------------ fastjson / HashMap 键序 ------------------------ */
120
+
121
+ /** Java String.hashCode */
122
+ function javaStringHash(text) {
123
+ let h = 0
124
+ for (let i = 0; i < text.length; i++) h = (Math.imul(31, h) + text.charCodeAt(i)) | 0
125
+ return h
126
+ }
127
+
128
+ /** HashMap 的扰动函数:h ^ (h >>> 16) */
129
+ function hashSpread(h) {
130
+ return (h ^ (h >>> 16)) >>> 0
131
+ }
132
+
133
+ /** 按元素个数推算 HashMap 表容量(默认 16,负载因子 0.75,翻倍扩容) */
134
+ function hashCapacity(size) {
135
+ let cap = 16
136
+ while (size > cap * 0.75) cap *= 2
137
+ return cap
138
+ }
139
+
140
+ /**
141
+ * 复现 Java HashMap 的迭代顺序
142
+ *
143
+ * 平台产物里的 JSON 由 fastjson 的 `JSONObject`(底层 HashMap)序列化,键序是**哈希桶顺序**
144
+ * 而不是插入顺序。产物只用于「替换线上文件」时,键序语义上无所谓;但只要希望与平台产物
145
+ * 逐字节一致(便于 diff 对拍),就必须复现这个顺序。
146
+ *
147
+ * 规则:cap = 16 起按 0.75 负载因子扩容;桶下标 = (h ^ h>>>16) & (cap-1);
148
+ * 按下标升序输出,同桶内保持插入顺序。
149
+ */
150
+ export function javaHashMapOrder(value) {
151
+ if (Array.isArray(value)) return value.map(javaHashMapOrder)
152
+ if (!value || typeof value !== 'object') return value
153
+ const entries = Object.entries(value)
154
+ const cap = hashCapacity(entries.length)
155
+ return entries
156
+ .map(([k, v], i) => ({ k, v: javaHashMapOrder(v), i, bucket: hashSpread(javaStringHash(k)) & (cap - 1) }))
157
+ .sort((a, b) => a.bucket - b.bucket || a.i - b.i)
158
+ .reduce((acc, x) => {
159
+ acc[x.k] = x.v
160
+ return acc
161
+ }, {})
162
+ }
163
+
119
164
  /**
120
165
  * JS 对象字面量的键名写法(平台口径)
121
166
  *
@@ -143,7 +188,7 @@ export function convertPropsH5(properties = []) {
143
188
  parts.push(`${key}:${convertPropsH5(prop.children)}`)
144
189
  } else if (prop.list && prop.list.length > 0) {
145
190
  const groups = prop.list.map((g) => convertPropsH5(g))
146
- parts.push(`${key}:[${groups.join(',')}]`)
191
+ parts.push(`${key}: [${groups.join(',')}]`)
147
192
  } else {
148
193
  parts.push(`${key}:${JSON.stringify(prop.itemValue)}`)
149
194
  }
@@ -161,7 +206,7 @@ export function convertPropsH5(properties = []) {
161
206
  /**
162
207
  * `$foo` → `s.$foo`
163
208
  *
164
- * 平台卡片状态挂在上下文实例的 `state` 上(生成代码里 `var s = c.state`),
209
+ * 平台卡片状态挂在上下文实例的 `s` 上(生成代码里 `var s = c.s`,ExpressionState),
165
210
  * 所以表达式里的 `$x` 必须改写为 `s.$x`。
166
211
  * 字符串字面量('…' / "…" / `…`)内部的 `$` 不得改写。
167
212
  *
@@ -14,7 +14,8 @@
14
14
  * 元素组件通过 `this.getElement('Text')` 取宿主 elements 注册表。
15
15
  *
16
16
  * 关键契约(照抄平台侧,改动会直接导致真机渲染空白):
17
- * - 数据挂在 `s = c.state`,表达式里的 `$foo` 已改写为 `s.$foo`;
17
+ * - 数据挂在 `s = c.s`(ExpressionState,提供 $key / $$),表达式里的 `$foo` 已改写为 `s.$foo`;
18
+ * 不能用 `c.state` —— 那是 MethodState,只有平铺键名,`s.$foo` 会一律求值为 undefined;
18
19
  * - `data` 里必须同时给 defaultState / state / n(n 是刷新因子,模板里靠它触发重算);
19
20
  * - watch.data 重新合并 state,宿主下发新数据时才能刷新;
20
21
  * - 每个事件的实现体被搬到 methods 里,on 只留转发(避免闭包捕获旧 state)。
@@ -27,6 +28,7 @@ import {
27
28
  convertPropsClient,
28
29
  convertPropsH5,
29
30
  hasText,
31
+ javaHashMapOrder,
30
32
  jsKey,
31
33
  pickVarProps,
32
34
  } from './common.js'
@@ -89,8 +91,12 @@ export function createH5Compiler(card, { warnings = [] } = {}) {
89
91
  for (const child of card.components) slot.components.push(convertComponent(child))
90
92
 
91
93
  function convertComponent(component) {
94
+ const desc = elementOf(component.className)
92
95
  const layer = {
93
- id: component.componentId,
96
+ // 列表 / 网格在平台侧会先被客户端编译改写成 `<cardCode>_sub_<componentId>`(共享同一份
97
+ // CardModel,H5 编译在其后,于是把这个改写后的 id 带进了 ref)。此处等价复现:
98
+ // 只影响容器自身的 ref,循环项内部的组件 id 保持原样。
99
+ id: desc.isNewTemplate ? `${card.code}_sub_${component.componentId}` : component.componentId,
94
100
  name: component.className,
95
101
  style: {},
96
102
  props: convertPropsH5(component.properties),
@@ -148,8 +154,12 @@ export function createH5Compiler(card, { warnings = [] } = {}) {
148
154
  return
149
155
  }
150
156
 
151
- if (showType === '01') layer.style.display = `(${expression}) ? 'block' : 'none'`
152
- else if (showType === '02') layer.style.display = `(${expression}) ? 'none' : 'block'`
157
+ // 可见时不能写死 `block`:布局元素的 display 由样式类决定(`.ce_row`/`.ce_column`
158
+ // 是 flex),内联 `display:block` 会盖掉 flex → justifyContent/alignItems 全失效。
159
+ // accountCreditedlist 的「更多转账伙伴」行就是被它压成 block,文字靠左 + 图标换行被裁掉。
160
+ // 空串等价于 v-show 的「显隐只切 none」——清掉内联值,恢复原生 display。
161
+ if (showType === '01') layer.style.display = `(${expression}) ? '' : 'none'`
162
+ else if (showType === '02') layer.style.display = `(${expression}) ? 'none' : ''`
153
163
  }
154
164
 
155
165
  /**
@@ -190,9 +200,16 @@ export function createH5Compiler(card, { warnings = [] } = {}) {
190
200
  const fn = constProps.function
191
201
  const code = fn && typeof fn === 'object' ? fn.code : null
192
202
  const compiled = fn && typeof fn === 'object' ? fn.compiled : null
193
- // 平台侧优先用 compiled(ES5 转译版),缺失才回退手写 code
194
- if (hasText(compiled)) return compiled
203
+ // 取 code 优先,compiled 只作兜底 —— 与编辑器预览(RenderTree.actionCode)和
204
+ // 运行时(preview 的 actionCode,`code || compiled`)保持同一优先级。
205
+ //
206
+ // 反过来(compiled 优先)会让「编辑器里改好、导出产物还是旧的」:两者是双份数据,
207
+ // compiled 是平台侧某次转译留下的快照,编辑器改 code 时它不会跟着变(SetCode 只覆写 code;
208
+ // ai-chat 的自动同步也只在「原本就同步」时生效)。实例:account 的 onMeasured 标题脚本,
209
+ // code 已是左对齐 `600 17px` / 浅绿 #D8F5E8 / x=0,compiled 仍停在居中的 `bold 16px` /
210
+ // #B9D0FA / x=canvas.width/2,产物跑旧版,标题位置与配色全错。
195
211
  if (hasText(code)) return code
212
+ if (hasText(compiled)) return compiled
196
213
  warnings.push(`事件 ${eventAction.eventKey} 的 customAction 缺少 code,已跳过`)
197
214
  return ''
198
215
  }
@@ -241,9 +258,9 @@ export function createH5Compiler(card, { warnings = [] } = {}) {
241
258
  const code = scopeSlots
242
259
  .map(
243
260
  (s) =>
244
- `${jsKey(s.slotKey)}:function(_sd_){return (function(c){var s = c.state; return [${s.components
261
+ `${jsKey(s.slotKey)}:function(_sd_){return (function(c){var s = c.s; return [${s.components
245
262
  .map(serializeComponent)
246
- .join(',')}]})(new CC(_vm,_sd_,c))}`
263
+ .join(',')}]})(new CC(_vm,_sd_,c)) }`
247
264
  )
248
265
  .join(',')
249
266
  options.push(`scopedSlots:{${code}}`)
@@ -271,7 +288,9 @@ export function createH5Compiler(card, { warnings = [] } = {}) {
271
288
  "var CC = this.getObject('context');" +
272
289
  'var e = this.getElement;' +
273
290
  'var c = new CC(this,this);' +
274
- 'var s = c.state;' +
291
+ // 必须是 c.s(ExpressionState,提供 $key / $$ 读取),不能用 c.state(MethodState,只有平铺键名)。
292
+ // 平台 Context 里两者并存:s 走表达式读、state 走方法写。写成 c.state 会让所有 s.$xxx 求值为 undefined。
293
+ 'var s = c.s;' +
275
294
  'var n = this.n;' +
276
295
  `return ${components.map(serializeComponent).join('')}` +
277
296
  '}'
@@ -286,7 +305,8 @@ export function createH5Compiler(card, { warnings = [] } = {}) {
286
305
  parts.push('props:{data:{type:Object,default:function(){ return {} }}}')
287
306
 
288
307
  const dataCode = [
289
- `defaultState:${JSON.stringify(defaultState)}`,
308
+ // 键序按 Java HashMap 桶序输出:与平台产物逐字节一致(语义等价,仅便于 diff 对拍)
309
+ `defaultState:${JSON.stringify(javaHashMapOrder(defaultState))}`,
290
310
  'state:{}',
291
311
  'n:0',
292
312
  ].join(',')
@@ -169,7 +169,7 @@ node skills/dsl-card-generator/scripts/render_card.mjs --id <id> --width 375 --o
169
169
  ```js
170
170
  // 仅当需要自定义页面交互/逐样式断言且宿主 dev server(5180) 已启动时使用;
171
171
  // 本 skill 的离线 render_card.mjs 不提供任意页面交互,复制 skill 到其他项目后该配方按需自建。
172
- import { chromium } from 'playwright';
172
+ import { chromium } from 'playwright-core';
173
173
  const ts = Date.now();
174
174
  const url = `http://localhost:5180/?card=<id>&t=${ts}`;
175
175
  const browser = await chromium.launch({ channel: 'chrome', args: ['--no-sandbox'] });
@@ -2,7 +2,8 @@
2
2
  // MBank mockup 截图工具:把 375×812 的 mockup HTML 导出为 750×1624 @2x PNG。
3
3
  // 用法:node <skill>/tools/screenshot.mjs <input.html> [output.png] [--full]
4
4
  // --full 整页截图(长页 mockup 用;默认 812pt 视口截断)
5
- // 依赖 playwright(仓库根 node_modules 已装;本脚本从自身位置向上解析)。
5
+ // 依赖 playwright-core(免浏览器下载的纯 JS API,从自身位置向上解析)。
6
+ // 浏览器优先走系统 Chrome(channel: 'chrome',仓库约定),未找到时才尝试内置浏览器。
6
7
  import { createRequire } from 'node:module';
7
8
  import { dirname, resolve } from 'node:path';
8
9
  import { fileURLToPath } from 'node:url';
@@ -10,7 +11,15 @@ import { pathToFileURL } from 'node:url';
10
11
 
11
12
  const here = dirname(fileURLToPath(import.meta.url));
12
13
  const require = createRequire(import.meta.url);
13
- const { chromium } = require('playwright');
14
+ let chromium;
15
+ try {
16
+ ({ chromium } = require('playwright-core'));
17
+ } catch {
18
+ console.error(
19
+ '[screenshot] 缺少依赖 playwright-core:请在本工程根执行 npm install(cardbuddy 依赖会带上),或手动 npm i playwright-core。',
20
+ );
21
+ process.exit(1);
22
+ }
14
23
 
15
24
  const args = process.argv.slice(2);
16
25
  const full = args.includes('--full');
@@ -22,15 +31,14 @@ if (!input) {
22
31
  }
23
32
  const output = positional[1] || input.replace(/\.html$/i, '') + '.png';
24
33
 
25
- // 优先用 Playwright 自带浏览器;未下载(ms-playwright 缓存为空)时回退系统 Chrome,
26
- // 与仓库「Playwright + 系统 Chrome 验证」约定一致,避免强依赖 npx playwright install。
34
+ // 优先用系统 Chrome(channel: 'chrome',仓库约定);不可用时回退内置浏览器。
27
35
  let browser;
28
36
  try {
29
- browser = await chromium.launch();
37
+ browser = await chromium.launch({ channel: 'chrome' });
30
38
  } catch (err) {
31
39
  if (!/Executable doesn't exist/.test(String(err?.message ?? err))) throw err;
32
- console.error('[screenshot] 未找到 Playwright 内置浏览器,回退系统 Chrome');
33
- browser = await chromium.launch({ channel: 'chrome' });
40
+ console.error('[screenshot] 未找到系统 Chrome,回退 Playwright 内置浏览器');
41
+ browser = await chromium.launch();
34
42
  }
35
43
  const page = await browser.newPage({
36
44
  viewport: { width: 375, height: 812 },
@@ -29,10 +29,18 @@ const scriptDir = path.dirname(fileURLToPath(import.meta.url));
29
29
  const skillDir = path.resolve(scriptDir, '..');
30
30
  // skill now lives under <repo-root>/skills/<name>: repo root is two levels up from skillDir
31
31
  const workspace = path.resolve(skillDir, '..', '..');
32
- // 与 platform_login.mjs 保持一致的解析方式:用裸标识符让 Node 从项目根 node_modules 解析
33
- // playwright。原硬编码路径指向 .workbuddy/skills/platform-session-navigator/scripts/node_modules
34
- // (该 node_modules 是已失效的断链符号链接),导致「Cannot find module」报错。
35
- const { chromium } = await import('playwright');
32
+ // 与 platform_login.mjs 保持一致的解析方式:用裸标识符让 Node 从项目根 node_modules 解析。
33
+ // 用 playwright-core(免浏览器下载的纯 JS API):浏览器走系统 Chrome + CDP,
34
+ // 不需要 playwright 自带浏览器;playwright 作为 optional 依赖安装失败时会被 npm
35
+ // 静默跳过,导致运行时报 Cannot find package(0.83.x 事故)。
36
+ let chromium;
37
+ try {
38
+ ({ chromium } = await import('playwright-core'));
39
+ } catch {
40
+ throw new Error(
41
+ '缺少依赖 playwright-core:请在本工程根执行 npm install(cardbuddy 依赖会带上 playwright-core),或手动 npm i playwright-core。',
42
+ );
43
+ }
36
44
 
37
45
  // 统一日志美化(容错:缺 server/core/logger 时回退极简实现,保证 demo / 老项目不崩)。
38
46
  // 实时项目与 build 后的 dist/server-core 均含该模块;demo 等缺少 server/ 目录的场景走 fallback。
@@ -8,22 +8,7 @@
8
8
  "name": "platform-session-navigator-scripts",
9
9
  "version": "1.0.0",
10
10
  "dependencies": {
11
- "playwright": "^1.62.1"
12
- }
13
- },
14
- "node_modules/playwright": {
15
- "version": "1.64.1",
16
- "resolved": "https://registry.npmmirror.com/playwright/-/playwright-1.64.1.tgz",
17
- "integrity": "sha512-HCrVhHr5QQuwYoswEUGTb7gGp522yflbv/ST8nF4sFWGuLjQCt17Q/EubpGGSNKYn1tsTCPZxqHhHCfVURu7UA==",
18
- "license": "Apache-2.0",
19
- "dependencies": {
20
- "playwright-core": "1.64.1"
21
- },
22
- "bin": {
23
- "playwright": "cli.js"
24
- },
25
- "engines": {
26
- "node": ">=20"
11
+ "playwright-core": "^1.62.1"
27
12
  }
28
13
  },
29
14
  "node_modules/playwright-core": {
@@ -3,8 +3,8 @@
3
3
  "version": "1.0.0",
4
4
  "private": true,
5
5
  "type": "module",
6
- "description": "Playwright-based 远端 UAT auto-login scripts",
6
+ "description": "playwright-core-based 远端自动登录脚本(纯 JS API,浏览器走系统 Chrome + CDP)",
7
7
  "dependencies": {
8
- "playwright": "^1.62.1"
8
+ "playwright-core": "^1.62.1"
9
9
  }
10
10
  }
@@ -1,4 +1,15 @@
1
- import { chromium } from "playwright";
1
+ // 依赖只取 playwright 的 JS API(浏览器由本脚本 spawn 系统 Chrome + connectOverCDP 连接,
2
+ // 不需要 playwright 自带浏览器),因此用免下载的 playwright-core:它是 cardbuddy 与工程
3
+ // devDependencies 里的普通依赖,npm install 即落地;不用 playwright(postinstall 下载
4
+ // 数百 MB 浏览器,作为 optionalDependency 安装失败时会被 npm 静默跳过,错误延迟到运行时)。
5
+ let chromium;
6
+ try {
7
+ ({ chromium } = await import("playwright-core"));
8
+ } catch {
9
+ throw new Error(
10
+ "缺少依赖 playwright-core:请在本工程根执行 npm install(cardbuddy 依赖会带上 playwright-core),或手动 npm i playwright-core。",
11
+ );
12
+ }
2
13
  import { readFile, writeFile, mkdir } from "node:fs/promises";
3
14
  import { existsSync } from "node:fs";
4
15
  import { spawn } from "node:child_process";
@@ -58,7 +58,7 @@ let _sessionCache = null; // { browser, login, at }
58
58
  * @param {object} [options]
59
59
  * @param {number} [options.debugPort=SYNC_PORT]
60
60
  * @param {boolean} [options.headless=true]
61
- * @returns {Promise<{ browser: import('playwright').Browser, login: object }>}
61
+ * @returns {Promise<{ browser: import('playwright-core').Browser, login: object }>}
62
62
  */
63
63
  export async function ensurePlatformSession({ debugPort = SYNC_PORT, headless = true } = {}) {
64
64
  // 热路径:进程内缓存未过期且 CDP 仍连通 → 直接复用(跳过 runPlatformLogin 的
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cardbuddy",
3
- "version": "0.24.0",
3
+ "version": "0.25.1",
4
4
  "description": "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 card source.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -30,14 +30,12 @@
30
30
  "codejar": "^4.3.0",
31
31
  "connect": "^3.7.0",
32
32
  "dotenv": "^18.0.9",
33
+ "playwright-core": "^1.62.1",
33
34
  "pngjs": "^7.0.0",
34
35
  "prismjs": "^1.30.0",
35
36
  "qrcode": "^1.5.4",
36
37
  "selfsigned": "^4.0.1",
37
38
  "vite": "^8.2.0",
38
39
  "vue": "^3.5.40"
39
- },
40
- "optionalDependencies": {
41
- "playwright": "^1.62.1"
42
40
  }
43
41
  }