cardbuddy 0.19.1 → 0.19.3

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.
package/README.md CHANGED
@@ -67,6 +67,38 @@ npm run preview # 本地服务(8787) + 内置编辑器前端 +
67
67
 
68
68
  ---
69
69
 
70
+ ## 升级 cardbuddy 后同步 skills(`cardbuddy skills upgrade`)
71
+
72
+ `init` 会把技能库复制进工程的 `skills/`;而运行时**项目 `skills/` 优先于包内快照**
73
+ (见 `server/core/skillPaths.js`),所以 `npm i cardbuddy@latest` 只是刷新了包内快照,
74
+ **不会**更新工程里正在用的 `skills/` 副本——必须显式拉一次。本命令就是为这个场景而生,
75
+ 且**只动 `skills/`**,绝不碰 `.env.cb` / `package.json` / `README.md`(那得用 `init --force`,有冲配置风险)。
76
+
77
+ ```bash
78
+ npm i cardbuddy@latest
79
+ npx cardbuddy skills upgrade # 新增的 skill 复制进来;本地改过的 shipped skill 默认跳过并黄色警告
80
+ npx cardbuddy skills upgrade --force # 确认要抛弃本地改动、用包内版本覆盖时
81
+ npx cardbuddy skills upgrade --dry-run # 先看会动哪些文件(四类统计,不落盘)
82
+ npx cardbuddy skills upgrade --prune # 额外删除包内已不存在的技能文件(默认保留,防误删你的私有 skill)
83
+ ```
84
+
85
+ 按**内容比对**(非 mtime)分四类处理:
86
+
87
+ | 分类 | 判定 | 默认行为 | 风险 |
88
+ | --- | --- | --- | --- |
89
+ | `ADDED` | 包内有、本地无 | 复制 | 无(新 skill) |
90
+ | `SAME` | 两边内容一致 | 跳过 | 无 |
91
+ | `MODIFIED` | 两边都有、内容不同 | **跳过 + 黄色警告列清单** | ⚠️ 你可能改过该 shipped skill;`--force` 才覆盖 |
92
+ | `ORPHAN` | 本地有、包内无 | **保留不删** | ⚠️ 可能是你私有的 skill,或上游已下架该 skill;`--prune` 才删 |
93
+
94
+ > `init` 生成的工程已带 `npm run upgrade:skills` 脚本(等价 `cardbuddy skills upgrade`),
95
+ > 要覆盖时用 `npm run upgrade:skills -- --force`。
96
+
97
+ 同步后写 `skills/.synced-from = 当前装包版本`,供下次 drift 检测(Phase 2:服务启动时可据此提示
98
+ "skills 停留在 X,包已 Y")。
99
+
100
+ ---
101
+
70
102
  ## Preview 模式(本地独立调试)
71
103
 
72
104
  ```bash
package/dist/cli.js CHANGED
@@ -19,6 +19,7 @@ import { startServer } from './standalone.js'
19
19
  import { installLogMirror } from './core.js'
20
20
  import { loadConfig } from './config.js'
21
21
  import { runInit } from './init.js'
22
+ import { runSkillsUpgrade } from './skills-upgrade.js'
22
23
 
23
24
  const args = process.argv.slice(2)
24
25
  const mcpMode = args.includes('--mcp')
@@ -66,6 +67,7 @@ if (args.includes('--help') || args.includes('-h')) {
66
67
  用法:
67
68
  cardbuddy [--preview] [--mcp] [--version] [--help]
68
69
  cardbuddy init [目标目录] [--force] [--dry-run] [--no-skills] [--no-sample]
70
+ cardbuddy skills upgrade [目标目录] [--force] [--dry-run] [--prune] [--strict]
69
71
 
70
72
  命令:
71
73
  init 生成「立刻能开发调试」的卡片工程最小实例(增量、幂等,不覆盖已存在文件):
@@ -77,6 +79,20 @@ if (args.includes('--help') || args.includes('-h')) {
77
79
  --no-sample 不生成示例卡
78
80
  典型用法:npx cardbuddy init my-cards && cd my-cards && npm i && npm run preview
79
81
 
82
+ skills upgrade 升级 cardbuddy 包后,把包内最新技能库同步进当前工程的 skills/(增量拉取 skill 更新):
83
+ 仅动 skills/,不碰 .env.cb / package.json / README.md(那要用 init --force,有冲配置风险)。
84
+ 运行时「项目 skills/ 优先于包内快照」,故只 npm i cardbuddy@latest 不会更新工程里的
85
+ skills 副本,需显式拉一次。按内容比对分四类:
86
+ ADDED 包内有、本地无 → 复制(安全)
87
+ SAME 两边一致 → 跳过
88
+ MODIFIED 两边都有、内容不同 → 默认跳过并黄色警告(防丢你改过的 shipped skill)
89
+ ORPHAN 本地有、包内无 → 默认保留不删(可能是你私有的或上游已下架)
90
+ --force 覆盖本地已修改的 shipped skill(丢弃你的改动)
91
+ --dry-run 只报告四类统计,不落盘
92
+ --prune 额外删除包内已不存在的技能文件(默认保留,防误删你的私有 skill)
93
+ --strict 存在未覆盖的 MODIFIED 时返回非 0(便于 CI 感知)
94
+ 典型用法:npm i cardbuddy@latest && npx cardbuddy skills upgrade
95
+
80
96
  选项:
81
97
  --preview, -p 同时托管打包后的编辑器前端(本地预览),替代线上地址,并自动打开浏览器
82
98
  旧名 --offline / -o 仍可用,已废弃
@@ -106,10 +122,12 @@ if (args.includes('--help') || args.includes('-h')) {
106
122
  process.exit(0)
107
123
  }
108
124
 
109
- // 未知子命令:非 flag 的位置参数只有 init 合法(避免 `cardbuddy inti` 静默起服务)
125
+ // 未知子命令:非 flag 的位置参数只有 init / skills 合法(避免 `cardbuddy inti` 静默起服务)
110
126
  const subCommand = args.find((a) => !a.startsWith('-'))
111
- if (subCommand && subCommand !== 'init') {
112
- console.error(srcTag + chalk.red(`未知命令:${subCommand}(可用:init;或直接启动服务,见 --help)`))
127
+ const isSkillsUpgrade =
128
+ args.includes('upgrade-skills') || (subCommand === 'skills' && args.includes('upgrade'))
129
+ if (subCommand && subCommand !== 'init' && subCommand !== 'skills' && !isSkillsUpgrade) {
130
+ console.error(srcTag + chalk.red(`未知命令:${subCommand}(可用:init、skills upgrade;或直接启动服务,见 --help)`))
113
131
  process.exit(1)
114
132
  }
115
133
 
@@ -144,6 +162,50 @@ if (subCommand === 'init') {
144
162
  }
145
163
  }
146
164
 
165
+ // skills upgrade 子命令:把已装包内最新技能库同步进当前工程的 skills/(升级包后拉取 skill 更新)。
166
+ // 只动 skills/,不碰 .env.cb / package.json / README.md。支持 `skills upgrade` 与 `upgrade-skills` 两种写法。
167
+ if (isSkillsUpgrade) {
168
+ const marker = args.includes('upgrade-skills')
169
+ ? args.indexOf('upgrade-skills')
170
+ : args.indexOf('upgrade')
171
+ const rest = args.slice(marker + 1)
172
+ const flags2 = new Set(rest.filter((a) => a.startsWith('-')))
173
+ const positional2 = rest.filter((a) => !a.startsWith('-'))
174
+ const knownFlags = ['--force', '--dry-run', '--prune', '--strict']
175
+ const unknown = [...flags2].filter((f) => !knownFlags.includes(f))
176
+ if (unknown.length) {
177
+ console.error(srcTag + chalk.red(`skills upgrade 不支持的参数:${unknown.join(' ')}(可用:${knownFlags.join(' ')})`))
178
+ process.exit(1)
179
+ }
180
+ if (positional2.length > 1) {
181
+ console.error(srcTag + chalk.red('skills upgrade 只接受一个目标目录参数(收到:' + positional2.join(' ') + ')'))
182
+ process.exit(1)
183
+ }
184
+ try {
185
+ process.exit(
186
+ await runSkillsUpgrade({
187
+ targetDir: positional2[0] || '.',
188
+ force: flags2.has('--force'),
189
+ dryRun: flags2.has('--dry-run'),
190
+ prune: flags2.has('--prune'),
191
+ strict: flags2.has('--strict'),
192
+ config: loadConfig(),
193
+ })
194
+ )
195
+ } catch (e) {
196
+ console.error(srcTag + chalk.red('skills upgrade 失败:') + (e && e.stack ? e.stack : e))
197
+ process.exit(1)
198
+ }
199
+ }
200
+
201
+ // 仅 `skills` 而无 upgrade:提示唯一合法子命令
202
+ if (subCommand === 'skills' && !isSkillsUpgrade) {
203
+ console.error(
204
+ srcTag + chalk.red('skills 子命令仅支持 upgrade(用法:cardbuddy skills upgrade [目标目录] [--force] [--dry-run] [--prune] [--strict])')
205
+ )
206
+ process.exit(1)
207
+ }
208
+
147
209
  if (mcpMode) {
148
210
  // stdio MCP server:不碰端口 / TLS / Vite,由 server.mjs 常驻监听 stdin 直到对端关闭。
149
211
  const { startMcpServer } = await import('./mcp.js')
package/dist/init.js CHANGED
@@ -197,6 +197,21 @@ skills/ AI 协作技能库(校验 / 同步 / 建卡管线脚本
197
197
  npm run link:skills # 交互式选择 Codex / Claude / WorkBuddy 等,建立项目级技能软链
198
198
  \`\`\`
199
199
 
200
+ ## 升级 cardbuddy 后同步 skills
201
+
202
+ \`cardbuddy init\` 会把技能库复制进本工程的 \`skills/\`,而运行时**项目 skills/ 优先于包内快照**
203
+ (见 \`server/core/skillPaths.js\`),所以只 \`npm i cardbuddy@latest\` 不会更新工程里的 skills 副本。
204
+ 装完新版后拉一次:
205
+
206
+ \`\`\`bash
207
+ npm run upgrade:skills # 新增的 skill 复制进来;本地改过的 shipped skill 默认跳过并警告
208
+ npx cardbuddy skills upgrade --force # 确认要抛弃本地改动、用包内版本覆盖时
209
+ npx cardbuddy skills upgrade --dry-run # 先看会动哪些文件(四类:新增/相同/已修改/本地私有)
210
+ \`\`\`
211
+
212
+ 该命令**只动 \`skills/\`**,绝不碰 \`.env.cb\` / \`package.json\` / \`README.md\`
213
+ (那得用 \`init --force\`,有冲掉端口 / FinMall 配置 / AI 网关占位的风险)。
214
+
200
215
  ## 把卡片工具挂给外部 AI agent(MCP,可选)
201
216
 
202
217
  编辑器内置的网页 AI 助手走 \`POST /__ai/chat\`。若要让 Codex / Claude 这类**外部** agent
@@ -259,6 +274,10 @@ function projectPackageJson(projectName, version) {
259
274
  // 因为 `npm run` 的横幅会打到 stdout、污染 JSON-RPC 协议流(详见生成的 README)。
260
275
  mcp: 'cardbuddy --mcp',
261
276
  'link:skills': 'node tools/link-skills.mjs',
277
+ // 升级 cardbuddy 后,把包内最新技能库同步进本工程的 skills/(仅动 skills/,
278
+ // 不碰 .env.cb / package.json / README.md;本地改过的 shipped skill 默认跳过并警告,
279
+ // 确认要抛弃改动时加 -- --force)。
280
+ 'upgrade:skills': 'cardbuddy skills upgrade',
262
281
  },
263
282
  devDependencies: {
264
283
  cardbuddy: `^${version}`,
@@ -397,6 +416,14 @@ export async function runInit({
397
416
  chalk.dim(`(新增 ${stat.copied} 文件 / ${fmtBytes(stat.bytes)},保留已存在 ${stat.skipped} 个${force ? ',--force 覆盖' : ''})`)
398
417
  )
399
418
  if (names.length) say(chalk.dim(` ${names.join('、')}`))
419
+ // 写同步标记(best-effort):记录本次 init 基于的 cardbuddy 版本,供 `skills upgrade` 做 drift 检测
420
+ if (!dryRun && fs.existsSync(dest)) {
421
+ try {
422
+ fs.writeFileSync(path.join(dest, '.synced-from'), ownVersion() + '\n', 'utf8')
423
+ } catch {
424
+ /* best-effort */
425
+ }
426
+ }
400
427
  }
401
428
  }
402
429
 
@@ -314,7 +314,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
314
314
  `:``}[已挂载本地文件(仅本机路径,未上传;需要内容请用工具读取)]\n${e.join(`
315
315
  `)}`}return{role:`user`,content:n}}let t=(e.toolCalls||[]).filter(e=>e.result!==void 0||e.error).map(e=>({toolCallId:e.toolCallId,toolName:e.toolName,args:e.args||{},result:typeof e.result==`string`?sF(e.toolName,e.result):e.result,error:e.error||``})),n=e.content||``;return e.status===`aborted`&&(n=n?`${n}\n\n[本轮生成被用户手动中止,以上为已完成部分;未完成的步骤没有执行,请从中断处继续。]`:`[上一轮被用户手动中止于思考/工具阶段,没有产出可用内容;请重新规划并继续完成用户的目标。]`),{role:`assistant`,content:n,toolCalls:t}})},onScroll(){let e=this.$refs.listEl;e&&(this.pinned=e.scrollHeight-e.scrollTop-e.clientHeight<48)},scrollToBottom(e=!1){let t=this.$refs.listEl;t&&(!e&&!this.pinned||(t.scrollTop=t.scrollHeight))},stopTickers(){this._saveTimer&&clearTimeout(this._saveTimer),this._saveTimer=0},startTurn(){let e=Date.now();this.turnId=``,this.lastSeq=0,this.turnStep=0,this.turnStage=null,this.turnToolLabel=``,this.turnStartedAt=e,this.lastByteAt=e,this.nowTs=e,this.stopTurnTimers(),this._liveTicker=setInterval(()=>{this.nowTs=Date.now(),this.$emit(`turn-progress`,{steps:this.turnStep,startedAt:this.turnStartedAt,ms:this.nowTs-this.turnStartedAt,stage:this.turnStage?`${this.turnStage.id} ${this.turnStage.label}`:this.turnToolLabel,stalled:this.stalled})},1e3),this._turnPersistTimer=setInterval(()=>this.persist(),8e3),this.persist(),this.$emit(`busy`,!0)},stopTurnTimers(){this._liveTicker&&clearInterval(this._liveTicker),this._liveTicker=0,this._turnPersistTimer&&clearInterval(this._turnPersistTimer),this._turnPersistTimer=0},onPagehide(){let e=this.cardId;if(!e||!this.messages.length)return;let t=this.fitArchive(this.serialize());this._dirtyFor=null,this._saveTimer&&=(clearTimeout(this._saveTimer),0),DA(e,t,{keepalive:!0}).catch(()=>{})},fmtClock:pk,pushEvent(e,t){let n=String(t&&t.message||``);!n||!e||(Array.isArray(e.events)||(e.events=[]),e.events.push({ts:Date.now(),level:t.level||`warn`,code:t.code||``,message:n}))},applyUsage(e,t){if(!e||!t)return;let n=e=>Number.isFinite(Number(e))?Number(e):0;e.usage={steps:n(t.steps),promptTokens:n(t.promptTokens),completionTokens:n(t.completionTokens),cachedTokens:n(t.cachedTokens),cacheMissTokens:n(t.cacheMissTokens),hitRate:n(t.hitRate),reported:t.reported!==!1}},loadCompact(){try{this.compactSteps=localStorage.getItem(vF)===`1`}catch{}},setCompact(e){let t=!!e;if(this.compactSteps!==t){this.compactSteps=t;try{t?localStorage.setItem(vF,`1`):localStorage.removeItem(vF)}catch{}this.$nextTick(()=>this.scrollToBottom())}},toggleCompact(){this.setCompact(!this.compactSteps)},onLocateNode(e){e&&this.$emit(`locate-node`,String(e))},onRollbackTurn(e){if(this.busy)return;let t=this.cardId;!t||!e||this.$emit(`rollback-turn`,{cardId:t,ts:Number(e.ts)||0,msgId:e.id})},notify(e,t){this.notice={type:e,text:t},this._noticeTimer&&clearTimeout(this._noticeTimer),this._noticeTimer=setTimeout(()=>{this.notice=null},6e3)}}},EF={class:`aichat__head`},DF={class:`aichat__titles`},OF={class:`aichat__sub`},kF={class:`aichat__acts`},AF=[`title`,`aria-pressed`],jF={viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,"stroke-width":`2`,"stroke-linecap":`round`,"stroke-linejoin":`round`,"aria-hidden":`true`},MF=[`href`],NF=[`disabled`],PF={key:0,class:`aichat__banner`},FF={class:`aichat__scroll`},IF={key:0,class:`aichat__archnote`,title:`长对话记录超出落盘体积上限时,较早的工具参数与结果会被裁剪归档;正文、思考与过程事件都完整保留`},LF={key:1,class:`aichat__loading`},RF={key:2,class:`aichat__empty`},zF={class:`aichat__empty-title`},BF={class:`aichat__empty-desc`},VF={class:`aichat__suggests`},HF=[`title`,`data-suggest-id`,`onClick`],UF={viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,"stroke-width":`2`,"stroke-linecap":`round`,"stroke-linejoin":`round`,"aria-hidden":`true`},WF=[`href`],GF={class:`aichat__suggest-text`},KF={class:`aichat__suggest-label`},qF={class:`aichat__suggest-hint`},JF={key:0,class:`aichat__suggest-badge`,title:`该操作会改动卡片`},YF={class:`aichat__live-text`},XF={key:1,class:`aichat__quick`},ZF=[`title`,`data-suggest-id`,`onClick`],QF={viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,"stroke-width":`2`,"stroke-linecap":`round`,"stroke-linejoin":`round`,"aria-hidden":`true`},$F=[`href`],eI={class:`ni-panel modal-card ccsw-dlg`,role:`dialog`,"aria-modal":`true`,"aria-label":`从 cc-switch 导入模型`},tI={class:`ni-head`},nI={class:`ni-body`},rI={class:`ni-scroll`},iI={key:0,class:`ccsw-dlg__msg`},aI={class:`ccsw-dlg__msg ccsw-dlg__msg--err`},oI={class:`ccsw-dlg__list`},sI=[`onClick`],cI={key:0,viewBox:`0 0 16 16`,fill:`none`,stroke:`currentColor`,"stroke-width":`1.8`,"stroke-linecap":`round`,"stroke-linejoin":`round`},lI={class:`ccsw-dlg__info`},uI={class:`ccsw-dlg__name`},dI={key:0,class:`ccsw-dlg__badge ccsw-dlg__badge--cur`,title:`cc-switch 中当前生效`},fI={key:1,class:`ccsw-dlg__badge ccsw-dlg__badge--dup`,title:`模型库中已存在同名模型`},pI={class:`ccsw-dlg__meta`},mI={class:`ccsw-dlg__model`},hI={class:`ccsw-dlg__host`},gI={class:`ni-foot`},_I={class:`ni-tip`},vI={class:`ni-foot-btns`},yI=[`disabled`];function bI(e,t,n,r,i,a){let o=L(`ChatMessage`),s=L(`ChatComposer`),c=L(`ModelList`),l=L(`AiSettings`),u=L(`ConfirmDialog`),d=L(`AiCommonSettings`);return B(),V(`aside`,{class:A([`aichat`,{"is-open":n.visible}]),style:k(a.panelStyle),role:`complementary`,"aria-label":`AI 开发助手`,onKeydown:t[21]||=vl((...e)=>a.onEscape&&a.onEscape(...e),[`esc`]),onClick:t[22]||=(...e)=>a.onImageClick&&a.onImageClick(...e)},[K(` 左缘拖拽把手:调宽(双击恢复默认 33%) `),U(`div`,{class:A([`aichat__grip`,{"is-dragging":i.resizing}]),title:`拖拽调整宽度(双击恢复默认)`,onPointerdown:t[0]||=q((...e)=>a.startResize&&a.startResize(...e),[`prevent`]),onDblclick:t[1]||=q((...e)=>a.resetWidth&&a.resetWidth(...e),[`prevent`])},null,34),K(` 头部:标题 + 当前卡片 / 模型 + 清空 / 设置 / 关闭 `),U(`header`,EF,[t[27]||=U(`span`,{class:`aichat__logo`},[U(`svg`,{viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,"stroke-width":`2`,"stroke-linecap":`round`,"stroke-linejoin":`round`,"aria-hidden":`true`},[U(`use`,{href:`#icon-sparkles`})])],-1),U(`span`,DF,[t[23]||=U(`span`,{class:`aichat__title`},`AI 开发助手`,-1),U(`span`,OF,[n.cardId?(B(),V(z,{key:0},[G(j(n.cardName||n.cardId),1)],64)):(B(),V(z,{key:1},[G(`未选择卡片`)],64)),i.config.model?(B(),V(z,{key:2},[G(` · `+j(i.config.model),1)],64)):K(`v-if`,!0)])]),U(`span`,kF,[K(` 紧凑视图(面板级):长任务里读取类调用能刷几十行,把写入与门禁挤到折叠线以下 `),U(`button`,{type:`button`,class:A([`aichat__iconbtn`,{"is-on":i.compactSteps}]),title:i.compactSteps?`紧凑视图已开启:已折叠读取类调用,点击显示全部步骤`:`只看关键步骤:折叠读取类调用(写入 / 跑脚本 / 报错始终显示)`,"aria-pressed":i.compactSteps?`true`:`false`,onClick:t[2]||=(...e)=>a.toggleCompact&&a.toggleCompact(...e)},[(B(),V(`svg`,jF,[U(`use`,{href:i.compactSteps?`#icon-eye-off`:`#icon-eye`},null,8,MF)]))],10,AF),U(`button`,{type:`button`,class:`aichat__iconbtn`,title:`清空本卡片对话记录`,disabled:!i.messages.length||i.busy,onClick:t[3]||=(...e)=>a.askClear&&a.askClear(...e)},[...t[24]||=[U(`svg`,{viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,"stroke-width":`2`,"stroke-linecap":`round`,"stroke-linejoin":`round`,"aria-hidden":`true`},[U(`use`,{href:`#icon-trash`})],-1)]],8,NF),U(`button`,{type:`button`,class:`aichat__iconbtn`,title:`模型库`,onClick:t[4]||=(...e)=>a.openModelList&&a.openModelList(...e)},[...t[25]||=[U(`svg`,{class:`is-solid-icon`,viewBox:`0 0 1024 1024`,"aria-hidden":`true`},[U(`use`,{href:`#icon-model`})],-1)]]),U(`button`,{type:`button`,class:`aichat__iconbtn`,title:`关闭(Esc)`,onClick:t[5]||=t=>e.$emit(`close`)},[...t[26]||=[U(`svg`,{viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,"stroke-width":`2`,"stroke-linecap":`round`,"stroke-linejoin":`round`,"aria-hidden":`true`},[U(`use`,{href:`#icon-x`})],-1)]])])]),K(` 未配置模型:常驻提示条,一键打开设置 `),a.configReady?K(`v-if`,!0):(B(),V(`div`,PF,[t[28]||=U(`svg`,{viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,"stroke-width":`2`,"stroke-linecap":`round`,"stroke-linejoin":`round`,"aria-hidden":`true`},[U(`use`,{href:`#icon-alert-triangle`})],-1),t[29]||=U(`span`,null,`尚未配置模型,无法对话。`,-1),U(`button`,{type:`button`,class:`aichat__banner-btn`,onClick:t[6]||=(...e)=>a.openModelList&&a.openModelList(...e)},`去设置`)])),K(` 消息区:滚动容器与悬浮「回到底部」共处一个定位上下文,按钮只贴消息区底缘,
316
316
  输入区拖高 / 反馈条出现都不会把它盖住或悬空 `),U(`div`,FF,[U(`div`,{ref:`listEl`,class:`aichat__list`,onScroll:t[10]||=(...e)=>a.onScroll&&a.onScroll(...e)},[K(` 体积治理痕迹:较早的工具结果被裁剪过就说清楚,别让人以为记录丢了 `),i.archiveTrimmed?(B(),V(`div`,IF,[...t[30]||=[U(`svg`,{viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,"stroke-width":`2`,"stroke-linecap":`round`,"stroke-linejoin":`round`,"aria-hidden":`true`},[U(`use`,{href:`#icon-activity`})],-1),U(`span`,null,`较早的工具参数 / 结果已裁剪归档(对话正文与过程事件完整保留)`,-1)]])):K(`v-if`,!0),i.loadingHistory?(B(),V(`div`,LF,`正在加载对话记录…`)):i.messages.length?(B(),V(z,{key:3},[K(` compact = 面板级「只看关键步骤」;can-rollback 只对当前卡片、非流式中开放(切了卡再回滚会写错对象) `),(B(!0),V(z,null,R(i.messages,(e,r)=>(B(),H(o,{key:e.id,msg:e,streaming:i.busy&&r===i.messages.length-1,"can-regenerate":e.role===`assistant`&&r===i.messages.length-1,compact:i.compactSteps,"can-rollback":e.role===`assistant`&&!i.busy&&!!n.cardId,"now-ts":i.busy&&r===i.messages.length-1?i.nowTs:0,"pick-owner":a.pickOwnerRow,onRegenerate:a.regenerate,onApproval:a.onApproval,onContinue:a.onContinue,onRollback:a.onRollbackTurn,onOpenSettings:a.openModelList,onShowAllSteps:t[8]||=e=>a.setCompact(!1),onLocateNode:a.onLocateNode,onPickBoard:a.onPickBoard},null,8,[`msg`,`streaming`,`can-regenerate`,`compact`,`can-rollback`,`now-ts`,`pick-owner`,`onRegenerate`,`onApproval`,`onContinue`,`onRollback`,`onOpenSettings`,`onLocateNode`,`onPickBoard`]))),128)),K(` 面板级进度:步数 / 已用时长 / 建卡阶段 / 最近收到数据多久前。
317
- 长任务里「还活着吗」全靠这一行——旧版只有一句「生成中…」,网关静默与真在跑无从区分 `),i.busy?(B(),V(`div`,{key:0,class:A([`aichat__live`,{"is-stalled":a.stalled}]),role:`status`,"aria-live":`polite`},[t[33]||=U(`span`,{class:`aichat__live-dot`},null,-1),U(`span`,YF,j(a.liveText),1),a.stalled?(B(),V(`button`,{key:0,type:`button`,class:`aichat__live-act`,title:`连接可能已中断,终止本轮(已落盘的产物会保留)`,onClick:t[9]||=(...e)=>a.stop&&a.stop(...e)},` 终止 `)):K(`v-if`,!0)],2)):K(`v-if`,!0)],64)):(B(),V(`div`,RF,[t[32]||=U(`span`,{class:`aichat__empty-logo`},[U(`svg`,{viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,"stroke-width":`2`,"stroke-linecap":`round`,"stroke-linejoin":`round`,"aria-hidden":`true`},[U(`use`,{href:`#icon-sparkles`})])],-1),U(`p`,zF,j(a.emptyTitle),1),U(`p`,BF,j(a.emptyDesc),1),U(`div`,VF,[(B(!0),V(z,null,R(a.suggestions,e=>(B(),V(`button`,{key:e.id,type:`button`,class:A([`aichat__suggest`,`is-${e.weight}`]),title:e.text,"data-suggest-id":e.id,onClick:t=>a.useSuggestion(e)},[(B(),V(`svg`,UF,[U(`use`,{href:e.icon},null,8,WF)])),U(`span`,GF,[U(`span`,KF,j(e.label),1),U(`span`,qF,j(e.hint),1)]),e.weight===`write`?(B(),V(`span`,JF,`改卡`)):K(`v-if`,!0)],10,HF))),128)),a.canShuffle?(B(),V(`button`,{key:0,type:`button`,class:`aichat__suggest-shuffle`,onClick:t[7]||=(...e)=>a.shuffleSuggestions&&a.shuffleSuggestions(...e)},[...t[31]||=[U(`svg`,{viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,"stroke-width":`2`,"stroke-linecap":`round`,"stroke-linejoin":`round`,"aria-hidden":`true`},[U(`use`,{href:`#icon-refresh`})],-1),U(`span`,null,`换一批`,-1)]])):K(`v-if`,!0)])]))],544),K(` 回到底部:脱离底部吸附时出现 `),I(U(`button`,{type:`button`,class:`aichat__jump`,title:`回到底部`,onClick:t[11]||=e=>a.scrollToBottom(!0)},[...t[34]||=[U(`svg`,{viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,"stroke-width":`2`,"stroke-linecap":`round`,"stroke-linejoin":`round`,"aria-hidden":`true`},[U(`use`,{href:`#icon-arrow-down`})],-1)]],512),[[Sc,!i.pinned&&i.messages.length]])]),K(` 常驻「常用」指令:有对话后空态建议区不再出现,这里保留一排轻量入口,随上下文同源 `),a.quickChips.length&&i.messages.length&&!i.busy?(B(),V(`div`,XF,[(B(!0),V(z,null,R(a.quickChips,e=>(B(),V(`button`,{key:e.id,type:`button`,class:A([`aichat__chip`,`is-${e.weight}`]),title:e.text,"data-suggest-id":e.id,onClick:t=>a.useSuggestion(e)},[(B(),V(`svg`,QF,[U(`use`,{href:e.icon},null,8,$F)])),U(`span`,null,j(e.label),1)],10,ZF))),128))])):K(`v-if`,!0),K(` 操作反馈(附件解析失败 / 历史保存失败等) `),i.notice?(B(),V(`div`,{key:2,class:A([`aichat__notice`,`aichat__notice--${i.notice.type}`])},[U(`span`,null,j(i.notice.text),1),U(`button`,{type:`button`,class:`aichat__notice-x`,title:`关闭`,onClick:t[12]||=e=>i.notice=null},[...t[35]||=[U(`svg`,{viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,"stroke-width":`2`,"stroke-linecap":`round`,"stroke-linejoin":`round`,"aria-hidden":`true`},[U(`use`,{href:`#icon-x`})],-1)]])],2)):K(`v-if`,!0),W(s,{ref:`composerEl`,busy:i.busy,model:i.config.model,attachments:i.draftAttachments,placeholder:a.configReady?`描述你要改的卡片,Enter 发送 / Shift+Enter 换行`:`请先在设置中配置模型…`,context:i.contextUsage,onSend:a.onSend,onStop:a.stop,onPick:a.onPick,onAddPath:a.onAddPath,onRemoveAttachment:a.removeAttachment,onDropFiles:a.onDropFiles,onPasteImage:a.onPasteImage,onEscape:a.onEscape},null,8,[`busy`,`model`,`attachments`,`placeholder`,`context`,`onSend`,`onStop`,`onPick`,`onAddPath`,`onRemoveAttachment`,`onDropFiles`,`onPasteImage`,`onEscape`]),K(` 一级面板:模型库。从齿轮 / 横幅 / 消息内「去设置」进入 `),i.modelListOpen?(B(),H(c,{key:3,models:i.models,"active-id":i.activeId,"turn-running":i.turnRunning,onClose:a.closeModelList,onAdd:a.onModelAdd,onEdit:a.onModelEdit,onDelete:a.onModelDelete,onActivate:a.onModelActivate,onImportCcswitch:a.onImportCcSwitch,onOpenCommon:a.openCommon},null,8,[`models`,`active-id`,`turn-running`,`onClose`,`onAdd`,`onEdit`,`onDelete`,`onActivate`,`onImportCcswitch`,`onOpenCommon`])):K(`v-if`,!0),K(` 新增 / 编辑模型:复用 AiSettings(modelId 区分,initialModel 预填) `),i.modelEditOpen?(B(),H(l,{key:4,config:i.config,"model-id":i.editModelId,"initial-model":i.editInitialModel,onClose:a.onModelEditClose,onSaved:a.onModelSaved},null,8,[`config`,`model-id`,`initial-model`,`onClose`,`onSaved`])):K(`v-if`,!0),K(` 删除模型确认:二次确认后才真正落盘删除 `),W(u,{visible:!!i.confirmDeleteModel,"close-on-mask":!1,title:`删除模型`,message:a.deleteModelMessage,"confirm-text":`删除`,severity:`danger`,onConfirm:a.doDeleteModel,onCancel:t[13]||=e=>i.confirmDeleteModel=null},null,8,[`visible`,`message`,`onConfirm`]),K(` cc-switch 导入弹窗:列出 cc-switch 的 Codex 类供应商,勾选后批量导入模型库(按 model 去重) `),W(ic,{name:`modal`},{default:F(()=>[i.ccSwitchOpen?(B(),V(`div`,{key:0,class:`ni-overlay`,onClick:t[17]||=q(e=>i.ccSwitchOpen=!1,[`self`])},[U(`div`,eI,[U(`div`,tI,[t[36]||=U(`span`,{class:`ni-title`},`从 cc-switch 导入`,-1),U(`button`,{class:`ni-close`,type:`button`,title:`关闭`,onClick:t[14]||=e=>i.ccSwitchOpen=!1})]),U(`div`,nI,[U(`div`,rI,[K(` 加载中 `),i.ccSwitchLoading?(B(),V(`p`,iI,`正在读取 cc-switch 供应商…`)):i.ccSwitchReason?(B(),V(z,{key:1},[K(` 无可导入项 `),U(`p`,aI,j(i.ccSwitchReason),1)],2112)):(B(),V(z,{key:2},[K(` 供应商列表 `),U(`ul`,oI,[(B(!0),V(z,null,R(i.ccSwitchProviders,e=>(B(),V(`li`,{key:e.id,class:A([`ccsw-dlg__item`,{"is-active":i.ccSwitchSelected[e.id],"is-dup":e.duplicate}]),onClick:t=>a.toggleCcSwitchItem(e)},[U(`span`,{class:A([`ccsw-dlg__check`,{"is-on":i.ccSwitchSelected[e.id]}])},[i.ccSwitchSelected[e.id]?(B(),V(`svg`,cI,[...t[37]||=[U(`use`,{href:`#icon-check`},null,-1)]])):K(`v-if`,!0)],2),U(`span`,lI,[U(`span`,uI,[G(j(e.name||e.model)+` `,1),e.isCurrent?(B(),V(`span`,dI,`当前`)):K(`v-if`,!0),e.duplicate?(B(),V(`span`,fI,`已存在`)):K(`v-if`,!0)]),U(`span`,pI,[U(`span`,mI,j(e.model),1),t[38]||=U(`span`,{class:`ccsw-dlg__dot`},`·`,-1),U(`span`,hI,j(e.baseUrl),1)])])],10,sI))),128))])],2112))])]),U(`div`,gI,[U(`span`,_I,j(a.ccSwitchDupCount>0?`已有 ${a.ccSwitchDupCount} 个重复模型被自动跳过`:`勾选要导入的供应商,批量加入模型库`),1),U(`div`,vI,[U(`button`,{class:`ni-btn ni-btn--ghost`,type:`button`,onClick:t[15]||=e=>i.ccSwitchOpen=!1},`取消`),U(`button`,{class:`ni-btn`,type:`button`,disabled:i.ccSwitchLoading||!i.ccSwitchProviders.length||a.ccSwitchCheckedCount===0,onClick:t[16]||=(...e)=>a.confirmImportCcSwitch&&a.confirmImportCcSwitch(...e)},j(a.ccSwitchCheckedCount>0?`导入选中 ${a.ccSwitchCheckedCount} 项`:`导入`),9,yI)])])])])):K(`v-if`,!0)]),_:1}),K(` 通用设置:所有模型共享的执行策略 / 环境参数 `),i.commonOpen?(B(),H(d,{key:5,onClose:t[18]||=e=>i.commonOpen=!1,onSaved:a.onCommonSaved},null,8,[`onSaved`])):K(`v-if`,!0),W(u,{visible:i.confirmClear,"close-on-mask":!1,title:`清空对话记录`,message:`将删除卡片「${n.cardName||n.cardId||`当前卡片`}」的全部对话记录,此操作不可撤销。`,"confirm-text":`清空`,severity:`danger`,onConfirm:a.doClear,onCancel:t[19]||=e=>i.confirmClear=!1},null,8,[`visible`,`message`,`onConfirm`]),K(` 终止确认:生成中按 Esc 会走到这里。几十分钟的建卡被一次误按抹掉代价太大,故加一道确认 `),W(u,{visible:i.confirmStop,"close-on-mask":!1,title:`终止本次生成`,message:`已执行的写入与落盘产物都会保留,上下文也留在对话里,之后回复「继续」可从中断处接着做。确定现在终止?`,"confirm-text":`终止生成`,severity:`danger`,onConfirm:a.doStop,onCancel:t[20]||=e=>i.confirmStop=!1},null,8,[`visible`,`onConfirm`])],38)}var xI=X(TF,[[`render`,bI],[`__file`,`/Users/owlaford/Workspace/dsl-card/card-editor/components/ChatPanel.vue`]]),SI=`/assets/site-icon-CFMif8z4.png`,CI={includeMocks:!0};function wI(e){return e?JSON.parse(JSON.stringify(e,(e,t)=>e===`_schemaNode`?void 0:t)):null}function TI(e){let t=wI(e);if(t&&t.dictionary&&Array.isArray(t.dictionary.inputParams)){let e=k_(t);for(let n of t.dictionary.inputParams)n&&typeof n==`object`&&!A_(e,n.key)&&(n.defaultValue=void 0)}return t}function EI(e,t){return!e||!t?e===t:JSON.stringify(TI(e))===JSON.stringify(TI(t))}function DI(e,t){return JSON.stringify(e)===JSON.stringify(t)}function OI(e,t){return!t||!e?!1:!EI(e,t)}function kI(e,t,n){return!t||!e?!1:!DI(e,n??O_(t,CI))}function AI(e,t,n,r){return!t||!e?!1:OI(e,t)||kI(n,t,r)}var jI={includeMocks:!0},MI=2500,NI=new eu,PI=au({},NI);typeof window<`u`&&(window.__rt=PI);var FI=188,II=320,LI=200,RI=160,zI={name:`App`,components:{RenderTree:ap,DebugPanel:Tb,ApiCallPanel:Kb,CardSelect:ux,AlertCardLayer:bx,DeviceOverlay:eS,PickerOverlay:lS,DatePickerOverlay:wS,SelectionOverlay:OS,HoverGuides:PS,RemotePreview:SC,AppToast:kC,ImageLightbox:YC,ConfirmDialog:_p,CardGallery:gw,NewCardDialog:Iw,ComponentPalette:Zw,Switch:tT,ChatPanel:xI},provide(){return{adapter:NI,cardRuntime:PI,customComponents:{}}},data(){return{adapter:NI,rt:PI,logoUrl:SI,appVersion:`0.63.0`,cardList:[],cardCount:0,galleryOpen:!1,cardDialog:{visible:!1,mode:`create`,cardId:``,cardDesc:``},selectedId:``,externalReloadNote:``,externalReloadTimer:null,selfSaveId:``,selfSaveAt:0,preview:null,chromeOff:!1,theme:`dark`,cardReady:!1,reloadToken:0,moveCollapseToken:0,refreshing:!1,loadingCard:!1,currentSchema:null,currentCard:null,cardImportMeta:null,localSchema:null,localData:null,selectedNodeId:``,activeTab:`vars`,apiPanelOpen:!0,addNodePanelOpen:!1,addNodeTarget:null,validateBadge:{state:`idle`,errors:0,warnings:0,renderOk:null,ts:0},validateTimer:null,savedCard:!1,savingCard:!1,saveTimer:null,coverBusy:!1,savingRemote:!1,savedRemote:!1,verifiedRemote:null,saveRemoteTimer:null,syncingRemote:!1,syncedRemote:!1,remotePreviewOpen:!1,syncTarget:null,syncTimer:null,uploadingSlices:!1,publishing:!1,metaRefreshToken:0,loginModal:!1,loginName:``,loginPassword:``,loginSubmitting:!1,loginError:``,confirm:{show:!1,title:``,message:``,confirmText:`继续`,cancelText:`取消`,severity:`primary`,onConfirm:null},past:[],future:[],baseline:null,_commitTimer:null,viewportHeight:700,canvasBg:`gray`,refreshing:!1,bgOptions:[{key:`checker`,label:`棋盘`},{key:`white`,label:`白色`},{key:`gray`,label:`浅灰`},{key:`black`,label:`黑色`}],_previewWidthPx:372,_apiPanelWidthPx:360,presetSizes:[{key:375,value:`375`},{key:430,value:`430`},{key:188,value:`188`}],stageWidth:typeof window<`u`?window.innerWidth:1280,colsWidth:0,stageHeight:typeof window<`u`?window.innerHeight-52:748,dragging:!1,apiDragging:!1,_stageObserver:null,devServiceOpen:!1,aiChatOpen:(()=>{try{return sessionStorage.getItem(`cardbuddy:ai-chat-open`)===`1`}catch{return!1}})(),chatBusy:!1,chatProgress:null,fab:{x:null,y:null,dragging:!1},fabDrag:null,fabSuppressClick:!1,devServiceForm:Nl(),apiBaseResolved:Fl(),devSvcLive:`unknown`,devSvcDown:!1,_unsubCardsChanged:null}},computed:{fabTitle(){if(this.aiChatOpen)return`AI 开发助手(已打开)`;if(!this.chatBusy)return`AI 开发助手`;let e=this.chatProgress,t=e&&e.startedAt?`${mk(e.startedAt)} 开始 · `:``;return`AI 开发助手正在生成(${e?`${t}第 ${e.steps} 步 · 已用 ${pk(e.ms)}${e.stage?` · ${e.stage}`:``}${e.stalled?` · 疑似断流`:``}`:``}),点击展开查看`},fabStyle(){return this.fab.x==null||this.fab.y==null?{}:{right:this.fab.x+`px`,bottom:this.fab.y+`px`,left:`auto`,top:`auto`,transition:this.fab.dragging?`none`:``}},currentBg(){return this.bgOptions.find(e=>e.key===this.canvasBg)||this.bgOptions[0]},cardIds(){return this.cardList.map(e=>e&&e.id).filter(Boolean)},canUndo(){return this.past.length>0},canRedo(){return this.future.length>0},devSvcBlockReason(){let e=this.devServiceForm||{};return String(e.baseUrl||``).trim()?e.enabled?null:`disabled`:`empty`},canPaste(){return!!Hu()},hasCardSource(){return!!(this.cardImportMeta&&this.cardImportMeta.source)},historyDirty(){return AI(this.currentSchema,this.localSchema,this.rt&&this.rt.rootScope,this.localData)},devSvcConn(){return this.apiBaseResolved===null?`unconfigured`:this.devSvcLive===`open`?`open`:this.devSvcDown?`closed`:`connecting`},devSvcTitle(){if(this.apiBaseResolved===null)return this.devSvcBlockReason===`disabled`?`已填地址但未开启用本地服务`:`未配置本地开发服务(部署态必填)`;let e=this.apiBaseResolved===``?`同源开发服务`:`本地开发服务`;return this.devSvcLive===`open`?e+`已连接`:this.devSvcDown?e+`已断开(连接失败或服务已停止,重连中…)`:e+`连接中 / 重连中…`},devSvcClass(){return this.apiBaseResolved===null?`app__btn--warn`:this.devSvcLive===`open`?`app__btn--connected`:this.devSvcDown?`app__btn--disconnected`:`app__btn--reconnecting`},devSvcLabel(){return this.apiBaseResolved===null?`未配置本地服务`:this.devSvcLive===`open`?`本地服务已连接`:this.devSvcDown?`本地服务已断开`:`本地服务连接中…`},badgeText(){return{idle:`待校验`,pending:`校验中…`,ok:`校验通过`,error:`校验未通过`,nopython:`未配置校验`,fail:`校验服务异常`}[this.validateBadge.state]||`待校验`},badgeTitle(){let e=this.validateBadge;if(e.state===`ok`)return`结构合法 + 渲染不抛错(不保证画得对,视觉正确性见截图)`;if(e.state===`error`){let t=[];return e.errors&&t.push(`契约错误 ${e.errors} 个`),e.renderOk===!1&&t.push(`渲染抛出异常`),`校验未通过:`+(t.join(`;`)||`详见 /__validate 与 /__render-status`)}return e.state===`nopython`?`未找到 python3/python,无法跑契约校验(请安装 Python 或设置 DSL_PYTHON)`:e.state===`fail`?`校验服务异常(dev server 未启动或端点不可用)`:``},previewWidth(){if(this.chromeOff)return this.stageWidth;let{min:e,max:t}=this.previewBounds,n=this._previewWidthPx||FI;return Math.max(e,Math.min(t,n))},viewportWidth(){return this.stageWidth,typeof window<`u`?window.innerWidth:1280},debugSoftWidth(){return Math.max(LI,Math.floor(this.viewportWidth/3))},previewBounds(){let e=this.stageWidth||0,t=Math.floor(e/2),n=e-II-this.debugSoftWidth;return{min:FI,max:Math.max(FI,Math.min(t,n))}},debugMinWidth(){let e=this.colsWidth||Math.max(0,(this.stageWidth||0)-this.previewWidth);return Math.max(LI,Math.min(this.debugSoftWidth,Math.floor(e)-II))},apiPanelBounds(){let e=this.colsWidth||Math.max(0,(this.stageWidth||0)-this.previewWidth),t=Math.max(RI,Math.floor(e)-this.debugMinWidth);return{min:Math.min(II,t),max:t}},canvasW(){return Math.round(this.previewWidth||375)},selectedCardName(){let e=this.cardList.find(e=>e.id===this.selectedId);return e&&e.name||this.selectedId||``},cardEmpty(){let e=this.currentSchema;if(!e||!e.view)return!0;let t=e.view.components;return!Array.isArray(t)||t.length===0},apiPanelWidth(){let{min:e,max:t}=this.apiPanelBounds,n=this._apiPanelWidthPx||360;return Math.max(e,Math.min(t,n))},frameStyle(){return{}}},watch:{activeTab(e){e!==`structure`&&(this.selectedNodeId=``,jS())},aiChatOpen(e){try{sessionStorage.setItem(`cardbuddy:ai-chat-open`,e?`1`:`0`)}catch{}},theme(e){document.documentElement.dataset.theme=e},apiPanelOpen(e){try{localStorage.setItem(`cardbuddy:api-panel-open`,e?`1`:`0`)}catch{}}},beforeUnmount(){this.viewportObserver&&this.viewportObserver.disconnect(),this._stageObserver&&this._stageObserver.disconnect(),this.saveTimer&&clearTimeout(this.saveTimer),this.syncTimer&&clearTimeout(this.syncTimer),this.saveRemoteTimer&&clearTimeout(this.saveRemoteTimer),window.removeEventListener(`keydown`,this.onKeydown)},mounted(){this.viewportObserver=new ResizeObserver(e=>{for(let t of e)this.viewportHeight=Math.round(t.contentRect.height)}),this.$refs.phoneViewport&&this.viewportObserver.observe(this.$refs.phoneViewport),this.loadPreviewWidth(),this.loadApiPanelWidth(),this._stageObserver=new ResizeObserver(e=>{for(let t of e){if(t.target===this.$refs.colsEl){this.colsWidth=Math.round(t.contentRect.width);continue}this.stageWidth=Math.round(t.contentRect.width),this.stageHeight=Math.round(t.contentRect.height)}}),this.$refs.stageEl&&this._stageObserver.observe(this.$refs.stageEl),this.$refs.colsEl&&this._stageObserver.observe(this.$refs.colsEl),window.addEventListener(`keydown`,this.onKeydown),this.fabInitPosition(),window.addEventListener(`resize`,this.onFabResize)},beforeDestroy(){window.removeEventListener(`resize`,this.onFabResize)},async created(){Hx(PI);let e=new URLSearchParams(location.search).get(`chrome`);this.chromeOff=e===`off`||e===``;try{let e=localStorage.getItem(`cardbuddy:theme`);(e===`light`||e===`dark`)&&(this.theme=e)}catch{}document.documentElement.dataset.theme=this.theme;try{localStorage.getItem(`cardbuddy:api-panel-open`)===`0`&&(this.apiPanelOpen=!1)}catch{}await this.fetchCardList();let t=new URLSearchParams(location.search).get(`card`),n=null;try{let e=localStorage.getItem(`cardbuddy:selected-card`);e&&this.cardList.some(t=>t.id===e)&&(n=e)}catch{}let r=t&&this.cardList.some(e=>e.id===t)?t:n||this.cardList[0]&&this.cardList[0].id;r&&(this.selectedId=r,await this.loadCard(this.selectedId)),this.$watch(()=>this.rt.rootScope,()=>{this.syncDataToSchema()},{deep:!0}),this.setupCardsChanged()},methods:{fabBounds(){let e=this.$refs.aiFab,t=e?e.offsetWidth:56,n=e?e.offsetHeight:56,r=document.documentElement.clientWidth,i=document.documentElement.clientHeight;return{minX:0,minY:0,maxX:Math.max(0,r-t),maxY:Math.max(0,i-n)}},clampFab(e,t){let n=this.fabBounds();return[Math.min(Math.max(e,n.minX),n.maxX),Math.min(Math.max(t,n.minY),n.maxY)]},fabInitPosition(){let e=this.$refs.aiFab;if(!e)return;let t=this.loadFabPos(),n=document.documentElement.clientWidth,r=document.documentElement.clientHeight,i,a;if(t)[i,a]=this.clampFab(t.x,t.y);else{let t=e.getBoundingClientRect();[i,a]=this.clampFab(n-t.right,r-t.bottom)}this.fab.x=i,this.fab.y=a},loadFabPos(){try{let e=localStorage.getItem(`cardbuddy:ai-fab-pos`);if(!e)return null;let t=JSON.parse(e);if(typeof t.x==`number`&&typeof t.y==`number`&&isFinite(t.x)&&isFinite(t.y))return t}catch{}return null},saveFabPos(){try{localStorage.setItem(`cardbuddy:ai-fab-pos`,JSON.stringify({x:this.fab.x,y:this.fab.y}))}catch{}},onFabPointerDown(e){if(!this.aiChatOpen){this.fabSuppressClick=!1,this.fab.x??this.fabInitPosition(),this.fabDrag={pointerId:e.pointerId,startX:e.clientX,startY:e.clientY,originX:this.fab.x,originY:this.fab.y,moved:!1};try{e.currentTarget.setPointerCapture(e.pointerId)}catch{}}},onFabPointerMove(e){let t=this.fabDrag;if(!t||t.pointerId!==e.pointerId)return;let n=e.clientX-t.startX,r=e.clientY-t.startY;if(!t.moved){if(Math.hypot(n,r)<4)return;t.moved=!0,this.fab.dragging=!0}let[i,a]=this.clampFab(t.originX-n,t.originY-r);this.fab.x=i,this.fab.y=a},onFabPointerUp(e){let t=this.fabDrag;if(!t||t.pointerId!==e.pointerId){this.fab.dragging=!1;return}try{e.currentTarget.releasePointerCapture(e.pointerId)}catch{}t.moved&&(this.fabSuppressClick=!0,this.saveFabPos()),this.fab.dragging=!1,this.fabDrag=null},onFabClick(){if(this.fabSuppressClick){this.fabSuppressClick=!1;return}this.aiChatOpen=!this.aiChatOpen},onFabResize(){if(this.fab.x==null)return;let[e,t]=this.clampFab(this.fab.x,this.fab.y);this.fab.x=e,this.fab.y=t},setupCardsChanged(){if(this._unsubCardsChanged){try{this._unsubCardsChanged()}catch{}this._unsubCardsChanged=null}this.devSvcLive=`connecting`,this._unsubCardsChanged=Vl(e=>this.onCardsChanged(e),e=>this.onDevServiceState(e))},onDevServiceState(e){let t=this.devSvcLive===`connecting`;this.devSvcLive=e,e===`open`?this.devSvcDown=!1:e===`closed`&&(this.devSvcDown=!0),e===`connecting`?!t&&!this._svcWatchdog&&(this._svcWatchdog=setTimeout(()=>{this.devSvcLive===`connecting`&&(this.devSvcLive=`closed`,this.devSvcDown=!0),this._svcWatchdog=null},8e3)):this._svcWatchdog&&=(clearTimeout(this._svcWatchdog),null)},saveDevService(){Pl(this.devServiceForm),this.apiBaseResolved=Fl(),this.setupCardsChanged(),this.devServiceOpen=!1,Z(`本地开发服务配置已保存`,`ok`),this.fetchCardList()},beginColumnDrag(e,t,n){let r=e&&e.currentTarget;if(!r)return;let i=e.pointerId;try{r.setPointerCapture(i)}catch{}let a=()=>{r.removeEventListener(`pointermove`,t),r.removeEventListener(`pointerup`,a),r.removeEventListener(`pointercancel`,a),window.removeEventListener(`blur`,a);try{r.hasPointerCapture?.(i)&&r.releasePointerCapture(i)}catch{}document.body.style.cursor=``,document.body.style.userSelect=``,n()};document.body.style.cursor=`col-resize`,document.body.style.userSelect=`none`,r.addEventListener(`pointermove`,t),r.addEventListener(`pointerup`,a),r.addEventListener(`pointercancel`,a),window.addEventListener(`blur`,a)},startDrag(e){if(this.chromeOff)return;let t=this.$refs.stageEl;if(!t)return;let n=t.getBoundingClientRect().left;this.dragging=!0,this.beginColumnDrag(e,e=>{let{min:t,max:r}=this.previewBounds;this._previewWidthPx=Math.max(t,Math.min(r,Math.round(e.clientX-n)))},()=>{this.dragging=!1,this.savePreviewWidth()})},startApiDrag(e){if(this.chromeOff||!this.apiPanelOpen)return;let t=this.$refs.colsEl;if(!t)return;let n=t.getBoundingClientRect().right;this.apiDragging=!0,this.beginColumnDrag(e,e=>{let{min:t,max:r}=this.apiPanelBounds;this._apiPanelWidthPx=Math.max(t,Math.min(r,Math.round(n-e.clientX)))},()=>{this.apiDragging=!1,this.saveApiPanelWidth()})},loadPreviewWidth(){try{let e=localStorage.getItem(`cardbuddy:preview-width`);if(e!=null){let t=parseInt(e,10);!Number.isNaN(t)&&t>=188&&(this._previewWidthPx=t)}}catch{}},savePreviewWidth(){try{localStorage.setItem(`cardbuddy:preview-width`,String(Math.round(this._previewWidthPx)))}catch{}},setPresetWidth(e){if(this.chromeOff)return;let{min:t,max:n}=this.previewBounds;this._previewWidthPx=Math.max(t,Math.min(n,e)),this.savePreviewWidth()},loadApiPanelWidth(){try{let e=localStorage.getItem(`cardbuddy:api-panel-width`);if(e!=null){let t=parseInt(e,10);!Number.isNaN(t)&&t>=320&&(this._apiPanelWidthPx=t)}}catch{}},saveApiPanelWidth(){try{localStorage.setItem(`cardbuddy:api-panel-width`,String(Math.round(this.apiPanelWidth)))}catch{}},chooseBg(e){this.canvasBg=e},onSelect(e){if(e!==this.selectedId){if(this.chatBusy){Z(`AI 正在生成「${this.selectedId}」:先点 ■ 停止,或等本轮结束后再切卡`,`err`,3600);return}this.loadingCard=!0,this.selectedId=e,this.loadCard(e,{clearAll:!0})}},selectCard(e){this.selectedId=e;try{localStorage.setItem(`cardbuddy:selected-card`,e)}catch{}},openGallery(){this.cardList.length&&(this.galleryOpen=!0)},openNewCard(){this.cardDialog.mode=`create`,this.cardDialog.cardId=``,this.cardDialog.cardDesc=``,this.cardDialog.visible=!0,this.fetchCardList()},openRenameCard(){this.selectedId&&(this.cardDialog.mode=`rename`,this.cardDialog.cardId=this.selectedId,this.cardDialog.cardDesc=this.currentSchema&&this.currentSchema.desc||``,this.cardDialog.visible=!0,this.fetchCardList())},async onCardRenamed({oldId:e,id:t}){if(this.cardDialog.visible=!1,Z(t===e?`已更新卡片描述`:`已重命名卡片 ${e} → ${t}`,`ok`),t!==e)return this.applyCardRename(e,t);await this.fetchCardList(),await this.loadCard(t)},async applyCardRename(e,t){if(!e||!t||e===t)return;let n=`${e}->${t}`;if(this._renameAppliedKey===n)return;this._renameAppliedKey=n,setTimeout(()=>{this._renameAppliedKey===n&&(this._renameAppliedKey=``)},1e3);let r=this.selectedId===e;if(this.$refs.chatPanel?.migrateCard(e,t),r&&this.selectCard(t),this.selfSaveId=t,this.selfSaveAt=Date.now(),await this.fetchCardList(),r){if(this.historyDirty&&!this.confirm.show){this.requestConfirm({title:`卡片已重命名`,message:`卡片「${e}」已重命名为「${t}」,而编辑器内还有未保存的改动。\n重新载入会放弃这些未保存改动并显示磁盘版本;保留改动则继续用当前编辑器内容(后续保存会写到新目录 ${t})。`,confirmText:`重新载入`,cancelText:`保留我的改动`,onConfirm:()=>this.loadCard(t)});return}await this.loadCard(t)}},closeGallery(){this.galleryOpen=!1},async openCardFolder(){if(this.selectedId)try{let e=await J(`/__open-folder`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({id:this.selectedId})}),t=(e.headers.get(`content-type`)||``).includes(`application/json`)?await e.json().catch(()=>({})):{};!e.ok||!t.ok?Z(t.error||`打开文件夹失败`,`err`):Z(`已打开卡片文件夹`,`ok`)}catch{Z(`无法连接本地开发服务,请确认已启动`,`err`)}},async refreshCard(){if(!(!this.selectedId||this.refreshing)){this.refreshing=!0;try{await this.loadCard(this.selectedId)}finally{this.refreshing=!1}}},toggleTheme(){this.theme=this.theme===`light`?`dark`:`light`;try{localStorage.setItem(`cardbuddy:theme`,this.theme)}catch{}},toggleApiPanel(){this.apiPanelOpen=!this.apiPanelOpen},async refreshValidateBadge(){let e=this.selectedId;if(!(!e||!this.preview)&&(this.validateTimer&&clearTimeout(this.validateTimer),this.validateBadge={...this.validateBadge,state:`pending`},await new Promise(e=>{this.validateTimer=setTimeout(e,120)}),this.selectedId===e))try{let[t,n]=await Promise.all([J(`/__validate/${encodeURIComponent(e)}`),J(`/__render-status/${encodeURIComponent(e)}`)]);if(!t.ok){this.validateBadge={...this.validateBadge,state:t.status===503?`nopython`:`fail`};return}let r=await t.json().catch(()=>null),i=n.ok?await n.json().catch(()=>null):null;if(!r){this.validateBadge={...this.validateBadge,state:`fail`};return}let a=!!r.ok,o=!i||!!i.renderOk,s=a&&o?`ok`:`error`,c=this.validateBadge.state!==s;this.validateBadge={state:s,errors:(r.errors||[]).length,warnings:(r.warnings||[]).length,renderOk:o,ts:Date.now()},c&&(s===`ok`||s===`error`)&&this.celebrateValidate(s)}catch{this.validateBadge={...this.validateBadge,state:`fail`}}},celebrateValidate(e=`ok`){let t=this.$refs.validateBadgeEl;if(!t||window.matchMedia&&window.matchMedia(`(prefers-reduced-motion: reduce)`).matches)return;t.classList.remove(`is-celebrating`),t.offsetWidth,t.classList.add(`is-celebrating`),setTimeout(()=>t.classList.remove(`is-celebrating`),600);let n=t.getBoundingClientRect(),r={ok:`--ok`,error:`--danger`,pending:`--warn`,fail:`--text-faint`,nopython:`--text-faint`}[e]||`--ok`,i={ok:`#52c41a`,error:`#F5222D`,pending:`#FAAD14`,fail:`#8c8c8c`,nopython:`#8c8c8c`}[e]||`#52c41a`,a=getComputedStyle(t).getPropertyValue(r).trim()||i,o=document.createElement(`div`);o.className=`app__confetti`,o.style.left=n.left+n.width/2+`px`,o.style.top=n.top+n.height/2+`px`;let s=[`polygon(50% 0%, 0% 100%, 100% 100%)`,`polygon(62% 0%, 100% 40%, 45% 100%, 0% 52%)`,`polygon(0% 0%, 100% 18%, 82% 100%, 12% 70%)`,`polygon(22% 0%, 100% 8%, 88% 100%, 0% 76%)`,`polygon(0% 32%, 100% 0%, 76% 100%, 18% 86%)`,`polygon(8% 0%, 100% 22%, 70% 100%, 0% 64%)`,`polygon(0% 12%, 100% 0%, 100% 78%, 30% 100%, 12% 55%)`],c=n.width/2,l=n.height/2;for(let e=0;e<22;e++){let e=document.createElement(`span`);e.className=`app__confetti-bit`;let t=Math.random()*4|0,n,r;t===0?(n=(Math.random()*2-1)*c,r=-l):t===1?(n=c,r=(Math.random()*2-1)*l):t===2?(n=(Math.random()*2-1)*c,r=l):(n=-c,r=(Math.random()*2-1)*l);let i=Math.hypot(n,r)||1,u=Math.atan2(r/i,n/i)+(Math.random()-.5)*.5,d=16+Math.random()*26;e.style.setProperty(`--sx`,n.toFixed(1)+`px`),e.style.setProperty(`--sy`,r.toFixed(1)+`px`),e.style.setProperty(`--tx`,(Math.cos(u)*d).toFixed(1)+`px`),e.style.setProperty(`--ty`,(Math.sin(u)*d).toFixed(1)+`px`),e.style.setProperty(`--rot`,(Math.random()*720-360).toFixed(0)+`deg`),e.style.setProperty(`--clip`,s[Math.random()*s.length|0]),e.style.setProperty(`--c`,a),e.style.setProperty(`--o`,(.68+Math.random()*.27).toFixed(2));let f=Math.random(),p,m;f<.35?(p=1.5+Math.random()*2,m=10+Math.random()*10):f<.75?(p=4+Math.random()*5,m=p*(.6+Math.random()*.8)):(p=2+Math.random()*2.5,m=p),e.style.width=p.toFixed(0)+`px`,e.style.height=m.toFixed(0)+`px`,o.appendChild(e)}document.body.appendChild(o),setTimeout(()=>o.remove(),800)},async loadCard(e,t={}){if(e){this.loadingCard=!0;try{let n=await J(`/__card/${encodeURIComponent(e)}`);if(!n.ok){this.rt.pushError(`loadCard`,`HTTP `+n.status);return}let r=await n.json();if(!r.schema){this.rt.pushError(`loadCard`,`schema 读取失败`);return}this.selectCard(e),t.clearAll?this.rt.clearErrors():this.rt.clearBindingErrors(),this.rt.clearApiCalls(),t.keepScope||this.rt.setRootScope(O_(r.schema,jI)),this.currentCard={schema:r.schema},this.currentSchema=r.schema,this.cardImportMeta=null,this.fetchImportMeta(e),this.localSchema=this.cloneSchema(r.schema),this.localData=this.cloneData(O_(r.schema,jI)),this.preview=Pu(r.schema),typeof window<`u`&&(window.__cardModel=Bu(r.schema)),this.cardReady=!1,this.$nextTick(()=>{this.cardReady=!0}),this.syncDataToSchema(),this.selectedNodeId=``,this.baseline={schema:this.cloneSchema(this.currentSchema)},this.reloadToken+=1,gx(),Ux(),this.refreshValidateBadge(),await this.loadHistory(e)}finally{this.loadingCard=!1}}},cloneSchema(e){return e?JSON.parse(JSON.stringify(e,(e,t)=>e===`_schemaNode`?void 0:t)):null},cloneData(e){return e==null?e:JSON.parse(JSON.stringify(e,(e,t)=>e===`_schemaNode`?void 0:t))},applySchemaSnapshot(e){if(!this.selectedId||!e)return;let t=e.schema||e,n=e.data==null?O_(t,jI):e.data;this.currentSchema=this.cloneSchema(t),this.rt.setRootScope(this.cloneData(n)),this.preview=Pu(this.currentSchema),typeof window<`u`&&(window.__cardModel=Bu(this.currentSchema)),this.syncDataToSchema(),this.selectedNodeId=``,this.reloadToken+=1,this.refreshValidateBadge()},commitHistory(){if(!this.currentSchema||!this.baseline||!AI(this.currentSchema,this.baseline.schema,this.rt.rootScope))return;let e={schema:this.baseline.schema};this.past.push(e),this.past.length>30&&this.past.shift(),this.future=[],this.baseline={schema:this.cloneSchema(this.currentSchema)},this.persistSnapshot(e)},scheduleCommit(){this._commitTimer&&clearTimeout(this._commitTimer),this._commitTimer=setTimeout(()=>{this._commitTimer=null,this.commitHistory()},500)},persistSnapshot(e){let t=this.selectedId;if(!t)return;let n=e.schema||e;J(`/__history/${encodeURIComponent(t)}`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({schema:n},(e,t)=>e===`_schemaNode`?void 0:t,2)}).catch(e=>console.warn(`[历史快照落盘失败]`,e))},async loadHistory(e){this.past=[],this.future=[];try{let t=await J(`/__history/${encodeURIComponent(e)}`);if(!t.ok)return;let n=await t.json().catch(()=>null),r=n&&n.snapshots||[];this.past=r.slice(-30).map(e=>({schema:e.schema,data:e.data==null?O_(e.schema,jI):e.data}))}catch(e){console.warn(`[加载编辑历史失败]`,e)}},undo(){if(!this.canUndo)return;let e=this.past.pop();this.future.push(this.baseline),this.future.length>30&&this.future.shift(),this.baseline=e,this.applySchemaSnapshot(e)},redo(){if(!this.canRedo)return;let e=this.future.pop();this.past.push(this.baseline),this.past.length>30&&this.past.shift(),this.baseline=e,this.applySchemaSnapshot(e)},async applyHistoryToLocal(){this.loadingCard||!this.historyDirty||!this.preview||!this.selectedId||await this.saveCard()&&Z(`已将该版本保存到本地`,`ok`)},async generateCardCover(){if(!this.preview||!this.selectedId||this.coverBusy)return;if(this.historyDirty&&!await this.saveCard()){Z(`本地保存失败,已取消生成预览图`,`err`);return}let e=null;try{let t=await J(`/__card-image/`+encodeURIComponent(this.selectedId)+`?probe=1`),n=await t.json().catch(()=>({}));t.ok&&n.exists&&(e=n.file)}catch{}if(e){this.requestConfirm({title:`重新生成截图`,message:`该卡片已有截图(${e}),继续将覆盖它。确定重新生成吗?`,confirmText:`重新生成`,severity:`warn`,onConfirm:()=>this.doGenerateCover()});return}this.doGenerateCover()},async doGenerateCover(){this.coverBusy=!0;try{let e=await J(`/__card-image/`+encodeURIComponent(this.selectedId),{method:`POST`}),t=await e.json().catch(()=>({}));if(!e.ok)throw Error(t.message||`HTTP `+e.status);Z(`截图已生成(${t.width}×${t.height})→ ${t.file}`,`ok`,3e3)}catch(e){Z(`生成截图失败:`+String(e&&e.message||e),`err`,3600)}finally{this.coverBusy=!1}},onSchemaChanged(){!this.selectedId||!this.currentSchema||(this.reloadToken+=1,this.preview=Pu(this.currentSchema),typeof window<`u`&&(window.__cardModel=Bu(this.currentSchema)),this.syncDataToSchema(),this.selectedNodeId=``)},onSchemaCommit(){!this.selectedId||!this.currentSchema||(this.reloadToken+=1,this.preview=Pu(this.currentSchema),typeof window<`u`&&(window.__cardModel=Bu(this.currentSchema)),this.syncDataToSchema(),this.selectedNodeId=``,this.commitHistory())},syncDataToSchema(){let e=this.currentSchema;if(!e||!e.dictionary||!Array.isArray(e.dictionary.inputParams))return;let t=this.rt&&this.rt.rootScope;if(!t||typeof t!=`object`)return;let n=e.dictionary.inputParams,r=k_(e);for(let e of Object.keys(t)){let i=n.find(t=>t&&(t.key===e||t.name===e));if(!i)continue;let a=JSON.parse(JSON.stringify(t[e],(e,t)=>e===`_schemaNode`?void 0:t));r&&Object.prototype.hasOwnProperty.call(r,e)?r[e]=a:i.defaultValue=a}},onSelectNode(e){this.selectedNodeId=e||``},findPreviewNodeById(e,t){if(!e||t==null)return null;let n=String(t),r=e=>{if(!e)return null;if(String(e.id)===n)return e;let t=[];if(e.components&&t.push(...e.components),e.slots)for(let n of e.slots)n.components&&t.push(...n.components);for(let e of t){let t=r(e);if(t)return t}return null};return r(e)},copySelectedNode(){let e=this.selectedNodeId;if(!e||e===`_root`||!this.currentSchema||!this.preview)return;let t=this.findPreviewNodeById(this.preview.view,e);if(!t)return;let n=this.findSchemaNodeByComponentId(this.currentSchema.view,t.componentId);n&&(Wu(n.node,this.selectedId),Z(`已复制节点(含子节点)`,`ok`))},pasteNode(){let e=Hu();if(!e||!this.currentSchema)return;let t=Gu(e),n=[...new Set(t.filter(e=>!vu[e]))];if(n.length){Z(`粘贴失败:含未知组件类型 `+n.join(`、`),`err`);return}let r=JSON.parse(JSON.stringify(e,(e,t)=>e===`_schemaNode`?void 0:t));Ku(r);let i=this.selectedNodeId&&this.selectedNodeId!==`_root`?this.findPreviewNodeById(this.preview.view,this.selectedNodeId):null,a=`root`;i&&(a=ju(i.className)?`into`:`after`),this.insertClone(r,i,a);let o=Uu();o&&o!==this.selectedId?Z(`已从卡片「${o}」跨卡片粘贴到「${this.selectedId}」`,`ok`):Z(`已粘贴节点(含子节点)`,`ok`)},duplicateNode(){let e=this.selectedNodeId;if(!e||e===`_root`||!this.currentSchema||!this.preview)return;let t=this.findPreviewNodeById(this.preview.view,e);if(!t)return;let n=this.findSchemaNodeByComponentId(this.currentSchema.view,t.componentId);if(!n)return;let r=JSON.parse(JSON.stringify(n.node,(e,t)=>e===`_schemaNode`?void 0:t));Ku(r),n.parentArr.splice(n.index+1,0,r),this.afterStructuralEdit(r.componentId)},insertClone(e,t,n){let r=this.currentSchema;if(n===`root`)r.view.components||(r.view.components=[]),r.view.components.push(e);else if(n===`into`){let n=t&&t._schemaNode;if(!n){r.view.components||(r.view.components=[]),r.view.components.push(e),this.afterStructuralEdit(e.componentId);return}let i=Mu(t.className),a=(n.slots||[]).find(e=>e.slotKey===i);a||(n.slots||=[],a={slotKey:i,slotScope:!1,slotSwitch:`1`,components:[]},n.slots.push(a)),a.components||(a.components=[]),a.components.push(e)}else{let n=this.findSchemaNodeByComponentId(r.view,t.componentId);n?n.parentArr.splice(n.index+1,0,e):(r.view.components||(r.view.components=[]),r.view.components.push(e))}this.afterStructuralEdit(e.componentId)},afterStructuralEdit(e){let t=this.currentSchema;this.reloadToken+=1,this.preview=Pu(t),typeof window<`u`&&(window.__cardModel=Bu(t)),this.syncDataToSchema(),this.selectedNodeId=e&&this.findViewIdByComponentId(this.preview.view,e)||``,this.scheduleCommit()},deleteSelectedNode(){let e=this.selectedNodeId;if(!e||e===`_root`||!this.currentSchema||!this.preview)return;let t=this.findPreviewNodeById(this.preview.view,e);if(!t)return;let n=this.findSchemaNodeByComponentId(this.currentSchema.view,t.componentId);n&&(n.parentArr.splice(n.index,1),this.reloadToken+=1,this.preview=Pu(this.currentSchema),typeof window<`u`&&(window.__cardModel=Bu(this.currentSchema)),this.syncDataToSchema(),this.selectedNodeId=``,jS(),this.scheduleCommit())},hasTextSelection(){if(typeof window>`u`||!window.getSelection)return!1;let e=window.getSelection();return!!e&&!e.isCollapsed&&e.toString().trim()!==``},hasNodeSelection(){return!!this.selectedNodeId&&this.selectedNodeId!==`_root`},isEditableTarget(e){let t=e.target;if(!t)return!1;let n=t.tagName;return!!(n===`INPUT`||n===`TEXTAREA`||n===`SELECT`||t.isContentEditable)},onKeydown(e){if(this.chromeOff||!this.preview)return;let t=this.isEditableTarget(e),n=e.ctrlKey||e.metaKey,r=e.key;if(n&&(r===`c`||r===`C`)){if(t||this.hasTextSelection()||!this.hasNodeSelection())return;this.copySelectedNode(),e.preventDefault();return}if(n&&(r===`v`||r===`V`)){if(t||this.hasTextSelection()||!this.hasNodeSelection()||!Hu())return;this.pasteNode(),e.preventDefault();return}if(n&&(r===`d`||r===`D`)){if(t||this.hasTextSelection()||!this.hasNodeSelection())return;this.duplicateNode(),e.preventDefault();return}if(n&&(r===`z`||r===`Z`)){if(t)return;e.preventDefault(),e.shiftKey?this.redo():this.undo();return}if(n&&(r===`y`||r===`Y`)){if(t)return;e.preventDefault(),this.redo();return}if(r===`Delete`||r===`Backspace`){if(t||!this.selectedNodeId||this.selectedNodeId===`_root`)return;e.preventDefault(),this.deleteSelectedNode();return}if(r===`Escape`){this.selectedNodeId&&(this.selectedNodeId=``,e.preventDefault());return}},onHoverNode(e){if(!e){jS();return}if(e.leave){jS(e.id);return}let t=typeof document<`u`?document.querySelector(`.phone__screen`):null;if(!t){jS(e.id);return}let n=e=>window.CSS&&CSS.escape?CSS.escape(String(e)):String(e).replace(/["\\\]]/g,`\\$&`),r=null;e.componentId!=null&&(r=t.querySelector(`[data-cid="`+n(e.componentId)+`"]`)),!r&&e.id!=null&&(r=t.querySelector(`[data-cid="`+n(e.id)+`"]`)),r?AS(r,e.id):jS(e.id)},onCanvasClick(e){if(!e.ctrlKey&&!e.metaKey||this.chromeOff)return;let t=e.target&&e.target.closest?e.target.closest(`[data-cid]`):null;if(!t)return;let n=t.getAttribute(`data-cid`);if(!n)return;let r=this.findPreviewNodeByCid(n);r&&(this.activeTab=`structure`,this.selectedNodeId=r.id)},onAiLocateNode(e){let t=e?this.findPreviewNodeByCid(e):null;if(!t){Z(`没找到组件 ${e||``}:卡片可能已被改动,重新校验一次即可`,`err`);return}this.activeTab=`structure`,this.selectedNodeId=t.id},findPreviewNodeByCid(e){let t=this.preview&&this.preview.view;if(!t)return null;let n=String(e),r=e=>{if(!e)return null;if(String(e.componentId==null?e.id:e.componentId)===n)return e;let t=[];if(e.components&&t.push(...e.components),e.slots)for(let n of e.slots)n.components&&t.push(...n.components);for(let e of t){let t=r(e);if(t)return t}return null};return r(t)},onAddNode(e){this.currentSchema&&(this.addNodeTarget=e||null,this.addNodePanelOpen=!0)},cancelAddNode(){this.addNodePanelOpen=!1,this.addNodeTarget=null},findSchemaLocation(e,t){let n=e=>{for(let r=0;r<e.length;r++){let i=e[r];if(i===t)return{containerArr:e,index:r};let a=i.slots||[];for(let e of a){let t=n(e.components||[]);if(t)return t}}return null};return n(e&&e.components||[])},findViewIdByComponentId(e,t){if(!e)return null;if(e.componentId===t)return e.id;let n=[];if(e.components&&n.push(...e.components),e.slots)for(let t of e.slots)t.components&&n.push(...t.components);for(let e of n){let n=this.findViewIdByComponentId(e,t);if(n)return n}return null},findSchemaNodeByComponentId(e,t){let n=(e,r)=>{for(let i=0;i<e.length;i++){let a=e[i];if(a.componentId===t)return{node:a,parentArr:e,index:i,owner:r};let o=a.slots||[];for(let e of o){let t=n(e.components||[],a);if(t)return t}}return null};return n(e&&e.components||[],`root`)},isDescendantCompId(e,t){if(!t||!t.componentId)return!1;if(t.componentId===e)return!0;let n=[];if(t.components&&n.push(...t.components),t.slots)for(let e of t.slots)e.components&&n.push(...e.components);for(let t of n)if(this.isDescendantCompId(e,t))return!0;return!1},moveNode(e,t,n){let r=this.currentSchema;if(!r||!r.view||e===t)return!1;let i=this.findSchemaNodeByComponentId(r.view,e);if(!i)return!1;let a=i.node;if(this.isDescendantCompId(t,a))return!1;let o,s;if(t===`__root__`)r.view.components||(r.view.components=[]),o=r.view.components,s=o.length;else{let e=this.findSchemaNodeByComponentId(r.view,t);if(!e)return!1;let i=e.node;if(n===`inside`){if(!ju(i.className))return!1;let e=Mu(i.className),t=(i.slots||[]).find(t=>t.slotKey===e);t||(i.slots||=[],t={slotKey:e,slotScope:!1,slotSwitch:`1`,components:[]},i.slots.push(t)),t.components||(t.components=[]),o=t.components,s=o.length}else o=e.parentArr,s=n===`before`?e.index:e.index+1}let c=o===i.parentArr&&i.index<s;return i.parentArr.splice(i.index,1),c&&--s,o.splice(s,0,a),a},onMoveNode({sourceCompId:e,targetCompId:t,position:n}){if(!this.currentSchema||!this.moveNode(e,t,n))return;this.reloadToken+=1,this.preview=Pu(this.currentSchema),typeof window<`u`&&(window.__cardModel=Bu(this.currentSchema)),this.syncDataToSchema();let r=this.findViewIdByComponentId(this.preview.view,e);this.selectedNodeId=r||``,r&&(this.moveCollapseToken+=1)},onPickComponent(e){let t=this.addNodeTarget||{},n=t.node||null,r=t.mode||`auto`,i=this.currentSchema;if(!i){this.cancelAddNode();return}let a;try{a=Nu(e)}catch(e){console.error(`[创建组件节点失败]`,e),this.rt.pushError(`addNode`,String(e&&e.message)),this.cancelAddNode();return}let o=!n||n.className===`Root`;if((r===`before`||r===`after`)&&!o){let e=n._schemaNode,t=e?this.findSchemaLocation(i.view,e):null;if(!t)i.view.components||(i.view.components=[]),i.view.components.push(a);else{let e=r===`before`?t.index:t.index+1;t.containerArr.splice(e,0,a)}}else if(o)i.view.components||(i.view.components=[]),i.view.components.push(a);else{let e=n._schemaNode;if(!e){this.cancelAddNode();return}if(ju(n.className)){let t=Mu(n.className),r=(e.slots||[]).find(e=>e.slotKey===t);r||(e.slots||=[],r={slotKey:t,slotScope:!1,slotSwitch:`1`,components:[]},e.slots.push(r)),r.components||(r.components=[]),r.components.push(a)}else{let t=this.findSchemaLocation(i.view,e);t?t.containerArr.splice(t.index+1,0,a):(i.view.components||(i.view.components=[]),i.view.components.push(a))}}this.cancelAddNode(),this.reloadToken+=1,this.preview=Pu(i),typeof window<`u`&&(window.__cardModel=Bu(i)),this.syncDataToSchema();let s=this.findViewIdByComponentId(this.preview.view,a.componentId);this.selectedNodeId=s||``,this.scheduleCommit()},async runSync(e){if(!this.syncingRemote&&!(e&&!this.selectedId)){this.syncingRemote=!0,this.syncedRemote=!1,this.syncTarget=e?`selected`:`full`;try{let t=await J(`/__sync`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify(e?{cardCode:e}:{})});if(!t.ok){let e=await t.json().catch(()=>({}));throw Error(e.message||e.error||`HTTP `+t.status)}let n=await t.json();if(n.needCredentials){this.openLoginModal();return}if(!n.success){let e=(n.failed||[]).map(e=>`${e.id}: ${e.error}`).join(`; `);throw Error(e||`同步失败`)}this.syncedRemote=!0,this.syncTimer&&clearTimeout(this.syncTimer),this.syncTimer=setTimeout(()=>{this.syncedRemote=!1,this.syncTarget=null},1800),await this.onRefresh(),Z(this.syncTarget===`selected`?`已拉取最新卡到本地`:`拉取全部卡完成`,`ok`),this.persistRemoteVersion(this.selectedId,null)}catch(e){console.error(`[同步远程数据失败]`,e),Z(`同步失败:`+(e&&e.message),`err`)}finally{this.syncingRemote=!1}}},syncRemote(){return this.runSync()},syncSelected(){return this.runSync(this.selectedId)},openLoginModal(){this.loginModal=!0,this.loginName=``,this.loginPassword=``,this.loginError=``},closeLoginModal(){this.loginSubmitting||(this.loginModal=!1,this.loginPassword=``,this.loginError=``)},async submitLogin(){if(this.loginSubmitting)return;let e=this.loginName.trim(),t=this.loginPassword;if(!e||!t){this.loginError=`账号与密码均不能为空`;return}this.loginSubmitting=!0,this.loginError=``;try{let n=await J(`/__login-credentials`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({loginName:e,loginPassword:t})}),r=await n.json().catch(()=>({}));if(!n.ok||!r.ok)throw Error(r.message||r.error||`HTTP `+n.status);this.loginModal=!1,this.loginPassword=``,Z(`凭据已保存,正在重试同步…`,`ok`),await this.runSync(this.syncTarget===`selected`?this.selectedId:void 0)}catch(e){this.loginError=`保存失败:`+(e&&e.message)}finally{this.loginSubmitting=!1}},chooseSync(e){if(e===`full`){if(this.syncingRemote)return;this.requestConfirm({title:`拉取全部卡`,message:`即将从远端 FinMall 拉取完整卡片列表,并逐张覆盖本地同名卡片。本地独有卡片会保留,但被覆盖卡片的未保存改动将丢失,且无法撤销。是否继续?`,confirmText:`继续同步`,severity:`warn`,onConfirm:()=>this.syncRemote()});return}!this.preview||!this.selectedId||this.syncingRemote||this.requestConfirm({title:`拉取最新卡`,message:`即将从远端 FinMall 拉取当前卡片「${this.selectedId}」的最新版本并覆盖本地。本地未保存的改动将丢失,且无法撤销。是否继续?`,confirmText:`继续同步`,onConfirm:()=>this.syncSelected()})},chooseRsave(e){if(e===`save`){if(!this.preview||!this.selectedId||this.savingRemote||!this.hasCardSource)return;this.requestConfirm({title:`推送当前卡`,message:`即将把本地卡片「${this.selectedId}」的当前版本推送并覆盖到远端 FinMall。线上版本会被立即更新,且该操作不可撤销。是否继续?`,confirmText:`继续推送`,severity:`warn`,onConfirm:()=>this.saveToRemote()});return}if(e===`publish`){if(!this.preview||!this.selectedId||this.publishing||!this.hasCardSource)return;let e=this.historyDirty;this.requestConfirm({title:`卡片发布`,message:e?`当前卡片存在未保存到远端的改动,发布将使用远端「已保存」的版本(不含这些本地改动)。仍将以「最大版本号 +1」作为新版本号上线。确认继续?`:`即将取远端「${this.selectedId}」当前最大版本号并 +1,发布上线为新版本。发布的是远端已保存的卡片内容(建议先「推送当前卡」)。该操作不可撤销。确认继续?`,confirmText:`继续发布`,severity:e?`danger`:`warn`,onConfirm:()=>this.doPublish()});return}if(e===`slices`){if(!this.preview||!this.selectedId||this.uploadingSlices)return;this.requestConfirm({title:`当前卡切图上传`,message:`即将把当前卡片「${this.selectedId}」引用的本地切图(src/slices/)上传到 FinMall 图片库,并把卡片里的 /slices/ 引用替换为线上地址(@filestore/…)。本地切图文件保留不动;内容未变的切图会直接复用已有线上地址,不重复上传。`+(this.historyDirty?`
317
+ 长任务里「还活着吗」全靠这一行——旧版只有一句「生成中…」,网关静默与真在跑无从区分 `),i.busy?(B(),V(`div`,{key:0,class:A([`aichat__live`,{"is-stalled":a.stalled}]),role:`status`,"aria-live":`polite`},[t[33]||=U(`span`,{class:`aichat__live-dot`},null,-1),U(`span`,YF,j(a.liveText),1),a.stalled?(B(),V(`button`,{key:0,type:`button`,class:`aichat__live-act`,title:`连接可能已中断,终止本轮(已落盘的产物会保留)`,onClick:t[9]||=(...e)=>a.stop&&a.stop(...e)},` 终止 `)):K(`v-if`,!0)],2)):K(`v-if`,!0)],64)):(B(),V(`div`,RF,[t[32]||=U(`span`,{class:`aichat__empty-logo`},[U(`svg`,{viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,"stroke-width":`2`,"stroke-linecap":`round`,"stroke-linejoin":`round`,"aria-hidden":`true`},[U(`use`,{href:`#icon-sparkles`})])],-1),U(`p`,zF,j(a.emptyTitle),1),U(`p`,BF,j(a.emptyDesc),1),U(`div`,VF,[(B(!0),V(z,null,R(a.suggestions,e=>(B(),V(`button`,{key:e.id,type:`button`,class:A([`aichat__suggest`,`is-${e.weight}`]),title:e.text,"data-suggest-id":e.id,onClick:t=>a.useSuggestion(e)},[(B(),V(`svg`,UF,[U(`use`,{href:e.icon},null,8,WF)])),U(`span`,GF,[U(`span`,KF,j(e.label),1),U(`span`,qF,j(e.hint),1)]),e.weight===`write`?(B(),V(`span`,JF,`改卡`)):K(`v-if`,!0)],10,HF))),128)),a.canShuffle?(B(),V(`button`,{key:0,type:`button`,class:`aichat__suggest-shuffle`,onClick:t[7]||=(...e)=>a.shuffleSuggestions&&a.shuffleSuggestions(...e)},[...t[31]||=[U(`svg`,{viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,"stroke-width":`2`,"stroke-linecap":`round`,"stroke-linejoin":`round`,"aria-hidden":`true`},[U(`use`,{href:`#icon-refresh`})],-1),U(`span`,null,`换一批`,-1)]])):K(`v-if`,!0)])]))],544),K(` 回到底部:脱离底部吸附时出现 `),I(U(`button`,{type:`button`,class:`aichat__jump`,title:`回到底部`,onClick:t[11]||=e=>a.scrollToBottom(!0)},[...t[34]||=[U(`svg`,{viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,"stroke-width":`2`,"stroke-linecap":`round`,"stroke-linejoin":`round`,"aria-hidden":`true`},[U(`use`,{href:`#icon-arrow-down`})],-1)]],512),[[Sc,!i.pinned&&i.messages.length]])]),K(` 常驻「常用」指令:有对话后空态建议区不再出现,这里保留一排轻量入口,随上下文同源 `),a.quickChips.length&&i.messages.length&&!i.busy?(B(),V(`div`,XF,[(B(!0),V(z,null,R(a.quickChips,e=>(B(),V(`button`,{key:e.id,type:`button`,class:A([`aichat__chip`,`is-${e.weight}`]),title:e.text,"data-suggest-id":e.id,onClick:t=>a.useSuggestion(e)},[(B(),V(`svg`,QF,[U(`use`,{href:e.icon},null,8,$F)])),U(`span`,null,j(e.label),1)],10,ZF))),128))])):K(`v-if`,!0),K(` 操作反馈(附件解析失败 / 历史保存失败等) `),i.notice?(B(),V(`div`,{key:2,class:A([`aichat__notice`,`aichat__notice--${i.notice.type}`])},[U(`span`,null,j(i.notice.text),1),U(`button`,{type:`button`,class:`aichat__notice-x`,title:`关闭`,onClick:t[12]||=e=>i.notice=null},[...t[35]||=[U(`svg`,{viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,"stroke-width":`2`,"stroke-linecap":`round`,"stroke-linejoin":`round`,"aria-hidden":`true`},[U(`use`,{href:`#icon-x`})],-1)]])],2)):K(`v-if`,!0),W(s,{ref:`composerEl`,busy:i.busy,model:i.config.model,attachments:i.draftAttachments,placeholder:a.configReady?`描述你要改的卡片,Enter 发送 / Shift+Enter 换行`:`请先在设置中配置模型…`,context:i.contextUsage,onSend:a.onSend,onStop:a.stop,onPick:a.onPick,onAddPath:a.onAddPath,onRemoveAttachment:a.removeAttachment,onDropFiles:a.onDropFiles,onPasteImage:a.onPasteImage,onEscape:a.onEscape},null,8,[`busy`,`model`,`attachments`,`placeholder`,`context`,`onSend`,`onStop`,`onPick`,`onAddPath`,`onRemoveAttachment`,`onDropFiles`,`onPasteImage`,`onEscape`]),K(` 一级面板:模型库。从齿轮 / 横幅 / 消息内「去设置」进入 `),i.modelListOpen?(B(),H(c,{key:3,models:i.models,"active-id":i.activeId,"turn-running":i.turnRunning,onClose:a.closeModelList,onAdd:a.onModelAdd,onEdit:a.onModelEdit,onDelete:a.onModelDelete,onActivate:a.onModelActivate,onImportCcswitch:a.onImportCcSwitch,onOpenCommon:a.openCommon},null,8,[`models`,`active-id`,`turn-running`,`onClose`,`onAdd`,`onEdit`,`onDelete`,`onActivate`,`onImportCcswitch`,`onOpenCommon`])):K(`v-if`,!0),K(` 新增 / 编辑模型:复用 AiSettings(modelId 区分,initialModel 预填) `),i.modelEditOpen?(B(),H(l,{key:4,config:i.config,"model-id":i.editModelId,"initial-model":i.editInitialModel,onClose:a.onModelEditClose,onSaved:a.onModelSaved},null,8,[`config`,`model-id`,`initial-model`,`onClose`,`onSaved`])):K(`v-if`,!0),K(` 删除模型确认:二次确认后才真正落盘删除 `),W(u,{visible:!!i.confirmDeleteModel,"close-on-mask":!1,title:`删除模型`,message:a.deleteModelMessage,"confirm-text":`删除`,severity:`danger`,onConfirm:a.doDeleteModel,onCancel:t[13]||=e=>i.confirmDeleteModel=null},null,8,[`visible`,`message`,`onConfirm`]),K(` cc-switch 导入弹窗:列出 cc-switch 的 Codex 类供应商,勾选后批量导入模型库(按 model 去重) `),W(ic,{name:`modal`},{default:F(()=>[i.ccSwitchOpen?(B(),V(`div`,{key:0,class:`ni-overlay`,onClick:t[17]||=q(e=>i.ccSwitchOpen=!1,[`self`])},[U(`div`,eI,[U(`div`,tI,[t[36]||=U(`span`,{class:`ni-title`},`从 cc-switch 导入`,-1),U(`button`,{class:`ni-close`,type:`button`,title:`关闭`,onClick:t[14]||=e=>i.ccSwitchOpen=!1})]),U(`div`,nI,[U(`div`,rI,[K(` 加载中 `),i.ccSwitchLoading?(B(),V(`p`,iI,`正在读取 cc-switch 供应商…`)):i.ccSwitchReason?(B(),V(z,{key:1},[K(` 无可导入项 `),U(`p`,aI,j(i.ccSwitchReason),1)],2112)):(B(),V(z,{key:2},[K(` 供应商列表 `),U(`ul`,oI,[(B(!0),V(z,null,R(i.ccSwitchProviders,e=>(B(),V(`li`,{key:e.id,class:A([`ccsw-dlg__item`,{"is-active":i.ccSwitchSelected[e.id],"is-dup":e.duplicate}]),onClick:t=>a.toggleCcSwitchItem(e)},[U(`span`,{class:A([`ccsw-dlg__check`,{"is-on":i.ccSwitchSelected[e.id]}])},[i.ccSwitchSelected[e.id]?(B(),V(`svg`,cI,[...t[37]||=[U(`use`,{href:`#icon-check`},null,-1)]])):K(`v-if`,!0)],2),U(`span`,lI,[U(`span`,uI,[G(j(e.name||e.model)+` `,1),e.isCurrent?(B(),V(`span`,dI,`当前`)):K(`v-if`,!0),e.duplicate?(B(),V(`span`,fI,`已存在`)):K(`v-if`,!0)]),U(`span`,pI,[U(`span`,mI,j(e.model),1),t[38]||=U(`span`,{class:`ccsw-dlg__dot`},`·`,-1),U(`span`,hI,j(e.baseUrl),1)])])],10,sI))),128))])],2112))])]),U(`div`,gI,[U(`span`,_I,j(a.ccSwitchDupCount>0?`已有 ${a.ccSwitchDupCount} 个重复模型被自动跳过`:`勾选要导入的供应商,批量加入模型库`),1),U(`div`,vI,[U(`button`,{class:`ni-btn ni-btn--ghost`,type:`button`,onClick:t[15]||=e=>i.ccSwitchOpen=!1},`取消`),U(`button`,{class:`ni-btn`,type:`button`,disabled:i.ccSwitchLoading||!i.ccSwitchProviders.length||a.ccSwitchCheckedCount===0,onClick:t[16]||=(...e)=>a.confirmImportCcSwitch&&a.confirmImportCcSwitch(...e)},j(a.ccSwitchCheckedCount>0?`导入选中 ${a.ccSwitchCheckedCount} 项`:`导入`),9,yI)])])])])):K(`v-if`,!0)]),_:1}),K(` 通用设置:所有模型共享的执行策略 / 环境参数 `),i.commonOpen?(B(),H(d,{key:5,onClose:t[18]||=e=>i.commonOpen=!1,onSaved:a.onCommonSaved},null,8,[`onSaved`])):K(`v-if`,!0),W(u,{visible:i.confirmClear,"close-on-mask":!1,title:`清空对话记录`,message:`将删除卡片「${n.cardName||n.cardId||`当前卡片`}」的全部对话记录,此操作不可撤销。`,"confirm-text":`清空`,severity:`danger`,onConfirm:a.doClear,onCancel:t[19]||=e=>i.confirmClear=!1},null,8,[`visible`,`message`,`onConfirm`]),K(` 终止确认:生成中按 Esc 会走到这里。几十分钟的建卡被一次误按抹掉代价太大,故加一道确认 `),W(u,{visible:i.confirmStop,"close-on-mask":!1,title:`终止本次生成`,message:`已执行的写入与落盘产物都会保留,上下文也留在对话里,之后回复「继续」可从中断处接着做。确定现在终止?`,"confirm-text":`终止生成`,severity:`danger`,onConfirm:a.doStop,onCancel:t[20]||=e=>i.confirmStop=!1},null,8,[`visible`,`onConfirm`])],38)}var xI=X(TF,[[`render`,bI],[`__file`,`/Users/owlaford/Workspace/dsl-card/card-editor/components/ChatPanel.vue`]]),SI=`/assets/site-icon-CFMif8z4.png`,CI={includeMocks:!0};function wI(e){return e?JSON.parse(JSON.stringify(e,(e,t)=>e===`_schemaNode`?void 0:t)):null}function TI(e){let t=wI(e);if(t&&t.dictionary&&Array.isArray(t.dictionary.inputParams)){let e=k_(t);for(let n of t.dictionary.inputParams)n&&typeof n==`object`&&!A_(e,n.key)&&(n.defaultValue=void 0)}return t}function EI(e,t){return!e||!t?e===t:JSON.stringify(TI(e))===JSON.stringify(TI(t))}function DI(e,t){return JSON.stringify(e)===JSON.stringify(t)}function OI(e,t){return!t||!e?!1:!EI(e,t)}function kI(e,t,n){return!t||!e?!1:!DI(e,n??O_(t,CI))}function AI(e,t,n,r){return!t||!e?!1:OI(e,t)||kI(n,t,r)}var jI={includeMocks:!0},MI=2500,NI=new eu,PI=au({},NI);typeof window<`u`&&(window.__rt=PI);var FI=188,II=320,LI=200,RI=160,zI={name:`App`,components:{RenderTree:ap,DebugPanel:Tb,ApiCallPanel:Kb,CardSelect:ux,AlertCardLayer:bx,DeviceOverlay:eS,PickerOverlay:lS,DatePickerOverlay:wS,SelectionOverlay:OS,HoverGuides:PS,RemotePreview:SC,AppToast:kC,ImageLightbox:YC,ConfirmDialog:_p,CardGallery:gw,NewCardDialog:Iw,ComponentPalette:Zw,Switch:tT,ChatPanel:xI},provide(){return{adapter:NI,cardRuntime:PI,customComponents:{}}},data(){return{adapter:NI,rt:PI,logoUrl:SI,appVersion:`0.64.0`,cardList:[],cardCount:0,galleryOpen:!1,cardDialog:{visible:!1,mode:`create`,cardId:``,cardDesc:``},selectedId:``,externalReloadNote:``,externalReloadTimer:null,selfSaveId:``,selfSaveAt:0,preview:null,chromeOff:!1,theme:`dark`,cardReady:!1,reloadToken:0,moveCollapseToken:0,refreshing:!1,loadingCard:!1,currentSchema:null,currentCard:null,cardImportMeta:null,localSchema:null,localData:null,selectedNodeId:``,activeTab:`vars`,apiPanelOpen:!0,addNodePanelOpen:!1,addNodeTarget:null,validateBadge:{state:`idle`,errors:0,warnings:0,renderOk:null,ts:0},validateTimer:null,savedCard:!1,savingCard:!1,saveTimer:null,coverBusy:!1,savingRemote:!1,savedRemote:!1,verifiedRemote:null,saveRemoteTimer:null,syncingRemote:!1,syncedRemote:!1,remotePreviewOpen:!1,syncTarget:null,syncTimer:null,uploadingSlices:!1,publishing:!1,metaRefreshToken:0,loginModal:!1,loginName:``,loginPassword:``,loginSubmitting:!1,loginError:``,confirm:{show:!1,title:``,message:``,confirmText:`继续`,cancelText:`取消`,severity:`primary`,onConfirm:null},past:[],future:[],baseline:null,_commitTimer:null,viewportHeight:700,canvasBg:`gray`,refreshing:!1,bgOptions:[{key:`checker`,label:`棋盘`},{key:`white`,label:`白色`},{key:`gray`,label:`浅灰`},{key:`black`,label:`黑色`}],_previewWidthPx:372,_apiPanelWidthPx:360,presetSizes:[{key:375,value:`375`},{key:430,value:`430`},{key:188,value:`188`}],stageWidth:typeof window<`u`?window.innerWidth:1280,colsWidth:0,stageHeight:typeof window<`u`?window.innerHeight-52:748,dragging:!1,apiDragging:!1,_stageObserver:null,devServiceOpen:!1,aiChatOpen:(()=>{try{return sessionStorage.getItem(`cardbuddy:ai-chat-open`)===`1`}catch{return!1}})(),chatBusy:!1,chatProgress:null,fab:{x:null,y:null,dragging:!1},fabDrag:null,fabSuppressClick:!1,devServiceForm:Nl(),apiBaseResolved:Fl(),devSvcLive:`unknown`,devSvcDown:!1,_unsubCardsChanged:null}},computed:{fabTitle(){if(this.aiChatOpen)return`AI 开发助手(已打开)`;if(!this.chatBusy)return`AI 开发助手`;let e=this.chatProgress,t=e&&e.startedAt?`${mk(e.startedAt)} 开始 · `:``;return`AI 开发助手正在生成(${e?`${t}第 ${e.steps} 步 · 已用 ${pk(e.ms)}${e.stage?` · ${e.stage}`:``}${e.stalled?` · 疑似断流`:``}`:``}),点击展开查看`},fabStyle(){return this.fab.x==null||this.fab.y==null?{}:{right:this.fab.x+`px`,bottom:this.fab.y+`px`,left:`auto`,top:`auto`,transition:this.fab.dragging?`none`:``}},currentBg(){return this.bgOptions.find(e=>e.key===this.canvasBg)||this.bgOptions[0]},cardIds(){return this.cardList.map(e=>e&&e.id).filter(Boolean)},canUndo(){return this.past.length>0},canRedo(){return this.future.length>0},devSvcBlockReason(){let e=this.devServiceForm||{};return String(e.baseUrl||``).trim()?e.enabled?null:`disabled`:`empty`},canPaste(){return!!Hu()},hasCardSource(){return!!(this.cardImportMeta&&this.cardImportMeta.source)},historyDirty(){return AI(this.currentSchema,this.localSchema,this.rt&&this.rt.rootScope,this.localData)},devSvcConn(){return this.apiBaseResolved===null?`unconfigured`:this.devSvcLive===`open`?`open`:this.devSvcDown?`closed`:`connecting`},devSvcTitle(){if(this.apiBaseResolved===null)return this.devSvcBlockReason===`disabled`?`已填地址但未开启用本地服务`:`未配置本地开发服务(部署态必填)`;let e=this.apiBaseResolved===``?`同源开发服务`:`本地开发服务`;return this.devSvcLive===`open`?e+`已连接`:this.devSvcDown?e+`已断开(连接失败或服务已停止,重连中…)`:e+`连接中 / 重连中…`},devSvcClass(){return this.apiBaseResolved===null?`app__btn--warn`:this.devSvcLive===`open`?`app__btn--connected`:this.devSvcDown?`app__btn--disconnected`:`app__btn--reconnecting`},devSvcLabel(){return this.apiBaseResolved===null?`未配置本地服务`:this.devSvcLive===`open`?`本地服务已连接`:this.devSvcDown?`本地服务已断开`:`本地服务连接中…`},badgeText(){return{idle:`待校验`,pending:`校验中…`,ok:`校验通过`,error:`校验未通过`,nopython:`未配置校验`,fail:`校验服务异常`}[this.validateBadge.state]||`待校验`},badgeTitle(){let e=this.validateBadge;if(e.state===`ok`)return`结构合法 + 渲染不抛错(不保证画得对,视觉正确性见截图)`;if(e.state===`error`){let t=[];return e.errors&&t.push(`契约错误 ${e.errors} 个`),e.renderOk===!1&&t.push(`渲染抛出异常`),`校验未通过:`+(t.join(`;`)||`详见 /__validate 与 /__render-status`)}return e.state===`nopython`?`未找到 python3/python,无法跑契约校验(请安装 Python 或设置 DSL_PYTHON)`:e.state===`fail`?`校验服务异常(dev server 未启动或端点不可用)`:``},previewWidth(){if(this.chromeOff)return this.stageWidth;let{min:e,max:t}=this.previewBounds,n=this._previewWidthPx||FI;return Math.max(e,Math.min(t,n))},viewportWidth(){return this.stageWidth,typeof window<`u`?window.innerWidth:1280},debugSoftWidth(){return Math.max(LI,Math.floor(this.viewportWidth/3))},previewBounds(){let e=this.stageWidth||0,t=Math.floor(e/2),n=e-II-this.debugSoftWidth;return{min:FI,max:Math.max(FI,Math.min(t,n))}},debugMinWidth(){let e=this.colsWidth||Math.max(0,(this.stageWidth||0)-this.previewWidth);return Math.max(LI,Math.min(this.debugSoftWidth,Math.floor(e)-II))},apiPanelBounds(){let e=this.colsWidth||Math.max(0,(this.stageWidth||0)-this.previewWidth),t=Math.max(RI,Math.floor(e)-this.debugMinWidth);return{min:Math.min(II,t),max:t}},canvasW(){return Math.round(this.previewWidth||375)},selectedCardName(){let e=this.cardList.find(e=>e.id===this.selectedId);return e&&e.name||this.selectedId||``},cardEmpty(){let e=this.currentSchema;if(!e||!e.view)return!0;let t=e.view.components;return!Array.isArray(t)||t.length===0},apiPanelWidth(){let{min:e,max:t}=this.apiPanelBounds,n=this._apiPanelWidthPx||360;return Math.max(e,Math.min(t,n))},frameStyle(){return{}}},watch:{activeTab(e){e!==`structure`&&(this.selectedNodeId=``,jS())},aiChatOpen(e){try{sessionStorage.setItem(`cardbuddy:ai-chat-open`,e?`1`:`0`)}catch{}},theme(e){document.documentElement.dataset.theme=e},apiPanelOpen(e){try{localStorage.setItem(`cardbuddy:api-panel-open`,e?`1`:`0`)}catch{}}},beforeUnmount(){this.viewportObserver&&this.viewportObserver.disconnect(),this._stageObserver&&this._stageObserver.disconnect(),this.saveTimer&&clearTimeout(this.saveTimer),this.syncTimer&&clearTimeout(this.syncTimer),this.saveRemoteTimer&&clearTimeout(this.saveRemoteTimer),window.removeEventListener(`keydown`,this.onKeydown)},mounted(){this.viewportObserver=new ResizeObserver(e=>{for(let t of e)this.viewportHeight=Math.round(t.contentRect.height)}),this.$refs.phoneViewport&&this.viewportObserver.observe(this.$refs.phoneViewport),this.loadPreviewWidth(),this.loadApiPanelWidth(),this._stageObserver=new ResizeObserver(e=>{for(let t of e){if(t.target===this.$refs.colsEl){this.colsWidth=Math.round(t.contentRect.width);continue}this.stageWidth=Math.round(t.contentRect.width),this.stageHeight=Math.round(t.contentRect.height)}}),this.$refs.stageEl&&this._stageObserver.observe(this.$refs.stageEl),this.$refs.colsEl&&this._stageObserver.observe(this.$refs.colsEl),window.addEventListener(`keydown`,this.onKeydown),this.fabInitPosition(),window.addEventListener(`resize`,this.onFabResize)},beforeDestroy(){window.removeEventListener(`resize`,this.onFabResize)},async created(){Hx(PI);let e=new URLSearchParams(location.search).get(`chrome`);this.chromeOff=e===`off`||e===``;try{let e=localStorage.getItem(`cardbuddy:theme`);(e===`light`||e===`dark`)&&(this.theme=e)}catch{}document.documentElement.dataset.theme=this.theme;try{localStorage.getItem(`cardbuddy:api-panel-open`)===`0`&&(this.apiPanelOpen=!1)}catch{}await this.fetchCardList();let t=new URLSearchParams(location.search).get(`card`),n=null;try{let e=localStorage.getItem(`cardbuddy:selected-card`);e&&this.cardList.some(t=>t.id===e)&&(n=e)}catch{}let r=t&&this.cardList.some(e=>e.id===t)?t:n||this.cardList[0]&&this.cardList[0].id;r&&(this.selectedId=r,await this.loadCard(this.selectedId)),this.$watch(()=>this.rt.rootScope,()=>{this.syncDataToSchema()},{deep:!0}),this.setupCardsChanged()},methods:{fabBounds(){let e=this.$refs.aiFab,t=e?e.offsetWidth:56,n=e?e.offsetHeight:56,r=document.documentElement.clientWidth,i=document.documentElement.clientHeight;return{minX:0,minY:0,maxX:Math.max(0,r-t),maxY:Math.max(0,i-n)}},clampFab(e,t){let n=this.fabBounds();return[Math.min(Math.max(e,n.minX),n.maxX),Math.min(Math.max(t,n.minY),n.maxY)]},fabInitPosition(){let e=this.$refs.aiFab;if(!e)return;let t=this.loadFabPos(),n=document.documentElement.clientWidth,r=document.documentElement.clientHeight,i,a;if(t)[i,a]=this.clampFab(t.x,t.y);else{let t=e.getBoundingClientRect();[i,a]=this.clampFab(n-t.right,r-t.bottom)}this.fab.x=i,this.fab.y=a},loadFabPos(){try{let e=localStorage.getItem(`cardbuddy:ai-fab-pos`);if(!e)return null;let t=JSON.parse(e);if(typeof t.x==`number`&&typeof t.y==`number`&&isFinite(t.x)&&isFinite(t.y))return t}catch{}return null},saveFabPos(){try{localStorage.setItem(`cardbuddy:ai-fab-pos`,JSON.stringify({x:this.fab.x,y:this.fab.y}))}catch{}},onFabPointerDown(e){if(!this.aiChatOpen){this.fabSuppressClick=!1,this.fab.x??this.fabInitPosition(),this.fabDrag={pointerId:e.pointerId,startX:e.clientX,startY:e.clientY,originX:this.fab.x,originY:this.fab.y,moved:!1};try{e.currentTarget.setPointerCapture(e.pointerId)}catch{}}},onFabPointerMove(e){let t=this.fabDrag;if(!t||t.pointerId!==e.pointerId)return;let n=e.clientX-t.startX,r=e.clientY-t.startY;if(!t.moved){if(Math.hypot(n,r)<4)return;t.moved=!0,this.fab.dragging=!0}let[i,a]=this.clampFab(t.originX-n,t.originY-r);this.fab.x=i,this.fab.y=a},onFabPointerUp(e){let t=this.fabDrag;if(!t||t.pointerId!==e.pointerId){this.fab.dragging=!1;return}try{e.currentTarget.releasePointerCapture(e.pointerId)}catch{}t.moved&&(this.fabSuppressClick=!0,this.saveFabPos()),this.fab.dragging=!1,this.fabDrag=null},onFabClick(){if(this.fabSuppressClick){this.fabSuppressClick=!1;return}this.aiChatOpen=!this.aiChatOpen},onFabResize(){if(this.fab.x==null)return;let[e,t]=this.clampFab(this.fab.x,this.fab.y);this.fab.x=e,this.fab.y=t},setupCardsChanged(){if(this._unsubCardsChanged){try{this._unsubCardsChanged()}catch{}this._unsubCardsChanged=null}this.devSvcLive=`connecting`,this._unsubCardsChanged=Vl(e=>this.onCardsChanged(e),e=>this.onDevServiceState(e))},onDevServiceState(e){let t=this.devSvcLive===`connecting`;this.devSvcLive=e,e===`open`?this.devSvcDown=!1:e===`closed`&&(this.devSvcDown=!0),e===`connecting`?!t&&!this._svcWatchdog&&(this._svcWatchdog=setTimeout(()=>{this.devSvcLive===`connecting`&&(this.devSvcLive=`closed`,this.devSvcDown=!0),this._svcWatchdog=null},8e3)):this._svcWatchdog&&=(clearTimeout(this._svcWatchdog),null)},saveDevService(){Pl(this.devServiceForm),this.apiBaseResolved=Fl(),this.setupCardsChanged(),this.devServiceOpen=!1,Z(`本地开发服务配置已保存`,`ok`),this.fetchCardList()},beginColumnDrag(e,t,n){let r=e&&e.currentTarget;if(!r)return;let i=e.pointerId;try{r.setPointerCapture(i)}catch{}let a=()=>{r.removeEventListener(`pointermove`,t),r.removeEventListener(`pointerup`,a),r.removeEventListener(`pointercancel`,a),window.removeEventListener(`blur`,a);try{r.hasPointerCapture?.(i)&&r.releasePointerCapture(i)}catch{}document.body.style.cursor=``,document.body.style.userSelect=``,n()};document.body.style.cursor=`col-resize`,document.body.style.userSelect=`none`,r.addEventListener(`pointermove`,t),r.addEventListener(`pointerup`,a),r.addEventListener(`pointercancel`,a),window.addEventListener(`blur`,a)},startDrag(e){if(this.chromeOff)return;let t=this.$refs.stageEl;if(!t)return;let n=t.getBoundingClientRect().left;this.dragging=!0,this.beginColumnDrag(e,e=>{let{min:t,max:r}=this.previewBounds;this._previewWidthPx=Math.max(t,Math.min(r,Math.round(e.clientX-n)))},()=>{this.dragging=!1,this.savePreviewWidth()})},startApiDrag(e){if(this.chromeOff||!this.apiPanelOpen)return;let t=this.$refs.colsEl;if(!t)return;let n=t.getBoundingClientRect().right;this.apiDragging=!0,this.beginColumnDrag(e,e=>{let{min:t,max:r}=this.apiPanelBounds;this._apiPanelWidthPx=Math.max(t,Math.min(r,Math.round(n-e.clientX)))},()=>{this.apiDragging=!1,this.saveApiPanelWidth()})},loadPreviewWidth(){try{let e=localStorage.getItem(`cardbuddy:preview-width`);if(e!=null){let t=parseInt(e,10);!Number.isNaN(t)&&t>=188&&(this._previewWidthPx=t)}}catch{}},savePreviewWidth(){try{localStorage.setItem(`cardbuddy:preview-width`,String(Math.round(this._previewWidthPx)))}catch{}},setPresetWidth(e){if(this.chromeOff)return;let{min:t,max:n}=this.previewBounds;this._previewWidthPx=Math.max(t,Math.min(n,e)),this.savePreviewWidth()},loadApiPanelWidth(){try{let e=localStorage.getItem(`cardbuddy:api-panel-width`);if(e!=null){let t=parseInt(e,10);!Number.isNaN(t)&&t>=320&&(this._apiPanelWidthPx=t)}}catch{}},saveApiPanelWidth(){try{localStorage.setItem(`cardbuddy:api-panel-width`,String(Math.round(this.apiPanelWidth)))}catch{}},chooseBg(e){this.canvasBg=e},onSelect(e){if(e!==this.selectedId){if(this.chatBusy){Z(`AI 正在生成「${this.selectedId}」:先点 ■ 停止,或等本轮结束后再切卡`,`err`,3600);return}this.loadingCard=!0,this.selectedId=e,this.loadCard(e,{clearAll:!0})}},selectCard(e){this.selectedId=e;try{localStorage.setItem(`cardbuddy:selected-card`,e)}catch{}},openGallery(){this.cardList.length&&(this.galleryOpen=!0)},openNewCard(){this.cardDialog.mode=`create`,this.cardDialog.cardId=``,this.cardDialog.cardDesc=``,this.cardDialog.visible=!0,this.fetchCardList()},openRenameCard(){this.selectedId&&(this.cardDialog.mode=`rename`,this.cardDialog.cardId=this.selectedId,this.cardDialog.cardDesc=this.currentSchema&&this.currentSchema.desc||``,this.cardDialog.visible=!0,this.fetchCardList())},async onCardRenamed({oldId:e,id:t}){if(this.cardDialog.visible=!1,Z(t===e?`已更新卡片描述`:`已重命名卡片 ${e} → ${t}`,`ok`),t!==e)return this.applyCardRename(e,t);await this.fetchCardList(),await this.loadCard(t)},async applyCardRename(e,t){if(!e||!t||e===t)return;let n=`${e}->${t}`;if(this._renameAppliedKey===n)return;this._renameAppliedKey=n,setTimeout(()=>{this._renameAppliedKey===n&&(this._renameAppliedKey=``)},1e3);let r=this.selectedId===e;if(this.$refs.chatPanel?.migrateCard(e,t),r&&this.selectCard(t),this.selfSaveId=t,this.selfSaveAt=Date.now(),await this.fetchCardList(),r){if(this.historyDirty&&!this.confirm.show){this.requestConfirm({title:`卡片已重命名`,message:`卡片「${e}」已重命名为「${t}」,而编辑器内还有未保存的改动。\n重新载入会放弃这些未保存改动并显示磁盘版本;保留改动则继续用当前编辑器内容(后续保存会写到新目录 ${t})。`,confirmText:`重新载入`,cancelText:`保留我的改动`,onConfirm:()=>this.loadCard(t)});return}await this.loadCard(t)}},closeGallery(){this.galleryOpen=!1},async openCardFolder(){if(this.selectedId)try{let e=await J(`/__open-folder`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({id:this.selectedId})}),t=(e.headers.get(`content-type`)||``).includes(`application/json`)?await e.json().catch(()=>({})):{};!e.ok||!t.ok?Z(t.error||`打开文件夹失败`,`err`):Z(`已打开卡片文件夹`,`ok`)}catch{Z(`无法连接本地开发服务,请确认已启动`,`err`)}},async refreshCard(){if(!(!this.selectedId||this.refreshing)){this.refreshing=!0;try{await this.loadCard(this.selectedId)}finally{this.refreshing=!1}}},toggleTheme(){this.theme=this.theme===`light`?`dark`:`light`;try{localStorage.setItem(`cardbuddy:theme`,this.theme)}catch{}},toggleApiPanel(){this.apiPanelOpen=!this.apiPanelOpen},async refreshValidateBadge(){let e=this.selectedId;if(!(!e||!this.preview)&&(this.validateTimer&&clearTimeout(this.validateTimer),this.validateBadge={...this.validateBadge,state:`pending`},await new Promise(e=>{this.validateTimer=setTimeout(e,120)}),this.selectedId===e))try{let[t,n]=await Promise.all([J(`/__validate/${encodeURIComponent(e)}`),J(`/__render-status/${encodeURIComponent(e)}`)]);if(!t.ok){this.validateBadge={...this.validateBadge,state:t.status===503?`nopython`:`fail`};return}let r=await t.json().catch(()=>null),i=n.ok?await n.json().catch(()=>null):null;if(!r){this.validateBadge={...this.validateBadge,state:`fail`};return}let a=!!r.ok,o=!i||!!i.renderOk,s=a&&o?`ok`:`error`,c=this.validateBadge.state!==s;this.validateBadge={state:s,errors:(r.errors||[]).length,warnings:(r.warnings||[]).length,renderOk:o,ts:Date.now()},c&&(s===`ok`||s===`error`)&&this.celebrateValidate(s)}catch{this.validateBadge={...this.validateBadge,state:`fail`}}},celebrateValidate(e=`ok`){let t=this.$refs.validateBadgeEl;if(!t||window.matchMedia&&window.matchMedia(`(prefers-reduced-motion: reduce)`).matches)return;t.classList.remove(`is-celebrating`),t.offsetWidth,t.classList.add(`is-celebrating`),setTimeout(()=>t.classList.remove(`is-celebrating`),600);let n=t.getBoundingClientRect(),r={ok:`--ok`,error:`--danger`,pending:`--warn`,fail:`--text-faint`,nopython:`--text-faint`}[e]||`--ok`,i={ok:`#52c41a`,error:`#F5222D`,pending:`#FAAD14`,fail:`#8c8c8c`,nopython:`#8c8c8c`}[e]||`#52c41a`,a=getComputedStyle(t).getPropertyValue(r).trim()||i,o=document.createElement(`div`);o.className=`app__confetti`,o.style.left=n.left+n.width/2+`px`,o.style.top=n.top+n.height/2+`px`;let s=[`polygon(50% 0%, 0% 100%, 100% 100%)`,`polygon(62% 0%, 100% 40%, 45% 100%, 0% 52%)`,`polygon(0% 0%, 100% 18%, 82% 100%, 12% 70%)`,`polygon(22% 0%, 100% 8%, 88% 100%, 0% 76%)`,`polygon(0% 32%, 100% 0%, 76% 100%, 18% 86%)`,`polygon(8% 0%, 100% 22%, 70% 100%, 0% 64%)`,`polygon(0% 12%, 100% 0%, 100% 78%, 30% 100%, 12% 55%)`],c=n.width/2,l=n.height/2;for(let e=0;e<22;e++){let e=document.createElement(`span`);e.className=`app__confetti-bit`;let t=Math.random()*4|0,n,r;t===0?(n=(Math.random()*2-1)*c,r=-l):t===1?(n=c,r=(Math.random()*2-1)*l):t===2?(n=(Math.random()*2-1)*c,r=l):(n=-c,r=(Math.random()*2-1)*l);let i=Math.hypot(n,r)||1,u=Math.atan2(r/i,n/i)+(Math.random()-.5)*.5,d=16+Math.random()*26;e.style.setProperty(`--sx`,n.toFixed(1)+`px`),e.style.setProperty(`--sy`,r.toFixed(1)+`px`),e.style.setProperty(`--tx`,(Math.cos(u)*d).toFixed(1)+`px`),e.style.setProperty(`--ty`,(Math.sin(u)*d).toFixed(1)+`px`),e.style.setProperty(`--rot`,(Math.random()*720-360).toFixed(0)+`deg`),e.style.setProperty(`--clip`,s[Math.random()*s.length|0]),e.style.setProperty(`--c`,a),e.style.setProperty(`--o`,(.68+Math.random()*.27).toFixed(2));let f=Math.random(),p,m;f<.35?(p=1.5+Math.random()*2,m=10+Math.random()*10):f<.75?(p=4+Math.random()*5,m=p*(.6+Math.random()*.8)):(p=2+Math.random()*2.5,m=p),e.style.width=p.toFixed(0)+`px`,e.style.height=m.toFixed(0)+`px`,o.appendChild(e)}document.body.appendChild(o),setTimeout(()=>o.remove(),800)},async loadCard(e,t={}){if(e){this.loadingCard=!0;try{let n=await J(`/__card/${encodeURIComponent(e)}`);if(!n.ok){this.rt.pushError(`loadCard`,`HTTP `+n.status);return}let r=await n.json();if(!r.schema){this.rt.pushError(`loadCard`,`schema 读取失败`);return}this.selectCard(e),t.clearAll?this.rt.clearErrors():this.rt.clearBindingErrors(),this.rt.clearApiCalls(),t.keepScope||this.rt.setRootScope(O_(r.schema,jI)),this.currentCard={schema:r.schema},this.currentSchema=r.schema,this.cardImportMeta=null,this.fetchImportMeta(e),this.localSchema=this.cloneSchema(r.schema),this.localData=this.cloneData(O_(r.schema,jI)),this.preview=Pu(r.schema),typeof window<`u`&&(window.__cardModel=Bu(r.schema)),this.cardReady=!1,this.$nextTick(()=>{this.cardReady=!0}),this.syncDataToSchema(),this.selectedNodeId=``,this.baseline={schema:this.cloneSchema(this.currentSchema)},this.reloadToken+=1,gx(),Ux(),this.refreshValidateBadge(),await this.loadHistory(e)}finally{this.loadingCard=!1}}},cloneSchema(e){return e?JSON.parse(JSON.stringify(e,(e,t)=>e===`_schemaNode`?void 0:t)):null},cloneData(e){return e==null?e:JSON.parse(JSON.stringify(e,(e,t)=>e===`_schemaNode`?void 0:t))},applySchemaSnapshot(e){if(!this.selectedId||!e)return;let t=e.schema||e,n=e.data==null?O_(t,jI):e.data;this.currentSchema=this.cloneSchema(t),this.rt.setRootScope(this.cloneData(n)),this.preview=Pu(this.currentSchema),typeof window<`u`&&(window.__cardModel=Bu(this.currentSchema)),this.syncDataToSchema(),this.selectedNodeId=``,this.reloadToken+=1,this.refreshValidateBadge()},commitHistory(){if(!this.currentSchema||!this.baseline||!AI(this.currentSchema,this.baseline.schema,this.rt.rootScope))return;let e={schema:this.baseline.schema};this.past.push(e),this.past.length>30&&this.past.shift(),this.future=[],this.baseline={schema:this.cloneSchema(this.currentSchema)},this.persistSnapshot(e)},scheduleCommit(){this._commitTimer&&clearTimeout(this._commitTimer),this._commitTimer=setTimeout(()=>{this._commitTimer=null,this.commitHistory()},500)},persistSnapshot(e){let t=this.selectedId;if(!t)return;let n=e.schema||e;J(`/__history/${encodeURIComponent(t)}`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({schema:n},(e,t)=>e===`_schemaNode`?void 0:t,2)}).catch(e=>console.warn(`[历史快照落盘失败]`,e))},async loadHistory(e){this.past=[],this.future=[];try{let t=await J(`/__history/${encodeURIComponent(e)}`);if(!t.ok)return;let n=await t.json().catch(()=>null),r=n&&n.snapshots||[];this.past=r.slice(-30).map(e=>({schema:e.schema,data:e.data==null?O_(e.schema,jI):e.data}))}catch(e){console.warn(`[加载编辑历史失败]`,e)}},undo(){if(!this.canUndo)return;let e=this.past.pop();this.future.push(this.baseline),this.future.length>30&&this.future.shift(),this.baseline=e,this.applySchemaSnapshot(e)},redo(){if(!this.canRedo)return;let e=this.future.pop();this.past.push(this.baseline),this.past.length>30&&this.past.shift(),this.baseline=e,this.applySchemaSnapshot(e)},async applyHistoryToLocal(){this.loadingCard||!this.historyDirty||!this.preview||!this.selectedId||await this.saveCard()&&Z(`已将该版本保存到本地`,`ok`)},async generateCardCover(){if(!this.preview||!this.selectedId||this.coverBusy)return;if(this.historyDirty&&!await this.saveCard()){Z(`本地保存失败,已取消生成预览图`,`err`);return}let e=null;try{let t=await J(`/__card-image/`+encodeURIComponent(this.selectedId)+`?probe=1`),n=await t.json().catch(()=>({}));t.ok&&n.exists&&(e=n.file)}catch{}if(e){this.requestConfirm({title:`重新生成截图`,message:`该卡片已有截图(${e}),继续将覆盖它。确定重新生成吗?`,confirmText:`重新生成`,severity:`warn`,onConfirm:()=>this.doGenerateCover()});return}this.doGenerateCover()},async doGenerateCover(){this.coverBusy=!0;try{let e=await J(`/__card-image/`+encodeURIComponent(this.selectedId),{method:`POST`}),t=await e.json().catch(()=>({}));if(!e.ok)throw Error(t.message||`HTTP `+e.status);Z(`截图已生成(${t.width}×${t.height})→ ${t.file}`,`ok`,3e3)}catch(e){Z(`生成截图失败:`+String(e&&e.message||e),`err`,3600)}finally{this.coverBusy=!1}},onSchemaChanged(){!this.selectedId||!this.currentSchema||(this.reloadToken+=1,this.preview=Pu(this.currentSchema),typeof window<`u`&&(window.__cardModel=Bu(this.currentSchema)),this.syncDataToSchema(),this.selectedNodeId=``)},onSchemaCommit(){!this.selectedId||!this.currentSchema||(this.reloadToken+=1,this.preview=Pu(this.currentSchema),typeof window<`u`&&(window.__cardModel=Bu(this.currentSchema)),this.syncDataToSchema(),this.selectedNodeId=``,this.commitHistory())},syncDataToSchema(){let e=this.currentSchema;if(!e||!e.dictionary||!Array.isArray(e.dictionary.inputParams))return;let t=this.rt&&this.rt.rootScope;if(!t||typeof t!=`object`)return;let n=e.dictionary.inputParams,r=k_(e);for(let e of Object.keys(t)){let i=n.find(t=>t&&(t.key===e||t.name===e));if(!i)continue;let a=JSON.parse(JSON.stringify(t[e],(e,t)=>e===`_schemaNode`?void 0:t));r&&Object.prototype.hasOwnProperty.call(r,e)?r[e]=a:i.defaultValue=a}},onSelectNode(e){this.selectedNodeId=e||``},findPreviewNodeById(e,t){if(!e||t==null)return null;let n=String(t),r=e=>{if(!e)return null;if(String(e.id)===n)return e;let t=[];if(e.components&&t.push(...e.components),e.slots)for(let n of e.slots)n.components&&t.push(...n.components);for(let e of t){let t=r(e);if(t)return t}return null};return r(e)},copySelectedNode(){let e=this.selectedNodeId;if(!e||e===`_root`||!this.currentSchema||!this.preview)return;let t=this.findPreviewNodeById(this.preview.view,e);if(!t)return;let n=this.findSchemaNodeByComponentId(this.currentSchema.view,t.componentId);n&&(Wu(n.node,this.selectedId),Z(`已复制节点(含子节点)`,`ok`))},pasteNode(){let e=Hu();if(!e||!this.currentSchema)return;let t=Gu(e),n=[...new Set(t.filter(e=>!vu[e]))];if(n.length){Z(`粘贴失败:含未知组件类型 `+n.join(`、`),`err`);return}let r=JSON.parse(JSON.stringify(e,(e,t)=>e===`_schemaNode`?void 0:t));Ku(r);let i=this.selectedNodeId&&this.selectedNodeId!==`_root`?this.findPreviewNodeById(this.preview.view,this.selectedNodeId):null,a=`root`;i&&(a=ju(i.className)?`into`:`after`),this.insertClone(r,i,a);let o=Uu();o&&o!==this.selectedId?Z(`已从卡片「${o}」跨卡片粘贴到「${this.selectedId}」`,`ok`):Z(`已粘贴节点(含子节点)`,`ok`)},duplicateNode(){let e=this.selectedNodeId;if(!e||e===`_root`||!this.currentSchema||!this.preview)return;let t=this.findPreviewNodeById(this.preview.view,e);if(!t)return;let n=this.findSchemaNodeByComponentId(this.currentSchema.view,t.componentId);if(!n)return;let r=JSON.parse(JSON.stringify(n.node,(e,t)=>e===`_schemaNode`?void 0:t));Ku(r),n.parentArr.splice(n.index+1,0,r),this.afterStructuralEdit(r.componentId)},insertClone(e,t,n){let r=this.currentSchema;if(n===`root`)r.view.components||(r.view.components=[]),r.view.components.push(e);else if(n===`into`){let n=t&&t._schemaNode;if(!n){r.view.components||(r.view.components=[]),r.view.components.push(e),this.afterStructuralEdit(e.componentId);return}let i=Mu(t.className),a=(n.slots||[]).find(e=>e.slotKey===i);a||(n.slots||=[],a={slotKey:i,slotScope:!1,slotSwitch:`1`,components:[]},n.slots.push(a)),a.components||(a.components=[]),a.components.push(e)}else{let n=this.findSchemaNodeByComponentId(r.view,t.componentId);n?n.parentArr.splice(n.index+1,0,e):(r.view.components||(r.view.components=[]),r.view.components.push(e))}this.afterStructuralEdit(e.componentId)},afterStructuralEdit(e){let t=this.currentSchema;this.reloadToken+=1,this.preview=Pu(t),typeof window<`u`&&(window.__cardModel=Bu(t)),this.syncDataToSchema(),this.selectedNodeId=e&&this.findViewIdByComponentId(this.preview.view,e)||``,this.scheduleCommit()},deleteSelectedNode(){let e=this.selectedNodeId;if(!e||e===`_root`||!this.currentSchema||!this.preview)return;let t=this.findPreviewNodeById(this.preview.view,e);if(!t)return;let n=this.findSchemaNodeByComponentId(this.currentSchema.view,t.componentId);n&&(n.parentArr.splice(n.index,1),this.reloadToken+=1,this.preview=Pu(this.currentSchema),typeof window<`u`&&(window.__cardModel=Bu(this.currentSchema)),this.syncDataToSchema(),this.selectedNodeId=``,jS(),this.scheduleCommit())},hasTextSelection(){if(typeof window>`u`||!window.getSelection)return!1;let e=window.getSelection();return!!e&&!e.isCollapsed&&e.toString().trim()!==``},hasNodeSelection(){return!!this.selectedNodeId&&this.selectedNodeId!==`_root`},isEditableTarget(e){let t=e.target;if(!t)return!1;let n=t.tagName;return!!(n===`INPUT`||n===`TEXTAREA`||n===`SELECT`||t.isContentEditable)},onKeydown(e){if(this.chromeOff||!this.preview)return;let t=this.isEditableTarget(e),n=e.ctrlKey||e.metaKey,r=e.key;if(n&&(r===`c`||r===`C`)){if(t||this.hasTextSelection()||!this.hasNodeSelection())return;this.copySelectedNode(),e.preventDefault();return}if(n&&(r===`v`||r===`V`)){if(t||this.hasTextSelection()||!this.hasNodeSelection()||!Hu())return;this.pasteNode(),e.preventDefault();return}if(n&&(r===`d`||r===`D`)){if(t||this.hasTextSelection()||!this.hasNodeSelection())return;this.duplicateNode(),e.preventDefault();return}if(n&&(r===`z`||r===`Z`)){if(t)return;e.preventDefault(),e.shiftKey?this.redo():this.undo();return}if(n&&(r===`y`||r===`Y`)){if(t)return;e.preventDefault(),this.redo();return}if(r===`Delete`||r===`Backspace`){if(t||!this.selectedNodeId||this.selectedNodeId===`_root`)return;e.preventDefault(),this.deleteSelectedNode();return}if(r===`Escape`){this.selectedNodeId&&(this.selectedNodeId=``,e.preventDefault());return}},onHoverNode(e){if(!e){jS();return}if(e.leave){jS(e.id);return}let t=typeof document<`u`?document.querySelector(`.phone__screen`):null;if(!t){jS(e.id);return}let n=e=>window.CSS&&CSS.escape?CSS.escape(String(e)):String(e).replace(/["\\\]]/g,`\\$&`),r=null;e.componentId!=null&&(r=t.querySelector(`[data-cid="`+n(e.componentId)+`"]`)),!r&&e.id!=null&&(r=t.querySelector(`[data-cid="`+n(e.id)+`"]`)),r?AS(r,e.id):jS(e.id)},onCanvasClick(e){if(!e.ctrlKey&&!e.metaKey||this.chromeOff)return;let t=e.target&&e.target.closest?e.target.closest(`[data-cid]`):null;if(!t)return;let n=t.getAttribute(`data-cid`);if(!n)return;let r=this.findPreviewNodeByCid(n);r&&(this.activeTab=`structure`,this.selectedNodeId=r.id)},onAiLocateNode(e){let t=e?this.findPreviewNodeByCid(e):null;if(!t){Z(`没找到组件 ${e||``}:卡片可能已被改动,重新校验一次即可`,`err`);return}this.activeTab=`structure`,this.selectedNodeId=t.id},findPreviewNodeByCid(e){let t=this.preview&&this.preview.view;if(!t)return null;let n=String(e),r=e=>{if(!e)return null;if(String(e.componentId==null?e.id:e.componentId)===n)return e;let t=[];if(e.components&&t.push(...e.components),e.slots)for(let n of e.slots)n.components&&t.push(...n.components);for(let e of t){let t=r(e);if(t)return t}return null};return r(t)},onAddNode(e){this.currentSchema&&(this.addNodeTarget=e||null,this.addNodePanelOpen=!0)},cancelAddNode(){this.addNodePanelOpen=!1,this.addNodeTarget=null},findSchemaLocation(e,t){let n=e=>{for(let r=0;r<e.length;r++){let i=e[r];if(i===t)return{containerArr:e,index:r};let a=i.slots||[];for(let e of a){let t=n(e.components||[]);if(t)return t}}return null};return n(e&&e.components||[])},findViewIdByComponentId(e,t){if(!e)return null;if(e.componentId===t)return e.id;let n=[];if(e.components&&n.push(...e.components),e.slots)for(let t of e.slots)t.components&&n.push(...t.components);for(let e of n){let n=this.findViewIdByComponentId(e,t);if(n)return n}return null},findSchemaNodeByComponentId(e,t){let n=(e,r)=>{for(let i=0;i<e.length;i++){let a=e[i];if(a.componentId===t)return{node:a,parentArr:e,index:i,owner:r};let o=a.slots||[];for(let e of o){let t=n(e.components||[],a);if(t)return t}}return null};return n(e&&e.components||[],`root`)},isDescendantCompId(e,t){if(!t||!t.componentId)return!1;if(t.componentId===e)return!0;let n=[];if(t.components&&n.push(...t.components),t.slots)for(let e of t.slots)e.components&&n.push(...e.components);for(let t of n)if(this.isDescendantCompId(e,t))return!0;return!1},moveNode(e,t,n){let r=this.currentSchema;if(!r||!r.view||e===t)return!1;let i=this.findSchemaNodeByComponentId(r.view,e);if(!i)return!1;let a=i.node;if(this.isDescendantCompId(t,a))return!1;let o,s;if(t===`__root__`)r.view.components||(r.view.components=[]),o=r.view.components,s=o.length;else{let e=this.findSchemaNodeByComponentId(r.view,t);if(!e)return!1;let i=e.node;if(n===`inside`){if(!ju(i.className))return!1;let e=Mu(i.className),t=(i.slots||[]).find(t=>t.slotKey===e);t||(i.slots||=[],t={slotKey:e,slotScope:!1,slotSwitch:`1`,components:[]},i.slots.push(t)),t.components||(t.components=[]),o=t.components,s=o.length}else o=e.parentArr,s=n===`before`?e.index:e.index+1}let c=o===i.parentArr&&i.index<s;return i.parentArr.splice(i.index,1),c&&--s,o.splice(s,0,a),a},onMoveNode({sourceCompId:e,targetCompId:t,position:n}){if(!this.currentSchema||!this.moveNode(e,t,n))return;this.reloadToken+=1,this.preview=Pu(this.currentSchema),typeof window<`u`&&(window.__cardModel=Bu(this.currentSchema)),this.syncDataToSchema();let r=this.findViewIdByComponentId(this.preview.view,e);this.selectedNodeId=r||``,r&&(this.moveCollapseToken+=1)},onPickComponent(e){let t=this.addNodeTarget||{},n=t.node||null,r=t.mode||`auto`,i=this.currentSchema;if(!i){this.cancelAddNode();return}let a;try{a=Nu(e)}catch(e){console.error(`[创建组件节点失败]`,e),this.rt.pushError(`addNode`,String(e&&e.message)),this.cancelAddNode();return}let o=!n||n.className===`Root`;if((r===`before`||r===`after`)&&!o){let e=n._schemaNode,t=e?this.findSchemaLocation(i.view,e):null;if(!t)i.view.components||(i.view.components=[]),i.view.components.push(a);else{let e=r===`before`?t.index:t.index+1;t.containerArr.splice(e,0,a)}}else if(o)i.view.components||(i.view.components=[]),i.view.components.push(a);else{let e=n._schemaNode;if(!e){this.cancelAddNode();return}if(ju(n.className)){let t=Mu(n.className),r=(e.slots||[]).find(e=>e.slotKey===t);r||(e.slots||=[],r={slotKey:t,slotScope:!1,slotSwitch:`1`,components:[]},e.slots.push(r)),r.components||(r.components=[]),r.components.push(a)}else{let t=this.findSchemaLocation(i.view,e);t?t.containerArr.splice(t.index+1,0,a):(i.view.components||(i.view.components=[]),i.view.components.push(a))}}this.cancelAddNode(),this.reloadToken+=1,this.preview=Pu(i),typeof window<`u`&&(window.__cardModel=Bu(i)),this.syncDataToSchema();let s=this.findViewIdByComponentId(this.preview.view,a.componentId);this.selectedNodeId=s||``,this.scheduleCommit()},async runSync(e){if(!this.syncingRemote&&!(e&&!this.selectedId)){this.syncingRemote=!0,this.syncedRemote=!1,this.syncTarget=e?`selected`:`full`;try{let t=await J(`/__sync`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify(e?{cardCode:e}:{})});if(!t.ok){let e=await t.json().catch(()=>({}));throw Error(e.message||e.error||`HTTP `+t.status)}let n=await t.json();if(n.needCredentials){this.openLoginModal();return}if(!n.success){let e=(n.failed||[]).map(e=>`${e.id}: ${e.error}`).join(`; `);throw Error(e||`同步失败`)}this.syncedRemote=!0,this.syncTimer&&clearTimeout(this.syncTimer),this.syncTimer=setTimeout(()=>{this.syncedRemote=!1,this.syncTarget=null},1800),await this.onRefresh(),Z(this.syncTarget===`selected`?`已拉取最新卡到本地`:`拉取全部卡完成`,`ok`),this.persistRemoteVersion(this.selectedId,null)}catch(e){console.error(`[同步远程数据失败]`,e),Z(`同步失败:`+(e&&e.message),`err`)}finally{this.syncingRemote=!1}}},syncRemote(){return this.runSync()},syncSelected(){return this.runSync(this.selectedId)},openLoginModal(){this.loginModal=!0,this.loginName=``,this.loginPassword=``,this.loginError=``},closeLoginModal(){this.loginSubmitting||(this.loginModal=!1,this.loginPassword=``,this.loginError=``)},async submitLogin(){if(this.loginSubmitting)return;let e=this.loginName.trim(),t=this.loginPassword;if(!e||!t){this.loginError=`账号与密码均不能为空`;return}this.loginSubmitting=!0,this.loginError=``;try{let n=await J(`/__login-credentials`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({loginName:e,loginPassword:t})}),r=await n.json().catch(()=>({}));if(!n.ok||!r.ok)throw Error(r.message||r.error||`HTTP `+n.status);this.loginModal=!1,this.loginPassword=``,Z(`凭据已保存,正在重试同步…`,`ok`),await this.runSync(this.syncTarget===`selected`?this.selectedId:void 0)}catch(e){this.loginError=`保存失败:`+(e&&e.message)}finally{this.loginSubmitting=!1}},chooseSync(e){if(e===`full`){if(this.syncingRemote)return;this.requestConfirm({title:`拉取全部卡`,message:`即将从远端 FinMall 拉取完整卡片列表,并逐张覆盖本地同名卡片。本地独有卡片会保留,但被覆盖卡片的未保存改动将丢失,且无法撤销。是否继续?`,confirmText:`继续同步`,severity:`warn`,onConfirm:()=>this.syncRemote()});return}!this.preview||!this.selectedId||this.syncingRemote||this.requestConfirm({title:`拉取最新卡`,message:`即将从远端 FinMall 拉取当前卡片「${this.selectedId}」的最新版本并覆盖本地。本地未保存的改动将丢失,且无法撤销。是否继续?`,confirmText:`继续同步`,onConfirm:()=>this.syncSelected()})},chooseRsave(e){if(e===`save`){if(!this.preview||!this.selectedId||this.savingRemote||!this.hasCardSource)return;this.requestConfirm({title:`推送当前卡`,message:`即将把本地卡片「${this.selectedId}」的当前版本推送并覆盖到远端 FinMall。线上版本会被立即更新,且该操作不可撤销。是否继续?`,confirmText:`继续推送`,severity:`warn`,onConfirm:()=>this.saveToRemote()});return}if(e===`publish`){if(!this.preview||!this.selectedId||this.publishing||!this.hasCardSource)return;let e=this.historyDirty;this.requestConfirm({title:`卡片发布`,message:e?`当前卡片存在未保存到远端的改动,发布将使用远端「已保存」的版本(不含这些本地改动)。仍将以「最大版本号 +1」作为新版本号上线。确认继续?`:`即将取远端「${this.selectedId}」当前最大版本号并 +1,发布上线为新版本。发布的是远端已保存的卡片内容(建议先「推送当前卡」)。该操作不可撤销。确认继续?`,confirmText:`继续发布`,severity:e?`danger`:`warn`,onConfirm:()=>this.doPublish()});return}if(e===`slices`){if(!this.preview||!this.selectedId||this.uploadingSlices)return;this.requestConfirm({title:`当前卡切图上传`,message:`即将把当前卡片「${this.selectedId}」引用的本地切图(src/slices/)上传到 FinMall 图片库,并把卡片里的 /slices/ 引用替换为线上地址(@filestore/…)。本地切图文件保留不动;内容未变的切图会直接复用已有线上地址,不重复上传。`+(this.historyDirty?`
318
318
  当前卡片有未保存的改动,会先保存到本地再执行,避免被随后的重载丢弃。`:``)+`
319
319
  是否继续?`,confirmText:`继续上传`,onConfirm:()=>this.uploadCardSlices()});return}!this.preview||!this.selectedId||!this.hasCardSource||(this.remotePreviewOpen=!0)},requestConfirm({title:e,message:t,confirmText:n,cancelText:r,severity:i,onConfirm:a}){this.confirm={show:!0,title:e,message:t,confirmText:n||`继续`,cancelText:r||`取消`,severity:[`primary`,`warn`,`danger`].includes(i)?i:`primary`,onConfirm:a}},onConfirmDialog(){let e=this.confirm.onConfirm;this.confirm.show=!1,this.confirm.onConfirm=null,typeof e==`function`&&e()},onCancelDialog(){this.confirm.show=!1,this.confirm.onConfirm=null},async doPublish(){if(!(!this.preview||!this.selectedId||this.publishing||!this.hasCardSource)){this.publishing=!0;try{let e=await J(`/__publish`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({schema:this.currentSchema})});if(!e.ok){let t=await e.json().catch(()=>({}));if(t.needCredentials){this.openLoginModal();return}throw Error(t.message||t.error||`HTTP `+e.status)}let t=await e.json();if(!t.ok)throw Error(t.message||`发布失败`);Z(`已发布版本 ${t.nextVersion}`,`ok`),this.persistRemoteVersion(this.selectedId,t.nextVersion)}catch(e){console.error(`[卡片发布失败]`,e),Z(`发布失败:`+(e&&e.message),`err`)}finally{this.publishing=!1}}},async persistRemoteVersion(e,t){if(e)try{let n=t==null?`{}`:JSON.stringify({version:t}),r=await J(`/__card-version/${encodeURIComponent(e)}`,{method:`POST`,headers:{"Content-Type":`application/json`},body:n});if(!r.ok){let e=await r.json().catch(()=>({}));if(e.needCredentials)return;throw Error(e.message||`HTTP `+r.status)}}catch(e){console.warn(`[persistRemoteVersion 失败]`,e&&e.message)}},async saveCard(){if(!this.preview||!this.selectedId)return;this.savingCard=!0;let e=this.selectedId;this.selfSaveId=e,this.selfSaveAt=Date.now();try{try{let e=await this.fetchDiskCard(this.selectedId);if(e&&e.schema){let t=e.schema,n=this.currentSchema;n&&(t.pageId&&(n.pageId=t.pageId),t.name&&(n.name=t.name),t.desc!==void 0&&(n.desc=t.desc))}}catch{}this.syncDataToSchema();let e={schema:this.currentSchema},t=JSON.stringify(e,(e,t)=>e===`_schemaNode`?void 0:t,2),n=await J(`/__card/${encodeURIComponent(this.selectedId)}`,{method:`POST`,headers:{"Content-Type":`application/json`},body:t});if(!n.ok){let e=await n.json().catch(()=>({}));throw Error(e.message||`HTTP `+n.status)}return this.currentCard={schema:e.schema},this.localSchema=this.cloneSchema(this.currentSchema),this.localData=this.cloneData(O_(this.currentSchema,jI)),this.savedCard=!0,this.refreshValidateBadge(),this.saveTimer&&clearTimeout(this.saveTimer),this.saveTimer=setTimeout(()=>{this.savedCard=!1},1800),!0}catch(e){return console.error(`[本地保存失败]`,e),this.rt.pushError(`saveCard`,String(e&&e.message)),this.selfSaveId=``,this.selfSaveAt=0,!1}finally{this.savingCard=!1}},async fetchDiskCard(e){let t=await J(`/__card/${encodeURIComponent(e)}`);if(!t.ok)throw Error(`HTTP `+t.status);return t.json()},async fetchImportMeta(e){if(!e){this.cardImportMeta=null;return}try{let t=await J(`/__import-meta/${encodeURIComponent(e)}`);if(!t.ok){this.cardImportMeta=null;return}let n=await t.json().catch(()=>({}));this.cardImportMeta=n&&n.meta||null}catch{this.cardImportMeta=null}},async saveToRemote(){if(!(!this.preview||!this.selectedId||this.savingRemote)){this.savingRemote=!0,this.savedRemote=!1,this.verifiedRemote=null;try{try{let e=await this.fetchDiskCard(this.selectedId);e&&e.schema&&e.schema.pageId&&this.currentSchema&&(this.currentSchema.pageId=e.schema.pageId)}catch{}this.syncDataToSchema(),this.selfSaveId=this.selectedId,this.selfSaveAt=Date.now();let e=await J(`/__save`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({code:this.selectedId,schema:this.currentSchema},(e,t)=>e===`_schemaNode`?void 0:t,2)});if(!e.ok){let t=await e.json().catch(()=>({}));throw Error(t.message||t.error||`HTTP `+e.status)}let t=await e.json();if(!t.ok)throw Error(t.error||`远端保存失败`);if(this.verifiedRemote=t.verified?!!t.verified.ok:null,this.savedRemote=!0,this.verifiedRemote===!1){let e=t.verified||{};e.diff&&e.diff.length?``+e.diff.join(`;`):e.readBackError,console.warn(`[保存校验不符]`,e),Z(`已保存至远端,但读回校验未通过`,`err`)}else Z(`已保存至远端`,`ok`);this.metaRefreshToken++,this.saveRemoteTimer&&clearTimeout(this.saveRemoteTimer),this.saveRemoteTimer=setTimeout(()=>{this.savedRemote=!1,this.verifiedRemote=null},2500)}catch(e){console.error(`[保存至远端失败]`,e),Z(`保存至远端失败:`+(e&&e.message),`err`),this.selfSaveId=``,this.selfSaveAt=0}finally{this.savingRemote=!1}}},async uploadCardSlices(){if(!this.preview||!this.selectedId||this.uploadingSlices)return;let e=this.selectedId;this.uploadingSlices=!0;try{if(this.historyDirty&&!await this.saveCard())throw Error(`本地保存失败,已中止切图上传`);this.selfSaveId=e,this.selfSaveAt=Date.now();let t=await J(`/__slice-upload`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({cardId:e})});if(!t.ok){let e=await t.json().catch(()=>({}));throw Error(e.message||e.error||`HTTP `+t.status)}let n=await t.json();if(n.needCredentials){this.openLoginModal();return}if(this.selfSaveAt=Date.now(),!n.success){let e=(n.failed||[]).map(e=>`${e.name}: ${e.error}`).join(`; `);throw Error(e||`切图上传失败`)}await this.onRefresh();let r=(n.rewritten||[]).reduce((e,t)=>e+t.count,0);n.referenced?Z(`当前卡切图上传完成:新上传 ${n.uploaded.length} 张、复用 ${n.skipped.length} 张,替换引用 ${r} 处`,`ok`):Z(`当前卡片没有引用本地切图,无需上传`,`ok`),n.dangling&&n.dangling.length&&Z(`有 ${n.dangling.length} 处引用的切图在 src/slices 下不存在:${n.dangling.join(`、`)}`,`err`)}catch(e){console.error(`[切图上传失败]`,e),Z(`当前卡切图上传失败:`+(e&&e.message),`err`),this.selfSaveId=``,this.selfSaveAt=0}finally{this.uploadingSlices=!1}},async syncCardMeta(e){let t=await this.fetchCardMeta(e);if(!t)return;let n=this.cardList.findIndex(t=>t.id===e);n>=0?this.cardList.splice(n,1,t):this.cardList.push(t),this.cardCount=this.cardList.length},async fetchCardMeta(e){try{let t=await J(`/__card/`+encodeURIComponent(e)+`/meta`);return t.ok?await t.json():null}catch(e){return console.error(`[读取卡片列表项失败]`,e),null}},async fetchCardList(){try{let e=await J(`/__cards`);if(!e.ok)return;let t=await e.json(),n=t&&t.cards||[];this.cardList=n,this.cardCount=n.length}catch(e){console.error(`[加载卡片列表失败]`,e)}},async onGalleryDelete(e){try{let t=await J(`/__card/${encodeURIComponent(e)}`,{method:`DELETE`}),n=await t.json().catch(()=>null);if(!t.ok)throw Error(n&&(n.message||n.error)||`HTTP ${t.status}`);if(Z(`已删除本地卡片 ${e}`,`ok`),this.$refs.chatPanel?.discardCard(e),await this.fetchCardList(),this.selectedId===e){let e=this.cardList[0];e?await this.loadCard(e.id):(this.selectedId=``,this.currentSchema=null,this.preview=null)}}catch(e){Z(`删除卡片失败:${e.message}`,`err`)}},async onNewCardCreated(e){if(this.cardDialog.visible=!1,Z(`已创建空白卡片 ${e}`,`ok`),await this.fetchCardList(),this.chatBusy){Z(`AI 正在生成「${this.selectedId}」:新卡 ${e} 已建好,本轮结束后再切过去`,`err`,3600);return}await this.loadCard(e)},async onRefresh(){let e=this.selectedId;if(!(!e||this.refreshing)){this.refreshing=!0;try{await this.loadCard(e),await this.syncCardMeta(e);let t=this.cardList.find(t=>t.id===e);t&&this.currentSchema&&this.currentSchema.name&&(t.name=this.currentSchema.name)}catch(e){console.error(`[刷新卡片失败]`,e),this.rt.pushError(`refresh`,String(e&&e.message))}finally{this.refreshing=!1}}},onChatTurnFinished(e){if(this.chatProgress=null,this.aiChatOpen)return;let t=e&&e.steps||0,n=pk(e&&e.ms||0),r=mk(e&&e.finishedAt||Date.now());if(e&&e.status===`error`){Z(`AI 助手本轮报错(${r} · ${t} 步 / ${n}),展开面板查看原因`,`err`);return}if(e&&e.status===`aborted`){Z(`AI 助手本轮已终止(${r} · ${t} 步):展开面板可「从中断处继续」`,`ok`);return}Z(`AI 助手已完成本轮(${r} · ${t} 步 / ${n})`,`ok`)},async onChatRollbackTurn(e){let t=e&&e.cardId||this.selectedId;if(!t||this.loadingCard)return;if(t!==this.selectedId){Z(`请先切换到那张卡片再回滚`,`err`);return}let n=[];try{let e=await J(`/__history/${encodeURIComponent(t)}`);if(!e.ok)throw Error(`HTTP ${e.status}`);let r=await e.json().catch(()=>null);n=r&&r.snapshots||[]}catch(e){Z(`读取历史快照失败:${e.message||e}`,`err`);return}let r=Number(e&&e.ts)||0,i=r?n.filter(e=>Number(e.ts)>=r-2e3):[],a=i.length?i[0]:n[n.length-1];if(!a||!a.schema){Z(`没找到本轮之前的历史快照(可能已被 30 张上限裁掉,或本轮没写入过卡片)`,`err`);return}let o=new Date(Number(a.ts)||Date.now()).toLocaleString(`zh-CN`,{hour12:!1});this.requestConfirm({title:`回滚到 AI 动手之前`,message:`将把卡片「${this.selectedCardName||t}」恢复到 ${o} 的版本,本轮及其后的 ${i.length||1} 张快照都会被撤销。回滚只改当前编辑态,可用 Ctrl+Z 撤回;确认无误后仍需点顶栏「保存到本地」才写盘。`,confirmText:`回滚`,severity:`warn`,onConfirm:()=>this.applyTurnRollback(a)})},applyTurnRollback(e){!this.currentSchema||!e||(this.past.push({schema:this.cloneSchema(this.currentSchema)}),this.past.length>30&&this.past.shift(),this.future=[],this.baseline={schema:this.cloneSchema(e.schema)},this.applySchemaSnapshot({schema:e.schema,data:e.data}),Z(`已回滚到本轮之前的版本(未落盘,可撤销;确认后点「保存到本地」)`,`ok`))},onCardsChanged(e){let t=e&&e.file||``,n=e&&e.type||`change`;if(n===`rename`&&e.oldId&&e.id&&e.oldId!==e.id)return this.applyCardRename(e.oldId,e.id);if(/\.history[\\/]/.test(t)||/data\.json$/i.test(t)||/\.import-meta\.json(\.tmp)?$/i.test(t))return;let r=t.match(/src[\\/]cards[\\/]([^\\/]+)[\\/]/),i=r?r[1]:null;this._ccIds||=new Set,this._ccTypes||={},i&&(this._ccIds.add(i),this._ccTypes[i]=n),!this._ccTimer&&(this._ccTimer=setTimeout(async()=>{this._ccTimer=null;let e=this._ccIds,t=this._ccTypes;if(this._ccIds=new Set,this._ccTypes={},!e.size)return;let n=Array.from(e),r=n[n.length-1];if(e.size===1&&r&&r===this.selectedId){if(this.selfSaveId===r&&Date.now()-this.selfSaveAt<MI){this.selfSaveId=``,this.selfSaveAt=0;return}if(this.historyDirty)this.promptExternalReload(r);else{let e=this.currentSchema&&this.currentSchema.name||r;this.showExternalReloadNote(this.chatBusy?`AI 助手已写入卡片「${e}」,预览已自动刷新`:`卡片「${e}」已在编辑器外被修改,已自动重新载入预览`),this.loadCard(this.selectedId)}return}if(e.size>1){await this.fetchCardList();return}if((t[r]||`change`)===`unlink`){let e=this.cardList.findIndex(e=>e.id===r);e>=0&&(this.cardList.splice(e,1),this.cardCount=this.cardList.length);return}let i=await this.fetchCardMeta(r);if(!i){await this.fetchCardList();return}let a=this.cardList.findIndex(e=>e.id===r);a>=0?this.cardList.splice(a,1,i):this.cardList.push(i),this.cardCount=this.cardList.length},120))},promptExternalReload(e){if(this.confirm.show)return;let t=this.currentSchema&&this.currentSchema.name||e;this.requestConfirm({title:`卡片已在编辑器外被修改`,message:`卡片「${t}」的 schema / data 在编辑器外发生了变更(例如 IDE 或其他工具覆盖了文件)。\n当前编辑器存在未保存的改动,是否放弃本地未保存改动并重新载入磁盘版本?`,confirmText:`重新载入`,cancelText:`保留我的改动`,onConfirm:()=>this.loadCard(e)})},showExternalReloadNote(e){this.externalReloadNote=e,this.externalReloadTimer&&clearTimeout(this.externalReloadTimer),this.externalReloadTimer=setTimeout(()=>{this.externalReloadNote=``,this.externalReloadTimer=null},3500)}}},BI={class:`app__bar`},VI={class:`app__logo`},HI=[`src`],UI={class:`app__logo-version`},WI={class:`csel-combo`},GI={class:`app__btn-group`},KI=[`disabled`],qI=[`disabled`],JI=[`title`],YI={class:`app__badge-text`},XI={class:`app__bar-actions`},ZI={class:`app__op-group`,role:`group`,"aria-label":`编辑历史与本地操作`},QI=[`disabled`],$I=[`disabled`],eL=[`disabled`,`title`],tL=[`disabled`],nL=[`disabled`],rL={key:0,class:`app__icon app__icon--spin`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,"stroke-width":`2`,"stroke-linecap":`round`,"stroke-linejoin":`round`,"aria-hidden":`true`},iL={key:1,class:`app__icon`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,"stroke-width":`2`,"stroke-linecap":`round`,"stroke-linejoin":`round`,"aria-hidden":`true`},aL={key:2,class:`app__icon`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,"stroke-width":`2`,"stroke-linecap":`round`,"stroke-linejoin":`round`,"aria-hidden":`true`},oL={key:3,class:`app__icon`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,"stroke-width":`2`,"stroke-linecap":`round`,"stroke-linejoin":`round`,"aria-hidden":`true`},sL={class:`app__remote-pop dd__menu`,role:`menu`},cL=[`title`,`onClick`],lL=[`title`,`onClick`],uL=[`title`,`onClick`],dL=[`title`,`onClick`],fL=[`onClick`],pL=[`onClick`],mL=[`title`,`aria-pressed`],hL=[`aria-checked`,`title`],gL={class:`theme-toggle__track`},_L={class:`theme-toggle__thumb`},vL={key:0,class:`theme-toggle__icon`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,"stroke-width":`2`,"stroke-linecap":`round`,"stroke-linejoin":`round`,"aria-hidden":`true`},yL={key:1,class:`theme-toggle__icon`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,"stroke-width":`2`,"stroke-linecap":`round`,"stroke-linejoin":`round`,"aria-hidden":`true`},bL={class:`app__dev-svc`},xL=[`title`],SL={class:`app__dev-svc-head`},CL={class:`app__dev-svc-body`},wL={class:`app__dev-svc-row`},TL={class:`app__dev-svc-row app__dev-svc-row--col`},EL={class:`app__dev-svc-row app__dev-svc-row--col`},DL={key:0,class:`app__dev-svc-hint`},OL={class:`app__dev-svc-actions`},kL={style:{display:`none`}},AL=[`title`,`aria-expanded`],jL={key:0,class:`app__nosvc-banner`},ML={key:1,class:`app__nosvc-banner app__nosvc-banner--disconnected`},NL={key:2,class:`app__nosvc-banner app__extreload-banner`},PL={class:`app__stage`,ref:`stageEl`},FL={class:`phone__scaler`},IL=[`title`],LL={class:`canvas-bar__txt`},RL={class:`canvas-bar__chip`,title:`画布宽度预设(高度随列高自适应)`},zL={class:`canvas-bar__txt`},BL={class:`canvas-bar__size`,title:`画布实际尺寸(宽 × 高,高度随窗口自适应)`},VL=[`disabled`],HL=[`data-card-ready`],UL={class:`workside`},WL={class:`workside__cols`,ref:`colsEl`},GL={class:`login-modal modal-card`,role:`dialog`,"aria-modal":`true`},KL={class:`login-modal__row`},qL=[`disabled`],JL={class:`login-modal__row`},YL=[`disabled`],XL={key:0,class:`login-modal__error`},ZL={class:`login-modal__actions`},QL=[`disabled`],$L=[`disabled`];function eR(e,t,n,r,i,a){let o=L(`CardSelect`),s=L(`Dropdown`),c=L(`Switch`),l=L(`ChatPanel`),u=L(`RenderTree`),d=L(`SelectionOverlay`),f=L(`HoverGuides`),p=L(`AlertCardLayer`),m=L(`DeviceOverlay`),h=L(`PickerOverlay`),g=L(`DatePickerOverlay`),_=L(`DebugPanel`),v=L(`ApiCallPanel`),y=L(`RemotePreview`),b=L(`ComponentPalette`),x=L(`ConfirmDialog`),S=L(`CardGallery`),C=L(`NewCardDialog`),w=L(`AppToast`),ee=L(`ImageLightbox`);return B(),V(`div`,{class:A([`app`,{"app--chrome-off":i.chromeOff}]),style:k({"--preview-width":a.previewWidth+`px`,"--api-panel-width":a.apiPanelWidth+`px`,"--debug-min":a.debugMinWidth+`px`})},[I(U(`header`,BI,[U(`strong`,VI,[U(`img`,{class:`app__logo-icon`,src:i.logoUrl,alt:``},null,8,HI),t[41]||=G(` CardBuddy `,-1),U(`span`,UI,`v`+j(i.appVersion),1)]),U(`div`,WI,[W(o,{class:`csel--combo`,value:i.selectedId,options:i.cardList,onChange:a.onSelect},null,8,[`value`,`options`,`onChange`])]),K(` 卡片网格预览:图标按钮,打开本地全部卡片的缩略图墙,点击任一张载入工作台 `),U(`div`,GI,[U(`button`,{class:`app__btn app__btn--icon`,type:`button`,disabled:!i.cardList.length,onClick:t[0]||=(...e)=>a.openGallery&&a.openGallery(...e),title:`卡片网格预览`},[...t[42]||=[U(`svg`,{class:`app__icon`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,"stroke-width":`2`,"stroke-linecap":`round`,"stroke-linejoin":`round`,"aria-hidden":`true`},[U(`use`,{href:`#icon-grid`})],-1)]],8,KI),K(` 新增空白卡片:紧邻网格预览的图标按钮;弹窗收 ID(必填、本地重名前置拦截)+ 描述(选填),创建成功切换到新卡 `),U(`button`,{class:`app__btn app__btn--icon`,type:`button`,onClick:t[1]||=(...e)=>a.openNewCard&&a.openNewCard(...e),title:`新增空白卡片`},[...t[43]||=[U(`svg`,{class:`app__icon`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,"stroke-width":`2`,"stroke-linecap":`round`,"stroke-linejoin":`round`,"aria-hidden":`true`},[U(`use`,{href:`#icon-folder-plus`})],-1)]]),K(` 打开当前卡片所在文件夹:从卡片下拉里拆出,合并进本组作为第三位 `),U(`button`,{class:`app__btn app__btn--icon`,type:`button`,disabled:!i.selectedId,onClick:t[2]||=(...e)=>a.openCardFolder&&a.openCardFolder(...e),title:`在文件管理器中打开当前卡片所在文件夹`},[...t[44]||=[U(`svg`,{class:`app__icon`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,"stroke-width":`2`,"stroke-linecap":`round`,"stroke-linejoin":`round`,"aria-hidden":`true`},[U(`use`,{href:`#icon-folder`})],-1)]],8,qI)]),I(U(`div`,{ref:`validateBadgeEl`,class:A([`app__badge`,`app__badge--`+i.validateBadge.state]),title:a.badgeTitle},[t[45]||=U(`span`,{class:`app__badge-dot`},null,-1),U(`span`,YI,j(a.badgeText),1)],10,JI),[[Sc,!i.chromeOff]]),U(`div`,XI,[U(`div`,ZI,[K(` 撤销 / 重做:图标 + 文案;无可回退/重做版本时置灰 `),U(`button`,{class:`app__btn app__btn--hist`,type:`button`,disabled:!i.preview||!a.canUndo,onClick:t[3]||=(...e)=>a.undo&&a.undo(...e),title:`后退一步(撤销)`},[...t[46]||=[U(`svg`,{class:`app__icon`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,"stroke-width":`2`,"stroke-linecap":`round`,"stroke-linejoin":`round`,"aria-hidden":`true`},[U(`use`,{href:`#icon-undo`})],-1),U(`span`,null,`撤销`,-1)]],8,QI),U(`button`,{class:`app__btn app__btn--hist`,type:`button`,disabled:!i.preview||!a.canRedo,onClick:t[4]||=(...e)=>a.redo&&a.redo(...e),title:`前进一步(重做)`},[...t[47]||=[U(`svg`,{class:`app__icon`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,"stroke-width":`2`,"stroke-linecap":`round`,"stroke-linejoin":`round`,"aria-hidden":`true`},[U(`use`,{href:`#icon-redo`})],-1),U(`span`,null,`重做`,-1)]],8,$I),K(` 保存到本地:当前显示版本(撤销/重做导航到的历史版本,或编辑后未本地保存的版本)
320
320
  与本地磁盘版本不一致时高亮可用;一致时置灰。点击把当前版本真正写回磁盘,
@@ -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-1exc5zRY.js"></script>
11
+ <script type="module" crossorigin src="/assets/index-DRzBK4qj.js"></script>
12
12
  <link rel="stylesheet" crossorigin href="/assets/index-C_uqjKqI.css">
13
13
  </head>
14
14
  <body>
@@ -0,0 +1,220 @@
1
+ /**
2
+ * `cardbuddy skills upgrade` —— 把已装 cardbuddy 包内最新的技能库同步进当前工程的 skills/。
3
+ *
4
+ * 背景:init 时把 dist/vendored/skills 复制进 <root>/skills/(增量幂等,见 init.js)。
5
+ * 运行时 skillPaths 解析「项目 skills/ 优先于包内快照」(server/core/skillPaths.js),
6
+ * 故只 `npm i cardbuddy@latest` 不会更新工程里的 skills 副本——必须显式拉一次。本命令
7
+ * 就是为这个场景而生,且**只动 skills/**,绝不碰 .env.cb / package.json / README.md
8
+ * (那是 `init --force` 的危险面,可能冲掉端口 / FinMall 配置 / AI 网关占位)。
9
+ *
10
+ * 四类 diff(内容比对,非 mtime):
11
+ * ADDED 源有、目标无 → 复制(安全)
12
+ * SAME 两边内容一致 → 跳过
13
+ * MODIFIED 两边都有、内容不同 → 默认跳过 + 黄色警告(覆盖风险:消费者可能改过该 shipped skill)
14
+ * ORPHAN 目标有、源无 → 默认保留不删(可能是消费者私有 skill,或上游已下架该 skill)
15
+ *
16
+ * 标志:--force 覆盖 MODIFIED;--prune 额外删除 ORPHAN(危险,需显式);--dry-run 只报告;
17
+ * --strict 存在 MODIFIED 且未 --force 时返回非 0(便于 CI 感知未决覆盖)。
18
+ * 同步后写 skills/.synced-from = 当前装包版本,供下次 drift 检测与启动提示。
19
+ */
20
+ import fs from 'node:fs'
21
+ import path from 'node:path'
22
+ import { fileURLToPath } from 'node:url'
23
+ import chalk from 'chalk'
24
+ import { loadConfig } from './config.js'
25
+
26
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
27
+
28
+ const SKIP_DIRS = new Set(['node_modules', '__pycache__', '.git', '.local', '.cache', '.history'])
29
+ const SKIP_FILES = new Set(['.DS_Store', 'Thumbs.db'])
30
+ const MARKER = '.synced-from'
31
+
32
+ function isSkipped(name, isDir) {
33
+ if (isDir) return SKIP_DIRS.has(name)
34
+ return SKIP_FILES.has(name) || name.endsWith('.pyc')
35
+ }
36
+
37
+ function fmtBytes(n) {
38
+ if (n < 1024) return `${n} B`
39
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`
40
+ return `${(n / 1024 / 1024).toFixed(2)} MB`
41
+ }
42
+
43
+ /** 当前装包版本:发布态/源码态都读 <pkg>/package.json(dist 与 src 同深度)。 */
44
+ function ownVersion() {
45
+ try {
46
+ return JSON.parse(fs.readFileSync(path.resolve(__dirname, '..', 'package.json'), 'utf8')).version || '*'
47
+ } catch {
48
+ return '*'
49
+ }
50
+ }
51
+
52
+ function contentEquals(a, b) {
53
+ try {
54
+ const sa = fs.statSync(a)
55
+ const sb = fs.statSync(b)
56
+ if (sa.size !== sb.size) return false
57
+ return fs.readFileSync(a).equals(fs.readFileSync(b))
58
+ } catch {
59
+ return false
60
+ }
61
+ }
62
+
63
+ /** 递归列出 dir 下所有文件(跳过私有/缓存),返回相对 base 的正斜杠路径。 */
64
+ function walkFiles(dir, base = dir, out = []) {
65
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
66
+ const fp = path.join(dir, entry.name)
67
+ if (isSkipped(entry.name, entry.isDirectory())) continue
68
+ if (entry.isDirectory()) walkFiles(fp, base, out)
69
+ else if (entry.isFile()) out.push(path.relative(base, fp).split(path.sep).join('/'))
70
+ }
71
+ return out
72
+ }
73
+
74
+ /**
75
+ * 比对源/目标技能树(内容比对)。
76
+ * @returns {{ added:string[], same:string[], modified:string[], orphan:string[] }}
77
+ */
78
+ export function diffSkills(src, dest) {
79
+ const res = { added: [], same: [], modified: [], orphan: [] }
80
+ if (fs.existsSync(src)) {
81
+ for (const rel of walkFiles(src)) {
82
+ const sp = path.join(src, rel)
83
+ const dp = path.join(dest, rel)
84
+ if (!fs.existsSync(dp)) res.added.push(rel)
85
+ else if (contentEquals(sp, dp)) res.same.push(rel)
86
+ else res.modified.push(rel)
87
+ }
88
+ }
89
+ if (fs.existsSync(dest)) {
90
+ for (const rel of walkFiles(dest)) {
91
+ if (rel === MARKER) continue
92
+ if (!fs.existsSync(path.join(src, rel))) res.orphan.push(rel)
93
+ }
94
+ }
95
+ return res
96
+ }
97
+
98
+ /**
99
+ * 执行 skills 升级。
100
+ * @param {object} opts
101
+ * @param {string} [opts.targetDir='.'] 目标工程根(skills 落在 <targetDir>/skills)
102
+ * @param {boolean} [opts.force] 覆盖本地已修改的 shipped skill
103
+ * @param {boolean} [opts.dryRun] 只报告不落盘
104
+ * @param {boolean} [opts.prune] 额外删除包内已不存在的技能文件(默认保留)
105
+ * @param {boolean} [opts.strict] 存在未覆盖 MODIFIED 时返回非 0
106
+ * @param {ReturnType<import('./config.js').loadConfig>} [opts.config]
107
+ * @param {(msg: string) => void} [opts.log]
108
+ * @returns {Promise<number>} 进程退出码
109
+ */
110
+ export async function runSkillsUpgrade({
111
+ targetDir = '.',
112
+ force = false,
113
+ dryRun = false,
114
+ prune = false,
115
+ strict = false,
116
+ config,
117
+ log = (m) => console.log(m),
118
+ } = {}) {
119
+ const cfg = config || loadConfig()
120
+ const src = cfg.vendoredSkillsDir
121
+ const target = path.resolve(targetDir)
122
+ const destSkills = path.join(target, 'skills')
123
+
124
+ const tag = chalk.magenta('▌ ') + chalk.magenta.bold('cardbuddy skills upgrade') + ' '
125
+ const say = (m) => log(tag + m)
126
+ const ok = (m) => say(chalk.green(`• ${m}`))
127
+ const warn = (m) => say(chalk.yellow(`• ${m}`))
128
+ const dim = (m) => say(chalk.dim(`• ${m}`))
129
+
130
+ if (dryRun) say(chalk.yellow('(dry-run:只报告将要做的事,不落盘)'))
131
+
132
+ if (!src || !fs.existsSync(src)) {
133
+ log(chalk.red(`✖ 找不到包内技能库快照:vendoredSkillsDir 缺失(cardbuddy 安装不完整?)`))
134
+ return 1
135
+ }
136
+ if (path.resolve(src) === path.resolve(destSkills)) {
137
+ dim(`技能库源与目标同路径(${src}),无需升级`)
138
+ return 0
139
+ }
140
+
141
+ const version = ownVersion()
142
+ const diff = diffSkills(src, destSkills)
143
+
144
+ let copied = 0
145
+ let bytes = 0
146
+ let overwritten = 0
147
+ let deleted = 0
148
+
149
+ // ADDED 总是复制;MODIFIED 仅 --force 时复制
150
+ const toCopy = [...diff.added, ...(force ? diff.modified : [])]
151
+ for (const rel of toCopy) {
152
+ const sp = path.join(src, rel)
153
+ const dp = path.join(destSkills, rel)
154
+ const isModified = diff.modified.includes(rel)
155
+ if (!dryRun) {
156
+ fs.mkdirSync(path.dirname(dp), { recursive: true })
157
+ fs.copyFileSync(sp, dp)
158
+ }
159
+ bytes += fs.statSync(sp).size
160
+ copied++
161
+ if (isModified) overwritten++
162
+ }
163
+
164
+ // MODIFIED 未 --force:警告(不复制,防丢改动)
165
+ if (!force && diff.modified.length) {
166
+ warn(`⚠ 检测到 ${diff.modified.length} 个已修改的技能文件(本地副本与包内不一致),默认不覆盖,避免丢失你的改动:`)
167
+ for (const rel of diff.modified) say(chalk.yellow(` ${rel}`))
168
+ dim(' 如需用包内版本覆盖,加 --force;想先看差异,加 --dry-run 后人工对比。')
169
+ } else if (force && diff.modified.length) {
170
+ warn(`--force:已用包内版本覆盖 ${diff.modified.length} 个本地修改的技能文件(你的改动将被丢弃)`)
171
+ }
172
+
173
+ // ORPHAN:默认保留;--prune 才删除
174
+ if (diff.orphan.length) {
175
+ if (prune) {
176
+ warn(`⚠ --prune:将删除 ${diff.orphan.length} 个包内已不存在的技能文件(本地私有或上游已下架):`)
177
+ for (const rel of diff.orphan) {
178
+ const dp = path.join(destSkills, rel)
179
+ say(chalk.yellow(` ${rel}`))
180
+ if (!dryRun) {
181
+ fs.rmSync(dp, { force: true })
182
+ deleted++
183
+ }
184
+ }
185
+ } else {
186
+ dim(`${diff.orphan.length} 个本地独有技能文件(包内无对应,可能是你私有的或上游已下架)—— 已保留不删`)
187
+ if (diff.orphan.length <= 20) for (const rel of diff.orphan) dim(` ${rel}`)
188
+ else dim(` (仅显示前 20,共 ${diff.orphan.length} 个;加 --prune 会删除它们,谨慎)`)
189
+ }
190
+ }
191
+
192
+ // 写同步标记(best-effort)
193
+ if (!dryRun) {
194
+ try {
195
+ fs.mkdirSync(destSkills, { recursive: true })
196
+ fs.writeFileSync(path.join(destSkills, MARKER), version + '\n', 'utf8')
197
+ } catch {
198
+ /* best-effort */
199
+ }
200
+ }
201
+
202
+ // 汇总
203
+ log('')
204
+ say(chalk.green(dryRun ? 'dry-run 完成(未落盘)' : 'skills 升级完成'))
205
+ say(chalk.dim(` 源:包内 cardbuddy@${version}`))
206
+ const targetNote = fs.existsSync(destSkills) ? destSkills : `${destSkills}(将新建)`
207
+ say(chalk.dim(` 目标:${targetNote}`))
208
+ say(
209
+ chalk.dim(
210
+ ` 新增 ${diff.added.length} · 相同 ${diff.same.length} · 覆盖 ${overwritten} · 保留私有 ${diff.orphan.length}` +
211
+ `${prune ? ` · 删除孤儿 ${deleted}` : ''} · 体积 ${fmtBytes(bytes)}`
212
+ )
213
+ )
214
+
215
+ if (strict && diff.modified.length && !force) {
216
+ say(chalk.red('(--strict:存在未覆盖的 MODIFIED 文件,返回非 0)'))
217
+ return 2
218
+ }
219
+ return 0
220
+ }
@@ -191,7 +191,7 @@ export async function syncCards(opts = {}) {
191
191
  if (cardCodeFilter && card.id !== cardCodeFilter) continue;
192
192
  log.info('同步中', { page: `${pageNo}/${totalPages}`, index, id: card.id, title: card.title });
193
193
  try {
194
- const files = await syncCard(page, card, { outRoot, appId, workspaceId, summary, schemaHashes });
194
+ const files = await syncCard(page, card, { outRoot, appId, workspaceId, summary, schemaHashes, importMeta });
195
195
  // 空白卡不算失败,但要在结果里显式标记,避免与「抓到完整 schema」混为一谈。
196
196
  summary.synced.push({
197
197
  page: pageNo, index, id: card.id, title: card.title, files,
@@ -746,13 +746,13 @@ function filestoreToUrl(value) {
746
746
  return finmallConfig.filestoreUrl(value);
747
747
  }
748
748
 
749
- async function syncCard(page, card, { outRoot, appId, workspaceId, summary, schemaHashes }) {
749
+ async function syncCard(page, card, { outRoot, appId, workspaceId, summary, schemaHashes, importMeta }) {
750
750
  const cardDir = path.join(outRoot, card.id);
751
751
  await mkdir(cardDir, { recursive: true });
752
752
  const schema = await fetchCardSchema(page, card, { appId, workspaceId });
753
753
  // 空白卡(远端从未保存过 schema):合法状态,走专门的落盘分支,不再整批失败。
754
754
  if (!schema) {
755
- return syncBlankCard(page, card, { cardDir, appId, workspaceId });
755
+ return syncBlankCard(page, card, { cardDir, appId, workspaceId, importMeta });
756
756
  }
757
757
  // 推送前一致性闸门(基于实测不变量:IDC10001 列表 cardId ≡ schema 内 pageId)。
758
758
  // 若二者不等,说明抓取的 schema 与该实例错位(串卡 / 模板错挂),落盘后推送会按错 pageId 打错卡。
@@ -800,7 +800,7 @@ async function syncCard(page, card, { outRoot, appId, workspaceId, summary, sche
800
800
  * 全链路按这个数字主键定位线上卡片,错位会打到别的卡上)。
801
801
  * 本地无任何卡片时,落一份最小合法空白 schema,保证目录可被编辑器/预览正常加载。
802
802
  */
803
- async function syncBlankCard(page, card, { cardDir, appId, workspaceId }) {
803
+ async function syncBlankCard(page, card, { cardDir, appId, workspaceId, importMeta }) {
804
804
  const schemaPath = path.join(cardDir, 'schema.json');
805
805
  const dataPath = path.join(cardDir, 'data.json');
806
806
  const localSchema = await readJsonIfValid(schemaPath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cardbuddy",
3
- "version": "0.19.1",
3
+ "version": "0.19.3",
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": {