cardbuddy 0.12.2 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -95,6 +95,58 @@ cardbuddy --preview # 等价短参 -p
95
95
 
96
96
  ---
97
97
 
98
+ ## MCP:把 `card_*` 工具挂给外部 AI agent
99
+
100
+ ```bash
101
+ cardbuddy --mcp # stdio JSON-RPC 2.0;不启 HTTP 服务、不占端口
102
+ ```
103
+
104
+ 编辑器内置的网页 AI 助手走 `POST /__ai/chat`;`--mcp` 把**同一套** 16 个 `card_*` 工具
105
+ (实现同为 `mcp/tools.mjs` + `mcp/client.mjs`,不存在第二份契约逻辑)以 stdio 暴露给
106
+ Codex / Claude 这类外部 agent。工具清单与用法见 `skills/dsl-card-ai/SKILL.md`。
107
+
108
+ agent 侧配置(`cwd` 必须是卡片工程根,读写才落在 `src/cards/`):
109
+
110
+ ```jsonc
111
+ {
112
+ "mcpServers": {
113
+ "dsl-card": {
114
+ "command": "<工程绝对路径>/node_modules/.bin/cardbuddy",
115
+ "args": ["--mcp"]
116
+ }
117
+ }
118
+ }
119
+ ```
120
+
121
+ > **stdout 是协议信道。** 经 `npm run` 调用必须加 `--silent`:`npm run mcp` 会把
122
+ > `> 包名 脚本名` 横幅打到 stdout,多一行客户端就解析失败;直接调
123
+ > `node_modules/.bin/cardbuddy --mcp` 无此问题。诊断一律走 stderr 且默认静默,
124
+ > `CARDBUDDY_MCP_VERBOSE=1` 打开(会打出注入后的 `root=` / `apiBase=`)。
125
+
126
+ `cardbuddy init` 生成的工程已带 `npm run mcp` 脚本。回归见 `tools/tests/cardbuddy-mcp.test.mjs`。
127
+
128
+ ### 工具对本地服务的依赖分档
129
+
130
+ `--mcp` 自己不启服务。启动时自动注入 `CARD_ROOT=<工程根>` 与
131
+ `CARD_API_BASE=http(s)://127.0.0.1:<端口>`(端口取 `FINMALL_CARD_PORT` / `DSL_DEV_PORT`,默认
132
+ 8787;已有同名环境变量时不覆盖),工具据此决定走 HTTP 还是本地降级:
133
+
134
+ | 档 | 工具 |
135
+ | --- | --- |
136
+ | 纯本地,从不需要服务 | `card_route_input` `card_scaffold` `card_add_component` |
137
+ | 优先服务,连不上自动降级为直接读写盘(`validate` 跑 Python 校验器) | `card_list` `card_read` `card_write` `card_validate` `card_history` `card_rollback` |
138
+ | 优先服务,降级到包内独立离线渲染器 | `card_screenshot` |
139
+ | **必须**另起一个 `cardbuddy` | `card_render_status` `card_node_models` `card_import_meta` |
140
+ | **必须**另起服务 **且**已登录 FinMall | `card_sync` `card_save` `card_slice_upload` |
141
+
142
+ > **别直接跑包内 `dist/vendored/mcp/server.mjs`。** `mcp/client.mjs` 在**模块顶层**就 resolve
143
+ > `CARD_ROOT` / `CARD_API_BASE`(不是每次调用时读),绕过 CLI 包装层时前者会落到包内快照目录
144
+ > (`card_list` 返回 0 张卡)、后者回退 `devApiBase()` 的 5180(消费者工程没有 Vite 把 `/__*`
145
+ > 代理到 8787,`card_render_status` 直接 `dev-server-required`)。仓库内跑源码时入口回退
146
+ > `<repo>/mcp/server.mjs`,无需先 build。
147
+
148
+ ---
149
+
98
150
  ## 配置(环境变量 / `.env`)
99
151
 
100
152
  在仓库根目录的 `.env` 中配置,或直接用环境变量:
package/dist/cli.js CHANGED
@@ -17,9 +17,14 @@ import { installLogMirror } from './core.js'
17
17
  import { loadConfig } from './config.js'
18
18
  import { runInit } from './init.js'
19
19
 
20
+ const args = process.argv.slice(2)
21
+ const mcpMode = args.includes('--mcp')
22
+
20
23
  // 尽早镜像 stdout/stderr:让启动期日志(配置解析、端口占用提示等)也进入
21
24
  // 编辑器底部「服务日志」面板的环形缓冲。幂等,CARDBUDDY_STREAM_LOGS=0 时自动跳过。
22
- installLogMirror()
25
+ // --mcp 下整个跳过:该模式不启 HTTP 服务,/__logs 没有消费者,让镜像去逐帧拦截
26
+ // JSON-RPC 报文纯属白做功;协议信道的纯净由「这条路径上不打日志」保证(见 src/mcp.js)。
27
+ if (!mcpMode) installLogMirror()
23
28
 
24
29
  // 被 dev.mjs 拉起时让出来源前缀(由启动器带背景色 chip 标注),避免双重来源标签。
25
30
  const srcTag =
@@ -40,12 +45,11 @@ for (const p of ['.env', '.env.cb', '.env.cb.local']) {
40
45
  }
41
46
  }
42
47
 
43
- const args = process.argv.slice(2)
44
48
  if (args.includes('--help') || args.includes('-h')) {
45
49
  console.log(`cardbuddy — CardBuddy 本地开发服务
46
50
 
47
51
  用法:
48
- cardbuddy [--preview] [--help]
52
+ cardbuddy [--preview] [--mcp] [--help]
49
53
  cardbuddy init [目标目录] [--force] [--dry-run] [--no-skills] [--no-sample]
50
54
 
51
55
  命令:
@@ -61,6 +65,13 @@ if (args.includes('--help') || args.includes('-h')) {
61
65
  选项:
62
66
  --preview, -p 同时托管打包后的编辑器前端(本地预览),替代线上地址,并自动打开浏览器
63
67
  旧名 --offline / -o 仍可用,已废弃
68
+ --mcp 以 stdio 起 MCP server(JSON-RPC 2.0),把 16 个 card_* 工具挂给 Codex / Claude
69
+ 等外部 agent;不启 HTTP 服务、不占端口。stdout 只走协议(经 npm run 调用要加
70
+ --silent,否则横幅污染协议流),诊断走 stderr(CARDBUDDY_MCP_VERBOSE=1 打开)。
71
+ 本命令自身不起服务,工具按依赖分档:route_input / scaffold / add_component 纯本地;
72
+ list / read / write / validate / history / rollback 降级为直接读写盘;screenshot
73
+ 降级到包内离线渲染器;render_status / node_models / import_meta 必须另起一个
74
+ cardbuddy;sync / save / slice_upload 还必须已登录 FinMall。
64
75
  --help, -h 显示帮助
65
76
 
66
77
  环境变量:
@@ -117,26 +128,32 @@ if (subCommand === 'init') {
117
128
  }
118
129
  }
119
130
 
120
- const previewFlag = args.includes('--preview') || args.includes('-p')
121
- const legacyOfflineFlag = args.includes('--offline') || args.includes('-o')
122
- if (legacyOfflineFlag) {
123
- console.warn(
124
- srcTag + chalk.yellow('⚠ --offline 已废弃,请改用 --preview(本次仍按 preview 模式启动)')
125
- )
131
+ if (mcpMode) {
132
+ // stdio MCP server:不碰端口 / TLS / Vite,由 server.mjs 常驻监听 stdin 直到对端关闭。
133
+ const { startMcpServer } = await import('./mcp.js')
134
+ await startMcpServer(loadConfig())
135
+ } else {
136
+ const previewFlag = args.includes('--preview') || args.includes('-p')
137
+ const legacyOfflineFlag = args.includes('--offline') || args.includes('-o')
138
+ if (legacyOfflineFlag) {
139
+ console.warn(
140
+ srcTag + chalk.yellow('⚠ --offline 已废弃,请改用 --preview(本次仍按 preview 模式启动)')
141
+ )
142
+ }
143
+ // 只在显式给了 flag 时才覆盖配置:无条件传 { preview: false } 会把 DSL_DEV_PREVIEW=1 顶成
144
+ // false —— 旧实现正因如此,环境变量开关一直是失效的。
145
+ startServer(previewFlag || legacyOfflineFlag ? { preview: true } : {})
146
+ .then(({ shutdown }) => {
147
+ const stop = async () => {
148
+ console.log('\n' + srcTag + chalk.cyan('正在关闭…'))
149
+ await shutdown().catch(() => {})
150
+ process.exit(0)
151
+ }
152
+ process.on('SIGINT', stop)
153
+ process.on('SIGTERM', stop)
154
+ })
155
+ .catch((e) => {
156
+ console.error(srcTag + chalk.red('启动失败:') + e)
157
+ process.exit(1)
158
+ })
126
159
  }
127
- // 只在显式给了 flag 时才覆盖配置:无条件传 { preview: false } 会把 DSL_DEV_PREVIEW=1 顶成
128
- // false —— 旧实现正因如此,环境变量开关一直是失效的。
129
- startServer(previewFlag || legacyOfflineFlag ? { preview: true } : {})
130
- .then(({ shutdown }) => {
131
- const stop = async () => {
132
- console.log('\n' + srcTag + chalk.cyan('正在关闭…'))
133
- await shutdown().catch(() => {})
134
- process.exit(0)
135
- }
136
- process.on('SIGINT', stop)
137
- process.on('SIGTERM', stop)
138
- })
139
- .catch((e) => {
140
- console.error(srcTag + chalk.red('启动失败:') + e)
141
- process.exit(1)
142
- })
package/dist/config.js CHANGED
@@ -94,6 +94,12 @@ export function loadConfig(env = process.env) {
94
94
  vendoredRuntimeDir: path.join(vendorRoot, 'card-editor'),
95
95
  // 仓库内开发回退:从仓库根运行时(cwd=仓库根)能直接命中 card-editor/,无需先 build。
96
96
  repoEditorDir: path.resolve(process.cwd(), 'card-editor'),
97
+ // MCP stdio server 入口(`cardbuddy --mcp`):发布态命中包内快照 vendored/mcp/server.mjs;
98
+ // 仓库内跑源码时 src/vendored 不存在,回退到 <repo>/mcp/server.mjs —— src 与 dist 同深度,
99
+ // 故 __dirname 相对路径在两种情形下都对(不像 root 那样依赖 cwd)。
100
+ mcpServerEntry: fs.existsSync(path.join(vendorRoot, 'mcp', 'server.mjs'))
101
+ ? path.join(vendorRoot, 'mcp', 'server.mjs')
102
+ : path.resolve(__dirname, '../../../mcp/server.mjs'),
97
103
  // 包内技能快照(build 阶段从仓库 skills/ 全量内联,排除 .local / node_modules / __pycache__):
98
104
  // 既是运行时的只读回退,也是 `cardbuddy init` 释放到项目根的源。
99
105
  vendoredSkillsDir,
package/dist/init.js CHANGED
@@ -9,7 +9,7 @@
9
9
  * 释放后既跑得快也写得了(FinMall 凭据落在项目内,不进 node_modules)
10
10
  * tools/link-skills.mjs 把 skills/ 链到 Codex / Claude / WorkBuddy 等 AI 工具的项目级技能目录
11
11
  * .env.cb 共享配置模板(端口 / 远端基址 / AI 网关占位;密钥写 .env.cb.local)
12
- * package.json 声明 cardbuddy 依赖与 dev / preview / link:skills 脚本
12
+ * package.json 声明 cardbuddy 依赖与 dev / preview / mcp / link:skills 脚本
13
13
  * .gitignore 私密层与产物不入库
14
14
  * README.md 上手说明
15
15
  *
@@ -188,6 +188,43 @@ skills/ AI 协作技能库(校验 / 同步 / 建卡管线脚本
188
188
  npm run link:skills # 交互式选择 Codex / Claude / WorkBuddy 等,建立项目级技能软链
189
189
  \`\`\`
190
190
 
191
+ ## 把卡片工具挂给外部 AI agent(MCP,可选)
192
+
193
+ 编辑器内置的网页 AI 助手走 \`POST /__ai/chat\`。若要让 Codex / Claude 这类**外部** agent
194
+ 直接操作卡片,用随包提供的 MCP stdio server(16 个 \`card_*\` 工具,清单见
195
+ \`skills/dsl-card-ai/SKILL.md\`):
196
+
197
+ \`\`\`jsonc
198
+ // agent 的 MCP 配置;cwd 必须是本工程根,卡片读写才落在 src/cards/
199
+ {
200
+ "mcpServers": {
201
+ "dsl-card": {
202
+ "command": "<本工程绝对路径>/node_modules/.bin/cardbuddy",
203
+ "args": ["--mcp"]
204
+ }
205
+ }
206
+ }
207
+ \`\`\`
208
+
209
+ 也可用 \`npm run --silent mcp\`。**\`--silent\` 不能省**:\`npm run\` 会把
210
+ \`> 包名 脚本名\` 横幅打到 **stdout**,而 stdout 是 JSON-RPC 协议信道,多一行就让客户端解析失败。
211
+
212
+ \`--mcp\` 自身不启服务、不占端口,工具按对本地服务的依赖分档:
213
+
214
+ | 档 | 工具 |
215
+ | --- | --- |
216
+ | 纯本地,从不需要服务 | \`card_route_input\` \`card_scaffold\` \`card_add_component\` |
217
+ | 优先服务,连不上自动降级为直接读写盘(\`validate\` 跑 Python 校验器) | \`card_list\` \`card_read\` \`card_write\` \`card_validate\` \`card_history\` \`card_rollback\` |
218
+ | 优先服务,降级到包内独立离线渲染器 | \`card_screenshot\` |
219
+ | **必须**另起一个 \`npm run dev\` | \`card_render_status\` \`card_node_models\` \`card_import_meta\` |
220
+ | **必须**另起服务 **且**已登录 FinMall | \`card_sync\` \`card_save\` \`card_slice_upload\` |
221
+
222
+ \`--mcp\` 会自动把 \`CARD_ROOT\` 指到本工程根、\`CARD_API_BASE\` 指到 \`.env.cb\` 的
223
+ \`FINMALL_CARD_PORT\`(默认 8787)。**别直接跑包内 \`vendored/mcp/server.mjs\`**:那样
224
+ \`CARD_ROOT\` 会落到 node_modules 里的快照目录(\`card_list\` 返回 0 张卡),端口也回退到
225
+ 5180(\`card_render_status\` 直接报 \`dev-server-required\`)。排查时可设
226
+ \`CARDBUDDY_MCP_VERBOSE=1\` 让启动信息打到 stderr。
227
+
191
228
  ## FinMall 远端能力(可选)
192
229
 
193
230
  同步 / 保存 / 切图上传需要已登录的 FinMall 会话与 \`playwright\`
@@ -208,6 +245,10 @@ function projectPackageJson(projectName, version) {
208
245
  dev: 'cardbuddy',
209
246
  start: 'cardbuddy',
210
247
  preview: 'cardbuddy --preview',
248
+ // 给外部 agent(Codex / Claude)挂 card_* 工具的 stdio MCP server。
249
+ // 注意:MCP 客户端要走 `npm run --silent mcp` 或直接调 node_modules/.bin/cardbuddy --mcp,
250
+ // 因为 `npm run` 的横幅会打到 stdout、污染 JSON-RPC 协议流(详见生成的 README)。
251
+ mcp: 'cardbuddy --mcp',
211
252
  'link:skills': 'node tools/link-skills.mjs',
212
253
  },
213
254
  devDependencies: {
@@ -382,7 +423,7 @@ export async function runInit({
382
423
  const pkgState = writeFileSafe(path.join(target, 'package.json'), projectPackageJson(projectName, version), opts)
383
424
  pkgState === 'skipped'
384
425
  ? dim('package.json 已存在(跳过,--force 可覆盖)')
385
- : ok(`${dryRun ? '将生成' : '已生成'} package.json(cardbuddy ^${version} + dev / preview / link:skills 脚本)`)
426
+ : ok(`${dryRun ? '将生成' : '已生成'} package.json(cardbuddy ^${version} + dev / preview / mcp / link:skills 脚本)`)
386
427
 
387
428
  // 6) tools/link-skills.mjs(把 skills/ 链给 AI 工具)
388
429
  const linkSrc = resolveLinkSkillsSource(config)
package/dist/mcp.js ADDED
@@ -0,0 +1,57 @@
1
+ /**
2
+ * `cardbuddy --mcp` —— 以 stdio 起 MCP server(JSON-RPC 2.0,newline-delimited)。
3
+ *
4
+ * 为什么要有它:包内快照早就带了 mcp/tools.mjs + client.mjs(AI 工具调度的实现真身),
5
+ * 却没带 server.mjs 这个 stdio 入口 —— 于是消费者工程只能用编辑器内置的网页 AI 助手
6
+ * (/__ai/chat),没法把同一套 card_* 工具挂给 Codex / Claude 这类外部 agent。而随包释放的
7
+ * dsl-card-ai 技能整篇是以「只调 dsl-card MCP 工具、绝不重实现契约逻辑」为前提写的,
8
+ * 缺了入口那份技能在消费者工程里就落不了地。
9
+ *
10
+ * 两个必须守住的前提:
11
+ * 1. **stdout 是协议信道**,多一行非 JSON 就让客户端解析失败(`npm run` 的 `> 包名 脚本名`
12
+ * 横幅正是典型污染源,故推荐 `.bin/cardbuddy --mcp` 或 `npm run --silent mcp`)。这条路径
13
+ * 上因此一律不打日志:cli.js 跳过 installLogMirror()(该镜像服务于编辑器「服务日志」面板,
14
+ * MCP 模式没有 HTTP 服务、/__logs 无消费者),启动诊断只走 stderr 且默认静默。
15
+ * 2. **CARD_ROOT / CARD_API_BASE 必须在 import 之前落定**。mcp/client.mjs 在模块顶层
16
+ * 就读这两个变量(不是每次调用时读),晚一步注入就无效:
17
+ * - CARD_ROOT 不给,client 会把「mcp/ 的上一级」当工程根 —— 发布态那是包内快照目录,
18
+ * 根本找不到 src/cards;
19
+ * - CARD_API_BASE 不给,client 回退 devApiBase() 即 5180(仓库里由 Vite 把 /__* 代理到
20
+ * 8787),而消费者工程通常只有 cardbuddy 的 8787、没有 Vite,于是连不上,
21
+ * 且不支持离线降级的 6 个工具(render_status / node_models / import_meta / sync /
22
+ * save / slice_upload)直接报 dev-server-required。
23
+ *
24
+ * 本地服务没起也能用大部分:route_input / scaffold / add_component 纯本地;list / read / write /
25
+ * validate / history / rollback 降级为直接读写盘(validate 跑 Python 校验器);screenshot 降级到
26
+ * 包内独立离线渲染器。只有上述 6 个必须另起一个 cardbuddy(sync / save / slice_upload 还需已登录
27
+ * FinMall)。完整分档表见 README「MCP:把 card_* 工具挂给外部 AI agent」一节。
28
+ */
29
+ import fs from 'node:fs'
30
+ import { pathToFileURL } from 'node:url'
31
+
32
+ /** 启动信息只在对 stderr 可见时才打(交互式调试);被 MCP 客户端接管时保持静默。 */
33
+ function diag(line) {
34
+ if (process.stderr.isTTY || process.env.CARDBUDDY_MCP_VERBOSE === '1') {
35
+ process.stderr.write(`[cardbuddy --mcp] ${line}\n`)
36
+ }
37
+ }
38
+
39
+ /**
40
+ * @param {ReturnType<import('./config.js').loadConfig>} config
41
+ * @returns {Promise<void>} 解析后 server.mjs 自行常驻监听 stdin,本函数不返回直到进程结束
42
+ */
43
+ export async function startMcpServer(config) {
44
+ const entry = config.mcpServerEntry
45
+ if (!entry || !fs.existsSync(entry)) {
46
+ // 走 stderr + 非 0 退出:MCP 客户端会把它显示成 server 启动失败,而不是静默挂着
47
+ process.stderr.write(`[cardbuddy --mcp] 找不到 MCP server 入口:${entry || '(未解析)'}\n`)
48
+ process.exit(1)
49
+ }
50
+ if (!process.env.CARD_ROOT) process.env.CARD_ROOT = config.root
51
+ if (!process.env.CARD_API_BASE) {
52
+ process.env.CARD_API_BASE = `${config.https ? 'https' : 'http'}://127.0.0.1:${config.port}`
53
+ }
54
+ diag(`root=${process.env.CARD_ROOT}`)
55
+ diag(`apiBase=${process.env.CARD_API_BASE}(本地服务未起时 list/read/write/validate 自动降级为直接读盘)`)
56
+ await import(pathToFileURL(entry).href)
57
+ }
@@ -248,7 +248,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
248
248
 
249
249
  `:``}[已挂载本地文件(仅本机路径,未上传;需要内容请用工具读取)]\n${e.join(`
250
250
  `)}`}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`?e.result.slice(0,4e3):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},notify(e,t){this.notice={type:e,text:t},this._noticeTimer&&clearTimeout(this._noticeTimer),this._noticeTimer=setTimeout(()=>{this.notice=null},6e3)}}},Bk={class:`aichat__head`},Vk={class:`aichat__titles`},Hk={class:`aichat__sub`},Uk={class:`aichat__acts`},Wk=[`disabled`],Gk={key:0,class:`aichat__banner`},Kk={class:`aichat__scroll`},qk={key:0,class:`aichat__loading`},Jk={key:1,class:`aichat__empty`},Yk={class:`aichat__suggests`},Xk=[`onClick`],Zk={viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,"stroke-width":`2`,"stroke-linecap":`round`,"stroke-linejoin":`round`,"aria-hidden":`true`},Qk=[`href`],$k={key:0,class:`aichat__live`};function eA(e,t,n,r,i,a){let o=L(`ChatMessage`),s=L(`ChatComposer`),c=L(`AiSettings`),l=L(`ConfirmDialog`);return B(),V(`aside`,{class:j([`aichat`,{"is-open":n.visible}]),style:A({width:a.panelWidth+`px`}),role:`complementary`,"aria-label":`AI 开发助手`,onKeydown:t[11]||=vl((...e)=>a.onEscape&&a.onEscape(...e),[`esc`])},[K(` 左缘拖拽把手:调宽(双击恢复默认 33%) `),U(`div`,{class:j([`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`,Bk,[t[16]||=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`,Vk,[t[12]||=U(`span`,{class:`aichat__title`},`AI 开发助手`,-1),U(`span`,Hk,[n.cardId?(B(),V(z,{key:0},[G(M(n.cardName||n.cardId),1)],64)):(B(),V(z,{key:1},[G(`未选择卡片`)],64)),i.config.model?(B(),V(z,{key:2},[G(` · `+M(i.config.model),1)],64)):K(`v-if`,!0)])]),U(`span`,Uk,[U(`button`,{type:`button`,class:`aichat__iconbtn`,title:`清空本卡片对话记录`,disabled:!i.messages.length||i.busy,onClick:t[2]||=(...e)=>a.askClear&&a.askClear(...e)},[...t[13]||=[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,Wk),U(`button`,{type:`button`,class:`aichat__iconbtn`,title:`模型设置`,onClick:t[3]||=(...e)=>a.openSettings&&a.openSettings(...e)},[...t[14]||=[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-settings`})],-1)]]),U(`button`,{type:`button`,class:`aichat__iconbtn`,title:`关闭(Esc)`,onClick:t[4]||=t=>e.$emit(`close`)},[...t[15]||=[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`,Gk,[t[17]||=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[18]||=U(`span`,null,`尚未配置模型,无法对话。`,-1),U(`button`,{type:`button`,class:`aichat__banner-btn`,onClick:t[5]||=(...e)=>a.openSettings&&a.openSettings(...e)},`去设置`)])),K(` 消息区:滚动容器与悬浮「回到底部」共处一个定位上下文,按钮只贴消息区底缘,
251
- 输入区拖高 / 反馈条出现都不会把它盖住或悬空 `),U(`div`,Kk,[U(`div`,{ref:`listEl`,class:`aichat__list`,onScroll:t[6]||=(...e)=>a.onScroll&&a.onScroll(...e)},[i.loadingHistory?(B(),V(`div`,qk,`正在加载对话记录…`)):i.messages.length?(B(),V(z,{key:2},[(B(!0),V(z,null,R(i.messages,(e,t)=>(B(),H(o,{key:e.id,msg:e,streaming:i.busy&&t===i.messages.length-1,"can-regenerate":e.role===`assistant`&&t===i.messages.length-1,onRegenerate:a.regenerate,onApproval:a.onApproval,onContinue:a.onContinue},null,8,[`msg`,`streaming`,`can-regenerate`,`onRegenerate`,`onApproval`,`onContinue`]))),128)),i.busy?(B(),V(`div`,$k,`生成中… 可随时终止`)):K(`v-if`,!0)],64)):(B(),V(`div`,Jk,[t[19]||=as(`<span class="aichat__empty-logo"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="#icon-sparkles"></use></svg></span><p class="aichat__empty-title">让 AI 直接改这张卡</p><p class="aichat__empty-desc"> 描述需求即可,AI 会调用卡片工具读改校验并截图自检;也可以把设计稿标注目录、参考图拖进来,只传本机路径不上传。 </p>`,3),U(`div`,Yk,[(B(!0),V(z,null,R(i.suggestions,e=>(B(),V(`button`,{key:e.text,type:`button`,class:`aichat__suggest`,onClick:t=>a.useSuggestion(e)},[(B(),V(`svg`,Zk,[U(`use`,{href:e.icon},null,8,Qk)])),U(`span`,null,M(e.label),1)],8,Xk))),128))])]))],544),K(` 回到底部:脱离底部吸附时出现 `),I(U(`button`,{type:`button`,class:`aichat__jump`,title:`回到底部`,onClick:t[7]||=e=>a.scrollToBottom(!0)},[...t[20]||=[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(` 操作反馈(附件解析失败 / 历史保存失败等) `),i.notice?(B(),V(`div`,{key:1,class:j([`aichat__notice`,`aichat__notice--${i.notice.type}`])},[U(`span`,null,M(i.notice.text),1),U(`button`,{type:`button`,class:`aichat__notice-x`,title:`关闭`,onClick:t[8]||=e=>i.notice=null},[...t[21]||=[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(` 模型设置(面板内浮层,不额外抬 z-index) `),i.settingsOpen?(B(),H(c,{key:2,config:i.config,onClose:t[9]||=e=>i.settingsOpen=!1,onSaved:a.onConfigSaved},null,8,[`config`,`onSaved`])):K(`v-if`,!0),W(l,{visible:i.confirmClear,title:`清空对话记录`,message:`将删除卡片「${n.cardName||n.cardId||`当前卡片`}」的全部对话记录,此操作不可撤销。`,"confirm-text":`清空`,severity:`danger`,onConfirm:a.doClear,onCancel:t[10]||=e=>i.confirmClear=!1},null,8,[`visible`,`message`,`onConfirm`])],38)}var tA=X(zk,[[`render`,eA],[`__file`,`/Users/owlaford/Workspace/dsl-card/card-editor/components/ChatPanel.vue`]]),nA=`/assets/site-icon-CFMif8z4.png`,rA={includeMocks:!0},iA=2500,aA=new eu,oA=au({},aA);typeof window<`u`&&(window.__rt=oA);var sA=188,cA=320,lA=200,uA=160,dA={name:`App`,components:{RenderTree:ap,DebugPanel:wy,ApiCallPanel:By,CardSelect:ib,AlertCardLayer:mb,DeviceOverlay:Yb,PickerOverlay:ix,DatePickerOverlay:yx,SelectionOverlay:Cx,HoverGuides:kx,RemotePreview:_S,AppToast:TS,ConfirmDialog:_p,CardGallery:KS,NewCardDialog:uC,ComponentPalette:TC,Switch:kC,ChatPanel:tA},provide(){return{adapter:aA,cardRuntime:oA,customComponents:{}}},data(){return{adapter:aA,rt:oA,logoUrl:nA,appVersion:`0.16.2`,cardList:[],cardCount:0,galleryOpen:!1,newCardOpen:!1,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,copiedSchema:!1,copyTimer:null,savedCard:!1,savingCard:!1,saveTimer:null,savingRemote:!1,savedRemote:!1,verifiedRemote:null,saveRemoteTimer:null,syncingRemote:!1,syncedRemote:!1,remotePreviewOpen:!1,syncTarget:null,syncTimer:null,uploadingSlices:!1,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}})(),devServiceForm:Nl(),apiBaseResolved:Fl(),devSvcLive:`unknown`,devSvcDown:!1,_unsubCardsChanged:null}},computed:{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!this.localSchema||!this.currentSchema?!1:!this.structuralEqual(this.currentSchema,this.localSchema)||!this.dataEqual(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||sA;return Math.max(e,Math.min(t,n))},viewportWidth(){return this.stageWidth,typeof window<`u`?window.innerWidth:1280},debugSoftWidth(){return Math.max(lA,Math.floor(this.viewportWidth/3))},previewBounds(){let e=this.stageWidth||0,t=Math.floor(e/2),n=e-cA-this.debugSoftWidth;return{min:sA,max:Math.max(sA,Math.min(t,n))}},debugMinWidth(){let e=this.colsWidth||Math.max(0,(this.stageWidth||0)-this.previewWidth);return Math.max(lA,Math.min(this.debugSoftWidth,Math.floor(e)-cA))},apiPanelBounds(){let e=this.colsWidth||Math.max(0,(this.stageWidth||0)-this.previewWidth),t=Math.max(uA,Math.floor(e)-this.debugMinWidth);return{min:Math.min(cA,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||``},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=``,Ex())},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)},async created(){Ib(oA);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:{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){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.newCardOpen=!0,this.fetchCardList()},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(Xg(r.schema,rA)),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(Xg(r.schema,rA)),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,ub(),Lb(),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},normalizeForCompare(e){let t=this.cloneSchema(e);if(t&&t.dictionary&&Array.isArray(t.dictionary.inputParams))for(let e of t.dictionary.inputParams)e&&typeof e==`object`&&(e.defaultValue=void 0);return t},structuralEqual(e,t){return!e||!t?e===t:JSON.stringify(this.normalizeForCompare(e))===JSON.stringify(this.normalizeForCompare(t))},cloneData(e){return e==null?e:JSON.parse(JSON.stringify(e,(e,t)=>e===`_schemaNode`?void 0:t))},dataEqual(e,t){return JSON.stringify(e)===JSON.stringify(t)},applySchemaSnapshot(e){if(!this.selectedId||!e)return;let t=e.schema||e,n=e.data==null?Xg(t,rA):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)return;let e=this.structuralEqual(this.currentSchema,this.baseline.schema),t=this.dataEqual(this.rt.rootScope,Xg(this.baseline.schema,rA));if(e&&t)return;let n={schema:this.baseline.schema};this.past.push(n),this.past.length>30&&this.past.shift(),this.future=[],this.baseline={schema:this.cloneSchema(this.currentSchema)},this.persistSnapshot(n)},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?Xg(e.schema,rA):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`)},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=Zg(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=``,Ex(),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){Ex();return}if(e.leave){Ex(e.id);return}let t=typeof document<`u`?document.querySelector(`.phone__screen`):null;if(!t){Ex(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?Tx(r,e.id):Ex(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)},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 copySchema(){if(!this.currentSchema)return;this.syncDataToSchema();let e=``;try{let t={...this.currentSchema};this.rt&&this.rt.rootScope&&(t.data=this.rt.rootScope),e=JSON.stringify(t,(e,t)=>{if(e!==`_schemaNode`)return t},2)}catch{e=`(无法序列化)`}await this.doCopy(e)&&(this.copiedSchema=!0,this.copyTimer&&clearTimeout(this.copyTimer),this.copyTimer=setTimeout(()=>{this.copiedSchema=!1},1500))},async doCopy(e){try{if(navigator.clipboard&&navigator.clipboard.writeText)return await navigator.clipboard.writeText(e),!0}catch{}try{let t=document.createElement(`textarea`);t.value=e,t.style.position=`fixed`,t.style.opacity=`0`,document.body.appendChild(t),t.select();let n=document.execCommand(`copy`);return document.body.removeChild(t),n}catch{return!1}},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`)}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===`slices`){if(!this.preview||!this.selectedId||this.uploadingSlices)return;this.requestConfirm({title:`切图上传`,message:`即将把当前卡片「${this.selectedId}」引用的本地切图(src/slices/)上传到 FinMall 图片库,并把卡片里的 /slices/ 引用替换为线上地址(@filestore/…)。本地切图文件保留不动;内容未变的切图会直接复用已有线上地址,不重复上传。`+(this.historyDirty?`
251
+ 输入区拖高 / 反馈条出现都不会把它盖住或悬空 `),U(`div`,Kk,[U(`div`,{ref:`listEl`,class:`aichat__list`,onScroll:t[6]||=(...e)=>a.onScroll&&a.onScroll(...e)},[i.loadingHistory?(B(),V(`div`,qk,`正在加载对话记录…`)):i.messages.length?(B(),V(z,{key:2},[(B(!0),V(z,null,R(i.messages,(e,t)=>(B(),H(o,{key:e.id,msg:e,streaming:i.busy&&t===i.messages.length-1,"can-regenerate":e.role===`assistant`&&t===i.messages.length-1,onRegenerate:a.regenerate,onApproval:a.onApproval,onContinue:a.onContinue},null,8,[`msg`,`streaming`,`can-regenerate`,`onRegenerate`,`onApproval`,`onContinue`]))),128)),i.busy?(B(),V(`div`,$k,`生成中… 可随时终止`)):K(`v-if`,!0)],64)):(B(),V(`div`,Jk,[t[19]||=as(`<span class="aichat__empty-logo"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="#icon-sparkles"></use></svg></span><p class="aichat__empty-title">让 AI 直接改这张卡</p><p class="aichat__empty-desc"> 描述需求即可,AI 会调用卡片工具读改校验并截图自检;也可以把设计稿标注目录、参考图拖进来,只传本机路径不上传。 </p>`,3),U(`div`,Yk,[(B(!0),V(z,null,R(i.suggestions,e=>(B(),V(`button`,{key:e.text,type:`button`,class:`aichat__suggest`,onClick:t=>a.useSuggestion(e)},[(B(),V(`svg`,Zk,[U(`use`,{href:e.icon},null,8,Qk)])),U(`span`,null,M(e.label),1)],8,Xk))),128))])]))],544),K(` 回到底部:脱离底部吸附时出现 `),I(U(`button`,{type:`button`,class:`aichat__jump`,title:`回到底部`,onClick:t[7]||=e=>a.scrollToBottom(!0)},[...t[20]||=[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(` 操作反馈(附件解析失败 / 历史保存失败等) `),i.notice?(B(),V(`div`,{key:1,class:j([`aichat__notice`,`aichat__notice--${i.notice.type}`])},[U(`span`,null,M(i.notice.text),1),U(`button`,{type:`button`,class:`aichat__notice-x`,title:`关闭`,onClick:t[8]||=e=>i.notice=null},[...t[21]||=[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(` 模型设置(面板内浮层,不额外抬 z-index) `),i.settingsOpen?(B(),H(c,{key:2,config:i.config,onClose:t[9]||=e=>i.settingsOpen=!1,onSaved:a.onConfigSaved},null,8,[`config`,`onSaved`])):K(`v-if`,!0),W(l,{visible:i.confirmClear,title:`清空对话记录`,message:`将删除卡片「${n.cardName||n.cardId||`当前卡片`}」的全部对话记录,此操作不可撤销。`,"confirm-text":`清空`,severity:`danger`,onConfirm:a.doClear,onCancel:t[10]||=e=>i.confirmClear=!1},null,8,[`visible`,`message`,`onConfirm`])],38)}var tA=X(zk,[[`render`,eA],[`__file`,`/Users/owlaford/Workspace/dsl-card/card-editor/components/ChatPanel.vue`]]),nA=`/assets/site-icon-CFMif8z4.png`,rA={includeMocks:!0},iA=2500,aA=new eu,oA=au({},aA);typeof window<`u`&&(window.__rt=oA);var sA=188,cA=320,lA=200,uA=160,dA={name:`App`,components:{RenderTree:ap,DebugPanel:wy,ApiCallPanel:By,CardSelect:ib,AlertCardLayer:mb,DeviceOverlay:Yb,PickerOverlay:ix,DatePickerOverlay:yx,SelectionOverlay:Cx,HoverGuides:kx,RemotePreview:_S,AppToast:TS,ConfirmDialog:_p,CardGallery:KS,NewCardDialog:uC,ComponentPalette:TC,Switch:kC,ChatPanel:tA},provide(){return{adapter:aA,cardRuntime:oA,customComponents:{}}},data(){return{adapter:aA,rt:oA,logoUrl:nA,appVersion:`0.17.1`,cardList:[],cardCount:0,galleryOpen:!1,newCardOpen:!1,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,copiedSchema:!1,copyTimer:null,savedCard:!1,savingCard:!1,saveTimer:null,savingRemote:!1,savedRemote:!1,verifiedRemote:null,saveRemoteTimer:null,syncingRemote:!1,syncedRemote:!1,remotePreviewOpen:!1,syncTarget:null,syncTimer:null,uploadingSlices:!1,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}})(),devServiceForm:Nl(),apiBaseResolved:Fl(),devSvcLive:`unknown`,devSvcDown:!1,_unsubCardsChanged:null}},computed:{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!this.localSchema||!this.currentSchema?!1:!this.structuralEqual(this.currentSchema,this.localSchema)||!this.dataEqual(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||sA;return Math.max(e,Math.min(t,n))},viewportWidth(){return this.stageWidth,typeof window<`u`?window.innerWidth:1280},debugSoftWidth(){return Math.max(lA,Math.floor(this.viewportWidth/3))},previewBounds(){let e=this.stageWidth||0,t=Math.floor(e/2),n=e-cA-this.debugSoftWidth;return{min:sA,max:Math.max(sA,Math.min(t,n))}},debugMinWidth(){let e=this.colsWidth||Math.max(0,(this.stageWidth||0)-this.previewWidth);return Math.max(lA,Math.min(this.debugSoftWidth,Math.floor(e)-cA))},apiPanelBounds(){let e=this.colsWidth||Math.max(0,(this.stageWidth||0)-this.previewWidth),t=Math.max(uA,Math.floor(e)-this.debugMinWidth);return{min:Math.min(cA,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||``},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=``,Ex())},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)},async created(){Ib(oA);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:{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){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.newCardOpen=!0,this.fetchCardList()},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(Xg(r.schema,rA)),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(Xg(r.schema,rA)),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,ub(),Lb(),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},normalizeForCompare(e){let t=this.cloneSchema(e);if(t&&t.dictionary&&Array.isArray(t.dictionary.inputParams))for(let e of t.dictionary.inputParams)e&&typeof e==`object`&&(e.defaultValue=void 0);return t},structuralEqual(e,t){return!e||!t?e===t:JSON.stringify(this.normalizeForCompare(e))===JSON.stringify(this.normalizeForCompare(t))},cloneData(e){return e==null?e:JSON.parse(JSON.stringify(e,(e,t)=>e===`_schemaNode`?void 0:t))},dataEqual(e,t){return JSON.stringify(e)===JSON.stringify(t)},applySchemaSnapshot(e){if(!this.selectedId||!e)return;let t=e.schema||e,n=e.data==null?Xg(t,rA):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)return;let e=this.structuralEqual(this.currentSchema,this.baseline.schema),t=this.dataEqual(this.rt.rootScope,Xg(this.baseline.schema,rA));if(e&&t)return;let n={schema:this.baseline.schema};this.past.push(n),this.past.length>30&&this.past.shift(),this.future=[],this.baseline={schema:this.cloneSchema(this.currentSchema)},this.persistSnapshot(n)},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?Xg(e.schema,rA):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`)},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=Zg(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=``,Ex(),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){Ex();return}if(e.leave){Ex(e.id);return}let t=typeof document<`u`?document.querySelector(`.phone__screen`):null;if(!t){Ex(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?Tx(r,e.id):Ex(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)},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 copySchema(){if(!this.currentSchema)return;this.syncDataToSchema();let e=``;try{let t={...this.currentSchema};this.rt&&this.rt.rootScope&&(t.data=this.rt.rootScope),e=JSON.stringify(t,(e,t)=>{if(e!==`_schemaNode`)return t},2)}catch{e=`(无法序列化)`}await this.doCopy(e)&&(this.copiedSchema=!0,this.copyTimer&&clearTimeout(this.copyTimer),this.copyTimer=setTimeout(()=>{this.copiedSchema=!1},1500))},async doCopy(e){try{if(navigator.clipboard&&navigator.clipboard.writeText)return await navigator.clipboard.writeText(e),!0}catch{}try{let t=document.createElement(`textarea`);t.value=e,t.style.position=`fixed`,t.style.opacity=`0`,document.body.appendChild(t),t.select();let n=document.execCommand(`copy`);return document.body.removeChild(t),n}catch{return!1}},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`)}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===`slices`){if(!this.preview||!this.selectedId||this.uploadingSlices)return;this.requestConfirm({title:`切图上传`,message:`即将把当前卡片「${this.selectedId}」引用的本地切图(src/slices/)上传到 FinMall 图片库,并把卡片里的 /slices/ 引用替换为线上地址(@filestore/…)。本地切图文件保留不动;内容未变的切图会直接复用已有线上地址,不重复上传。`+(this.historyDirty?`
252
252
  当前卡片有未保存的改动,会先保存到本地再执行,避免被随后的重载丢弃。`:``)+`
253
253
  是否继续?`,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 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(Xg(this.currentSchema,rA)),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.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 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`),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){this.newCardOpen=!1,Z(`已创建空白卡片 ${e}`,`ok`),await this.fetchCardList(),await this.loadCard(e)},async onRefresh(){let e=this.selectedId;if(!(!e||this.refreshing)){this.refreshing=!0;try{await this.loadCard(e),await this.fetchCardList();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}}},onCardsChanged(e){let t=e&&e.file||``;if(/\.history[\\/]/.test(t)||/data\.json$/i.test(t))return;let n=t.match(/src[\\/]cards[\\/]([^\\/]+)[\\/]/),r=n?n[1]:null;this._cardsChangedTimer||=setTimeout(()=>{if(this._cardsChangedTimer=null,this.fetchCardList(),r&&r===this.selectedId){if(this.selfSaveId===r&&Date.now()-this.selfSaveAt<iA){this.selfSaveId=``,this.selfSaveAt=0;return}if(this.historyDirty)this.promptExternalReload(r);else{let e=this.currentSchema&&this.currentSchema.name||r;this.showExternalReloadNote(`卡片「${e}」已在编辑器外被修改,已自动重新载入预览`),this.loadCard(this.selectedId)}}},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)}}},fA={class:`app__bar`},pA={class:`app__logo`},mA=[`src`],hA={class:`app__logo-version`},gA={class:`csel-combo`},_A=[`disabled`],vA=[`disabled`],yA=[`title`],bA={class:`app__badge-text`},xA={class:`app__bar-actions`},SA={class:`app__op-group`,role:`group`,"aria-label":`编辑历史与本地操作`},CA=[`disabled`],wA=[`disabled`],TA=[`disabled`,`title`],EA=[`disabled`],DA=[`disabled`],OA={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`},kA={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`},AA={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`},jA={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`},MA={class:`app__remote-pop dd__menu`,role:`menu`},NA=[`title`,`onClick`],PA=[`title`,`onClick`],FA=[`onClick`],IA=[`onClick`],LA=[`title`,`onClick`],RA=[`title`,`aria-pressed`],zA=[`aria-checked`,`title`],BA={class:`theme-toggle__track`},VA={class:`theme-toggle__thumb`},HA={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`},UA={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`},WA={class:`app__dev-svc`},GA=[`title`],KA={class:`app__dev-svc-head`},qA={class:`app__dev-svc-body`},JA={class:`app__dev-svc-row`},YA={class:`app__dev-svc-row app__dev-svc-row--col`},XA={class:`app__dev-svc-row app__dev-svc-row--col`},ZA={key:0,class:`app__dev-svc-hint`},QA={class:`app__dev-svc-actions`},$A={style:{display:`none`}},ej=[`title`,`aria-expanded`],tj={key:0,class:`app__nosvc-banner`},nj={key:1,class:`app__nosvc-banner app__nosvc-banner--disconnected`},rj={key:2,class:`app__nosvc-banner app__extreload-banner`},ij={class:`app__stage`,ref:`stageEl`},aj={class:`phone__scaler`},oj=[`title`],sj={class:`canvas-bar__txt`},cj={class:`canvas-bar__chip`,title:`画布宽度预设(高度随列高自适应)`},lj={class:`canvas-bar__txt`},uj={class:`canvas-bar__size`,title:`画布实际尺寸(宽 × 高,高度随窗口自适应)`},dj=[`disabled`],fj=[`data-card-ready`],pj={class:`workside`},mj={class:`workside__cols`,ref:`colsEl`},hj={class:`login-modal modal-card`,role:`dialog`,"aria-modal":`true`},gj={class:`login-modal__row`},_j=[`disabled`],vj={class:`login-modal__row`},yj=[`disabled`],bj={key:0,class:`login-modal__error`},xj={class:`login-modal__actions`},Sj=[`disabled`],Cj=[`disabled`];function wj(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`);return B(),V(`div`,{class:j([`app`,{"app--chrome-off":i.chromeOff}]),style:A({"--preview-width":a.previewWidth+`px`,"--api-panel-width":a.apiPanelWidth+`px`,"--debug-min":a.debugMinWidth+`px`})},[I(U(`header`,fA,[U(`strong`,pA,[U(`img`,{class:`app__logo-icon`,src:i.logoUrl,alt:``},null,8,mA),t[35]||=G(` CardBuddy `,-1),U(`span`,hA,`v`+M(i.appVersion),1)]),U(`div`,gA,[W(o,{class:`csel--combo`,value:i.selectedId,options:i.cardList,onChange:a.onSelect},null,8,[`value`,`options`,`onChange`]),U(`button`,{class:`csel-combo__btn`,type:`button`,disabled:!i.selectedId,onClick:t[0]||=(...e)=>a.openCardFolder&&a.openCardFolder(...e),title:`在文件管理器中打开当前卡片所在文件夹`},[...t[36]||=[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(`path`,{d:`M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7z`}),U(`path`,{d:`M3 9h18`})],-1)]],8,_A)]),K(` 卡片网格预览:图标按钮,打开本地全部卡片的缩略图墙,点击任一张载入工作台 `),U(`button`,{class:`app__btn app__btn--icon`,type:`button`,disabled:!i.cardList.length,onClick:t[1]||=(...e)=>a.openGallery&&a.openGallery(...e),title:`卡片网格预览`},[...t[37]||=[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,vA),K(` 新增空白卡片:紧邻网格预览的图标按钮;弹窗收 ID(必填、本地重名前置拦截)+ 描述(选填),创建成功切换到新卡 `),U(`button`,{class:`app__btn app__btn--icon`,type:`button`,onClick:t[2]||=(...e)=>a.openNewCard&&a.openNewCard(...e),title:`新增空白卡片`},[...t[38]||=[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)]]),I(U(`div`,{ref:`validateBadgeEl`,class:j([`app__badge`,`app__badge--`+i.validateBadge.state]),title:a.badgeTitle},[t[39]||=U(`span`,{class:`app__badge-dot`},null,-1),U(`span`,bA,M(a.badgeText),1)],10,yA),[[Sc,!i.chromeOff]]),U(`div`,xA,[U(`div`,SA,[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[40]||=[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,CA),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[41]||=[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,wA),K(` 保存到本地:当前显示版本(撤销/重做导航到的历史版本,或编辑后未本地保存的版本)
254
254
  与本地磁盘版本不一致时高亮可用;一致时置灰。点击把当前版本真正写回磁盘,
@@ -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-CjZzK7u7.js"></script>
11
+ <script type="module" crossorigin src="/assets/index-Cp_eg64w.js"></script>
12
12
  <link rel="stylesheet" crossorigin href="/assets/index-CQScMhyg.css">
13
13
  </head>
14
14
  <body>
@@ -0,0 +1,113 @@
1
+ import readline from 'node:readline'
2
+ import { TOOLS, dispatch } from './tools.mjs'
3
+
4
+ /**
5
+ * 无依赖的 MCP stdio server(JSON-RPC 2.0,newline-delimited)。
6
+ *
7
+ * 支持的方法:
8
+ * - initialize → 返回 protocolVersion / capabilities / serverInfo
9
+ * - ping → 返回 {}
10
+ * - tools/list → 返回 TOOLS
11
+ * - tools/call → 分发到 client 函数,结果转成 MCP content
12
+ * - notifications/* → 不回复
13
+ *
14
+ * 设计:零第三方依赖(项目未装 @modelcontextprotocol/sdk),手写最小可用实现;
15
+ * 仅用 Node 22 内置 fetch / readline。截图 png 以 image content 块返回,供多模态直接阅读。
16
+ */
17
+
18
+ const rl = readline.createInterface({ input: process.stdin, terminal: false })
19
+
20
+ function send(obj) {
21
+ process.stdout.write(JSON.stringify(obj) + '\n')
22
+ }
23
+
24
+ /** 把 client 返回结果转成 MCP tools/call 的 result(content + isError) */
25
+ function resultToContent(name, args, result) {
26
+ // 截图 png:base64 直接转 image 块
27
+ if (name === 'card_screenshot' && args.format !== 'json' && result && result.base64 && !result.error) {
28
+ return { content: [{ type: 'image', data: result.base64, mimeType: result.mime || 'image/png' }], isError: false }
29
+ }
30
+ const text = typeof result === 'string' ? result : JSON.stringify(result, null, 2)
31
+ const isError = !!(result && typeof result === 'object' && result.error)
32
+ return { content: [{ type: 'text', text }], isError }
33
+ }
34
+
35
+ // 在途请求计数 + 有界等待:stdin 关闭(对端断开,或 one-shot 管道写完即 EOF)时**不能立刻退出**。
36
+ // tools/call 的处理是 async 的(要先等 HTTP 失败才走本地降级,截图还要拉起浏览器),直接
37
+ // process.exit(0) 会把尚未写出的响应截断 —— 客户端只看到请求、永远等不到回包。
38
+ // 但也不能无限等:对端可能正卡在一个永不响应的请求上断开,故 DRAIN_GRACE_MS 后强制退出,
39
+ // 兜住「不 linger」这条底线(正常的 list/read/write/validate 收口在毫秒级,够用)。
40
+ const DRAIN_GRACE_MS = Number(process.env.CARD_MCP_DRAIN_GRACE_MS || 5000)
41
+ let inFlight = 0
42
+ let stdinEnded = false
43
+ let drainTimer = null
44
+
45
+ function exitWhenDrained() {
46
+ if (stdinEnded && inFlight === 0) process.exit(0)
47
+ }
48
+
49
+ rl.on('line', async (line) => {
50
+ const t = line.trim()
51
+ if (!t) return
52
+ let msg
53
+ try {
54
+ msg = JSON.parse(t)
55
+ } catch {
56
+ return
57
+ }
58
+ const { id, method, params } = msg
59
+ inFlight++
60
+ if (drainTimer) {
61
+ clearTimeout(drainTimer)
62
+ drainTimer = null
63
+ }
64
+ try {
65
+ if (method === 'initialize') {
66
+ send({
67
+ jsonrpc: '2.0',
68
+ id,
69
+ result: {
70
+ protocolVersion: params?.protocolVersion || '2024-11-05',
71
+ capabilities: { tools: {} },
72
+ serverInfo: { name: 'dsl-card-mcp', version: '1.0.0' },
73
+ },
74
+ })
75
+ } else if (method === 'ping') {
76
+ if (id != null) send({ jsonrpc: '2.0', id, result: {} })
77
+ } else if (method === 'tools/list') {
78
+ send({ jsonrpc: '2.0', id, result: { tools: TOOLS } })
79
+ } else if (method === 'tools/call') {
80
+ const name = params?.name
81
+ const args = params?.arguments || {}
82
+ let result
83
+ try {
84
+ result = await dispatch(name, args)
85
+ } catch (e) {
86
+ result = { error: 'dispatch-failed', message: String((e && e.message) || e) }
87
+ }
88
+ send({ jsonrpc: '2.0', id, result: resultToContent(name, args, result) })
89
+ } else if (method && method.startsWith('notifications/')) {
90
+ // 通知类(如 notifications/initialized)不回复
91
+ } else {
92
+ if (id != null) {
93
+ send({ jsonrpc: '2.0', id, error: { code: -32601, message: `Method not found: ${method}` } })
94
+ }
95
+ }
96
+ } catch (e) {
97
+ if (id != null) {
98
+ send({ jsonrpc: '2.0', id, error: { code: -32603, message: String((e && e.message) || e) } })
99
+ }
100
+ } finally {
101
+ inFlight--
102
+ exitWhenDrained()
103
+ }
104
+ })
105
+
106
+ process.stdin.on('end', () => {
107
+ stdinEnded = true
108
+ if (inFlight === 0) {
109
+ process.exit(0)
110
+ return
111
+ }
112
+ drainTimer = setTimeout(() => process.exit(0), DRAIN_GRACE_MS)
113
+ })
@@ -23,7 +23,7 @@ dsl-card 项目的 AI 技能层。它是**编排层**:把卡片编辑任务拆
23
23
  4. **读字段结构用 `card_node_models`**:不要去解析 raw DSL + component-meta。该工具返回每节点的可编辑字段树。
24
24
  5. **render_status.ok ≠ 画对了**(rules.md §七):视觉正确性以 `card_screenshot` 为准。**但视觉验证只对 UI 变更做**(rules.md §七.1 铁律):改 `style.*` / 节点增删移动 / 静态文案 / 图片资源 / v-show 条件才截图;改绑定表达式取值逻辑 / `inputParams` / 事件代码 / `data.json` 属非 UI 变更,用单元断言收口(`evalExpr` 比对返回值、`new Function(code).call(假 this)` 跑 customAction 断言 `state`),不截图。
25
25
  6. **改坏可回滚**:`card_write` 每次写卡前自动把当前版本存为历史快照(与编辑器改卡同语义);编辑失误时用 `card_rollback`(默认回滚到最新快照)撤销,用 `card_history` 挑选更早的回滚目标。
26
- 7. **dev server 前置**:`card_render_status` / `card_screenshot` / `card_node_models` / `card_sync` / `card_save` 必须 dev server 已启动(`npm run dev`,默认 5180);前三者 dev server 未起时返回 `dev-server-required`。`card_list` / `card_read` / `card_write` / `card_validate` 会自动降级为直接读盘/写盘/跑 Python(render_status 标 skipped)。
26
+ 7. **dev server 前置**:`card_render_status` / `card_node_models` / `card_import_meta` 必须 dev server 已启动,未起时返回 `dev-server-required`;`card_sync` / `card_save` / `card_slice_upload` 除 dev server 外还需已登录 FinMall。(本仓库 `npm run dev` 默认 5180,由 Vite 把 `/__*` 代理到 8787;`cardbuddy init` 出来的工程 `npm run dev` 直接是 8787。)`card_list` / `card_read` / `card_write` / `card_validate` / `card_history` / `card_rollback` 会自动降级为直接读盘/写盘/跑 Python(render_status 标 skipped);`card_screenshot` 降级到包内独立离线渲染器(`source:'standalone'`)。
27
27
  8. **建卡先判输入形态**(Workflow D):收到设计稿输入先调 `card_route_input({path})`,按返回的 `kind` 分流,**不要凭感觉决定要不要跑上游**。判为 `meaxure-export`(Sketch MeaXure 标注导出)时必须先经 `sketch-spec-to-card` 前处理产出 `UI/<稿名>/` 要素包,**禁止直接读 `preview/` 整图建卡**;`preprocess.needed=false` 表示已前处理过,直接复用存量产物。`card_route_input` 纯离线,不需要 dev server。
28
28
 
29
29
  ## 1. 可用 MCP 工具(dsl-card 服务器,命名空间 `mcp__dsl-card__card_*`)
@@ -36,7 +36,7 @@ dsl-card 项目的 AI 技能层。它是**编排层**:把卡片编辑任务拆
36
36
  | `card_write` | 写回 schema/data;默认自动 `verify`(validate + render_status)一并返回 | 直接写盘 + 本地 Python 硬校验 |
37
37
  | `card_validate` | Python 硬契约校验 `{ok, errors, warnings}` | 直接跑 Python |
38
38
  | `card_render_status` | SSR 自检:结构统计 + `bindingErrors` / `runtimeErrors` / `jsapiCalls` | 不支持 |
39
- | `card_screenshot` | 无头截图(json `{base64}` / png 图片) | 不支持 |
39
+ | `card_screenshot` | 无头截图(json `{base64}` / png 图片) | 优先 dev server;未起时降级到包内独立离线渲染器(返回 `source:'standalone'`),渲染器也失败才报 `dev-server-required` |
40
40
  | `card_history` | 编辑历史快照元数据(ts/时间/字节数/节点数),用于选择回滚目标 | 直接读 `.history/` |
41
41
  | `card_rollback` | 回滚到指定快照(默认最新);回滚前自动把当前版本另存为安全快照,回滚本身可撤销;写回走 card_write 同一收口 | 直接读写盘 + Python 硬校验 |
42
42
  | `card_sync` | 从 FinMall 远端拉最新卡写回 `src/cards/` | 不支持 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cardbuddy",
3
- "version": "0.12.2",
3
+ "version": "0.13.1",
4
4
  "description": "Local dev service for the CardBuddy card editor — decouples card CRUD (sync/save/validate/render-status/screenshot) from the static public editor, so the editor can be deployed to any static host while each developer runs a local service against their own card source.",
5
5
  "type": "module",
6
6
  "bin": {