cardbuddy 0.16.0 → 0.17.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/init.js +0 -2
- package/dist/preview-editor/assets/index-CXDf5tVg.css +1 -0
- package/dist/preview-editor/assets/index-DLQkCPLT.js +337 -0
- package/dist/preview-editor/index.html +2 -2
- package/dist/vendored/server/core/browser.js +17 -0
- package/dist/vendored/server/core/cardFiles.js +49 -1
- package/dist/vendored/server/core/cardImage.js +103 -2
- package/dist/vendored/server/core/screenshot.js +94 -0
- package/dist/vendored/skills/card-skill-trainer/SKILL.md +7 -7
- package/dist/vendored/skills/card-skill-trainer/scripts/plan_next_round.mjs +1 -1
- package/dist/vendored/skills/card-skill-trainer/scripts/run_round.mjs +7 -2
- package/dist/vendored/skills/dsl-card-generator/references/component_rules.md +14 -0
- package/dist/vendored/skills/dsl-card-generator/references/generation_pitfalls.md +48 -0
- package/dist/vendored/skills/dsl-card-generator/references/previewer_contract.md +21 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/scaffold.mjs +48 -1
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/v3_assemble.mjs +41 -4
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/v3_extract.mjs +57 -6
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/v3_shared.mjs +17 -3
- package/dist/vendored/skills/dsl-card-generator/scripts/v3_regression.mjs +8 -1
- package/dist/vendored/skills/finmall-card-sync/scripts/api_batch_sync.mjs +46 -2
- package/dist/vendored/skills/mbank-design-gen/SKILL.md +76 -0
- package/dist/vendored/skills/mbank-design-gen/assets/icons/icon-apps.png +0 -0
- package/dist/vendored/skills/mbank-design-gen/assets/icons/icon-arrow-right-blue.png +0 -0
- package/dist/vendored/skills/mbank-design-gen/assets/icons/icon-check.png +0 -0
- package/dist/vendored/skills/mbank-design-gen/assets/icons/icon-settings.png +0 -0
- package/dist/vendored/skills/mbank-design-gen/assets/icons/icon-sound.png +0 -0
- package/dist/vendored/skills/mbank-design-gen/assets/icons/icon-voice-switch.png +0 -0
- package/dist/vendored/skills/mbank-design-gen/assets/icons/progress-thumb.png +0 -0
- package/dist/vendored/skills/mbank-design-gen/assets/icons/tag-bg.png +0 -0
- package/dist/vendored/skills/mbank-design-gen/assets/icons/vs-badge.png +0 -0
- package/dist/vendored/skills/mbank-design-gen/assets/ref/ref-chat-options.png +0 -0
- package/dist/vendored/skills/mbank-design-gen/assets/ref/ref-home-scene.png +0 -0
- package/dist/vendored/skills/mbank-design-gen/assets/ref/ref-product-detail.png +0 -0
- package/dist/vendored/skills/mbank-design-gen/assets/ref/ref-rate-table.png +0 -0
- package/dist/vendored/skills/mbank-design-gen/assets/robot.png +0 -0
- package/dist/vendored/skills/mbank-design-gen/references/component-library.md +127 -0
- package/dist/vendored/skills/mbank-design-gen/references/design-spec.md +261 -0
- package/dist/vendored/skills/mbank-design-gen/templates/mockup-template.html +138 -0
- package/dist/vendored/skills/mbank-design-gen/tools/screenshot.mjs +34 -0
- package/dist/vendored/skills/shared/spec-contract.md +10 -1
- package/dist/vendored/skills/sketch-spec-to-card/SKILL.md +1 -1
- package/dist/vendored/skills/sketch-spec-to-card/scripts/parse_meaxure.py +30 -0
- package/dist/vendored/skills/sketch-spec-to-card/scripts/test_text_only_artboard.py +25 -3
- package/dist/vendored/skills/sketch-spec-to-card/scripts/vision.py +15 -3
- package/dist/vendored/templates/helloCard/schema.json +62 -1
- package/dist/vendored/tools/finmall-env.mjs +1 -18
- package/package.json +1 -1
- package/dist/preview-editor/assets/index-BkbBxDTT.css +0 -1
- package/dist/preview-editor/assets/index-Bro07DTa.js +0 -313
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
<link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon-16-Cg6Nrosk.png" />
|
|
9
9
|
<link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon-32-B_1744sp.png" />
|
|
10
10
|
<link rel="apple-touch-icon" sizes="180x180" href="/assets/apple-touch-icon-BQcES9QK.png" />
|
|
11
|
-
<script type="module" crossorigin src="/assets/index-
|
|
12
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
11
|
+
<script type="module" crossorigin src="/assets/index-DLQkCPLT.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CXDf5tVg.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
|
15
15
|
<div id="app"></div>
|
|
@@ -49,6 +49,22 @@ async function getBrowser() {
|
|
|
49
49
|
return browserPromise
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/** 立即关闭单例浏览器并复位(测试 / 脚本收尾用;生产路径靠空闲 5 分钟自动关) */
|
|
53
|
+
async function closeBrowser() {
|
|
54
|
+
if (idleTimer) {
|
|
55
|
+
clearTimeout(idleTimer)
|
|
56
|
+
idleTimer = null
|
|
57
|
+
}
|
|
58
|
+
const p = browserPromise
|
|
59
|
+
browserPromise = null
|
|
60
|
+
if (p) {
|
|
61
|
+
try {
|
|
62
|
+
const b = await p
|
|
63
|
+
if (b && typeof b.close === 'function') await b.close()
|
|
64
|
+
} catch (_) { /* 关闭失败忽略 */ }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
52
68
|
function resetIdleTimer() {
|
|
53
69
|
if (idleTimer) clearTimeout(idleTimer)
|
|
54
70
|
idleTimer = setTimeout(async () => {
|
|
@@ -112,6 +128,7 @@ export {
|
|
|
112
128
|
NAV_TIMEOUT_MS,
|
|
113
129
|
SETTLE_MS,
|
|
114
130
|
getBrowser,
|
|
131
|
+
closeBrowser,
|
|
115
132
|
serialize,
|
|
116
133
|
openCardScreen,
|
|
117
134
|
}
|
|
@@ -8,6 +8,54 @@ import { deleteJournal, turnsDir } from './turnJournal.js'
|
|
|
8
8
|
import { log } from './logger.js'
|
|
9
9
|
import { buildDataFromSchema } from './buildDataFromSchema.js'
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* 空白卡根样式:margin / padding / gap 显式归零(而非「未设置」)。
|
|
13
|
+
* 渲染器对无显式样式的容器会注入默认值(padding:2px / gap:4px / justify-content:center),
|
|
14
|
+
* 空白卡必须显式归零压掉注入,否则新卡自带隐性间距且属性面板显示「未设置」。
|
|
15
|
+
* 形状取自存量卡根节点(见 style-meta.json:marginType / paddingType,'0' = 全边统一值)。
|
|
16
|
+
* 同一形状还有两份内联副本(禁跨包 import):skills/dsl-card-generator lib/scaffold.mjs、
|
|
17
|
+
* skills/finmall-card-sync api_batch_sync.mjs 的 buildBlankSchema —— 改这里必须同步改那两处。
|
|
18
|
+
*/
|
|
19
|
+
function blankResetStyle() {
|
|
20
|
+
return {
|
|
21
|
+
properties: [
|
|
22
|
+
{
|
|
23
|
+
itemId: '100',
|
|
24
|
+
itemKey: 'flex',
|
|
25
|
+
itemValueFrom: '0',
|
|
26
|
+
itemValue: {},
|
|
27
|
+
itemChildren: {
|
|
28
|
+
properties: [{ itemId: '003', itemKey: 'gap', itemValueFrom: '0', itemValue: '0' }],
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
itemId: '400',
|
|
33
|
+
itemKey: 'padding',
|
|
34
|
+
itemValueFrom: '0',
|
|
35
|
+
itemValue: {},
|
|
36
|
+
itemChildren: {
|
|
37
|
+
properties: [
|
|
38
|
+
{ itemId: '001', itemKey: 'paddingType', itemValueFrom: '0', itemValue: '0' },
|
|
39
|
+
{ itemId: '002', itemKey: 'padding', itemValueFrom: '0', itemValue: '0' },
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
itemId: '500',
|
|
45
|
+
itemKey: 'margin',
|
|
46
|
+
itemValueFrom: '0',
|
|
47
|
+
itemValue: {},
|
|
48
|
+
itemChildren: {
|
|
49
|
+
properties: [
|
|
50
|
+
{ itemId: '001', itemKey: 'marginType', itemValueFrom: '0', itemValue: '0' },
|
|
51
|
+
{ itemId: '002', itemKey: 'margin', itemValueFrom: '0', itemValue: '0' },
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
11
59
|
/**
|
|
12
60
|
* 单张卡片 / 卡片列表的按需读取 / 写回服务端处理(dev 下实时读磁盘,无缓存)。
|
|
13
61
|
* - GET /__cards :实时扫描 src/cards/* 目录,返回卡片列表 [{id,name,pageId}]。
|
|
@@ -344,7 +392,7 @@ export function createCardFilesMiddleware({ root, notify = null }) {
|
|
|
344
392
|
view: {
|
|
345
393
|
width: '375',
|
|
346
394
|
className: 'Root',
|
|
347
|
-
style:
|
|
395
|
+
style: blankResetStyle(),
|
|
348
396
|
properties: [],
|
|
349
397
|
eventactions: [],
|
|
350
398
|
components: [],
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
-
import { takeScreenshot } from './screenshot.js'
|
|
3
|
+
import { takeScreenshot, generateCardCover } from './screenshot.js'
|
|
4
|
+
import { serialize, MAX_QUEUED } from './browser.js'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* 卡片预览图本地静态服务:把卡片目录下的「同名预览图」映射为
|
|
@@ -14,6 +15,15 @@ import { takeScreenshot } from './screenshot.js'
|
|
|
14
15
|
* 若卡片目录没有同名预览图,则回退到实时无头截图(takeScreenshot,复用 /__screenshot
|
|
15
16
|
* 同一套渲染内核),保证任何卡片都能出预览。截图失败(如缺 Chrome)则 404,
|
|
16
17
|
* 由调用方回退占位。仅服务 src/cards 下的卡片预览图,禁止目录穿越。
|
|
18
|
+
*
|
|
19
|
+
* POST /__card-image/<id>:为该卡生成封面预览图并写盘(顶栏「预览图」按钮触发)。
|
|
20
|
+
* 无头渲染磁盘态 schema → 透明底、与卡片实际渲染尺寸 1:1 的 PNG → 覆盖写 src/cards/<id>/<id>.png。
|
|
21
|
+
* 用于手动搭建 / 生成卡片后补齐封面:省去手工截图 + 裁切。约定:
|
|
22
|
+
* - 未保存的编辑器改动不参与渲染,前端负责先落盘再调用;
|
|
23
|
+
* - 同 id 其它扩展名的旧预览图(jpg/webp 等)一并清除,保证「同名预览图」单一来源
|
|
24
|
+
* (GET 按 readdir 顺序取第一个同名位图,双封面并存会让新 PNG 输给陈旧旧图);
|
|
25
|
+
* - 空白卡(schema 无 components)拒绝生成(422),与 GET 回退 404 的语义一致;
|
|
26
|
+
* - 原子写(临时文件 + rename),并发读取不会拿到半截 PNG。
|
|
17
27
|
*/
|
|
18
28
|
|
|
19
29
|
const MIME = {
|
|
@@ -46,6 +56,98 @@ export function createCardImageMiddleware({ root }) {
|
|
|
46
56
|
if (!dir.startsWith(cardsRoot + path.sep) && dir !== cardsRoot) return next()
|
|
47
57
|
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) return next()
|
|
48
58
|
|
|
59
|
+
// POST:生成封面预览图并覆盖写 <id>.png(顶栏「预览图」按钮触发,见文件头注释)
|
|
60
|
+
if ((req.method || 'GET').toUpperCase() === 'POST') {
|
|
61
|
+
// 空白卡(schema 无 components)截出来是无意义纯透明图:直接拒绝
|
|
62
|
+
let cardWidth = 375
|
|
63
|
+
try {
|
|
64
|
+
const schema = JSON.parse(fs.readFileSync(path.join(dir, 'schema.json'), 'utf8'))
|
|
65
|
+
const comps = schema && schema.view && schema.view.components
|
|
66
|
+
if (Array.isArray(comps) && !comps.length) {
|
|
67
|
+
res.statusCode = 422
|
|
68
|
+
res.setHeader('Content-Type', 'application/json')
|
|
69
|
+
return res.end(
|
|
70
|
+
JSON.stringify({ error: 'empty-card', message: '空白卡(无组件)没有可预览的内容,不生成预览图' }),
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
const w = parseInt((schema && schema.view && schema.view.width) || '', 10)
|
|
74
|
+
if (w > 0) cardWidth = w
|
|
75
|
+
} catch {
|
|
76
|
+
/* schema 缺失 / 损坏:按默认卡宽走,渲染失败会有结构化报错 */
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const shot = await serialize(() => generateCardCover(id, { width: cardWidth }))
|
|
80
|
+
if (shot.tooBusy) {
|
|
81
|
+
res.statusCode = 429
|
|
82
|
+
res.setHeader('Content-Type', 'application/json')
|
|
83
|
+
return res.end(
|
|
84
|
+
JSON.stringify({ error: 'too-busy', message: '截图队列已满(并发上限 ' + MAX_QUEUED + '),请稍后重试' }),
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
if (!shot.buffer) {
|
|
88
|
+
res.statusCode = shot.code || 503
|
|
89
|
+
res.setHeader('Content-Type', 'application/json')
|
|
90
|
+
return res.end(JSON.stringify({ error: shot.error, message: shot.message, fix: shot.fix }))
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const file = path.join(dir, id + '.png')
|
|
94
|
+
const overwritten = fs.existsSync(file)
|
|
95
|
+
// 原子写:先落临时文件再 rename,并发读取(网格缩略图)不会拿到半截 PNG
|
|
96
|
+
const tmp = file + '.tmp'
|
|
97
|
+
fs.writeFileSync(tmp, shot.buffer)
|
|
98
|
+
fs.renameSync(tmp, file)
|
|
99
|
+
// 清掉同 id 其它扩展名的旧预览图:GET 按 readdir 顺序取第一个同名位图,
|
|
100
|
+
// 双封面并存会让「刚生成的 PNG」可能输给陈旧的 jpg/webp(单一来源纪律)
|
|
101
|
+
for (const entry of fs.readdirSync(dir)) {
|
|
102
|
+
const dot = entry.lastIndexOf('.')
|
|
103
|
+
if (dot < 0) continue
|
|
104
|
+
const base = entry.slice(0, dot)
|
|
105
|
+
const ext = entry.slice(dot).toLowerCase()
|
|
106
|
+
if (base.toLowerCase() === id.toLowerCase() && MIME[ext] && entry !== id + '.png') {
|
|
107
|
+
try {
|
|
108
|
+
fs.unlinkSync(path.join(dir, entry))
|
|
109
|
+
} catch {
|
|
110
|
+
/* 清理失败不阻塞:多一张旧图只是回退序靠前,不影响本次交付 */
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
res.statusCode = 200
|
|
116
|
+
res.setHeader('Content-Type', 'application/json')
|
|
117
|
+
res.setHeader('Cache-Control', 'no-store')
|
|
118
|
+
return res.end(
|
|
119
|
+
JSON.stringify({
|
|
120
|
+
ok: true,
|
|
121
|
+
id,
|
|
122
|
+
width: shot.width,
|
|
123
|
+
height: shot.height,
|
|
124
|
+
file: 'src/cards/' + id + '/' + id + '.png',
|
|
125
|
+
overwritten,
|
|
126
|
+
}),
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// GET ?probe=1:轻量探测「同名预览图是否已存在」——只 readdir、命中即返回,
|
|
131
|
+
// 不触发回退截图。供顶栏「截图」按钮在覆盖已有封面之前弹二次确认。
|
|
132
|
+
const q = new URL(req.url, 'http://localhost')
|
|
133
|
+
if (q.searchParams.get('probe') === '1') {
|
|
134
|
+
let found = null
|
|
135
|
+
for (const entry of fs.readdirSync(dir)) {
|
|
136
|
+
const dot = entry.lastIndexOf('.')
|
|
137
|
+
if (dot < 0) continue
|
|
138
|
+
const base = entry.slice(0, dot)
|
|
139
|
+
const ext = entry.slice(dot).toLowerCase()
|
|
140
|
+
if (base.toLowerCase() === id.toLowerCase() && MIME[ext]) {
|
|
141
|
+
found = 'src/cards/' + id + '/' + entry
|
|
142
|
+
break
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
res.statusCode = 200
|
|
146
|
+
res.setHeader('Content-Type', 'application/json')
|
|
147
|
+
res.setHeader('Cache-Control', 'no-store')
|
|
148
|
+
return res.end(JSON.stringify({ exists: !!found, file: found }))
|
|
149
|
+
}
|
|
150
|
+
|
|
49
151
|
// 取与 id 同名的位图(大小写不敏感,扩展名见 MIME)
|
|
50
152
|
let file = null
|
|
51
153
|
for (const entry of fs.readdirSync(dir)) {
|
|
@@ -84,7 +186,6 @@ export function createCardImageMiddleware({ root }) {
|
|
|
84
186
|
|
|
85
187
|
// 无同名预览图:回退实时无头截图(复用 /__screenshot 渲染内核)。
|
|
86
188
|
// width 由查询参数决定(默认 390),与 /__screenshot 一致。
|
|
87
|
-
const q = new URL(req.url, 'http://localhost')
|
|
88
189
|
const width = parseInt(q.searchParams.get('width') || '390', 10) || 390
|
|
89
190
|
const shot = await takeScreenshot(id, width, 'png')
|
|
90
191
|
if (!shot || !shot.buffer) {
|
|
@@ -12,6 +12,25 @@ import { getBrowser, serialize, openCardScreen, MAX_QUEUED } from './browser.js'
|
|
|
12
12
|
* 浏览器单例/页面加载逻辑已抽到 ./browser.js,与 /__bbox 共用同一实例。
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* 压掉编辑器壳层的悬浮物(AI FAB / toast / 抽屉面板 / 右键菜单等 position:fixed 元素)。
|
|
17
|
+
* 元素截图(ElementHandle.screenshot)截的是页面合成结果:任何叠在目标区域上的
|
|
18
|
+
* fixed 悬浮物都会被一起画进去 —— 「封面截图把 ai-fab 圆钮截进去」事故的根因。
|
|
19
|
+
* 按 computed position 判定而非类名清单:悬浮组件会不断增加,清单式每加一个就漏一个。
|
|
20
|
+
* 卡片自身(.ce_root 子树内)的 fixed 定位属于卡片设计,保留。页面随后即 close,无需恢复。
|
|
21
|
+
* (导出仅为测试可断言其 DOM 语义。)
|
|
22
|
+
*/
|
|
23
|
+
export async function hideFloatingOverlays(page) {
|
|
24
|
+
await page.evaluate(() => {
|
|
25
|
+
document.querySelectorAll('body *').forEach((el) => {
|
|
26
|
+
if (el.closest('.ce_root')) return
|
|
27
|
+
if (getComputedStyle(el).position === 'fixed') {
|
|
28
|
+
el.style.setProperty('visibility', 'hidden', 'important')
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
15
34
|
/**
|
|
16
35
|
* 对单卡截图。纯函数、无 HTTP。任何失败都返回 { error, code, message, ... } 而非抛出。
|
|
17
36
|
* @param {string} id 卡片 id
|
|
@@ -42,6 +61,7 @@ export async function takeScreenshot(id, width, format) {
|
|
|
42
61
|
if (!screen) {
|
|
43
62
|
return { error: 'capture-target-not-found', code: 500, message: '.phone__screen 元素未找到' }
|
|
44
63
|
}
|
|
64
|
+
await hideFloatingOverlays(page)
|
|
45
65
|
|
|
46
66
|
const buf = await screen.screenshot({ type: 'png' })
|
|
47
67
|
if (format === 'json') {
|
|
@@ -76,6 +96,80 @@ export async function takeScreenshot(id, width, format) {
|
|
|
76
96
|
}
|
|
77
97
|
}
|
|
78
98
|
|
|
99
|
+
/**
|
|
100
|
+
* 生成卡片封面预览图:透明底(不含编辑器外壳背景色)、尺寸 = 卡片实际渲染尺寸 1:1。
|
|
101
|
+
* 与 takeScreenshot 的三点差异:
|
|
102
|
+
* - deviceScaleFactor:1 —— 截图像素 = CSS 像素(takeScreenshot 是 2 倍视网膜);
|
|
103
|
+
* - 截图目标是卡片根元素 .ce_root —— .phone__screen 是固定高度的手机屏滚动容器,
|
|
104
|
+
* 直截会带大片空白;.ce_root 高度即内容实际高度;
|
|
105
|
+
* - omitBackground + 透明化外壳底色(chrome=off 下手机屏固定挂 --bg-white),
|
|
106
|
+
* 产出透明底 PNG;卡片自身根节点的背景 / 渐变属于卡片设计,保留。
|
|
107
|
+
* 纯函数、无 HTTP、不写盘:返回 { buffer, width, height },落盘由调用方(/__card-image POST)负责。
|
|
108
|
+
* 任何失败返回 { error, code, message, fix? } 结构化结果,不抛出(与 takeScreenshot 同约定)。
|
|
109
|
+
*/
|
|
110
|
+
export async function generateCardCover(id, { width: cardWidth = 375 } = {}) {
|
|
111
|
+
let browser
|
|
112
|
+
try {
|
|
113
|
+
browser = await getBrowser()
|
|
114
|
+
} catch (e) {
|
|
115
|
+
return {
|
|
116
|
+
error: 'browser-launch-failed',
|
|
117
|
+
code: 503,
|
|
118
|
+
message: String((e && e.message) || e),
|
|
119
|
+
fix: '系统 Chrome 未找到或未启动。请安装 Google Chrome,并确保 playwright 以 channel:"chrome" 启动(本端点固定使用该通道)。',
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let context
|
|
124
|
+
try {
|
|
125
|
+
context = await browser.newContext({
|
|
126
|
+
viewport: { width: cardWidth + 80, height: 900 },
|
|
127
|
+
deviceScaleFactor: 1,
|
|
128
|
+
})
|
|
129
|
+
const { page, screen } = await openCardScreen(context, id)
|
|
130
|
+
try {
|
|
131
|
+
if (!screen) {
|
|
132
|
+
return { error: 'capture-target-not-found', code: 500, message: '.phone__screen 元素未找到' }
|
|
133
|
+
}
|
|
134
|
+
// 压掉编辑器外壳的底色与投影:omitBackground 只能去掉「页面默认白底」,管不到
|
|
135
|
+
// 元素自己的样式 —— .app(深色)/.phone(径向渐变)/.phone__screen--bg-white 这些
|
|
136
|
+
// 祖先/外壳会把不透明底画在卡片后面,整条祖先链必须一并透明化。
|
|
137
|
+
await page.addStyleTag({
|
|
138
|
+
content:
|
|
139
|
+
'html,body,.app,.app__stage,.phone,.phone__frame,.phone__scaler,.phone-viewport,.phone__screen' +
|
|
140
|
+
'{background:transparent!important;background-image:none!important;box-shadow:none!important}',
|
|
141
|
+
})
|
|
142
|
+
await hideFloatingOverlays(page)
|
|
143
|
+
const cardEl = await page.$('.phone__screen .ce_root')
|
|
144
|
+
if (!cardEl) {
|
|
145
|
+
return { error: 'card-root-not-found', code: 500, message: '.ce_root 卡片根元素未找到' }
|
|
146
|
+
}
|
|
147
|
+
const buf = await cardEl.screenshot({ type: 'png', omitBackground: true })
|
|
148
|
+
const box = await cardEl.boundingBox()
|
|
149
|
+
return {
|
|
150
|
+
buffer: buf,
|
|
151
|
+
width: box ? Math.round(box.width) : null,
|
|
152
|
+
height: box ? Math.round(box.height) : null,
|
|
153
|
+
}
|
|
154
|
+
} finally {
|
|
155
|
+
await page.close().catch(() => {})
|
|
156
|
+
}
|
|
157
|
+
} catch (e) {
|
|
158
|
+
// 导航超时 / data-card-ready 未出现 / 截图异常:转成结构化错误,绝不向上抛
|
|
159
|
+
const isTimeout = /Timeout\s*\d+ms exceeded/.test(String((e && e.message) || e))
|
|
160
|
+
return {
|
|
161
|
+
error: isTimeout ? 'render-wait-timeout' : 'screenshot-failed',
|
|
162
|
+
code: isTimeout ? 504 : 500,
|
|
163
|
+
message: String((e && e.message) || e),
|
|
164
|
+
fix: isTimeout
|
|
165
|
+
? '卡片首帧未在限定时间内挂载(data-card-ready 未出现)。检查该卡是否在 chrome=off 模式下渲染报错,或卡片依赖真实运行时数据(customAction/RPC)而无法在纯渲染模式落定。'
|
|
166
|
+
: undefined,
|
|
167
|
+
}
|
|
168
|
+
} finally {
|
|
169
|
+
if (context) await context.close().catch(() => {})
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
79
173
|
export function createScreenshotMiddleware({ root }) {
|
|
80
174
|
return async function screenshotMiddleware(req, res, next) {
|
|
81
175
|
const pathname = decodeURIComponent((req.url || '').split('?')[0])
|
|
@@ -103,7 +103,7 @@ DSL_CARD_SLICES_DIR=<training-run>/slices
|
|
|
103
103
|
|
|
104
104
|
- `annotationCompleteness`:画板内叶子标注是否被完整归属到聚焦卡。
|
|
105
105
|
- `textRetention`:卡片内文案是否被完整保留。
|
|
106
|
-
- `structuralFocus
|
|
106
|
+
- `structuralFocus`:候选是否具备卡片级结构,而不是气泡、按钮条或空壳。**图像卡通道(r264 起)**:整卡被设计稿导成一张大切图时(解析器兜底产 1 张卡、卡内只有该 slice),`sliceAreaRatio ≥ 0.6` 即视为结构成立,按正常像素/尺寸门评分。
|
|
107
107
|
- `trainingEligible`:弹窗、toast、底部抽屉、纯文本板(`artboardType=text_only`,整板只有对话/问答/提示文字)必须为 false。
|
|
108
108
|
|
|
109
109
|
任一门失败优先归因 `PARSER`,修复目标是 `vision.py` / `parse_meaxure.py` / `boundary.json` 的通用几何规则,不允许用业务名救某个样本。
|
|
@@ -178,7 +178,7 @@ DSL_CARD_SLICES_DIR=<training-run>/slices
|
|
|
178
178
|
> 基线载体是**冻结锚点轮目录**(含 `reports/` 全量证据)+ **入库压缩锚点** `training/anchors/<round>.json`
|
|
179
179
|
> (P0-2 起:干净克隆与 CI 上唯一的锚点载体,`round_diff.mjs` / `plan_next_round.mjs` 都直接吃它)。
|
|
180
180
|
> 本 Skill 目录下的历史 `baselines/v3-r0*.json` 范式已废弃并删除(指标快照改由 summary.json + 压缩锚点承载)。
|
|
181
|
-
> 晋级 =
|
|
181
|
+
> 晋级 = 新轮成为下一冻结锚点候选,通用规则回灌 `training/lessons/lessons.jsonl`(`sink_rules.mjs`),并重新生成压缩锚点入库。
|
|
182
182
|
|
|
183
183
|
### 泛化盲测门(generalization gate)
|
|
184
184
|
|
|
@@ -197,7 +197,7 @@ DSL_CARD_SLICES_DIR=<training-run>/slices
|
|
|
197
197
|
**固定跨批次晋级盲测集**(解决单批次 eval/holdout 只有 2 例、凑不出 24 例的问题):
|
|
198
198
|
用 `build_sample.mjs` 从 v6 + v7 全量池各抽代表,合并成 `training/datasets/blindset/train-set.jsonl`
|
|
199
199
|
(≥24 例,结构聚类去重后覆盖列表/表单/结果/媒体/空状态),晋级轮一律用它盲测,
|
|
200
|
-
并在 `training/
|
|
200
|
+
并在 `training/lessons/lessons.jsonl` 记录该盲测集的 provenance 与轮号。盲测集**不参与任何修复迭代**。
|
|
201
201
|
### 收敛检测与早停
|
|
202
202
|
|
|
203
203
|
当出现以下任一信号时,**停止脚本微调,转而修订 Skill 文档规则或范式**:
|
|
@@ -238,7 +238,7 @@ r198 记录的共享 40 例均值为 fidelity 96.4% / pixel 3.8% / position 95.7
|
|
|
238
238
|
|
|
239
239
|
- v5(转账二批次 17 代表)r199 = 14/17、avgFid 95.8%、avgPix 4.0%;
|
|
240
240
|
- v6(存款+理财 30 代表)r200 = 4/30、avgFid 81.7%——新布局族缺口,仍是泛化主战场
|
|
241
|
-
(见 `training/
|
|
241
|
+
(见 `training/datasets/v6/dataset-summary.json` 与本节指标水位);
|
|
242
242
|
- v7(焦点集)r243→r262 逐例收敛:`945b562caad1f0f4` 散点带/段落壳/评分仪表盘通道落地后
|
|
243
243
|
pix 0.1348→0.0681、pos 0.532→0.905、comp 0.852;`86ae6d41957935d8` 渐变壳绑定修正后 pix 0.055。
|
|
244
244
|
对应规则见 `dsl-card-generator/references/generation_pitfalls.md §21-24` 与 lessons `r255-l01..l05` / `r261-l01`。
|
|
@@ -272,7 +272,7 @@ r198 记录的共享 40 例均值为 fidelity 96.4% / pixel 3.8% / position 95.7
|
|
|
272
272
|
3. 修复优先级:可见媒体与重影 → 语义顺序与纵向节奏 → 背景/圆角/留白 → 未见样本泛化。
|
|
273
273
|
4. 缺失/状态错切图优先画板裁切(`cropSlice`,含宽条切图与纯矢量 logo 组;矩形内有文本叠加或背景级大矩形禁裁),裁切不可用才纯色近似并标记缺陷;弥散/光晕类纹理特事特办,计入局部还原度豁免。
|
|
274
274
|
5. 焦点样本通过后必须复跑冻结锚点全量;每 3 轮做不少于 24 例未见样本盲测(固定跨批次盲测集见上文)。
|
|
275
|
-
6. 晋级门、焦点 ID
|
|
275
|
+
6. 晋级门、焦点 ID 和产物路径以本轮 `reports/summary.json` 与 `training/anchors/<最新>.json` 为准。
|
|
276
276
|
7. 新布局通道(如散点带 §21、段落壳归属 §22、行锚点/尾随切片 §23)落盘生成器后,同步 lessons.jsonl + generation_pitfalls/quickref/previewer_contract,并复跑冻结锚点确认 0 回归;通道判据只认几何/样式结构,不认业务词。
|
|
277
277
|
|
|
278
278
|
训练清单可记录样本 ID 和画板名作为证据;运行时代码、Skill 判定和校验器不得引用业务词、画板名或样本专属文案。
|
|
@@ -291,7 +291,7 @@ r198 记录的共享 40 例均值为 fidelity 96.4% / pixel 3.8% / position 95.7
|
|
|
291
291
|
7. 通用规则写入 `training/lessons/lessons.jsonl`(新格式 `{lessonId, signature, failureClass, evidenceCases,
|
|
292
292
|
rule, targetFiles, status, round}`;`plan_next_round.mjs` 产的 `lesson-drafts.jsonl` 是待补 rule 的草稿),
|
|
293
293
|
并同步到对应 Skill 的 `SKILL.md` 或 `references/`;业务词、画板名和样本专属文案不得进入规则。
|
|
294
|
-
8.
|
|
294
|
+
8. 通过后以新轮目录作为下一冻结锚点候选,回灌 lessons 并重生成压缩锚点入库
|
|
295
295
|
(`training/anchors/<round>.json`);未通过的候选规则保持 `candidate`,不修改正式 Skill 行为。
|
|
296
296
|
9. `npm run ci` 全绿(9 硬门禁:card-check / smoke-preview / verify-event-model / verify-model-expose /
|
|
297
297
|
magic-values / regress-data / unit-tests / mcp-doc-sync / lint-ratchet;另有 regress-fidelity、lint 两道软门禁):评分尺子改动被
|
|
@@ -463,7 +463,7 @@ python3 training/tools/split_report.py <runDir> [topN] # 按 split 聚合指
|
|
|
463
463
|
|
|
464
464
|
> `training/datasets/`、`training/runs/` 仍被 gitignore(体积 + 源稿版权)。数据集可复现性靠
|
|
465
465
|
> `dataset-summary.json` 的 `provenance`(sourceRoot / sourceNames / toolchainRev / 解析器参数)
|
|
466
|
-
> 与 `training/
|
|
466
|
+
> 与 `training/lessons/lessons.jsonl` 的轮次记录;锚点回归靠冻结轮目录(`training/runs/r176`)**或入库压缩锚点**
|
|
467
467
|
> (`training/anchors/r176.json`,干净克隆唯一可用载体)。已入库的训练设施还有
|
|
468
468
|
> `training/tools/*`、`training/reports/font-metrics.json`、`training/fixtures/ui-drafts/`(保真度软门禁兜底稿)。
|
|
469
469
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* 此前的闭环断点:run_round 产出 review-queue / semantic-review,score_round 产出签名与门禁,
|
|
6
6
|
* round_diff 产出回归清单,但「下一轮到底修什么、修哪个文件、是否已经收敛」全靠人读三份产物。
|
|
7
|
-
*
|
|
7
|
+
* 单卡实测:99.7% 的时间花在分析与返工上,
|
|
8
8
|
* 不是工具耗时——所以这里把归因、聚类、焦点集、门禁判定、收敛早停一次算完。
|
|
9
9
|
*
|
|
10
10
|
* 用法:
|
|
@@ -317,8 +317,13 @@ function upstreamQuality(spec, card) {
|
|
|
317
317
|
? retainedTextCount / expectedTexts.length
|
|
318
318
|
: 1
|
|
319
319
|
const metrics = card.focusMetrics || {}
|
|
320
|
-
|
|
321
|
-
|
|
320
|
+
// 结构聚焦两条路(2026-09-28):
|
|
321
|
+
// a) 常规卡:rich ≥ 2 且有文本或切片;
|
|
322
|
+
// b) 图像卡:切片覆盖 ≥ 60% 卡区(整卡被设计稿导成一张大图,卡内无其他标注)。
|
|
323
|
+
// 该形态解析器兜底产 1 张卡,结构上只有这张切片,按像素/尺寸门正常评分。
|
|
324
|
+
const structuralFocus = ((metrics.richComponentCount || 0) >= 2 &&
|
|
325
|
+
((metrics.textCount || 0) > 0 || (metrics.sliceCount || 0) > 0)) ||
|
|
326
|
+
(metrics.sliceAreaRatio || 0) >= 0.6
|
|
322
327
|
const passed = card.trainingEligible !== false &&
|
|
323
328
|
!!card.image &&
|
|
324
329
|
structuralFocus &&
|
|
@@ -74,3 +74,17 @@
|
|
|
74
74
|
| 脚手架占位文本残留 | **lint** |
|
|
75
75
|
|
|
76
76
|
未覆盖的(人工兜底):Icon 双触发、Canvas 坐标系、v-show tab↔header 面板泄漏、compiled 与 code 同步、孤儿字段保留。
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
### single-button-width-magic-ratio(r273 训练回灌)
|
|
80
|
+
> lesson: `r273-l02` status: `promoted`
|
|
81
|
+
|
|
82
|
+
单按钮宽度:标注宽度 < 卡宽 ⇒ 用设计宽 + flexShrink:0;只有按钮宽 ≈ 卡宽时才撑满行(width:100%)。旧判据用『卡宽 × 0.8』这条魔数比例,设计宽落在 80%~100% 区间的按钮被并进『撑满行』分支、宽度被抹成行内容盒宽(实测 279/343 的按钮渲染成 311 满宽,两侧各丢 16px 内缩;右锚按钮还会因撑满行把左缘推到卡片左缘)。水平位置仍按断言侧给单边几何内缩(flex-end→右缘、flex-start→左缘、居中→通用卡内边距)。反面纪律:切忌两侧同时写几何内缩——那会把 width:100% 的按钮挤进『卡宽 − 左内缩 − 右内缩』的内容盒,右锚按钮文案换行、盒高溢出(实测 33 例 textOverflow 由 1.0 掉到 0.667,整轮 33 例 pass→fail,平均还原度 −0.8pp)。诊断可复用:把两轮产物 schema 按节点逐行 diff,若差异集中在同一类节点的单个样式子项(本例 33 例全部只有一行 paddingLeft 差异),即为单点成因,先定位再改,不要凭猜分组回退。
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
### amount-currency-prefix-must-reach-expression(r285 训练回灌)
|
|
87
|
+
> lesson: `r285-l01` status: `promoted`
|
|
88
|
+
|
|
89
|
+
金额表达式必须输出货币符号:`amountExpr` 算出 `curPrefix`/`hasCurrency` 却从不写进返回值,spec 把「¥」与「5,638.00」拆成两层时裸符号层被合并段吞掉,表达式是符号唯一载体 —— 不输出前缀渲染就只剩数字(「¥5,638.00」→「5,638.00」)。前缀取自原文(`currencyPrefixOf`,兼容 HK$/US$ 字母+符号组合),与运行时负号拼接顺序固定(负号在前);纯数字原文不写前缀。`amountValueExpr(..., {currencyPrefix:false})` 依赖 ` + '字面量'` 形态做剥除,改写表达式时必须保持该形态。
|
|
90
|
+
|
|
@@ -1218,3 +1218,51 @@ sameRow 只能消费带 rect/sourceEl 的原始标注对象;语义投影对象
|
|
|
1218
1218
|
|
|
1219
1219
|
列表标题、列表项和查看更多必须注册语义边界;相邻块间距只计一次。列表行内边距由标注行高减内容高推导,ListLayout 行间距显式归零,容器高度继续由内容撑开。
|
|
1220
1220
|
|
|
1221
|
+
|
|
1222
|
+
## auto. shell-right-column-column-edge(r273 训练回灌)
|
|
1223
|
+
> lesson: `r273-l01` status: `promoted`
|
|
1224
|
+
|
|
1225
|
+
内容壳(面板壳/渐变壳)内的「前导媒体 + 右列文本」两列拓扑:右列左界此前固定取『壳宽 45%』,该比例隐含『前导媒体宽 ≈ 壳宽一半』的假设。前导媒体较窄时(前导图 80px / 壳 311px ≈ 26%)右列文本起点落在比例线左侧,整条右列被划到左列一侧 → 左列文本数恒 < 2 → 两列拓扑被整条判死,右列文本退化成全宽竖排行,继而被辅助文案的『x 坐标 > 100 即视为居中』判据甩到内容盒中央且丢掉 spec 左缩进(实测 dy 26~46px / dx 26px)。修复两处:① 列界改由壳内前导媒体的右缘决定(判据纯几何:高 ≥ max(24, 壳内文本高中位数×1.8)、右缘落在壳左半区的切片或整块图),无前导媒体才回退『壳宽 45%』;② 左列成员放宽为『≥2 条文本 或 存在前导媒体』——设计上左列常是一张整块图而非文本,只认文本会让该形态永远进不了两列通道。
|
|
1226
|
+
|
|
1227
|
+
|
|
1228
|
+
## auto. single-button-width-magic-ratio(r273 训练回灌)
|
|
1229
|
+
> lesson: `r273-l02` status: `promoted`
|
|
1230
|
+
|
|
1231
|
+
单按钮宽度:标注宽度 < 卡宽 ⇒ 用设计宽 + flexShrink:0;只有按钮宽 ≈ 卡宽时才撑满行(width:100%)。旧判据用『卡宽 × 0.8』这条魔数比例,设计宽落在 80%~100% 区间的按钮被并进『撑满行』分支、宽度被抹成行内容盒宽(实测 279/343 的按钮渲染成 311 满宽,两侧各丢 16px 内缩;右锚按钮还会因撑满行把左缘推到卡片左缘)。水平位置仍按断言侧给单边几何内缩(flex-end→右缘、flex-start→左缘、居中→通用卡内边距)。反面纪律:切忌两侧同时写几何内缩——那会把 width:100% 的按钮挤进『卡宽 − 左内缩 − 右内缩』的内容盒,右锚按钮文案换行、盒高溢出(实测 33 例 textOverflow 由 1.0 掉到 0.667,整轮 33 例 pass→fail,平均还原度 −0.8pp)。诊断可复用:把两轮产物 schema 按节点逐行 diff,若差异集中在同一类节点的单个样式子项(本例 33 例全部只有一行 paddingLeft 差异),即为单点成因,先定位再改,不要凭猜分组回退。
|
|
1232
|
+
|
|
1233
|
+
|
|
1234
|
+
## auto. badge-row-content-double-claim(r265 训练回灌)
|
|
1235
|
+
> lesson: `r265-l01` status: `promoted`
|
|
1236
|
+
|
|
1237
|
+
序号徽章行(数字壳 + 右侧内容)的成员文本必须由徽章行通道整体接管,不得再被列表标题 / 列表项 / 首列文本通道认领。判据只用几何归属(该文本元素是否已被徽章行通道收编),不得涉及文案内容。二次认领有两个后果:同一文本双份渲染(徽章行 + 列表项重影,重复度门扣分),且被误认成列表标题后会把辅助文案的 y 上界截断在该假标题处,其下方的段落与页脚文案整批静默丢失(实测 5 段文案蒸发、整卡内容缺失)。
|
|
1238
|
+
|
|
1239
|
+
|
|
1240
|
+
## auto. aux-geometry-normalized-shape-and-bound(r266 训练回灌)
|
|
1241
|
+
> lesson: `r266-l01` status: `promoted`
|
|
1242
|
+
|
|
1243
|
+
辅助文案通道有三处口径必须与渲染端严格同源,否则整卡纵向漂移:① 按钮数组是归一化对象(几何挂在顶层 x/y/width/height,没有 rect 字段)——用取 rect 的通用 helper 读会恒取 0,使表单为空时『表单顶沿』塌成 0,上半段辅助文案整批并进页脚段,行内边距计算返回空值又被当作单值 padding 解析、paddingTop 回落 0(实测整卡下漂 400+px);读归一化字段优先、spec 元素 rect 兜底。② 行底沿估算必须复用与文本节点盒高策略同源的估算函数,不得另用膨胀系数,否则下一行顶距压小、逐行累积上移。③ 多行段落块(按视觉行重组的富文本块)占据真实纵向空间,必须登记语义边界(顶 = 块顶,底 = max(块顶 + 容器高, 最后一条视觉行的底沿)),否则下游行的顶距只能从更早的语义底沿起算、逐段累积下漂(实测首段之后整体 +88px)。
|
|
1244
|
+
|
|
1245
|
+
|
|
1246
|
+
## auto. amount-currency-prefix-must-reach-expression(r285 训练回灌)
|
|
1247
|
+
> lesson: `r285-l01` status: `promoted`
|
|
1248
|
+
|
|
1249
|
+
金额表达式必须输出货币符号:`amountExpr` 算出 `curPrefix`/`hasCurrency` 却从不写进返回值,spec 把「¥」与「5,638.00」拆成两层时裸符号层被合并段吞掉,表达式是符号唯一载体 —— 不输出前缀渲染就只剩数字(「¥5,638.00」→「5,638.00」)。前缀取自原文(`currencyPrefixOf`,兼容 HK$/US$ 字母+符号组合),与运行时负号拼接顺序固定(负号在前);纯数字原文不写前缀。`amountValueExpr(..., {currencyPrefix:false})` 依赖 ` + '字面量'` 形态做剥除,改写表达式时必须保持该形态。
|
|
1250
|
+
|
|
1251
|
+
|
|
1252
|
+
## auto. collapse-btn-bottom-band-truth-gate(r285 训练回灌)
|
|
1253
|
+
> lesson: `r285-l02` status: `promoted`
|
|
1254
|
+
|
|
1255
|
+
「卡底折叠/展开控件」判据必须锚在卡底区域,不能只锚「按钮区之下」:无按钮的卡里按钮形状集为空会让该下界塌成 0,于是卡中任何「小尺寸近方形 + 横向居中」的切片都被误认(如账户行左侧的圆形卡标),同一图标渲染两遍且卡底多出的那一行把底部按钮顶出内容区。补两道几何真值门:切片底沿必须落在卡内下缘带内(带高 ≤ 卡高 22%),且顶沿不低于内容底沿(文本+媒体)减去自身高度容差 —— 控件在内容之下,不在内容之中。
|
|
1256
|
+
|
|
1257
|
+
|
|
1258
|
+
## auto. formrow-icon-single-consumption(r285 训练回灌)
|
|
1259
|
+
> lesson: `r285-l03` status: `promoted`
|
|
1260
|
+
|
|
1261
|
+
表单行认领的图标(左侧 icon / 值前导媒体 / 行尾媒体 / 右侧 affordance)渲染后必须从孤立媒体兜底池同步回收 —— 回收逻辑此前只存在于对比面板的连接件分支,普通表单行走不到:同一张切片渲染两遍(重影 + duplication 掉分),兜底行还会另起一段流式内容把后续按钮整体下推。
|
|
1262
|
+
|
|
1263
|
+
|
|
1264
|
+
## auto. fixed-px-chips-exempt-from-shrink-relax(r285 训练回灌)
|
|
1265
|
+
> lesson: `r285-l04` status: `promoted`
|
|
1266
|
+
|
|
1267
|
+
`fixFlexShrinkOverflow` 的「全 shrink:0 时放开末位为可缩」必须排除「子节点全为固定 px 宽」的情形:并排药丸/标签/统计块的宽度是设计稿逐个标注出来的真值,强行压缩会让内部文案换行、行高撑高,连带 textOverflow 与 structure 一起掉(实测 149+12+150 恰等于组宽 311 却被压成两行,textOverflow 0.889 < 门 0.90)。真正的溢出兜底是子节点混有 auto/百分比宽(内容长度不可知)的场景。
|
|
1268
|
+
|
|
@@ -62,3 +62,24 @@
|
|
|
62
62
|
- other.overflow 缺省为 hidden:需要溢出可见(标签宽于槽位等)必须显式写 other.overflow=visible。
|
|
63
63
|
- margin 组(marginType=1 逐边)支持负值,是 flex 行内唯一回拉/重叠手段。**逐边负值必须显式 marginType='1'**:只写 marginLeft 时渲染器按统一 margin 解析,负值静默失效(散点带列重叠还原全丢,r253→r254 实测)。
|
|
64
64
|
- 同一 style 组内同名子项必须唯一:绑定值改写 makeStyle 自带子项(itemValueFrom=1),追加同名子项会导致取值错乱(背景透明实测)。
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
### shell-right-column-column-edge(r273 训练回灌)
|
|
68
|
+
> lesson: `r273-l01` status: `promoted`
|
|
69
|
+
|
|
70
|
+
内容壳(面板壳/渐变壳)内的「前导媒体 + 右列文本」两列拓扑:右列左界此前固定取『壳宽 45%』,该比例隐含『前导媒体宽 ≈ 壳宽一半』的假设。前导媒体较窄时(前导图 80px / 壳 311px ≈ 26%)右列文本起点落在比例线左侧,整条右列被划到左列一侧 → 左列文本数恒 < 2 → 两列拓扑被整条判死,右列文本退化成全宽竖排行,继而被辅助文案的『x 坐标 > 100 即视为居中』判据甩到内容盒中央且丢掉 spec 左缩进(实测 dy 26~46px / dx 26px)。修复两处:① 列界改由壳内前导媒体的右缘决定(判据纯几何:高 ≥ max(24, 壳内文本高中位数×1.8)、右缘落在壳左半区的切片或整块图),无前导媒体才回退『壳宽 45%』;② 左列成员放宽为『≥2 条文本 或 存在前导媒体』——设计上左列常是一张整块图而非文本,只认文本会让该形态永远进不了两列通道。
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
### aux-geometry-normalized-shape-and-bound(r266 训练回灌)
|
|
75
|
+
> lesson: `r266-l01` status: `promoted`
|
|
76
|
+
|
|
77
|
+
辅助文案通道有三处口径必须与渲染端严格同源,否则整卡纵向漂移:① 按钮数组是归一化对象(几何挂在顶层 x/y/width/height,没有 rect 字段)——用取 rect 的通用 helper 读会恒取 0,使表单为空时『表单顶沿』塌成 0,上半段辅助文案整批并进页脚段,行内边距计算返回空值又被当作单值 padding 解析、paddingTop 回落 0(实测整卡下漂 400+px);读归一化字段优先、spec 元素 rect 兜底。② 行底沿估算必须复用与文本节点盒高策略同源的估算函数,不得另用膨胀系数,否则下一行顶距压小、逐行累积上移。③ 多行段落块(按视觉行重组的富文本块)占据真实纵向空间,必须登记语义边界(顶 = 块顶,底 = max(块顶 + 容器高, 最后一条视觉行的底沿)),否则下游行的顶距只能从更早的语义底沿起算、逐段累积下漂(实测首段之后整体 +88px)。
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
### fixed-px-chips-exempt-from-shrink-relax(r285 训练回灌)
|
|
82
|
+
> lesson: `r285-l04` status: `promoted`
|
|
83
|
+
|
|
84
|
+
`fixFlexShrinkOverflow` 的「全 shrink:0 时放开末位为可缩」必须排除「子节点全为固定 px 宽」的情形:并排药丸/标签/统计块的宽度是设计稿逐个标注出来的真值,强行压缩会让内部文案换行、行高撑高,连带 textOverflow 与 structure 一起掉(实测 149+12+150 恰等于组宽 311 却被压成两行,textOverflow 0.889 < 门 0.90)。真正的溢出兜底是子节点混有 auto/百分比宽(内容长度不可知)的场景。
|
|
85
|
+
|
|
@@ -388,6 +388,53 @@ function inferInputParams(bindings) {
|
|
|
388
388
|
|
|
389
389
|
const INTENTS = ['blank', 'list', 'detail', 'form', 'dialog', 'tab', 'canvas-head']
|
|
390
390
|
|
|
391
|
+
/**
|
|
392
|
+
* 空白根样式:margin / padding / gap 显式归零,压掉渲染器对无样式容器的默认注入
|
|
393
|
+
* (padding:2px / gap:4px / justify-content:center)。形状与存量卡根节点一致
|
|
394
|
+
* (style-meta.json:marginType / paddingType,'0' = 全边统一值)。
|
|
395
|
+
* 同一形状还有两份内联副本(禁跨包 import):server/core/cardFiles.js 的 blankResetStyle、
|
|
396
|
+
* skills/finmall-card-sync api_batch_sync.mjs 的 buildBlankSchema —— 改这里必须同步改那两处。
|
|
397
|
+
*/
|
|
398
|
+
function blankResetStyle() {
|
|
399
|
+
return {
|
|
400
|
+
properties: [
|
|
401
|
+
{
|
|
402
|
+
itemId: '100',
|
|
403
|
+
itemKey: 'flex',
|
|
404
|
+
itemValueFrom: '0',
|
|
405
|
+
itemValue: {},
|
|
406
|
+
itemChildren: {
|
|
407
|
+
properties: [{ itemId: '003', itemKey: 'gap', itemValueFrom: '0', itemValue: '0' }],
|
|
408
|
+
},
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
itemId: '400',
|
|
412
|
+
itemKey: 'padding',
|
|
413
|
+
itemValueFrom: '0',
|
|
414
|
+
itemValue: {},
|
|
415
|
+
itemChildren: {
|
|
416
|
+
properties: [
|
|
417
|
+
{ itemId: '001', itemKey: 'paddingType', itemValueFrom: '0', itemValue: '0' },
|
|
418
|
+
{ itemId: '002', itemKey: 'padding', itemValueFrom: '0', itemValue: '0' },
|
|
419
|
+
],
|
|
420
|
+
},
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
itemId: '500',
|
|
424
|
+
itemKey: 'margin',
|
|
425
|
+
itemValueFrom: '0',
|
|
426
|
+
itemValue: {},
|
|
427
|
+
itemChildren: {
|
|
428
|
+
properties: [
|
|
429
|
+
{ itemId: '001', itemKey: 'marginType', itemValueFrom: '0', itemValue: '0' },
|
|
430
|
+
{ itemId: '002', itemKey: 'margin', itemValueFrom: '0', itemValue: '0' },
|
|
431
|
+
],
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
],
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
391
438
|
/**
|
|
392
439
|
* 装配一张合法的最小卡片骨架。
|
|
393
440
|
* @returns {{schema:object, data:object}}
|
|
@@ -402,7 +449,7 @@ export function buildCardSkeleton(intent, opts = {}) {
|
|
|
402
449
|
const view = {
|
|
403
450
|
width: '375',
|
|
404
451
|
className: 'Root',
|
|
405
|
-
style:
|
|
452
|
+
style: blankResetStyle(),
|
|
406
453
|
properties: [],
|
|
407
454
|
eventactions: [],
|
|
408
455
|
components: rootChildren,
|