cardbuddy 0.14.2 → 0.15.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/dist/cli.js +0 -0
- package/dist/preview-editor/assets/{index-BN79pTkQ.js → index-CIIfMlwI.js} +43 -40
- package/dist/preview-editor/assets/index-DQn0zQBP.css +1 -0
- package/dist/preview-editor/index.html +2 -2
- package/dist/standalone.js +3 -0
- package/dist/vendored/mcp/client.mjs +8 -1
- package/dist/vendored/server/core/ai-chat.js +1521 -124
- package/dist/vendored/server/core/browser.js +12 -3
- package/dist/vendored/server/core/cardFiles.js +3 -0
- package/dist/vendored/server/core/cardHistory.js +1 -0
- package/dist/vendored/server/core/cardImportMeta.js +7 -6
- package/dist/vendored/server/core/cardOpenFolder.js +1 -0
- package/dist/vendored/server/core/cardSave.js +1 -0
- package/dist/vendored/server/core/cardSync.js +3 -0
- package/dist/vendored/server/core/codeBridge.js +5 -1
- package/dist/vendored/server/core/logStream.js +1 -0
- package/dist/vendored/server/core/sliceStatic.js +24 -4
- package/dist/vendored/server/core/sliceUpload.js +1 -0
- package/dist/vendored/server/core/turnJournal.js +6 -0
- package/dist/vendored/skills/card-skill-trainer/SKILL.md +196 -195
- package/dist/vendored/skills/dsl-card-ai/SKILL.md +1 -1
- package/dist/vendored/skills/dsl-card-generator/SKILL.md +142 -380
- package/dist/vendored/skills/dsl-card-generator/references/generation_pitfalls.md +65 -18
- package/dist/vendored/skills/dsl-card-generator/references/ir-format.md +116 -2
- package/dist/vendored/skills/dsl-card-generator/references/iteration_protocol.md +58 -0
- package/dist/vendored/skills/dsl-card-generator/references/previewer_contract.md +1 -1
- package/dist/vendored/skills/dsl-card-generator/references/quickref.md +15 -7
- package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/title-underline-overlay.json +59 -11
- package/dist/vendored/skills/dsl-card-generator/references/recipes.md +14 -0
- package/dist/vendored/skills/dsl-card-generator/references/rules.md +23 -4
- package/dist/vendored/skills/dsl-card-generator/references/scripts_reference.md +51 -0
- package/dist/vendored/skills/dsl-card-generator/references/semantic_adjudication.md +23 -0
- package/dist/vendored/skills/dsl-card-generator/references/slices_convention.md +4 -3
- package/dist/vendored/skills/dsl-card-generator/references/spec_to_schema_patterns.md +32 -0
- package/dist/vendored/skills/dsl-card-generator/references/ui_fidelity_rules.md +13 -1
- package/dist/vendored/skills/dsl-card-generator/scripts/autofix_schema.py +45 -2
- package/dist/vendored/skills/dsl-card-generator/scripts/check_fidelity.mjs +116 -10
- package/dist/vendored/skills/dsl-card-generator/scripts/check_magic_values.mjs +6 -1
- package/dist/vendored/skills/dsl-card-generator/scripts/crop_slices.mjs +92 -6
- package/dist/vendored/skills/dsl-card-generator/scripts/expand_ir.mjs +227 -23
- package/dist/vendored/skills/dsl-card-generator/scripts/gate.mjs +144 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/generate_cards.mjs +12 -8
- package/dist/vendored/skills/dsl-card-generator/scripts/ir_merge.mjs +288 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/ir_restore.mjs +175 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/iterate.mjs +644 -42
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/geometry_audit.mjs +131 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/layout_rules.mjs +160 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/slicePath.mjs +17 -5
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/slice_dedupe.mjs +102 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/v3_assemble.mjs +5443 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/v3_extract.mjs +4108 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/v3_shared.mjs +1116 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lint_card.mjs +214 -4
- package/dist/vendored/skills/dsl-card-generator/scripts/preflight.mjs +48 -2
- package/dist/vendored/skills/dsl-card-generator/scripts/render_card.mjs +91 -43
- package/dist/vendored/skills/dsl-card-generator/scripts/route_input.mjs +4 -3
- package/dist/vendored/skills/dsl-card-generator/scripts/spec_audit.mjs +150 -38
- package/dist/vendored/skills/dsl-card-generator/scripts/spec_to_schema_v3.mjs +43 -10499
- package/dist/vendored/skills/dsl-card-generator/scripts/v3_regression.mjs +186 -0
- package/dist/vendored/skills/sketch-spec-to-card/SKILL.md +18 -12
- package/dist/vendored/skills/sketch-spec-to-card/references/spec-schema.md +1 -1
- package/dist/vendored/skills/sketch-spec-to-card/scripts/build_prompt.py +47 -1
- package/dist/vendored/skills/sketch-spec-to-card/scripts/parse_meaxure.py +460 -49
- package/dist/vendored/skills/sketch-spec-to-card/scripts/png_utils.py +51 -0
- package/dist/vendored/skills/sketch-spec-to-card/scripts/test_text_only_artboard.py +157 -0
- package/dist/vendored/skills/sketch-spec-to-card/scripts/vision.py +24 -0
- package/package.json +1 -1
- package/dist/preview-editor/assets/index-cbTCHp8J.css +0 -1
|
@@ -15,7 +15,17 @@
|
|
|
15
15
|
* 输出(stdout JSON):
|
|
16
16
|
* { card, design:{scale}, texts[], shapes[], slices[], bands[], gaps{}, colors[], missing[],
|
|
17
17
|
* bindHints[], listCandidates[] }
|
|
18
|
+
* - shapes[] 每条 = { x,y,w,h, bg, gradient, bgImage, border, radius, opacity }
|
|
19
|
+
* · **gradient / border / bgImage 必须带出来**:只给 bg 时,渐变底板与描边层在模型眼里
|
|
20
|
+
* 就是一个「什么都没有的空节点」,会被整块丢掉(2026-09-21 helloCard:顶部渐变区 343×154、
|
|
21
|
+
* 两块产品底板 311×62、红标签 0.5px #F6434F 描边、311×0.5 分割线,四处全丢)。
|
|
22
|
+
* 没有视觉能力的模型只能靠这份清单知道「这里有个东西要画」。
|
|
23
|
+
* · gradient 是 CSS 串(spec 里已是该格式),可直接喂 expand_ir 的 backgroundGradient。
|
|
18
24
|
* - slices[].verdict ∈ full-asset / missing-file / reused-differs / reused-identical / content-twin
|
|
25
|
+
* · ref = 设计稿图层**完整**引用(含 `8.icon/通用/` 这类分组路径);name = **消毒后的落盘/引用名**,
|
|
26
|
+
* 两者不同时另带 originalName。存在性判定按完整 ref + `@2x/@3x` 后缀查
|
|
27
|
+
* (MeaXure 常态落点是 `slices/<分组路径>/<名>@3x.png`)——只按 basename 找扁平文件会把
|
|
28
|
+
* **真实存在**的资产报成 missing-file,下游白重裁或模型改用图层名当引用(浏览器 404)。
|
|
19
29
|
* · content-twin:导出资产缺文件,但**既有切图里已有像素同内容**(共用/语义重命名/跨画板)→ 直接复用,不裁切
|
|
20
30
|
* · crops[] 按**指纹分组**产出:同组(内容相同)的多个矩形共用一张派生图(alsoRects 列同组其余矩形),
|
|
21
31
|
不再「每个矩形一个 _altN」;派生前先对既有切图做像素级判重,命中给 reuseOf(引用改绑既有名,不裁切)
|
|
@@ -30,6 +40,7 @@ import path from 'node:path'
|
|
|
30
40
|
|
|
31
41
|
const { PNG } = await import('pngjs')
|
|
32
42
|
import { cropPng, findPixelTwin } from './lib/slice_pixels.mjs'
|
|
43
|
+
import { sanitizeSliceFileName } from './lib/slice_dedupe.mjs'
|
|
33
44
|
|
|
34
45
|
function parseArgs(argv) {
|
|
35
46
|
const out = { spec: '', card: 1, slicesDir: '', design: '', projectSlices: '' }
|
|
@@ -41,10 +52,13 @@ function parseArgs(argv) {
|
|
|
41
52
|
else if (a === '--design') out.design = argv[++i] || ''
|
|
42
53
|
else if (a === '--project-slices') out.projectSlices = argv[++i] || ''
|
|
43
54
|
else if (a === '--save') out.save = argv[++i] || ''
|
|
55
|
+
else if (a === '--no-save') out.noSave = true
|
|
44
56
|
else if (a === '-h' || a === '--help') {
|
|
45
|
-
console.log('Usage: node spec_audit.mjs --spec <spec.json> [--card 1] [--slices-dir <dir>] [--design <card-N.png>] [--save <path.json>]')
|
|
46
|
-
console.log(' --save
|
|
57
|
+
console.log('Usage: node spec_audit.mjs --spec <spec.json> [--card 1] [--slices-dir <dir>] [--design <card-N.png>] [--save <path.json>] [--no-save]')
|
|
58
|
+
console.log(' --save:整份审计 JSON 落盘路径;不给就默认落 <工程根>/.cache/spec-audit/<spec 名>-card<N>.json。')
|
|
47
59
|
console.log(' 历史事故:模型用手抄的 156 字节「审计摘要」喂 crop_slices,切图名/矩形全错;审计必须整份落盘复用,禁止手抄。')
|
|
60
|
+
console.log(' --no-save:不落盘(只在明确不需要 crop_slices --from-audit 时才用)。')
|
|
61
|
+
console.log(' stdout 一律是紧凑 JSON 审计简报;超过 9000 字会改出「摘要 + savedTo」。')
|
|
48
62
|
process.exit(0)
|
|
49
63
|
}
|
|
50
64
|
}
|
|
@@ -103,7 +117,6 @@ function analyzeSlice(file) {
|
|
|
103
117
|
return { px: [png.width, png.height], bg, label: rows > 0 && labelRows / rows > 0.2 }
|
|
104
118
|
}
|
|
105
119
|
|
|
106
|
-
/** 切图名可能不带扩展名(spec 里存的是图层名),逐个候选扩展名找文件 */
|
|
107
120
|
/**
|
|
108
121
|
* manifest 别名表:语义重命名(slices_normalize / 手工改名)后,导出原名 → 新名的映射。
|
|
109
122
|
* 读各目录 manifest.json 的 renamedFrom / mergedFrom(§1.1「改名要成套做」的机器可读痕迹)。
|
|
@@ -123,18 +136,59 @@ function buildAliasMap(dirs) {
|
|
|
123
136
|
return aliases
|
|
124
137
|
}
|
|
125
138
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
139
|
+
/** 路径输出:在 cwd 内给相对路径(好读、可复现),在 cwd 外给绝对路径(否则一串 `../../..`)。 */
|
|
140
|
+
function relOrAbs(file) {
|
|
141
|
+
const rel = path.relative(process.cwd(), file)
|
|
142
|
+
return rel && !rel.startsWith('..') ? rel.split(path.sep).join('/') : file
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 解析切图引用 → 实际文件。
|
|
147
|
+
*
|
|
148
|
+
* **必须吃全引用路径(含子目录)与 `@Nx` 倍图后缀**。历史事故(2026-09-21 helloCard):
|
|
149
|
+
* 旧实现 `path.basename(ref)` 后再拼 `<名>.png` 找一个扁平文件,而 MeaXure 导出把切图放在
|
|
150
|
+
* `slices/<图层分组路径>/<名字>@3x.png` —— 于是**磁盘上明明存在**的资产被判成 `missing-file`,
|
|
151
|
+
* 下游要么白重裁一遍(丢原图品质),要么模型改用设计稿图层名当引用、浏览器 404(图片空白零报错)。
|
|
152
|
+
*
|
|
153
|
+
* 候选优先级:完整 ref → basename → manifest 别名;每层再补 `@3x/@2x` 与常见扩展名。
|
|
154
|
+
* 循环以「名字」为外层、「目录」为内层:**精确 ref 命中优先于模糊命中**,
|
|
155
|
+
* 避免 bundle 里的 `@3x` 猜中而把工程里真名同名的扁平切图挤掉。
|
|
156
|
+
*/
|
|
157
|
+
function resolveSliceFile(dirs, ref, aliases = null) {
|
|
158
|
+
const raw = String(ref || '').replace(/\\/g, '/').replace(/^\/+/, '').trim()
|
|
159
|
+
if (!raw) return null
|
|
160
|
+
const base = path.basename(raw)
|
|
161
|
+
const dirPart = path.dirname(raw) === '.' ? '' : path.dirname(raw)
|
|
162
|
+
const stripExt = (s) => (path.extname(s) ? s.slice(0, -path.extname(s).length) : s)
|
|
163
|
+
const alias = aliases && (aliases.get(raw) || aliases.get(base) || aliases.get(stripExt(base)))
|
|
164
|
+
const rels = [...new Set([raw, base, ...(alias ? [alias, path.basename(alias)] : [])].filter(Boolean))]
|
|
165
|
+
const names = []
|
|
166
|
+
for (const rel of rels) {
|
|
167
|
+
const ext = path.extname(rel)
|
|
168
|
+
const core = stripExt(rel)
|
|
169
|
+
const exts = ext ? [ext] : ['.png', '.jpg', '.jpeg', '.webp', '.svg']
|
|
170
|
+
for (const e of exts) names.push(`${core}${e}`)
|
|
171
|
+
// 倍图后缀:MeaXure 常态是 `<名>@3x.png`,仅当引用本身没带 @Nx 时补
|
|
172
|
+
if (!/@\dx$/i.test(core)) {
|
|
173
|
+
for (const e of exts) names.push(`${core}@3x${e}`, `${core}@2x${e}`)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
for (const nm of names) {
|
|
177
|
+
for (const dir of dirs) {
|
|
178
|
+
const f = path.join(dir, nm)
|
|
179
|
+
if (fs.existsSync(f)) return f
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// 兜底:ref 带子目录但导出时把目录压平了(少数导出脚本行为),按 basename 在子目录里搜一层
|
|
183
|
+
if (dirPart) {
|
|
184
|
+
for (const dir of dirs) {
|
|
185
|
+
const sub = path.join(dir, dirPart)
|
|
186
|
+
if (!fs.existsSync(sub)) continue
|
|
187
|
+
try {
|
|
188
|
+
for (const f of fs.readdirSync(sub)) {
|
|
189
|
+
if (stripExt(f) === stripExt(base)) return path.join(sub, f)
|
|
190
|
+
}
|
|
191
|
+
} catch {}
|
|
138
192
|
}
|
|
139
193
|
}
|
|
140
194
|
return null
|
|
@@ -210,21 +264,40 @@ function main() {
|
|
|
210
264
|
})
|
|
211
265
|
} else if (n.type === 'slice') {
|
|
212
266
|
const raw = typeof n.slices === 'string' ? n.slices : (n.slices && (n.slices.name || n.slices.path)) || n.name || ''
|
|
213
|
-
|
|
267
|
+
// ref 保留**完整**引用(含 `8.icon/通用/` 这类分组路径)——basename 会让存在性判定失准;
|
|
268
|
+
// name 给的是**消毒后的落盘名**,模型必须拿它写 /slices 引用(含 `#`/`%`/`&` 会被浏览器截断)。
|
|
269
|
+
const ref = String(raw).replace(/\\/g, '/').replace(/^\/+/, '').trim()
|
|
270
|
+
sliceEls.push({ ref, name: sanitizeSliceFileName(path.basename(ref)), rect })
|
|
214
271
|
} else if (n.type === 'shape') {
|
|
215
272
|
const st = n.style || {}
|
|
216
|
-
const
|
|
217
|
-
|
|
273
|
+
const fill = st.fill || {}
|
|
274
|
+
const background = st.background || {}
|
|
275
|
+
const bd = st.border || {}
|
|
276
|
+
shapes.push({
|
|
277
|
+
...rect,
|
|
278
|
+
// background 优先(与设计工具的语义一致:background = 容器底色,fill = 形状填色)
|
|
279
|
+
bg: background.color || fill.color || null,
|
|
280
|
+
// 渐变层必须显式带出来。只给 bg:null 时模型看到的是「一个什么都没有的空节点」,
|
|
281
|
+
// 于是渐变底板 / 顶部渐变区会被整块丢掉(helloCard 实测丢 3 处:顶部 343×154 + 两块 311×62 底板)。
|
|
282
|
+
// spec 里 gradient 已经是 CSS 串,原样透传(与 expand_ir 的 backgroundGradient 同格式)。
|
|
283
|
+
gradient: background.gradient || fill.gradient || null,
|
|
284
|
+
bgImage: background.image || fill.image || null,
|
|
285
|
+
// 描边同理:红标签的 0.5px #F6434F 边框不报出来,模型就只会写一个裸 Text。
|
|
286
|
+
border: bd.isBorder ? { color: bd.color || null, width: bd.width ?? null } : null,
|
|
287
|
+
radius: st.radius ?? null,
|
|
288
|
+
opacity: st.opacity ?? 1,
|
|
289
|
+
})
|
|
218
290
|
}
|
|
219
291
|
;(n.children || []).forEach(walk)
|
|
220
292
|
}
|
|
221
293
|
;(card.elements || []).forEach(walk)
|
|
222
294
|
|
|
223
295
|
// 切图清单:存在性 / 复用 / 烘焙内容 / 复用但内容不同
|
|
224
|
-
|
|
296
|
+
// 按**完整 ref** 分组(不是 basename):分组路径是图层身份的一部分,两个不同分组的同名图层是两张图。
|
|
297
|
+
const byRef = new Map()
|
|
225
298
|
for (const el of sliceEls) {
|
|
226
|
-
if (!
|
|
227
|
-
|
|
299
|
+
if (!byRef.has(el.ref)) byRef.set(el.ref, [])
|
|
300
|
+
byRef.get(el.ref).push(el.rect)
|
|
228
301
|
}
|
|
229
302
|
const slicesDir = args.slicesDir || path.join(path.dirname(args.spec), 'slices')
|
|
230
303
|
// 派生切图的唯一落点是 src/slices/:已派生过的缺口不应再报 missing(否则每轮都建议重裁)
|
|
@@ -244,7 +317,8 @@ function main() {
|
|
|
244
317
|
})
|
|
245
318
|
return groups
|
|
246
319
|
}
|
|
247
|
-
/**
|
|
320
|
+
/**
|
|
321
|
+
* 设计稿矩形区域 ↔ 既有切图的像素级判重(sha 快路径 + 容差兜底,slice_pixels.mjs)。
|
|
248
322
|
* 覆盖「共用切图已语义重命名 / 在别的画板已派生过」——名字对不上也能认出来。 */
|
|
249
323
|
const regionTwin = (rect) => {
|
|
250
324
|
if (!design) return null
|
|
@@ -255,11 +329,15 @@ function main() {
|
|
|
255
329
|
}
|
|
256
330
|
return null
|
|
257
331
|
}
|
|
258
|
-
for (const [
|
|
259
|
-
|
|
332
|
+
for (const [ref, rects] of byRef) {
|
|
333
|
+
// `ref` = 设计稿里的完整图层引用(含分组路径);`name` = 消毒后的落盘/引用名。
|
|
334
|
+
// 两者都报出来:模型按 name 写 /slices 引用,按 ref 回查设计稿图层。
|
|
335
|
+
const originalName = path.basename(ref)
|
|
336
|
+
const name = sanitizeSliceFileName(originalName)
|
|
337
|
+
const stem = path.extname(name) ? name.slice(0, -path.extname(name).length) : name
|
|
338
|
+
let file = resolveSliceFile(sliceDirs, ref, aliases)
|
|
260
339
|
let exists = !!file
|
|
261
340
|
let info = exists ? analyzeSlice(file) : null
|
|
262
|
-
const logical = info ? [Math.round(info.px[0] / (info.px[0] / rects[0].w)), Math.round(info.px[1] / (info.px[1] / rects[0].h))] : null
|
|
263
341
|
let verdict = exists ? 'full-asset' : 'missing-file'
|
|
264
342
|
let reuseOf = null
|
|
265
343
|
let crops = null
|
|
@@ -274,17 +352,16 @@ function main() {
|
|
|
274
352
|
file = path.join(twin0.dir, twin0.twin)
|
|
275
353
|
info = analyzeSlice(file)
|
|
276
354
|
} else {
|
|
277
|
-
const base0 = path.parse(name).name
|
|
278
355
|
crops = []
|
|
279
356
|
groups.forEach((g, gi) => {
|
|
280
357
|
const rep = rects[g.idxs[0]]
|
|
281
|
-
const cn = gi === 0 ?
|
|
358
|
+
const cn = sanitizeSliceFileName(gi === 0 ? stem : `${stem}_alt${gi + 1}`)
|
|
282
359
|
const twin = gi === 0 ? twin0 : regionTwin(rep)
|
|
283
360
|
const entry = { name: cn, exists: !!resolveSliceFile(sliceDirs, cn, aliases), ...rep }
|
|
284
361
|
if (g.idxs.length > 1) entry.alsoRects = g.idxs.slice(1).map((i) => rects[i])
|
|
285
362
|
if (twin) { entry.exists = true; entry.reuseOf = twin.twin; entry.from = path.basename(twin.dir) }
|
|
286
363
|
crops.push(entry)
|
|
287
|
-
if (!entry.exists) missing.push({ reason: 'slice-file-missing', name, crop: entry })
|
|
364
|
+
if (!entry.exists) missing.push({ reason: 'slice-file-missing', name, originalName, crop: entry })
|
|
288
365
|
})
|
|
289
366
|
if (twin0) { verdict = 'content-twin-partial'; reuseOf = twin0.twin }
|
|
290
367
|
}
|
|
@@ -299,11 +376,10 @@ function main() {
|
|
|
299
376
|
// 历史事故②(2026-09-18 ynet-card):旧实现只跟 rects[0] 比指纹、每矩形一个 _altN,
|
|
300
377
|
// 内容相同的 5 个矩形裁出 5 份字节级重复副本;且不做既有切图判重,共用图标已重命名
|
|
301
378
|
// 落地仍被重裁。现按指纹分组 + 像素级复用根治。
|
|
302
|
-
const baseN = path.parse(name).name
|
|
303
379
|
crops = []
|
|
304
380
|
for (let gi = 1; gi < groups.length; gi++) {
|
|
305
381
|
const rep = rects[groups[gi].idxs[0]]
|
|
306
|
-
const cn = `${
|
|
382
|
+
const cn = sanitizeSliceFileName(`${stem}_alt${gi + 1}`)
|
|
307
383
|
const entry = { name: cn, exists: !!resolveSliceFile(sliceDirs, cn, aliases), ...rep }
|
|
308
384
|
if (groups[gi].idxs.length > 1) entry.alsoRects = groups[gi].idxs.slice(1).map((i) => rects[i])
|
|
309
385
|
if (!entry.exists) {
|
|
@@ -311,13 +387,19 @@ function main() {
|
|
|
311
387
|
if (twin) { entry.exists = true; entry.reuseOf = twin.twin; entry.from = path.basename(twin.dir) }
|
|
312
388
|
}
|
|
313
389
|
crops.push(entry)
|
|
314
|
-
if (!entry.exists) missing.push({ reason: 'reused-slice-content-differs', name, crop: entry })
|
|
390
|
+
if (!entry.exists) missing.push({ reason: 'reused-slice-content-differs', name, originalName, crop: entry })
|
|
315
391
|
}
|
|
316
392
|
}
|
|
317
393
|
}
|
|
318
394
|
slices.push({
|
|
319
395
|
name,
|
|
396
|
+
// ref = 设计稿图层完整引用;引用名与设计稿图层名不一致时模型才查得回去
|
|
397
|
+
ref,
|
|
398
|
+
...(name !== originalName ? { originalName } : {}),
|
|
320
399
|
exists,
|
|
400
|
+
// 解析到的实际文件路径:crop_slices --from-audit 据此把「审计判定存在」的资产
|
|
401
|
+
// 按消毒名搬进工程 src/slices/(否则模型拿着正确名字仍然 404 —— 图片空白零报错)。
|
|
402
|
+
file: file ? relOrAbs(file) : null,
|
|
321
403
|
from: file ? path.basename(path.dirname(file)) : null,
|
|
322
404
|
reuseOf,
|
|
323
405
|
rects,
|
|
@@ -532,18 +614,48 @@ function main() {
|
|
|
532
614
|
bindHints,
|
|
533
615
|
listCandidates,
|
|
534
616
|
}
|
|
535
|
-
|
|
617
|
+
// 默认落盘:审计是「整份复用」的产物(crop_slices --from-audit 直接吃),
|
|
618
|
+
// 靠模型每次记得加 --save 是不可靠的;不给路径就落到工程根 .cache/spec-audit/ 下。
|
|
619
|
+
// 默认路径要在工程根下(cwd 即工程根),别按脚本位置推。
|
|
620
|
+
const savePath = args.noSave ? '' : args.save || defaultAuditPath(args.spec, args.card)
|
|
621
|
+
if (savePath) {
|
|
536
622
|
try {
|
|
537
|
-
fs.mkdirSync(path.dirname(path.resolve(
|
|
538
|
-
fs.writeFileSync(
|
|
539
|
-
brief.savedTo =
|
|
540
|
-
console.error(`[spec_audit] 审计已落盘 ${
|
|
623
|
+
fs.mkdirSync(path.dirname(path.resolve(savePath)), { recursive: true })
|
|
624
|
+
fs.writeFileSync(savePath, JSON.stringify(brief, null, 2) + '\n')
|
|
625
|
+
brief.savedTo = savePath
|
|
626
|
+
console.error(`[spec_audit] 审计已落盘 ${savePath}(crop_slices --from-audit 直接用它,禁止手抄摘要)`)
|
|
541
627
|
} catch (e) {
|
|
542
|
-
console.error(`[spec_audit]
|
|
628
|
+
console.error(`[spec_audit] 落盘失败:${e.message}`)
|
|
543
629
|
}
|
|
544
630
|
}
|
|
545
631
|
// 紧凑输出:审计简报的读者是模型,缩进纯浪费 token(也更容易整份进 12000 字回喂窗口)
|
|
546
|
-
|
|
632
|
+
const compact = JSON.stringify(brief)
|
|
633
|
+
if (compact.length <= STDOUT_MAX_CHARS) {
|
|
634
|
+
console.log(compact)
|
|
635
|
+
} else {
|
|
636
|
+
// 超长时改出摘要:**每一个顶层键都在、可决策的信息一个不少**,只把「按位置复述内容」的
|
|
637
|
+
// bands[].items(它是 texts/slices 的重排,纯冗余)折成计数,完整版指向 savedTo。
|
|
638
|
+
// 为什么不做「截断 stdout」:截断会把 slices/missing 这类靠后的关键字段整段切掉,
|
|
639
|
+
// 模型既不知道少了什么、又会为了补读再跑一遍 40s 的解析。
|
|
640
|
+
const digest = {
|
|
641
|
+
...brief,
|
|
642
|
+
bands: (brief.bands || []).map((b) => ({ y: b.y, h: b.h, itemCount: (b.items || []).length })),
|
|
643
|
+
stdoutDigest: true,
|
|
644
|
+
note: `stdout 已折成摘要(完整审计 ${compact.length} 字在 ${brief.savedTo || '(本次未落盘)'}):bands[].items 只是 texts/slices 的位置重排,需要时用 fs_read 读 savedTo;crop_slices --from-audit 一律用 savedTo,禁止手抄。`,
|
|
645
|
+
}
|
|
646
|
+
console.log(JSON.stringify(digest))
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/** stdout 预算:审计简报要喂给模型,超过这个长度就折摘要(完整版落盘) */
|
|
651
|
+
const STDOUT_MAX_CHARS = 9000
|
|
652
|
+
|
|
653
|
+
/** 默认落盘路径:工程根 .cache/spec-audit/<要素包目录名>-card<N>.json(cwd 即工程根)。
|
|
654
|
+
* 用目录名而不是文件名:所有要素包里的文件都叫 spec.json,只取 basename 会互相覆盖。 */
|
|
655
|
+
function defaultAuditPath(spec, card) {
|
|
656
|
+
const dir = path.basename(path.dirname(path.resolve(String(spec))))
|
|
657
|
+
const base = dir || path.basename(String(spec)).replace(/\.json$/i, '') || 'spec'
|
|
658
|
+
return path.join('.cache', 'spec-audit', `${base}-card${card}.json`)
|
|
547
659
|
}
|
|
548
660
|
|
|
549
661
|
main()
|