cardbuddy 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +4 -4
  2. package/dist/cli.js +14 -6
  3. package/dist/config.js +13 -2
  4. package/dist/preview-editor/assets/index-B4VZgNSK.css +1 -0
  5. package/dist/preview-editor/assets/index-jl4YJzKv.js +180 -0
  6. package/dist/preview-editor/index.html +2 -2
  7. package/dist/server-core/_vendor/finmall-env.mjs +4 -4
  8. package/dist/server-core/ai-chat.js +3710 -0
  9. package/dist/server-core/cardFiles.js +95 -2
  10. package/dist/server-core/cardImage.js +14 -0
  11. package/dist/server-core/cardSave.js +2 -0
  12. package/dist/server-core/cardValidate.js +3 -0
  13. package/dist/server-core/index.js +3 -0
  14. package/dist/server-core/sliceUpload.js +8 -1
  15. package/dist/vendored/App.vue +269 -79
  16. package/dist/vendored/api.js +4 -4
  17. package/dist/vendored/assets/svgIcons/icons.svg +28 -0
  18. package/dist/vendored/components/AiSettings.vue +392 -0
  19. package/dist/vendored/components/CardGallery.vue +51 -5
  20. package/dist/vendored/components/ChatComposer.vue +413 -0
  21. package/dist/vendored/components/ChatMessage.vue +383 -0
  22. package/dist/vendored/components/ChatPanel.vue +937 -0
  23. package/dist/vendored/components/DebugPanel.vue +30 -33
  24. package/dist/vendored/components/Dropdown.vue +4 -2
  25. package/dist/vendored/components/MarkdownBody.vue +64 -0
  26. package/dist/vendored/components/NewCardDialog.vue +132 -0
  27. package/dist/vendored/components/RemotePreview.vue +1 -1
  28. package/dist/vendored/main.js +1 -0
  29. package/dist/vendored/runtime/adapter.js +1 -1
  30. package/dist/vendored/runtime/ai-api.js +165 -0
  31. package/dist/vendored/runtime/markdown.js +140 -0
  32. package/dist/vendored/skills/.manifest.json +1101 -0
  33. package/dist/vendored/skills/dsl-card-generator/scripts/autofix_schema.py +249 -0
  34. package/dist/vendored/skills/dsl-card-generator/scripts/check_topology.py +678 -0
  35. package/dist/vendored/skills/dsl-card-generator/scripts/derive_data_json.py +81 -0
  36. package/dist/vendored/skills/dsl-card-generator/scripts/lib/paths.py +105 -0
  37. package/dist/vendored/skills/dsl-card-generator/scripts/validate_schema.py +624 -0
  38. package/dist/vendored/skills/finmall-card-sync/scripts/api_batch_sync.mjs +900 -0
  39. package/dist/vendored/skills/finmall-card-sync/scripts/slice_upload.mjs +699 -0
  40. package/dist/vendored/skills/shared/finmall-config.mjs +124 -0
  41. package/dist/vendored/styles/ai-chat.css +1850 -0
  42. package/dist/vendored/styles/app.css +107 -3
  43. package/dist/vendored/styles/card-gallery.css +45 -7
  44. package/dist/vendored/styles/node-inspector.css +11 -0
  45. package/package.json +1 -1
  46. package/dist/preview-editor/assets/index-C_UKS0NF.css +0 -1
  47. package/dist/preview-editor/assets/index-D1iIJ4sl.js +0 -81
@@ -7,8 +7,11 @@ import { buildDataFromSchema } from './buildDataFromSchema.js'
7
7
  /**
8
8
  * 单张卡片 / 卡片列表的按需读取 / 写回服务端处理(dev 下实时读磁盘,无缓存)。
9
9
  * - GET /__cards :实时扫描 src/cards/* 目录,返回卡片列表 [{id,name,pageId}]。
10
+ * - POST /__cards :新建空白卡片 { id, desc? }(空骨架 schema + 派生 data),网格顶栏「新增空白卡片」触发。
11
+ * desc 选填:留空时卡片显示名(schema.name)回落到 ID,列表 / 顶栏下拉不至于出现空名。
10
12
  * - GET /__card/<id> :实时读 src/cards/<id>/schema.json,data 由 schema 派生返回。
11
13
  * - POST /__card/<id> :把预览内编辑后的最新 schema 写回 src/cards/<id>/,data 单向重算。
14
+ * - DELETE /__card/<id> :递归删除本地卡片文件夹(网格预览删除钮触发)。
12
15
  *
13
16
  * 以下纯函数被 mcp/ 客户端复用(降级模式直接读盘/写盘),保持单一实现、避免逻辑漂移。
14
17
  */
@@ -94,7 +97,9 @@ export function readCardFiles(root, id) {
94
97
  function writeJsonFile(dir, filename, value) {
95
98
  const text =
96
99
  value !== null && typeof value === 'object'
97
- ? JSON.stringify(value, (k, v) => (k === '_schemaNode' ? undefined : v), 2)
100
+ ? // 末尾补换行:仓库里多数卡片 JSON 以 \n 结尾,缺了它每次保存都会在 git diff 里
101
+ // 多出一条「\」噪音(POSIX 文本文件也应以换行收尾)
102
+ `${JSON.stringify(value, (k, v) => (k === '_schemaNode' ? undefined : v), 2)}\n`
98
103
  : String(value)
99
104
  const target = path.join(dir, filename)
100
105
  const tmp = target + '.tmp'
@@ -138,10 +143,83 @@ export function createCardFilesMiddleware({ root }) {
138
143
  return res.end(JSON.stringify({ cards }))
139
144
  }
140
145
 
146
+ // 新建空白卡片:写空骨架(与 scaffold lib 的 buildCardSkeleton 同形状、components 空数组),
147
+ // 不生成封面 —— 空白卡在 /__card-image 直接 404,网格回退统一占位图。
148
+ if (pathname === '/__cards' && method === 'POST') {
149
+ let body = ''
150
+ req.on('data', (c) => {
151
+ body += c
152
+ if (body.length > 64 * 1024) req.destroy()
153
+ })
154
+ req.on('end', () => {
155
+ const send = (code, payload) => {
156
+ res.statusCode = code
157
+ res.setHeader('Content-Type', 'application/json')
158
+ res.end(JSON.stringify(payload))
159
+ }
160
+ let payload
161
+ try {
162
+ payload = JSON.parse(body || '{}')
163
+ } catch (e) {
164
+ return send(400, { error: 'invalid json', message: String(e.message) })
165
+ }
166
+ const id = String(payload.id || '').trim()
167
+ const desc = String(payload.desc || '').trim()
168
+ if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(id)) {
169
+ return send(400, { error: 'invalid id', message: '卡片 ID 只能以字母/数字开头,含字母/数字/_/-(≤64 字符)' })
170
+ }
171
+ if (desc.length > 200) return send(400, { error: 'invalid desc', message: '卡片描述过长(≤200 字)' })
172
+ const dir = path.join(root, 'src', 'cards', id)
173
+ // 重名兜底闸:前端已按本地卡片列表前置拦截,这里防的是「列表过期 / 别的窗口刚建过 /
174
+ // macOS / Windows 文件系统不区分大小写导致同名不同大小写撞车」。带 message,弹窗才能直接显示人话
175
+ if (fs.existsSync(dir)) {
176
+ return send(409, { error: 'card exists', id, message: `卡片 ID 已存在:src/cards/${id} 已在本地,换一个 ID` })
177
+ }
178
+ try {
179
+ fs.mkdirSync(dir, { recursive: true })
180
+ const schema = {
181
+ version: '1.0.0',
182
+ pageId: id,
183
+ name: desc || id,
184
+ desc,
185
+ platform: '002',
186
+ dictionary: { dictionaryList: [], inputParams: [], mocks: {} },
187
+ view: {
188
+ width: '375',
189
+ className: 'Root',
190
+ style: { properties: [] },
191
+ properties: [],
192
+ eventactions: [],
193
+ components: [],
194
+ },
195
+ }
196
+ writeJsonFile(dir, 'schema.json', schema)
197
+ writeJsonFile(dir, 'data.json', buildDataFromSchema(schema))
198
+ log.ok('新建空白卡片', id)
199
+ return send(200, { ok: true, id })
200
+ } catch (e) {
201
+ // 写盘失败不留半截卡:整个目录回滚删掉
202
+ try { fs.rmSync(dir, { recursive: true, force: true }) } catch { /* 忽略清理失败 */ }
203
+ log.error('新建空白卡片失败', e)
204
+ return send(500, { error: 'create failed', message: String(e.message) })
205
+ }
206
+ })
207
+ return
208
+ }
209
+
141
210
  const m = pathname.match(/^\/__card\/([^/]+)$/)
142
211
  if (!m) return next()
143
212
  const id = decodeURIComponent(m[1])
144
- const dir = path.join(root, 'src', 'cards', id)
213
+ const cardsRoot = path.join(root, 'src', 'cards')
214
+ const dir = path.join(cardsRoot, id)
215
+ // 路径 containment 硬校验:id 形如 ..%2F.. 时 path.join 会逃出 src/cards,
216
+ // GET/POST/DELETE 全方法受益(DELETE 是递归删目录,越权即灾难)
217
+ const rel = path.relative(cardsRoot, dir)
218
+ if (rel.startsWith('..') || path.isAbsolute(rel) || !rel) {
219
+ res.statusCode = 404
220
+ res.setHeader('Content-Type', 'application/json')
221
+ return res.end(JSON.stringify({ error: 'card not found', id }))
222
+ }
145
223
  if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
146
224
  log.warn('卡片未找到', { id })
147
225
  res.statusCode = 404
@@ -188,6 +266,21 @@ export function createCardFilesMiddleware({ root }) {
188
266
  })
189
267
  return
190
268
  }
269
+ if (method === 'DELETE') {
270
+ // 删除本地卡片文件夹(网格预览格子右下角删除钮触发,前端有警告确认兜底)
271
+ try {
272
+ fs.rmSync(dir, { recursive: true, force: true })
273
+ log.ok('删除卡片', id)
274
+ res.statusCode = 200
275
+ res.setHeader('Content-Type', 'application/json')
276
+ return res.end(JSON.stringify({ ok: true, id }))
277
+ } catch (e) {
278
+ log.error('删除卡片失败', e)
279
+ res.statusCode = 500
280
+ res.setHeader('Content-Type', 'application/json')
281
+ return res.end(JSON.stringify({ error: 'delete failed', message: String(e.message) }))
282
+ }
283
+ }
191
284
  return next()
192
285
  }
193
286
  }
@@ -68,6 +68,20 @@ export function createCardImageMiddleware({ root }) {
68
68
  return
69
69
  }
70
70
 
71
+ // 空白卡(schema 无 components):无头截图只会得到一张无意义纯色图,
72
+ // 直接 404 让前端网格回退统一占位图,省一次渲染拉起。
73
+ try {
74
+ const schema = JSON.parse(fs.readFileSync(path.join(dir, 'schema.json'), 'utf8'))
75
+ const comps = schema && schema.view && schema.view.components
76
+ if (Array.isArray(comps) && !comps.length) {
77
+ res.statusCode = 404
78
+ res.setHeader('Cache-Control', 'no-store')
79
+ return res.end()
80
+ }
81
+ } catch {
82
+ /* schema 缺失 / 损坏:按旧逻辑回退截图 */
83
+ }
84
+
71
85
  // 无同名预览图:回退实时无头截图(复用 /__screenshot 渲染内核)。
72
86
  // width 由查询参数决定(默认 390),与 /__screenshot 一致。
73
87
  const q = new URL(req.url, 'http://localhost')
@@ -1,6 +1,8 @@
1
1
  import fs from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import { pathToFileURL } from 'node:url'
4
+ import { fileURLToPath } from 'node:url'
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
4
6
 
5
7
  const skillDir = path.resolve(process.cwd(), 'skills', 'finmall-card-sync')
6
8
  const skillPath = path.join(skillDir, 'scripts', 'api_batch_sync.mjs')
@@ -2,6 +2,9 @@ import fs from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import { execFile, execFileSync } from 'node:child_process'
4
4
 
5
+ import { fileURLToPath } from 'node:url'
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
7
+
5
8
  /**
6
9
  * 契约校验端点(L0 闭环之一)。
7
10
  *
@@ -26,6 +26,7 @@ import { createSliceStaticMiddleware } from './sliceStatic.js'
26
26
  import { createCardImageMiddleware } from './cardImage.js'
27
27
  import { createOpenFolderMiddleware } from './cardOpenFolder.js'
28
28
  import { createCodeBridgeMiddleware } from './codeBridge.js'
29
+ import { createAiChatMiddleware } from './ai-chat.js'
29
30
  import {
30
31
  createLogMiddleware,
31
32
  installLogMirror,
@@ -108,4 +109,6 @@ export function mountCoreMiddlewares(app, { root, viteServer = null, upstream =
108
109
  // 资源代理:由 8787(cardbuddy)统一提供,Vite 彻底零后端插件。
109
110
  // upstream 空串时中间件仍会注册,但 /minio 等请求只会走占位图兜底,不会崩溃。
110
111
  app.use(createAssetProxyMiddleware({ root, upstream }))
112
+ // AI 聊天:POST /__ai/chat 流式调用用户自带 LLM,工具调度复用 mcp/tools.mjs
113
+ app.use(createAiChatMiddleware({ root }))
111
114
  }
@@ -43,7 +43,14 @@ export function createSliceUploadMiddleware({ root }) {
43
43
  )
44
44
  }
45
45
  try {
46
- const skillPath = path.join(root, 'skills', 'finmall-card-sync', 'scripts', 'slice_upload.mjs')
46
+ function resolveUploadScript(root) {
47
+ const primary = path.join(root, 'skills', 'finmall-card-sync', 'scripts', 'slice_upload.mjs')
48
+ if (fs.existsSync(primary)) return primary
49
+ const vendored = path.resolve(__dirname, '..', 'vendored', 'skills', 'finmall-card-sync', 'scripts', 'slice_upload.mjs')
50
+ if (fs.existsSync(vendored)) return vendored
51
+ return primary
52
+ }
53
+ const skillPath = resolveUploadScript(root)
47
54
  const { uploadSlices } = await import(pathToFileURL(skillPath).href)
48
55
  const summary = await uploadSlices({
49
56
  cardId: String(payload.cardId),