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.
- package/README.md +4 -4
- package/dist/cli.js +14 -6
- package/dist/config.js +13 -2
- package/dist/preview-editor/assets/index-B4VZgNSK.css +1 -0
- package/dist/preview-editor/assets/index-jl4YJzKv.js +180 -0
- package/dist/preview-editor/index.html +2 -2
- package/dist/server-core/_vendor/finmall-env.mjs +4 -4
- package/dist/server-core/ai-chat.js +3710 -0
- package/dist/server-core/cardFiles.js +95 -2
- package/dist/server-core/cardImage.js +14 -0
- package/dist/server-core/cardSave.js +2 -0
- package/dist/server-core/cardValidate.js +3 -0
- package/dist/server-core/index.js +3 -0
- package/dist/server-core/sliceUpload.js +8 -1
- package/dist/vendored/App.vue +269 -79
- package/dist/vendored/api.js +4 -4
- package/dist/vendored/assets/svgIcons/icons.svg +28 -0
- package/dist/vendored/components/AiSettings.vue +392 -0
- package/dist/vendored/components/CardGallery.vue +51 -5
- package/dist/vendored/components/ChatComposer.vue +413 -0
- package/dist/vendored/components/ChatMessage.vue +383 -0
- package/dist/vendored/components/ChatPanel.vue +937 -0
- package/dist/vendored/components/DebugPanel.vue +30 -33
- package/dist/vendored/components/Dropdown.vue +4 -2
- package/dist/vendored/components/MarkdownBody.vue +64 -0
- package/dist/vendored/components/NewCardDialog.vue +132 -0
- package/dist/vendored/components/RemotePreview.vue +1 -1
- package/dist/vendored/main.js +1 -0
- package/dist/vendored/runtime/adapter.js +1 -1
- package/dist/vendored/runtime/ai-api.js +165 -0
- package/dist/vendored/runtime/markdown.js +140 -0
- package/dist/vendored/skills/.manifest.json +1101 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/autofix_schema.py +249 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/check_topology.py +678 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/derive_data_json.py +81 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/paths.py +105 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/validate_schema.py +624 -0
- package/dist/vendored/skills/finmall-card-sync/scripts/api_batch_sync.mjs +900 -0
- package/dist/vendored/skills/finmall-card-sync/scripts/slice_upload.mjs +699 -0
- package/dist/vendored/skills/shared/finmall-config.mjs +124 -0
- package/dist/vendored/styles/ai-chat.css +1850 -0
- package/dist/vendored/styles/app.css +107 -3
- package/dist/vendored/styles/card-gallery.css +45 -7
- package/dist/vendored/styles/node-inspector.css +11 -0
- package/package.json +1 -1
- package/dist/preview-editor/assets/index-C_UKS0NF.css +0 -1
- 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
|
-
?
|
|
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
|
|
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')
|
|
@@ -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
|
-
|
|
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),
|