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.
Files changed (167) hide show
  1. package/README.md +211 -0
  2. package/dist/cli.js +76 -0
  3. package/dist/config.js +63 -0
  4. package/dist/core.js +2 -0
  5. package/dist/preview-editor/assets/apple-touch-icon-BQcES9QK.png +0 -0
  6. package/dist/preview-editor/assets/default-image-wzZYQ86K.png +0 -0
  7. package/dist/preview-editor/assets/favicon-16-Cg6Nrosk.png +0 -0
  8. package/dist/preview-editor/assets/favicon-32-B_1744sp.png +0 -0
  9. package/dist/preview-editor/assets/index-BYB0hngG.js +72 -0
  10. package/dist/preview-editor/assets/index-CpBZEQDy.css +1 -0
  11. package/dist/preview-editor/assets/site-icon-CFMif8z4.png +0 -0
  12. package/dist/preview-editor/assets/svg-symbols-B2exBrAr.svg +118 -0
  13. package/dist/preview-editor/index.html +17 -0
  14. package/dist/server-core/_vendor/finmall-env.mjs +96 -0
  15. package/dist/server-core/assetProxy.js +154 -0
  16. package/dist/server-core/bbox.js +153 -0
  17. package/dist/server-core/browser.js +108 -0
  18. package/dist/server-core/buildDataFromSchema.js +50 -0
  19. package/dist/server-core/cardFiles.js +188 -0
  20. package/dist/server-core/cardHistory.js +156 -0
  21. package/dist/server-core/cardImage.js +85 -0
  22. package/dist/server-core/cardImportMeta.js +139 -0
  23. package/dist/server-core/cardListCache.js +68 -0
  24. package/dist/server-core/cardOpenFolder.js +135 -0
  25. package/dist/server-core/cardSave.js +127 -0
  26. package/dist/server-core/cardSidecar.js +72 -0
  27. package/dist/server-core/cardSync.js +259 -0
  28. package/dist/server-core/cardValidate.js +134 -0
  29. package/dist/server-core/index.js +82 -0
  30. package/dist/server-core/logger.js +241 -0
  31. package/dist/server-core/nodeModels.js +52 -0
  32. package/dist/server-core/renderStatus.js +207 -0
  33. package/dist/server-core/screenshot.js +123 -0
  34. package/dist/server-core/sliceStatic.js +44 -0
  35. package/dist/standalone.js +447 -0
  36. package/dist/vendored/App.vue +2330 -0
  37. package/dist/vendored/api.js +142 -0
  38. package/dist/vendored/assets/apple-touch-icon.png +0 -0
  39. package/dist/vendored/assets/default-image.png +0 -0
  40. package/dist/vendored/assets/favicon-16.png +0 -0
  41. package/dist/vendored/assets/favicon-32.png +0 -0
  42. package/dist/vendored/assets/logo.png +0 -0
  43. package/dist/vendored/assets/site-icon.png +0 -0
  44. package/dist/vendored/assets/svg-symbols.svg +118 -0
  45. package/dist/vendored/assets/svgIcons/icons.svg +54 -0
  46. package/dist/vendored/assets/svgIcons/index.js +39 -0
  47. package/dist/vendored/components/AbsoluteLayout.vue +42 -0
  48. package/dist/vendored/components/AlertCardInstance.vue +58 -0
  49. package/dist/vendored/components/AlertCardLayer.vue +59 -0
  50. package/dist/vendored/components/ApiPayloadTree.vue +168 -0
  51. package/dist/vendored/components/AppToast.vue +37 -0
  52. package/dist/vendored/components/Button.vue +37 -0
  53. package/dist/vendored/components/Canvas.vue +65 -0
  54. package/dist/vendored/components/CardGallery.vue +243 -0
  55. package/dist/vendored/components/CardPreview.vue +72 -0
  56. package/dist/vendored/components/CardSelect.vue +200 -0
  57. package/dist/vendored/components/CheckGroup.vue +131 -0
  58. package/dist/vendored/components/Column.vue +25 -0
  59. package/dist/vendored/components/ComponentPalette.vue +294 -0
  60. package/dist/vendored/components/ConfirmDialog.vue +39 -0
  61. package/dist/vendored/components/CustomPlaceholder.vue +33 -0
  62. package/dist/vendored/components/DatePickerOverlay.vue +345 -0
  63. package/dist/vendored/components/DebugPanel.vue +806 -0
  64. package/dist/vendored/components/DeviceOverlay.vue +70 -0
  65. package/dist/vendored/components/Divider.vue +22 -0
  66. package/dist/vendored/components/Dropdown.vue +191 -0
  67. package/dist/vendored/components/EventEditor.vue +247 -0
  68. package/dist/vendored/components/GridLayout.vue +67 -0
  69. package/dist/vendored/components/HoverGuides.vue +122 -0
  70. package/dist/vendored/components/Icon.vue +34 -0
  71. package/dist/vendored/components/Image.vue +34 -0
  72. package/dist/vendored/components/Input.vue +90 -0
  73. package/dist/vendored/components/JsonTree.vue +516 -0
  74. package/dist/vendored/components/ListLayout.vue +68 -0
  75. package/dist/vendored/components/LogValue.vue +51 -0
  76. package/dist/vendored/components/Map.vue +110 -0
  77. package/dist/vendored/components/MultiColumn.vue +49 -0
  78. package/dist/vendored/components/MultiText.vue +58 -0
  79. package/dist/vendored/components/NodeInspector.vue +295 -0
  80. package/dist/vendored/components/PickerOverlay.vue +131 -0
  81. package/dist/vendored/components/ProgressSlider.vue +167 -0
  82. package/dist/vendored/components/PropControl.vue +241 -0
  83. package/dist/vendored/components/PropTree.vue +128 -0
  84. package/dist/vendored/components/RadioButton.vue +74 -0
  85. package/dist/vendored/components/RemotePreview.vue +171 -0
  86. package/dist/vendored/components/RenderTree.vue +437 -0
  87. package/dist/vendored/components/Root.vue +30 -0
  88. package/dist/vendored/components/Row.vue +25 -0
  89. package/dist/vendored/components/SelectionOverlay.vue +105 -0
  90. package/dist/vendored/components/StructureTree.vue +949 -0
  91. package/dist/vendored/components/SvgCode.vue +19 -0
  92. package/dist/vendored/components/SvgSymbols.vue +30 -0
  93. package/dist/vendored/components/Switch.vue +38 -0
  94. package/dist/vendored/components/Text.vue +32 -0
  95. package/dist/vendored/components/Video.vue +70 -0
  96. package/dist/vendored/components/ViewContainer.vue +52 -0
  97. package/dist/vendored/components/base.js +12 -0
  98. package/dist/vendored/components/icons.js +47 -0
  99. package/dist/vendored/components/index.js +58 -0
  100. package/dist/vendored/components/setters/SetBgPosition.vue +64 -0
  101. package/dist/vendored/components/setters/SetBgSize.vue +90 -0
  102. package/dist/vendored/components/setters/SetCode.vue +90 -0
  103. package/dist/vendored/components/setters/SetColor.vue +244 -0
  104. package/dist/vendored/components/setters/SetCustomSelect.vue +58 -0
  105. package/dist/vendored/components/setters/SetExpr.vue +107 -0
  106. package/dist/vendored/components/setters/SetGradient.vue +125 -0
  107. package/dist/vendored/components/setters/SetGrid9.vue +68 -0
  108. package/dist/vendored/components/setters/SetIconSelect.vue +67 -0
  109. package/dist/vendored/components/setters/SetIfForm.vue +61 -0
  110. package/dist/vendored/components/setters/SetImage.vue +43 -0
  111. package/dist/vendored/components/setters/SetList.vue +139 -0
  112. package/dist/vendored/components/setters/SetNumber.vue +118 -0
  113. package/dist/vendored/components/setters/SetNumberStyle.vue +113 -0
  114. package/dist/vendored/components/setters/SetPadding.vue +77 -0
  115. package/dist/vendored/components/setters/SetSegment.vue +160 -0
  116. package/dist/vendored/components/setters/SetSelect.vue +51 -0
  117. package/dist/vendored/components/setters/SetSwitch.vue +49 -0
  118. package/dist/vendored/components/setters/SetText.vue +52 -0
  119. package/dist/vendored/components/setters/props.js +23 -0
  120. package/dist/vendored/fidelity/baseline-messageOverview.json +87 -0
  121. package/dist/vendored/fidelity/calibration.json +29 -0
  122. package/dist/vendored/fidelity/fidelity-messageOverview-after.html +114 -0
  123. package/dist/vendored/fidelity/fidelity-messageOverview-after.json +272 -0
  124. package/dist/vendored/fidelity/fidelity-messageOverview.html +185 -0
  125. package/dist/vendored/fidelity/fidelity-messageOverview.json +336 -0
  126. package/dist/vendored/fidelity/fidelity-messageOverview_v2.html +262 -0
  127. package/dist/vendored/fidelity/fidelity-messageOverview_v2.json +502 -0
  128. package/dist/vendored/fidelity/fidelity-messageOverview_v3.html +201 -0
  129. package/dist/vendored/fidelity/fidelity-messageOverview_v3.json +417 -0
  130. package/dist/vendored/fidelity/fidelity-messageOverview_visual.html +125 -0
  131. package/dist/vendored/fidelity/fidelity-messageOverview_visual.json +338 -0
  132. package/dist/vendored/index.html +16 -0
  133. package/dist/vendored/main.js +36 -0
  134. package/dist/vendored/meta/action-meta.json +141 -0
  135. package/dist/vendored/meta/component-meta.json +2714 -0
  136. package/dist/vendored/meta/directive-meta.json +40 -0
  137. package/dist/vendored/meta/style-meta.json +1086 -0
  138. package/dist/vendored/runtime/StyleParser.js +179 -0
  139. package/dist/vendored/runtime/adapter.js +166 -0
  140. package/dist/vendored/runtime/alert-card.js +125 -0
  141. package/dist/vendored/runtime/app-toast.js +33 -0
  142. package/dist/vendored/runtime/assets.js +16 -0
  143. package/dist/vendored/runtime/canvas-context.js +141 -0
  144. package/dist/vendored/runtime/card-runtime.js +289 -0
  145. package/dist/vendored/runtime/clipboard-node.js +83 -0
  146. package/dist/vendored/runtime/clipboard.js +29 -0
  147. package/dist/vendored/runtime/evaluate.js +152 -0
  148. package/dist/vendored/runtime/event-model.js +300 -0
  149. package/dist/vendored/runtime/hoverGuide.js +31 -0
  150. package/dist/vendored/runtime/jsapi-mock.js +256 -0
  151. package/dist/vendored/runtime/node-model.js +217 -0
  152. package/dist/vendored/runtime/property.js +364 -0
  153. package/dist/vendored/runtime/selectionOverlay.js +53 -0
  154. package/dist/vendored/runtime/toPreview.js +648 -0
  155. package/dist/vendored/standalone-main.js +36 -0
  156. package/dist/vendored/standalone.html +12 -0
  157. package/dist/vendored/styles/app.css +2565 -0
  158. package/dist/vendored/styles/card-gallery.css +234 -0
  159. package/dist/vendored/styles/card-select.css +281 -0
  160. package/dist/vendored/styles/components.css +383 -0
  161. package/dist/vendored/styles/dropdown.css +337 -0
  162. package/dist/vendored/styles/json-tree.css +308 -0
  163. package/dist/vendored/styles/node-inspector.css +1003 -0
  164. package/dist/vendored/styles/setters.css +962 -0
  165. package/dist/vendored/styles/standalone.css +27 -0
  166. package/dist/vendored/styles/structure-tree.css +320 -0
  167. package/package.json +42 -0
@@ -0,0 +1,207 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import crypto from 'node:crypto'
4
+ import { createSSRApp, h } from 'vue'
5
+ import { renderToString } from '@vue/server-renderer'
6
+
7
+ /**
8
+ * SSR 渲染自检端点(L0 闭环之一):
9
+ * - GET /__render-status/<id> → 用 dev server 自身 vite 实例 ssrLoadModule 渲染,
10
+ * 返回结构统计 + 绑定/运行时/渲染错误 + JSAPI 清单(静态扫描 schema 的 callJSAPI)。
11
+ * - 复用 vite 实例(省冷启动、HMR 生效);语义边界:只保证「结构合法 + 绑定可解析 +
12
+ * 渲染不抛错」,视觉正确性交给 /__screenshot。错误来源:bindingErrors 读 buffer
13
+ * (SSR 不跑 mounted)、runtimeErrors 读 rt.eventErrors(created 已执行 load-action)、
14
+ * renderErrors 走 app.config.errorHandler。
15
+ */
16
+
17
+ /** 递归统计 schema 组件树:分布 + 未知组件(metaMissing) */
18
+ function walkSchema(node, dist, metaMissing, getComponent) {
19
+ if (!node || typeof node !== 'object') return
20
+ const cn = node.className
21
+ if (cn) {
22
+ dist[cn] = (dist[cn] || 0) + 1
23
+ if (getComponent && !getComponent(cn) && !metaMissing.includes(cn)) metaMissing.push(cn)
24
+ }
25
+ const comps = node.components || []
26
+ for (const c of comps) walkSchema(c, dist, metaMissing, getComponent)
27
+ for (const slot of node.slots || []) {
28
+ for (const c of slot.components || []) walkSchema(c, dist, metaMissing, getComponent)
29
+ }
30
+ }
31
+
32
+ /** 组件树指纹:className+componentId 序列的 sha1 前 12 位,供版本 diff */
33
+ function structureDigest(view) {
34
+ const parts = []
35
+ const rec = (node) => {
36
+ if (!node || typeof node !== 'object') return
37
+ parts.push(`${node.className || '?'}:${node.componentId || ''}`)
38
+ for (const c of node.components || []) rec(c)
39
+ for (const slot of node.slots || []) for (const c of slot.components || []) rec(c)
40
+ }
41
+ rec(view)
42
+ return 'sha1:' + crypto.createHash('sha1').update(parts.join('|')).digest('hex').slice(0, 12)
43
+ }
44
+
45
+ /** 静态扫描 schema 里所有 callJSAPI('x') 调用,去重 */
46
+ function scanJsapi(schema) {
47
+ const text = JSON.stringify(schema)
48
+ const re = /callJSAPI\(\s*['"]([\w]+)['"]/g
49
+ const set = new Set()
50
+ let m
51
+ while ((m = re.exec(text))) set.add(m[1])
52
+ return [...set]
53
+ }
54
+
55
+ /**
56
+ * 渲染单卡并返回自检结果。纯函数、无 HTTP,便于 CI 的 card-check 直接复用。
57
+ * @param {import('vite').ViteDevServer} viteServer 含 ssrLoadModule 的 vite 实例
58
+ * @param {string} id 卡片 id
59
+ * @param {string} dir 卡片目录(含 schema.json / data.json)
60
+ */
61
+ export async function renderStatusFor(viteServer, id, dir) {
62
+ const t0 = Date.now()
63
+ let schema
64
+ try {
65
+ schema = JSON.parse(fs.readFileSync(path.join(dir, 'schema.json'), 'utf8'))
66
+ } catch (e) {
67
+ return { id, ok: false, fatal: 'schema-parse-error', message: String(e && e.message), durationMs: Date.now() - t0 }
68
+ }
69
+ let data = null
70
+ try {
71
+ data = JSON.parse(fs.readFileSync(path.join(dir, 'data.json'), 'utf8'))
72
+ } catch {
73
+ /* 无 data.json 时以空作用域渲染,绑定错误会据此暴露 */
74
+ }
75
+
76
+ // 每次请求都走 ssrLoadModule:模块未失效时 Vite 直接返回缓存(无重编译),
77
+ // 被 HMR 失效后自动返回新版 —— 兼顾性能与「改 preview/runtime 免重启」。
78
+ const { default: RenderTree } = await viteServer.ssrLoadModule('/components/RenderTree.vue')
79
+ const { default: Adapter } = await viteServer.ssrLoadModule('/runtime/adapter.js')
80
+ const { createCardRuntime } = await viteServer.ssrLoadModule('/runtime/card-runtime.js')
81
+ const { buildPreview } = await viteServer.ssrLoadModule('/runtime/toPreview.js')
82
+ const { getComponent } = await viteServer.ssrLoadModule('/components/index.js')
83
+
84
+ const adapter = new Adapter()
85
+ const rt = createCardRuntime(data || {}, adapter)
86
+ const preview = buildPreview(schema)
87
+
88
+ // SSR 沙箱:卡片 customAction 脚本(含 setTimeout 里)会直接引用全局 `ynet` /
89
+ // `window` / `self`(宿主运行时提供,SSR 下本不存在)。不 mock 会在 created()
90
+ // 同步执行或异步定时器触发时报 ReferenceError 炸进程。这里补上与浏览器一致的
91
+ // 桩,并**持久保留**(不还原)——因为 setTimeout 在 renderToString 之后才触发。
92
+ // 这是预期内的「SSR 无真实运行时」兜底,不影响错误收集。
93
+ const ynetMock = {
94
+ callJSAPI(api, p, cb) {
95
+ try {
96
+ rt.pushApiCall(api, p)
97
+ } catch {
98
+ /* ignore */
99
+ }
100
+ if (typeof cb === 'function') {
101
+ try {
102
+ cb({})
103
+ } catch {
104
+ /* ignore */
105
+ }
106
+ }
107
+ },
108
+ }
109
+ if (typeof globalThis.ynet === 'undefined') globalThis.ynet = ynetMock
110
+ if (typeof globalThis.window === 'undefined') {
111
+ globalThis.window = { innerWidth: 375, devicePixelRatio: 2, ynet: ynetMock }
112
+ }
113
+ if (typeof globalThis.self === 'undefined') globalThis.self = globalThis.window
114
+
115
+ const componentDist = {}
116
+ const metaMissing = []
117
+ walkSchema(schema.view, componentDist, metaMissing, getComponent)
118
+ const componentCount = Object.values(componentDist).reduce((a, b) => a + b, 0)
119
+
120
+ const wrapper = {
121
+ name: 'RenderStatusWrapper',
122
+ provide() {
123
+ return { adapter, cardRuntime: rt, customComponents: {} }
124
+ },
125
+ render() {
126
+ return h(RenderTree, { node: preview.view, scope: rt.rootScope })
127
+ },
128
+ }
129
+ const app = createSSRApp(wrapper)
130
+ const renderErrors = []
131
+ app.config.errorHandler = (err, vm, info) => {
132
+ renderErrors.push(`${info}: ${err && err.message}`)
133
+ }
134
+
135
+ // runLoadActions 在 created 里执行 customAction,会触发 console.error;
136
+ // 这些内容 rt 已结构化收集,这里静音避免刷屏(全局 console 短时替换,影响可忽略)。
137
+ const origErr = console.error
138
+ console.error = () => {}
139
+ let html = ''
140
+ let threw = null
141
+ try {
142
+ html = await renderToString(app)
143
+ } catch (e) {
144
+ threw = { message: e && e.message, stack: (e && e.stack ? String(e.stack).split('\n').slice(0, 3) : []) }
145
+ } finally {
146
+ console.error = origErr
147
+ }
148
+
149
+ const bindingErrors = (rt.getBindingBuffer() || []).map((e) => ({
150
+ path: e.path,
151
+ expr: e.expr,
152
+ message: e.msg,
153
+ }))
154
+ const runtimeErrors = [...(rt.eventErrors || []), ...(rt.errors || [])].map((e) => ({
155
+ kind: e.kind,
156
+ msg: e.msg,
157
+ }))
158
+ const jsapiCalls = scanJsapi(schema)
159
+ const renderOk = renderErrors.length === 0 && !threw
160
+ // 约定:ok 反映「渲染不抛错」。bindingErrors 单独列出——SSR 下异步数据缺失会
161
+ // 产生误报(卡片在真实运行时由 customAction/RPC 补数据),故不计入 ok,避免 CI 泛洪。
162
+ const ok = renderOk
163
+
164
+ return {
165
+ id,
166
+ ok,
167
+ renderOk,
168
+ bindingErrorCount: bindingErrors.length,
169
+ componentCount,
170
+ componentDist,
171
+ metaMissing,
172
+ structureDigest: structureDigest(schema.view),
173
+ bindingErrors,
174
+ runtimeErrors,
175
+ renderErrors,
176
+ jsapiCalls,
177
+ htmlLength: html.length,
178
+ durationMs: Date.now() - t0,
179
+ threw,
180
+ }
181
+ }
182
+
183
+ export function createRenderStatusMiddleware({ root, viteServer }) {
184
+ return async function renderStatusMiddleware(req, res, next) {
185
+ const pathname = decodeURIComponent((req.url || '').split('?')[0])
186
+ const m = pathname.match(/^\/__render-status\/([^/]+)$/)
187
+ if (!m) return next()
188
+ const id = decodeURIComponent(m[1])
189
+ const dir = path.join(root, 'src', 'cards', id)
190
+ if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
191
+ res.statusCode = 404
192
+ res.setHeader('Content-Type', 'application/json')
193
+ return res.end(JSON.stringify({ error: 'card not found', id }))
194
+ }
195
+ try {
196
+ const status = await renderStatusFor(viteServer, id, dir)
197
+ res.statusCode = 200
198
+ res.setHeader('Content-Type', 'application/json')
199
+ res.setHeader('Cache-Control', 'no-store')
200
+ return res.end(JSON.stringify(status))
201
+ } catch (e) {
202
+ res.statusCode = 500
203
+ res.setHeader('Content-Type', 'application/json')
204
+ return res.end(JSON.stringify({ error: 'render-status failed', id, message: String(e && e.message) }))
205
+ }
206
+ }
207
+ }
@@ -0,0 +1,123 @@
1
+ import path from 'node:path'
2
+ import fs from 'node:fs'
3
+ import { getBrowser, serialize, openCardScreen, MAX_QUEUED } from './browser.js'
4
+
5
+ /**
6
+ * 无头截图端点(L0 闭环之一,Step 4):GET /__screenshot/<id>?width=390&format=png|json,
7
+ * 仅截卡片根(.phone__screen)。要点:①固定 chromium.launch({channel:'chrome'}) 走系统 Chrome;
8
+ * ②等 [data-card-ready] 而非 networkidle;③截图只代表「真实画出」,视觉正确性以截图为准
9
+ * (互补 /__render-status 的结构/绑定检查);④单例浏览器 + 串行队列,>10 排队 429;
10
+ * ⑤惰性启动 + 空闲 5 分钟关闭;⑥失败一律结构化响应(429/500/503/504),不抛未捕获异常;
11
+ * ⑦Chrome 缺失返回 503 + 修复指引。playwright 动态 import(build 不依赖它)。
12
+ * 浏览器单例/页面加载逻辑已抽到 ./browser.js,与 /__bbox 共用同一实例。
13
+ */
14
+
15
+ /**
16
+ * 对单卡截图。纯函数、无 HTTP。任何失败都返回 { error, code, message, ... } 而非抛出。
17
+ * @param {string} id 卡片 id
18
+ * @param {number} width 视口宽度(卡片框固定 375px,这里给外层留白)
19
+ * @param {'png'|'json'} format
20
+ */
21
+ export async function takeScreenshot(id, width, format) {
22
+ let browser
23
+ try {
24
+ browser = await getBrowser()
25
+ } catch (e) {
26
+ return {
27
+ error: 'browser-launch-failed',
28
+ code: 503,
29
+ message: String((e && e.message) || e),
30
+ fix: '系统 Chrome 未找到或未启动。请安装 Google Chrome,并确保 playwright 以 channel:"chrome" 启动(本端点固定使用该通道)。',
31
+ }
32
+ }
33
+
34
+ let context
35
+ try {
36
+ context = await browser.newContext({
37
+ viewport: { width: width + 80, height: 800 },
38
+ deviceScaleFactor: 2,
39
+ })
40
+ const { page, screen } = await openCardScreen(context, id)
41
+ try {
42
+ if (!screen) {
43
+ return { error: 'capture-target-not-found', code: 500, message: '.phone__screen 元素未找到' }
44
+ }
45
+
46
+ const buf = await screen.screenshot({ type: 'png' })
47
+ if (format === 'json') {
48
+ const box = await screen.boundingBox()
49
+ return {
50
+ json: {
51
+ id,
52
+ mime: 'image/png',
53
+ width: box ? Math.round(box.width) : null,
54
+ height: box ? Math.round(box.height) : null,
55
+ base64: buf.toString('base64'),
56
+ },
57
+ }
58
+ }
59
+ return { buffer: buf }
60
+ } finally {
61
+ await page.close().catch(() => {})
62
+ }
63
+ } catch (e) {
64
+ // 导航超时 / data-card-ready 未出现 / 截图异常:转成结构化错误,绝不向上抛
65
+ const isTimeout = /Timeout\s*\d+ms exceeded/.test(String((e && e.message) || e))
66
+ return {
67
+ error: isTimeout ? 'render-wait-timeout' : 'screenshot-failed',
68
+ code: isTimeout ? 504 : 500,
69
+ message: String((e && e.message) || e),
70
+ fix: isTimeout
71
+ ? '卡片首帧未在限定时间内挂载(data-card-ready 未出现)。检查该卡是否在 chrome=off 模式下渲染报错,或卡片依赖真实运行时数据(customAction/RPC)而无法在纯渲染模式落定。'
72
+ : undefined,
73
+ }
74
+ } finally {
75
+ if (context) await context.close().catch(() => {})
76
+ }
77
+ }
78
+
79
+ export function createScreenshotMiddleware({ root }) {
80
+ return async function screenshotMiddleware(req, res, next) {
81
+ const pathname = decodeURIComponent((req.url || '').split('?')[0])
82
+ const m = pathname.match(/^\/__screenshot\/([^/]+)$/)
83
+ if (!m) return next()
84
+ const id = decodeURIComponent(m[1])
85
+
86
+ const dir = path.join(root, 'src', 'cards', id)
87
+ if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
88
+ res.statusCode = 404
89
+ res.setHeader('Content-Type', 'application/json')
90
+ return res.end(JSON.stringify({ error: 'card not found', id }))
91
+ }
92
+
93
+ const url = new URL(req.url, 'http://localhost')
94
+ const width = parseInt(url.searchParams.get('width') || '390', 10) || 390
95
+ const format = url.searchParams.get('format') === 'json' ? 'json' : 'png'
96
+
97
+ const result = await serialize(() => takeScreenshot(id, width, format))
98
+ if (result.tooBusy) {
99
+ res.statusCode = 429
100
+ res.setHeader('Content-Type', 'application/json')
101
+ return res.end(
102
+ JSON.stringify({ error: 'too-busy', message: '截图队列已满(并发上限 ' + MAX_QUEUED + '),请稍后重试' }),
103
+ )
104
+ }
105
+ if (result.error) {
106
+ const code = result.code || 503
107
+ res.statusCode = code
108
+ res.setHeader('Content-Type', 'application/json')
109
+ return res.end(JSON.stringify({ error: result.error, message: result.message, fix: result.fix }))
110
+ }
111
+
112
+ if (format === 'json') {
113
+ res.statusCode = 200
114
+ res.setHeader('Content-Type', 'application/json')
115
+ res.setHeader('Cache-Control', 'no-store')
116
+ return res.end(JSON.stringify(result.json))
117
+ }
118
+ res.statusCode = 200
119
+ res.setHeader('Content-Type', 'image/png')
120
+ res.setHeader('Cache-Control', 'no-store')
121
+ return res.end(result.buffer)
122
+ }
123
+ }
@@ -0,0 +1,44 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+
4
+ /**
5
+ * 切图本地静态服务(issue-slices):把卡片 schema 引用的 /slices/<扁平名>@3x.png
6
+ * 映射到仓库级单一落点 src/slices/<扁平名>@3x.png。
7
+ *
8
+ * 与 dsl-card-generator 的「切图统一输出到 src/slices + schema 以 /slices/ 引用」约定对齐。
9
+ * 该中间件同时挂到两处:
10
+ * - Vite(5180):编辑器预览同源访问 /slices/*;
11
+ * - cardbuddy(8787):本地开发服务若直接托管预览页,也能取到切图。
12
+ * 仅服务本地生成产物,不做任何远端回源(远端切图走 assetProxy 的 /minio 等前缀)。
13
+ */
14
+
15
+ const MIME = {
16
+ '.png': 'image/png',
17
+ '.jpg': 'image/jpeg',
18
+ '.jpeg': 'image/jpeg',
19
+ '.webp': 'image/webp',
20
+ '.svg': 'image/svg+xml',
21
+ '.gif': 'image/gif',
22
+ }
23
+
24
+ /**
25
+ * @param {{ root: string }} opts root = 仓库根(含 src/slices)
26
+ * @returns {(req, res, next) => void}
27
+ */
28
+ export function createSliceStaticMiddleware({ root }) {
29
+ const slicesRoot = path.join(root, 'src', 'slices')
30
+ return (req, res, next) => {
31
+ const url = (req.url || '').split('?')[0]
32
+ if (!url.startsWith('/slices/')) return next()
33
+ const rel = decodeURIComponent(url.slice('/slices/'.length))
34
+ // 防目录穿越:禁止 .. 且解析后必须仍在 src/slices 内
35
+ if (rel.includes('..')) return next()
36
+ const file = path.join(slicesRoot, rel)
37
+ if (!file.startsWith(slicesRoot + path.sep) && file !== slicesRoot) return next()
38
+ if (!fs.existsSync(file) || !fs.statSync(file).isFile()) return next()
39
+ const ext = path.extname(file).toLowerCase()
40
+ res.setHeader('Content-Type', MIME[ext] || 'application/octet-stream')
41
+ res.setHeader('Cache-Control', 'public, max-age=0')
42
+ fs.createReadStream(file).pipe(res)
43
+ }
44
+ }