cardbuddy 0.6.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 +211 -0
- package/dist/cli.js +76 -0
- package/dist/config.js +63 -0
- package/dist/core.js +2 -0
- package/dist/preview-editor/assets/apple-touch-icon-BQcES9QK.png +0 -0
- package/dist/preview-editor/assets/default-image-wzZYQ86K.png +0 -0
- package/dist/preview-editor/assets/favicon-16-Cg6Nrosk.png +0 -0
- package/dist/preview-editor/assets/favicon-32-B_1744sp.png +0 -0
- package/dist/preview-editor/assets/index-BYB0hngG.js +72 -0
- package/dist/preview-editor/assets/index-CpBZEQDy.css +1 -0
- package/dist/preview-editor/assets/site-icon-CFMif8z4.png +0 -0
- package/dist/preview-editor/assets/svg-symbols-B2exBrAr.svg +118 -0
- package/dist/preview-editor/index.html +17 -0
- package/dist/server-core/_vendor/finmall-env.mjs +96 -0
- package/dist/server-core/assetProxy.js +154 -0
- package/dist/server-core/bbox.js +153 -0
- package/dist/server-core/browser.js +108 -0
- package/dist/server-core/buildDataFromSchema.js +50 -0
- package/dist/server-core/cardFiles.js +188 -0
- package/dist/server-core/cardHistory.js +156 -0
- package/dist/server-core/cardImage.js +85 -0
- package/dist/server-core/cardImportMeta.js +139 -0
- package/dist/server-core/cardListCache.js +68 -0
- package/dist/server-core/cardOpenFolder.js +135 -0
- package/dist/server-core/cardSave.js +127 -0
- package/dist/server-core/cardSidecar.js +72 -0
- package/dist/server-core/cardSync.js +259 -0
- package/dist/server-core/cardValidate.js +134 -0
- package/dist/server-core/index.js +82 -0
- package/dist/server-core/logger.js +241 -0
- package/dist/server-core/nodeModels.js +52 -0
- package/dist/server-core/renderStatus.js +207 -0
- package/dist/server-core/screenshot.js +123 -0
- package/dist/server-core/sliceStatic.js +44 -0
- package/dist/standalone.js +447 -0
- package/dist/vendored/App.vue +2330 -0
- package/dist/vendored/api.js +142 -0
- package/dist/vendored/assets/apple-touch-icon.png +0 -0
- package/dist/vendored/assets/default-image.png +0 -0
- package/dist/vendored/assets/favicon-16.png +0 -0
- package/dist/vendored/assets/favicon-32.png +0 -0
- package/dist/vendored/assets/logo.png +0 -0
- package/dist/vendored/assets/site-icon.png +0 -0
- package/dist/vendored/assets/svg-symbols.svg +118 -0
- package/dist/vendored/assets/svgIcons/icons.svg +54 -0
- package/dist/vendored/assets/svgIcons/index.js +39 -0
- package/dist/vendored/components/AbsoluteLayout.vue +42 -0
- package/dist/vendored/components/AlertCardInstance.vue +58 -0
- package/dist/vendored/components/AlertCardLayer.vue +59 -0
- package/dist/vendored/components/ApiPayloadTree.vue +168 -0
- package/dist/vendored/components/AppToast.vue +37 -0
- package/dist/vendored/components/Button.vue +37 -0
- package/dist/vendored/components/Canvas.vue +65 -0
- package/dist/vendored/components/CardGallery.vue +243 -0
- package/dist/vendored/components/CardPreview.vue +72 -0
- package/dist/vendored/components/CardSelect.vue +200 -0
- package/dist/vendored/components/CheckGroup.vue +131 -0
- package/dist/vendored/components/Column.vue +25 -0
- package/dist/vendored/components/ComponentPalette.vue +294 -0
- package/dist/vendored/components/ConfirmDialog.vue +39 -0
- package/dist/vendored/components/CustomPlaceholder.vue +33 -0
- package/dist/vendored/components/DatePickerOverlay.vue +345 -0
- package/dist/vendored/components/DebugPanel.vue +806 -0
- package/dist/vendored/components/DeviceOverlay.vue +70 -0
- package/dist/vendored/components/Divider.vue +22 -0
- package/dist/vendored/components/Dropdown.vue +191 -0
- package/dist/vendored/components/EventEditor.vue +247 -0
- package/dist/vendored/components/GridLayout.vue +67 -0
- package/dist/vendored/components/HoverGuides.vue +122 -0
- package/dist/vendored/components/Icon.vue +34 -0
- package/dist/vendored/components/Image.vue +34 -0
- package/dist/vendored/components/Input.vue +90 -0
- package/dist/vendored/components/JsonTree.vue +516 -0
- package/dist/vendored/components/ListLayout.vue +68 -0
- package/dist/vendored/components/LogValue.vue +51 -0
- package/dist/vendored/components/Map.vue +110 -0
- package/dist/vendored/components/MultiColumn.vue +49 -0
- package/dist/vendored/components/MultiText.vue +58 -0
- package/dist/vendored/components/NodeInspector.vue +295 -0
- package/dist/vendored/components/PickerOverlay.vue +131 -0
- package/dist/vendored/components/ProgressSlider.vue +167 -0
- package/dist/vendored/components/PropControl.vue +241 -0
- package/dist/vendored/components/PropTree.vue +128 -0
- package/dist/vendored/components/RadioButton.vue +74 -0
- package/dist/vendored/components/RemotePreview.vue +171 -0
- package/dist/vendored/components/RenderTree.vue +437 -0
- package/dist/vendored/components/Root.vue +30 -0
- package/dist/vendored/components/Row.vue +25 -0
- package/dist/vendored/components/SelectionOverlay.vue +105 -0
- package/dist/vendored/components/StructureTree.vue +949 -0
- package/dist/vendored/components/SvgCode.vue +19 -0
- package/dist/vendored/components/SvgSymbols.vue +30 -0
- package/dist/vendored/components/Switch.vue +38 -0
- package/dist/vendored/components/Text.vue +32 -0
- package/dist/vendored/components/Video.vue +70 -0
- package/dist/vendored/components/ViewContainer.vue +52 -0
- package/dist/vendored/components/base.js +12 -0
- package/dist/vendored/components/icons.js +47 -0
- package/dist/vendored/components/index.js +58 -0
- package/dist/vendored/components/setters/SetBgPosition.vue +64 -0
- package/dist/vendored/components/setters/SetBgSize.vue +90 -0
- package/dist/vendored/components/setters/SetCode.vue +90 -0
- package/dist/vendored/components/setters/SetColor.vue +244 -0
- package/dist/vendored/components/setters/SetCustomSelect.vue +58 -0
- package/dist/vendored/components/setters/SetExpr.vue +107 -0
- package/dist/vendored/components/setters/SetGradient.vue +125 -0
- package/dist/vendored/components/setters/SetGrid9.vue +68 -0
- package/dist/vendored/components/setters/SetIconSelect.vue +67 -0
- package/dist/vendored/components/setters/SetIfForm.vue +61 -0
- package/dist/vendored/components/setters/SetImage.vue +43 -0
- package/dist/vendored/components/setters/SetList.vue +139 -0
- package/dist/vendored/components/setters/SetNumber.vue +118 -0
- package/dist/vendored/components/setters/SetNumberStyle.vue +113 -0
- package/dist/vendored/components/setters/SetPadding.vue +77 -0
- package/dist/vendored/components/setters/SetSegment.vue +160 -0
- package/dist/vendored/components/setters/SetSelect.vue +51 -0
- package/dist/vendored/components/setters/SetSwitch.vue +49 -0
- package/dist/vendored/components/setters/SetText.vue +52 -0
- package/dist/vendored/components/setters/props.js +23 -0
- package/dist/vendored/fidelity/baseline-messageOverview.json +87 -0
- package/dist/vendored/fidelity/calibration.json +29 -0
- package/dist/vendored/fidelity/fidelity-messageOverview-after.html +114 -0
- package/dist/vendored/fidelity/fidelity-messageOverview-after.json +272 -0
- package/dist/vendored/fidelity/fidelity-messageOverview.html +185 -0
- package/dist/vendored/fidelity/fidelity-messageOverview.json +336 -0
- package/dist/vendored/fidelity/fidelity-messageOverview_v2.html +262 -0
- package/dist/vendored/fidelity/fidelity-messageOverview_v2.json +502 -0
- package/dist/vendored/fidelity/fidelity-messageOverview_v3.html +201 -0
- package/dist/vendored/fidelity/fidelity-messageOverview_v3.json +417 -0
- package/dist/vendored/fidelity/fidelity-messageOverview_visual.html +125 -0
- package/dist/vendored/fidelity/fidelity-messageOverview_visual.json +338 -0
- package/dist/vendored/index.html +16 -0
- package/dist/vendored/main.js +36 -0
- package/dist/vendored/meta/action-meta.json +141 -0
- package/dist/vendored/meta/component-meta.json +2714 -0
- package/dist/vendored/meta/directive-meta.json +40 -0
- package/dist/vendored/meta/style-meta.json +1086 -0
- package/dist/vendored/runtime/StyleParser.js +179 -0
- package/dist/vendored/runtime/adapter.js +166 -0
- package/dist/vendored/runtime/alert-card.js +125 -0
- package/dist/vendored/runtime/app-toast.js +33 -0
- package/dist/vendored/runtime/assets.js +16 -0
- package/dist/vendored/runtime/canvas-context.js +141 -0
- package/dist/vendored/runtime/card-runtime.js +289 -0
- package/dist/vendored/runtime/clipboard-node.js +83 -0
- package/dist/vendored/runtime/clipboard.js +29 -0
- package/dist/vendored/runtime/evaluate.js +152 -0
- package/dist/vendored/runtime/event-model.js +300 -0
- package/dist/vendored/runtime/hoverGuide.js +31 -0
- package/dist/vendored/runtime/jsapi-mock.js +256 -0
- package/dist/vendored/runtime/node-model.js +217 -0
- package/dist/vendored/runtime/property.js +364 -0
- package/dist/vendored/runtime/selectionOverlay.js +53 -0
- package/dist/vendored/runtime/toPreview.js +648 -0
- package/dist/vendored/standalone-main.js +36 -0
- package/dist/vendored/standalone.html +12 -0
- package/dist/vendored/styles/app.css +2565 -0
- package/dist/vendored/styles/card-gallery.css +234 -0
- package/dist/vendored/styles/card-select.css +281 -0
- package/dist/vendored/styles/components.css +383 -0
- package/dist/vendored/styles/dropdown.css +337 -0
- package/dist/vendored/styles/json-tree.css +308 -0
- package/dist/vendored/styles/node-inspector.css +1003 -0
- package/dist/vendored/styles/setters.css +962 -0
- package/dist/vendored/styles/standalone.css +27 -0
- package/dist/vendored/styles/structure-tree.css +320 -0
- package/package.json +42 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { execFile, execFileSync } from 'node:child_process'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 契约校验端点(L0 闭环之一)。
|
|
7
|
+
*
|
|
8
|
+
* - GET /__validate/<id> :单卡校验,返回 { id, ok, errors, warnings, durationMs }
|
|
9
|
+
* - GET /__validate :全量校验,逐卡跑 Python 校验器,汇总返回
|
|
10
|
+
*
|
|
11
|
+
* 实现:子进程调用 `skills/dsl-card-generator/scripts/validate_schema.py --json`。
|
|
12
|
+
* Python 解释器解析顺序:`DSL_PYTHON` → `python3` → `python`,都不存在时返回
|
|
13
|
+
* **503 {error:'python-not-found'}** 而非 500,让 Agent / CI 能区分「环境没装」和「卡片有问题」。
|
|
14
|
+
*
|
|
15
|
+
* @param {{ root: string }} opts
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** 校验脚本在仓库根下的固定相对路径(导出供 MCP 复用) */
|
|
19
|
+
export function scriptPath(root) {
|
|
20
|
+
return path.join(
|
|
21
|
+
root,
|
|
22
|
+
'skills',
|
|
23
|
+
'dsl-card-generator',
|
|
24
|
+
'scripts',
|
|
25
|
+
'validate_schema.py',
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 解析可用的 python 解释器;找不到返回 null(导出供 MCP 复用) */
|
|
30
|
+
export function resolvePython() {
|
|
31
|
+
if (process.env.DSL_PYTHON) return process.env.DSL_PYTHON
|
|
32
|
+
for (const cmd of ['python3', 'python']) {
|
|
33
|
+
try {
|
|
34
|
+
execFileSync(cmd, ['--version'], { stdio: 'ignore', timeout: 5000 })
|
|
35
|
+
return cmd
|
|
36
|
+
} catch {
|
|
37
|
+
/* 该命令不可用,试下一个 */
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 跑一次 Python 校验器,返回结构化结果。
|
|
45
|
+
* @returns {Promise<{ok:boolean, errors:string[], warnings:string[], durationMs:number, toolError?:string}>}
|
|
46
|
+
*/
|
|
47
|
+
export function validateCard(python, pyScript, schemaPath, { checkData = true, timeout = 8000 } = {}) {
|
|
48
|
+
return new Promise((resolve) => {
|
|
49
|
+
const args = [pyScript, schemaPath, '--json']
|
|
50
|
+
if (!checkData) args.push('--no-data')
|
|
51
|
+
execFile(python, args, { timeout, maxBuffer: 8 * 1024 * 1024, windowsHide: true }, (err, stdout) => {
|
|
52
|
+
if (err && err.code === 'ENOENT') {
|
|
53
|
+
return resolve({ ok: false, errors: [`python interpreter not found: ${python}`], warnings: [], toolError: 'python-not-found' })
|
|
54
|
+
}
|
|
55
|
+
const raw = (stdout || '').trim()
|
|
56
|
+
if (!raw) {
|
|
57
|
+
const reason = err && err.killed ? 'timeout' : err ? String(err.message) : 'empty output'
|
|
58
|
+
return resolve({ ok: false, errors: [`validator produced no output (${reason})`], warnings: [], toolError: err && err.killed ? 'timeout' : 'no-output' })
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const parsed = JSON.parse(raw)
|
|
62
|
+
return resolve({
|
|
63
|
+
ok: !!parsed.ok,
|
|
64
|
+
errors: parsed.errors || [],
|
|
65
|
+
warnings: parsed.warnings || [],
|
|
66
|
+
durationMs: parsed.durationMs || 0,
|
|
67
|
+
})
|
|
68
|
+
} catch (e) {
|
|
69
|
+
return resolve({ ok: false, errors: [`failed to parse validator output: ${e.message}`, raw.slice(0, 500)], warnings: [], toolError: 'parse-error' })
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function createValidateMiddleware({ root }) {
|
|
76
|
+
const pyScript = scriptPath(root)
|
|
77
|
+
return async function validateMiddleware(req, res, next) {
|
|
78
|
+
const pathname = decodeURIComponent((req.url || '').split('?')[0])
|
|
79
|
+
const m = pathname.match(/^\/__validate(?:\/([^/]+))?$/)
|
|
80
|
+
if (!m) return next()
|
|
81
|
+
const id = m[1] // undefined => 全量
|
|
82
|
+
|
|
83
|
+
const python = resolvePython()
|
|
84
|
+
if (!python) {
|
|
85
|
+
res.statusCode = 503
|
|
86
|
+
res.setHeader('Content-Type', 'application/json')
|
|
87
|
+
return res.end(JSON.stringify({
|
|
88
|
+
error: 'python-not-found',
|
|
89
|
+
message: '未找到 python3/python,请在 PATH 中安装 Python 或设置 DSL_PYTHON 环境变量',
|
|
90
|
+
}))
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const noData = /[?&]no-data\b/.test(req.url || '')
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
if (!id) {
|
|
97
|
+
const cardsDir = path.join(root, 'src', 'cards')
|
|
98
|
+
const results = []
|
|
99
|
+
if (fs.existsSync(cardsDir)) {
|
|
100
|
+
for (const cid of fs.readdirSync(cardsDir).sort()) {
|
|
101
|
+
const dir = path.join(cardsDir, cid)
|
|
102
|
+
if (!fs.statSync(dir).isDirectory()) continue
|
|
103
|
+
const sp = path.join(dir, 'schema.json')
|
|
104
|
+
if (!fs.existsSync(sp)) continue
|
|
105
|
+
const r = await validateCard(python, pyScript, sp, { checkData: !noData })
|
|
106
|
+
results.push({ id: cid, ...r })
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const allOk = results.length > 0 && results.every((r) => r.ok)
|
|
110
|
+
res.statusCode = 200
|
|
111
|
+
res.setHeader('Content-Type', 'application/json')
|
|
112
|
+
res.setHeader('Cache-Control', 'no-store')
|
|
113
|
+
return res.end(JSON.stringify({ ok: allOk, count: results.length, cards: results }))
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const dir = path.join(root, 'src', 'cards', id)
|
|
117
|
+
const sp = path.join(dir, 'schema.json')
|
|
118
|
+
if (!fs.existsSync(sp)) {
|
|
119
|
+
res.statusCode = 404
|
|
120
|
+
res.setHeader('Content-Type', 'application/json')
|
|
121
|
+
return res.end(JSON.stringify({ error: 'card not found', id }))
|
|
122
|
+
}
|
|
123
|
+
const r = await validateCard(python, pyScript, sp, { checkData: !noData })
|
|
124
|
+
res.statusCode = 200
|
|
125
|
+
res.setHeader('Content-Type', 'application/json')
|
|
126
|
+
res.setHeader('Cache-Control', 'no-store')
|
|
127
|
+
return res.end(JSON.stringify({ id, ...r }))
|
|
128
|
+
} catch (e) {
|
|
129
|
+
res.statusCode = 500
|
|
130
|
+
res.setHeader('Content-Type', 'application/json')
|
|
131
|
+
return res.end(JSON.stringify({ error: 'validate failed', message: String(e && e.message) }))
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* server/core —— 卡片后端中间件的「单一真相」与统一装配入口。
|
|
3
|
+
*
|
|
4
|
+
* 设计:
|
|
5
|
+
* - 每个端点都是独立 `(req, res, next)` 中间件工厂(见同目录各文件),与 Express / Vite
|
|
6
|
+
* 中间件容器解耦,因此可被两处复用:
|
|
7
|
+
* 1) Vite 插件(server/cardServer.js,编辑器内核开发 same-origin 路径);
|
|
8
|
+
* 2) 独立本地开发服务(packages/cardbuddy,公网静态编辑器指向本机路径)。
|
|
9
|
+
* - mountCoreMiddlewares(app, opts) 把全部中间件挂到任意 Connect 风格容器(Vite 的
|
|
10
|
+
* server.middlewares 或 connect 实例皆可),两处共用、改一处两处同步。
|
|
11
|
+
*
|
|
12
|
+
* 注意:renderStatus / nodeModels 需要活着的 vite 实例做 ssrLoadModule,故 mountCoreMiddlewares
|
|
13
|
+
* 接收 viteServer 参数透传;无 SSR 需求的场景可传 null(这两个端点会返回明确错误)。
|
|
14
|
+
*/
|
|
15
|
+
import { createSyncMiddleware, createPreviewMiddleware, createLoginCredentialsMiddleware } from './cardSync.js'
|
|
16
|
+
import { createCardFilesMiddleware } from './cardFiles.js'
|
|
17
|
+
import { createSaveMiddleware } from './cardSave.js'
|
|
18
|
+
import { createImportMetaMiddleware } from './cardImportMeta.js'
|
|
19
|
+
import { createHistoryMiddleware } from './cardHistory.js'
|
|
20
|
+
import { createValidateMiddleware } from './cardValidate.js'
|
|
21
|
+
import { createRenderStatusMiddleware } from './renderStatus.js'
|
|
22
|
+
import { createScreenshotMiddleware } from './screenshot.js'
|
|
23
|
+
import { createBboxMiddleware } from './bbox.js'
|
|
24
|
+
import { createNodeModelsMiddleware } from './nodeModels.js'
|
|
25
|
+
import { createAssetProxyMiddleware } from './assetProxy.js'
|
|
26
|
+
import { createSliceStaticMiddleware } from './sliceStatic.js'
|
|
27
|
+
import { createCardImageMiddleware } from './cardImage.js'
|
|
28
|
+
import { createOpenFolderMiddleware } from './cardOpenFolder.js'
|
|
29
|
+
import { buildDataFromSchema } from './buildDataFromSchema.js'
|
|
30
|
+
|
|
31
|
+
export {
|
|
32
|
+
createSyncMiddleware,
|
|
33
|
+
createPreviewMiddleware,
|
|
34
|
+
createLoginCredentialsMiddleware,
|
|
35
|
+
createCardFilesMiddleware,
|
|
36
|
+
createSaveMiddleware,
|
|
37
|
+
createImportMetaMiddleware,
|
|
38
|
+
createHistoryMiddleware,
|
|
39
|
+
createValidateMiddleware,
|
|
40
|
+
createRenderStatusMiddleware,
|
|
41
|
+
createScreenshotMiddleware,
|
|
42
|
+
createBboxMiddleware,
|
|
43
|
+
createNodeModelsMiddleware,
|
|
44
|
+
createAssetProxyMiddleware,
|
|
45
|
+
createSliceStaticMiddleware,
|
|
46
|
+
createCardImageMiddleware,
|
|
47
|
+
createOpenFolderMiddleware,
|
|
48
|
+
buildDataFromSchema,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 把全部卡片中间件挂到 Connect 风格容器(需实现 .use(mw))。
|
|
53
|
+
* @param {{ use: (mw: Function) => void }} app
|
|
54
|
+
* @param {{ root: string, viteServer?: import('vite').ViteDevServer | null, upstream?: string }} opts
|
|
55
|
+
* - upstream:FinMall 远端站点基址(来自 FINMALL_BASE_URL),资源代理回源用;空串表示不回源。
|
|
56
|
+
*/
|
|
57
|
+
export function mountCoreMiddlewares(app, { root, viteServer = null, upstream = '' }) {
|
|
58
|
+
app.use(createSyncMiddleware({ root }))
|
|
59
|
+
app.use(createPreviewMiddleware({ root }))
|
|
60
|
+
app.use(createLoginCredentialsMiddleware({ root }))
|
|
61
|
+
app.use(createCardFilesMiddleware({ root }))
|
|
62
|
+
app.use(createSaveMiddleware({ root }))
|
|
63
|
+
app.use(createHistoryMiddleware({ root }))
|
|
64
|
+
app.use(createValidateMiddleware({ root }))
|
|
65
|
+
app.use(createRenderStatusMiddleware({ root, viteServer }))
|
|
66
|
+
app.use(createNodeModelsMiddleware({ root, viteServer }))
|
|
67
|
+
app.use(createImportMetaMiddleware({ root }))
|
|
68
|
+
app.use(createScreenshotMiddleware({ root }))
|
|
69
|
+
// 渲染几何度量(UI 还原度对拍 P0):/__bbox/<id> 输出逐元素 bbox,供 fidelity_report.mjs 与设计稿 spec 对拍。
|
|
70
|
+
app.use(createBboxMiddleware({ root }))
|
|
71
|
+
// 在系统文件管理器中打开当前卡片本地目录(编辑器顶栏「打开目录」按钮触发,跨平台由后端完成)
|
|
72
|
+
app.use(createOpenFolderMiddleware({ root }))
|
|
73
|
+
// 卡片预览图静态服务:/__card-image/<id> → src/cards/<id>/<id>.<ext>(编辑器网格预览缩略图用,
|
|
74
|
+
// 复用本地静态服务,避免实时无头截图)。
|
|
75
|
+
app.use(createCardImageMiddleware({ root }))
|
|
76
|
+
// 本地切图静态服务:/slices/* → src/slices/*(与 dsl-card-generator 约定对齐)。
|
|
77
|
+
// 必须在资源代理之前挂载,避免被 /minio 等代理前缀误吞。
|
|
78
|
+
app.use(createSliceStaticMiddleware({ root }))
|
|
79
|
+
// 资源代理:由 8787(cardbuddy)统一提供,Vite 彻底零后端插件。
|
|
80
|
+
// upstream 空串时中间件仍会注册,但 /minio 等请求只会走占位图兜底,不会崩溃。
|
|
81
|
+
app.use(createAssetProxyMiddleware({ root, upstream }))
|
|
82
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* server/core/logger.js
|
|
3
|
+
* 后台服务统一日志美化(时间戳 + 级别色 + 框线 + 自动脱敏)。
|
|
4
|
+
*
|
|
5
|
+
* 设计目标
|
|
6
|
+
* - 可读性:时间戳、级别标签、对齐框线;
|
|
7
|
+
* - 安全性:redact() 对 schema / data / 远端响应体等大段正文只给规模概览,
|
|
8
|
+
* 绝不 dump 卡片正文;Error 只附 message + 简短 stack;
|
|
9
|
+
* - 双消费方:① server/core 内兄弟引用 './logger.js';
|
|
10
|
+
* ② skills/finmall-card-sync/scripts/*.mjs 经 '../../../server/core/logger.js' 借用;
|
|
11
|
+
* build 把 server/core 整体复制到 dist/server-core,兄弟引用在发布态仍成立。
|
|
12
|
+
*
|
|
13
|
+
* 颜色原语基于 chalk(零配置、自动探测 TTY / FORCE_COLOR / CI / NO_COLOR /
|
|
14
|
+
* TERM,支持真彩 level 3,无能力时优雅降级到 level 0 原样输出)。等级色直接取
|
|
15
|
+
* 项目设计 Token 的 RGB(design/深浅主题配色规范手册.md 深色变体),经
|
|
16
|
+
* chalk.rgb 走真彩(38;2);这既让终端日志与品牌色一致,又绕开部分终端“烂 8 色调色板”
|
|
17
|
+
* 把多级日志压成“纯色”的问题。早期的手写 ANSI + IS_TTY 朴素探测已废弃
|
|
18
|
+
* (旧逻辑在 IDE 集成终端 / 管道 / CI 下会整屏去色)。
|
|
19
|
+
* 其余逻辑:脱敏 fmt、CJK 宽度对齐、box/rule 维持原样,便于被 skill 跨目录 import。
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
// ——————————————————————————————————————————————
|
|
23
|
+
// 颜色(chalk:能力探测 + 真彩 + 优雅降级全托管,无需手写 IS_TTY/NO_COLOR)
|
|
24
|
+
// ——————————————————————————————————————————————
|
|
25
|
+
import chalk from 'chalk';
|
|
26
|
+
|
|
27
|
+
// 等级 → 设计 Token RGB(取自 design/深浅主题配色规范手册.md 深色变体;
|
|
28
|
+
// 终端编辑器外壳为深色主题)。走 chalk.rgb 真彩(38;2),能力不够时 chalk 自动
|
|
29
|
+
// 降级到最近 8/256 色或 level 0 原样,无需手写探测,也绕开部分终端“烂 8 色调色板”
|
|
30
|
+
// 把多级日志压成“纯色”的问题。
|
|
31
|
+
const PALETTE = {
|
|
32
|
+
info: [107, 131, 247], // 主色 deep #6B83F7
|
|
33
|
+
ok: [52, 211, 153], // 成功 deep #34D399
|
|
34
|
+
warn: [251, 191, 36], // 警告 amber #FBBF24
|
|
35
|
+
error: [248, 113, 113], // 错误 red #F87171
|
|
36
|
+
gray: [148, 163, 184], // 时间戳 slate #94A3B8
|
|
37
|
+
dim: [100, 116, 139], // 弱化 slate #64748B
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function rgbOf(color) {
|
|
41
|
+
return PALETTE[color] || PALETTE.gray;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 给文本上色:chalk.level=0 时自动原样返回,其余按 Token RGB 渲染 */
|
|
45
|
+
export function paint(color, s) {
|
|
46
|
+
const [r, g, b] = rgbOf(color);
|
|
47
|
+
return chalk.rgb(r, g, b)(s);
|
|
48
|
+
}
|
|
49
|
+
function paintBold(color, s) {
|
|
50
|
+
const [r, g, b] = rgbOf(color);
|
|
51
|
+
return chalk.bold(chalk.rgb(r, g, b)(s));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ——————————————————————————————————————————————
|
|
55
|
+
// 显示宽度(CJK / 全角计 2,保证框线对齐)
|
|
56
|
+
// ——————————————————————————————————————————————
|
|
57
|
+
function isWideCode(cp) {
|
|
58
|
+
return (
|
|
59
|
+
(cp >= 0x1100 && cp <= 0x115f) || // Hangul Jamo
|
|
60
|
+
(cp >= 0x2e80 && cp <= 0x303e) || // CJK 部首 / 康熙
|
|
61
|
+
(cp >= 0x3041 && cp <= 0x33ff) || // 假名 / CJK 符号
|
|
62
|
+
(cp >= 0x3400 && cp <= 0x4dbf) || // CJK 扩展 A
|
|
63
|
+
(cp >= 0x4e00 && cp <= 0x9fff) || // CJK 统一
|
|
64
|
+
(cp >= 0xa000 && cp <= 0xa4cf) || // 彝文
|
|
65
|
+
(cp >= 0xac00 && cp <= 0xd7a3) || // Hangul 音节
|
|
66
|
+
(cp >= 0xf900 && cp <= 0xfaff) || // CJK 兼容
|
|
67
|
+
(cp >= 0xfe30 && cp <= 0xfe4f) || // CJK 兼容形式
|
|
68
|
+
(cp >= 0xff00 && cp <= 0xff60) || // 全角 ASCII
|
|
69
|
+
(cp >= 0xffe0 && cp <= 0xffe6) // 全角符号
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 文本在终端的显示宽度(中文 / 全角按 2 计),用于框线对齐 */
|
|
74
|
+
export function dispWidth(str) {
|
|
75
|
+
let w = 0;
|
|
76
|
+
const s = String(str);
|
|
77
|
+
for (const ch of s) {
|
|
78
|
+
w += isWideCode(ch.codePointAt(0)) ? 2 : 1;
|
|
79
|
+
}
|
|
80
|
+
return w;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function stripAnsi(s) {
|
|
84
|
+
return String(s).replace(/\x1b\[[0-9;]*m/g, '');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ——————————————————————————————————————————————
|
|
88
|
+
// 大小格式化
|
|
89
|
+
// ——————————————————————————————————————————————
|
|
90
|
+
/** 字节数 → 人类可读(B / KB / MB) */
|
|
91
|
+
export function fmtBytes(n) {
|
|
92
|
+
if (n == null || Number.isNaN(n)) return '-';
|
|
93
|
+
if (n < 1024) return `${n} B`;
|
|
94
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
95
|
+
return `${(n / 1024 / 1024).toFixed(2)} MB`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ——————————————————————————————————————————————
|
|
99
|
+
// 紧凑化:把任意值格式化为单行日志安全表示(自动脱敏大段正文)
|
|
100
|
+
// ——————————————————————————————————————————————
|
|
101
|
+
// 这些键出现时不展开内容,只给规模概览,杜绝卡片 schema / data / 响应体刷屏。
|
|
102
|
+
const BULKY_KEYS = new Set([
|
|
103
|
+
'schema', 'data', 'response', 'verified', 'raw', 'payload', 'body',
|
|
104
|
+
'dictionary', 'view', 'inputParams', 'schemaValue', 'summary',
|
|
105
|
+
'components', 'children', 'nodes',
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
const MAX_INLINE = 160; // 字符串超过该长度截断
|
|
109
|
+
const MAX_KEYS = 8; // 对象最多展开键数
|
|
110
|
+
const MAX_ITEMS = 8; // 数组最多展开元素数
|
|
111
|
+
|
|
112
|
+
function brief(v) {
|
|
113
|
+
if (Array.isArray(v)) return `Array(${v.length})`;
|
|
114
|
+
if (v && typeof v === 'object') return `Object(${Object.keys(v).length})`;
|
|
115
|
+
if (typeof v === 'string') return `str(${v.length})`;
|
|
116
|
+
return typeof v;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** 把任意值格式化成单行、紧凑、已脱敏的日志表示 */
|
|
120
|
+
export function fmt(v, depth = 0) {
|
|
121
|
+
if (v == null) return 'null';
|
|
122
|
+
if (v instanceof Error) return `${v.name}: ${v.message}`;
|
|
123
|
+
if (typeof v === 'string') {
|
|
124
|
+
return v.length > MAX_INLINE ? `${v.slice(0, MAX_INLINE)}…(${v.length})` : v;
|
|
125
|
+
}
|
|
126
|
+
if (typeof v === 'number' || typeof v === 'boolean') return String(v);
|
|
127
|
+
if (typeof v === 'function') return `ƒ${v.name || 'anonymous'}`;
|
|
128
|
+
if (depth >= 3) return brief(v);
|
|
129
|
+
try {
|
|
130
|
+
if (Array.isArray(v)) {
|
|
131
|
+
if (v.length === 0) return '[]';
|
|
132
|
+
if (v.length > MAX_ITEMS) return `[Array(${v.length}) 首=${fmt(v[0], depth + 1)}]`;
|
|
133
|
+
return '[' + v.map((x) => fmt(x, depth + 1)).join(', ') + ']';
|
|
134
|
+
}
|
|
135
|
+
const keys = Object.keys(v);
|
|
136
|
+
if (keys.length === 0) return '{}';
|
|
137
|
+
const parts = [];
|
|
138
|
+
let truncated = false;
|
|
139
|
+
for (const k of keys) {
|
|
140
|
+
if (parts.length >= MAX_KEYS) {
|
|
141
|
+
truncated = true;
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
const val = v[k];
|
|
145
|
+
if (BULKY_KEYS.has(k) && (Array.isArray(val) || (val && typeof val === 'object'))) {
|
|
146
|
+
parts.push(`${k}:«${brief(val)} 已脱敏»`);
|
|
147
|
+
} else {
|
|
148
|
+
parts.push(`${k}:${fmt(val, depth + 1)}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const tail = truncated ? ` …(+${keys.length - parts.length})` : '';
|
|
152
|
+
return `{${parts.join(', ')}${tail}}`;
|
|
153
|
+
} catch {
|
|
154
|
+
return String(v);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ——————————————————————————————————————————————
|
|
159
|
+
// 框线
|
|
160
|
+
// ——————————————————————————————————————————————
|
|
161
|
+
const COLOR_OF = { info: 'blue', ok: 'green', warn: 'yellow', error: 'red' };
|
|
162
|
+
const TAG_OF = { info: 'INFO', ok: 'OK ', warn: 'WARN', error: 'ERR ' };
|
|
163
|
+
|
|
164
|
+
function padLine(content, width, color) {
|
|
165
|
+
const cw = dispWidth(stripAnsi(content));
|
|
166
|
+
const pad = Math.max(0, width - cw);
|
|
167
|
+
return paint(color, '│ ') + content + ' '.repeat(pad) + paint(color, ' │');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** 生成一个对齐的美化框(标题 + 多行内容) */
|
|
171
|
+
export function box({ title = '', lines = [], level = 'info' }) {
|
|
172
|
+
const color = COLOR_OF[level] || 'blue';
|
|
173
|
+
const rows = [];
|
|
174
|
+
if (title) rows.push(title);
|
|
175
|
+
for (const l of lines) rows.push(l);
|
|
176
|
+
let width = 0;
|
|
177
|
+
for (const r of rows) width = Math.max(width, dispWidth(stripAnsi(r)));
|
|
178
|
+
width = Math.max(width, 24);
|
|
179
|
+
const top = paint(color, '┌' + '─'.repeat(width + 2) + '┐');
|
|
180
|
+
const mid = paint(color, '├' + '─'.repeat(width + 2) + '┤');
|
|
181
|
+
const bot = paint(color, '└' + '─'.repeat(width + 2) + '┘');
|
|
182
|
+
const out = [top];
|
|
183
|
+
if (title) out.push(padLine(paintBold(color, title), width, color));
|
|
184
|
+
if (title && lines.length) out.push(mid);
|
|
185
|
+
for (const l of lines) out.push(padLine(l, width, color));
|
|
186
|
+
out.push(bot);
|
|
187
|
+
return out.join('\n');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** 分隔线(可选标签,便于在长日志中分段) */
|
|
191
|
+
export function rule(label = '', level = 'info') {
|
|
192
|
+
const color = COLOR_OF[level] || 'blue';
|
|
193
|
+
const total = 56;
|
|
194
|
+
if (!label) return paint(color, '─'.repeat(total));
|
|
195
|
+
const left = `─ ${label} `;
|
|
196
|
+
const right = '─'.repeat(Math.max(2, total - dispWidth(left)));
|
|
197
|
+
return paint(color, left + right);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ——————————————————————————————————————————————
|
|
201
|
+
// 主 API
|
|
202
|
+
// ——————————————————————————————————————————————
|
|
203
|
+
function stamp() {
|
|
204
|
+
// 本地时间 HH:MM:SS.mmm
|
|
205
|
+
const d = new Date();
|
|
206
|
+
const p = (n, w = 2) => String(n).padStart(w, '0');
|
|
207
|
+
return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}.${p(d.getMilliseconds(), 3)}`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function emit(level, msg, args) {
|
|
211
|
+
const color = COLOR_OF[level] || 'blue';
|
|
212
|
+
const head = `${paint('gray', stamp())} ${paintBold(color, TAG_OF[level] || 'INFO')}`;
|
|
213
|
+
const parts = [head, msg, ...args.map((a) => fmt(a))];
|
|
214
|
+
let extra = '';
|
|
215
|
+
if (level === 'error') {
|
|
216
|
+
const err = args.find((a) => a instanceof Error);
|
|
217
|
+
if (err && err.stack) {
|
|
218
|
+
const st = err.stack.split('\n').slice(1, 3).join('\n ').trim();
|
|
219
|
+
if (st) extra = '\n ' + paint('dim', st);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const stream = level === 'error' || level === 'warn' ? process.stderr : process.stdout;
|
|
223
|
+
stream.write(parts.join(' ') + extra + '\n');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** 统一日志对象:log.info / ok / warn / error / box / rule */
|
|
227
|
+
export const log = {
|
|
228
|
+
info: (msg, ...a) => emit('info', msg, a),
|
|
229
|
+
ok: (msg, ...a) => emit('ok', msg, a),
|
|
230
|
+
warn: (msg, ...a) => emit('warn', msg, a),
|
|
231
|
+
error: (msg, ...a) => emit('error', msg, a),
|
|
232
|
+
box: (opts) => {
|
|
233
|
+
process.stdout.write(box(opts) + '\n');
|
|
234
|
+
},
|
|
235
|
+
rule: (label, level = 'info') => {
|
|
236
|
+
const stream = level === 'error' || level === 'warn' ? process.stderr : process.stdout;
|
|
237
|
+
stream.write(rule(label, level) + '\n');
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
export default log;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 节点 Property 树模型暴露端点(Task #5)。
|
|
6
|
+
*
|
|
7
|
+
* - GET /__node-models/<id> → ssrLoadModule toPreview 后返回该卡的「可序列化 Property
|
|
8
|
+
* 树模型森林」(buildModelView),供 AI / skill / MCP 直接读取每个节点的可编辑字段结构,
|
|
9
|
+
* 无需解析原始 DSL + component-meta。
|
|
10
|
+
*
|
|
11
|
+
* 与 /__render-status 一致:复用 dev server 的 vite 实例做 ssrLoadModule(HMR 友好,
|
|
12
|
+
* 改 preview/runtime 免重启);首次请求有编译冷启动属正常。
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 产出单卡模型森林。纯函数、无 HTTP,便于复用。
|
|
17
|
+
* @param {import('vite').ViteDevServer} viteServer 含 ssrLoadModule 的 vite 实例
|
|
18
|
+
* @param {string} id 卡片 id
|
|
19
|
+
* @param {string} dir 卡片目录(含 schema.json)
|
|
20
|
+
*/
|
|
21
|
+
export async function nodeModelsFor(viteServer, id, dir) {
|
|
22
|
+
const schema = JSON.parse(fs.readFileSync(path.join(dir, 'schema.json'), 'utf8'))
|
|
23
|
+
const { buildModelView } = await viteServer.ssrLoadModule('/runtime/toPreview.js')
|
|
24
|
+
const model = buildModelView(schema)
|
|
25
|
+
return { id, model }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function createNodeModelsMiddleware({ root, viteServer }) {
|
|
29
|
+
return async function nodeModelsMiddleware(req, res, next) {
|
|
30
|
+
const pathname = decodeURIComponent((req.url || '').split('?')[0])
|
|
31
|
+
const m = pathname.match(/^\/__node-models\/([^/]+)$/)
|
|
32
|
+
if (!m) return next()
|
|
33
|
+
const id = decodeURIComponent(m[1])
|
|
34
|
+
const dir = path.join(root, 'src', 'cards', id)
|
|
35
|
+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
|
|
36
|
+
res.statusCode = 404
|
|
37
|
+
res.setHeader('Content-Type', 'application/json')
|
|
38
|
+
return res.end(JSON.stringify({ error: 'card not found', id }))
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const out = await nodeModelsFor(viteServer, id, dir)
|
|
42
|
+
res.statusCode = 200
|
|
43
|
+
res.setHeader('Content-Type', 'application/json')
|
|
44
|
+
res.setHeader('Cache-Control', 'no-store')
|
|
45
|
+
return res.end(JSON.stringify(out))
|
|
46
|
+
} catch (e) {
|
|
47
|
+
res.statusCode = 500
|
|
48
|
+
res.setHeader('Content-Type', 'application/json')
|
|
49
|
+
return res.end(JSON.stringify({ error: 'node-models failed', id, message: String(e && e.message) }))
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|