@chengzhao-dev/dsh-quarto-book 0.3.4 → 0.4.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/LICENSE +20 -20
- package/README.en.md +35 -4
- package/README.md +35 -4
- package/cordis.patch.yml +2 -2
- package/docs/plugin-flow.md +15 -4
- package/docs/versioning.md +9 -9
- package/lib/index.js +3 -1
- package/lib/tools/pass-report.js +332 -0
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +10 -2
- package/skills/designing-theme/SKILL.md +1 -0
- package/skills/designing-theme/assets/theme/css/base.css +368 -368
- package/skills/designing-theme/assets/theme/includes/footer.html +63 -63
- package/skills/designing-theme/assets/theme/palettes/github/meta.md +38 -38
- package/skills/designing-theme/knowledge/reading-surface.md +68 -68
- package/skills/designing-theme/scripts/measuring-pages.mjs +309 -309
- package/skills/writing-quarto/SKILL.md +44 -43
- package/skills/writing-quarto/scripts/checking-callouts.py +102 -102
- package/skills/writing-quarto/scripts/checking-dom.py +577 -577
- package/skills/writing-quarto/scripts/diagrams/rendering-coords.py +250 -250
- package/skills/writing-quarto/scripts/testing-check-ascii-names.py +71 -71
- package/skills/writing-quarto/scripts/testing-content.py +149 -149
- package/skills/writing-quarto/scripts/verifying-content.py +378 -378
- package/skills/writing-quarto/steps/09-check.md +13 -1
- package/templates/quarto-book-project/writing/chapter-map.md +22 -22
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
// `quarto_pass_report`:把一次写作或主题润色的结果压成固定行数的卡片。
|
|
2
|
+
//
|
|
3
|
+
// 入参是闭集:模式、文件、步骤、未改项、检查状态都只能取登记过的值。
|
|
4
|
+
// 卡片由工具自己拼接,返回的文本就是卡片本身,不附章节原文,也不贴改写后的句子。
|
|
5
|
+
// 没改也要出卡,这样「插件没跑」和「跑了但没改」在会话里分得开。
|
|
6
|
+
export const PASS_SKILLS = ['writing-quarto', 'designing-theme'];
|
|
7
|
+
export const PASS_MODES = ['polish', 'rewrite', 'theme', 'skipped'];
|
|
8
|
+
export const HELD_ITEMS = ['fences', 'facts', 'css', 'prose'];
|
|
9
|
+
export const CHECK_NAMES = ['content', 'html', 'layout'];
|
|
10
|
+
export const CHECK_STATUSES = ['pass', 'fail', 'skip'];
|
|
11
|
+
export const MAX_FILES = 8;
|
|
12
|
+
export const MAX_SKIP_REASON = 40;
|
|
13
|
+
/** 写作九步的短名,与 `skills/writing-quarto/SKILL.md` 的「九步」逐字对应。 */
|
|
14
|
+
export const WRITING_STEPS = {
|
|
15
|
+
'1': '定任务',
|
|
16
|
+
'2': '定结构',
|
|
17
|
+
'3': '排列内容块',
|
|
18
|
+
'4': '定段落',
|
|
19
|
+
'5': '定句子',
|
|
20
|
+
'6': '定用词',
|
|
21
|
+
'7': '定版式',
|
|
22
|
+
'8': '回读并退回',
|
|
23
|
+
'9': '检查与渲染',
|
|
24
|
+
};
|
|
25
|
+
/** 主题步骤表的名字,与 `skills/designing-theme/references/theme-spine.md` 逐字对应。 */
|
|
26
|
+
export const THEME_STEPS = ['定表面', '定字体', '定尺度', '定颜色', '定组件', '通过检查'];
|
|
27
|
+
const MODE_LABELS = {
|
|
28
|
+
polish: '增量润色',
|
|
29
|
+
rewrite: '全章重构',
|
|
30
|
+
theme: '主题',
|
|
31
|
+
skipped: '未执行',
|
|
32
|
+
};
|
|
33
|
+
const HELD_LABELS = {
|
|
34
|
+
fences: '围栏',
|
|
35
|
+
facts: '事实',
|
|
36
|
+
css: '样式',
|
|
37
|
+
prose: '正文',
|
|
38
|
+
};
|
|
39
|
+
const CHECK_LABELS = {
|
|
40
|
+
content: '内容',
|
|
41
|
+
html: 'HTML',
|
|
42
|
+
layout: '版式',
|
|
43
|
+
};
|
|
44
|
+
const PASS_VERDICTS = {
|
|
45
|
+
content: '通过',
|
|
46
|
+
html: '可用',
|
|
47
|
+
layout: '通过',
|
|
48
|
+
};
|
|
49
|
+
const FAIL_VERDICTS = {
|
|
50
|
+
content: '未通过',
|
|
51
|
+
html: '不可用',
|
|
52
|
+
layout: '未通过',
|
|
53
|
+
};
|
|
54
|
+
const TOP_LEVEL_KEYS = ['skill', 'executed', 'mode', 'files', 'steps', 'held', 'checks', 'skip_reason'];
|
|
55
|
+
function fail(message) {
|
|
56
|
+
throw new Error(message);
|
|
57
|
+
}
|
|
58
|
+
function isRecord(value) {
|
|
59
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
60
|
+
}
|
|
61
|
+
function isPlainCount(value) {
|
|
62
|
+
return typeof value === 'number' && Number.isInteger(value) && value >= 0;
|
|
63
|
+
}
|
|
64
|
+
function allowedSteps(skill) {
|
|
65
|
+
return skill === 'writing-quarto' ? Object.keys(WRITING_STEPS) : [...THEME_STEPS];
|
|
66
|
+
}
|
|
67
|
+
function stepLabel(skill, name) {
|
|
68
|
+
return skill === 'writing-quarto' ? (WRITING_STEPS[name] ?? name) : name;
|
|
69
|
+
}
|
|
70
|
+
function normalizeFiles(value) {
|
|
71
|
+
if (value === undefined)
|
|
72
|
+
return [];
|
|
73
|
+
if (!Array.isArray(value))
|
|
74
|
+
fail('files 必须是数组');
|
|
75
|
+
if (value.length > MAX_FILES)
|
|
76
|
+
fail(`files 最多 ${MAX_FILES} 个路径`);
|
|
77
|
+
return value.map((item) => {
|
|
78
|
+
if (!isRecord(item))
|
|
79
|
+
fail('files 的每一项必须是 { path, edits }');
|
|
80
|
+
const keys = Object.keys(item);
|
|
81
|
+
if (keys.some((key) => key !== 'path' && key !== 'edits'))
|
|
82
|
+
fail('files 的每一项只允许 path 和 edits');
|
|
83
|
+
const filePath = item.path;
|
|
84
|
+
if (typeof filePath !== 'string' || filePath.trim() === '')
|
|
85
|
+
fail('files 的 path 必须是非空字符串');
|
|
86
|
+
if (!isPlainCount(item.edits))
|
|
87
|
+
fail('files 的 edits 必须是非负整数');
|
|
88
|
+
return { path: filePath.trim(), edits: item.edits };
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
function normalizeSteps(value, skill) {
|
|
92
|
+
if (value === undefined)
|
|
93
|
+
return [];
|
|
94
|
+
if (!Array.isArray(value))
|
|
95
|
+
fail('steps 必须是数组');
|
|
96
|
+
const allowed = allowedSteps(skill);
|
|
97
|
+
const seen = new Set();
|
|
98
|
+
return value.map((item) => {
|
|
99
|
+
if (!isRecord(item))
|
|
100
|
+
fail('steps 的每一项必须是 { name, edits }');
|
|
101
|
+
const keys = Object.keys(item);
|
|
102
|
+
if (keys.some((key) => key !== 'name' && key !== 'edits'))
|
|
103
|
+
fail('steps 的每一项只允许 name 和 edits');
|
|
104
|
+
const name = item.name;
|
|
105
|
+
if (typeof name !== 'string' || !allowed.includes(name)) {
|
|
106
|
+
fail(`steps 的 name 只能是:${allowed.join('、')}`);
|
|
107
|
+
}
|
|
108
|
+
if (seen.has(name))
|
|
109
|
+
fail(`steps 的 name 重复:${name}`);
|
|
110
|
+
seen.add(name);
|
|
111
|
+
if (typeof item.edits !== 'number' || !Number.isInteger(item.edits) || item.edits < 1) {
|
|
112
|
+
fail('steps 的 edits 必须是正整数');
|
|
113
|
+
}
|
|
114
|
+
return { name, edits: item.edits };
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
function normalizeHeld(value) {
|
|
118
|
+
if (value === undefined)
|
|
119
|
+
return [];
|
|
120
|
+
if (!Array.isArray(value))
|
|
121
|
+
fail('held 必须是数组');
|
|
122
|
+
const seen = new Set();
|
|
123
|
+
return value.map((item) => {
|
|
124
|
+
if (typeof item !== 'string' || !HELD_ITEMS.includes(item)) {
|
|
125
|
+
fail(`held 只能是:${HELD_ITEMS.join('、')}`);
|
|
126
|
+
}
|
|
127
|
+
if (seen.has(item))
|
|
128
|
+
fail(`held 重复:${item}`);
|
|
129
|
+
seen.add(item);
|
|
130
|
+
return item;
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
function normalizeChecks(value) {
|
|
134
|
+
if (value === undefined)
|
|
135
|
+
return [];
|
|
136
|
+
if (!Array.isArray(value))
|
|
137
|
+
fail('checks 必须是数组');
|
|
138
|
+
const seen = new Set();
|
|
139
|
+
return value.map((item) => {
|
|
140
|
+
if (!isRecord(item))
|
|
141
|
+
fail('checks 的每一项必须是 { name, status }');
|
|
142
|
+
const keys = Object.keys(item);
|
|
143
|
+
if (keys.some((key) => key !== 'name' && key !== 'status'))
|
|
144
|
+
fail('checks 的每一项只允许 name 和 status');
|
|
145
|
+
const name = item.name;
|
|
146
|
+
if (typeof name !== 'string' || !CHECK_NAMES.includes(name)) {
|
|
147
|
+
fail(`checks 的 name 只能是:${CHECK_NAMES.join('、')}`);
|
|
148
|
+
}
|
|
149
|
+
if (seen.has(name))
|
|
150
|
+
fail(`checks 的 name 重复:${name}`);
|
|
151
|
+
seen.add(name);
|
|
152
|
+
const status = item.status;
|
|
153
|
+
if (typeof status !== 'string' || !CHECK_STATUSES.includes(status)) {
|
|
154
|
+
fail(`checks 的 status 只能是:${CHECK_STATUSES.join('、')}`);
|
|
155
|
+
}
|
|
156
|
+
return { name: name, status: status };
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
function checkLine(checks) {
|
|
160
|
+
const ordered = CHECK_NAMES.map((name) => checks.find((check) => check.name === name)).filter((check) => check !== undefined && check.status !== 'skip');
|
|
161
|
+
if (ordered.length === 0)
|
|
162
|
+
return undefined;
|
|
163
|
+
return '检查:' + ordered.map((check) => {
|
|
164
|
+
const label = CHECK_LABELS[check.name];
|
|
165
|
+
const verdict = check.status === 'pass' ? PASS_VERDICTS[check.name] : FAIL_VERDICTS[check.name];
|
|
166
|
+
// 中文标签直接接结论,英文标签(HTML)前面留一个空格。
|
|
167
|
+
return `${label}${/[A-Za-z]$/.test(label) ? ' ' : ''}${verdict}`;
|
|
168
|
+
}).join(';');
|
|
169
|
+
}
|
|
170
|
+
/** 校验入参并生成卡片;入参不合格时抛错,不猜测默认值。 */
|
|
171
|
+
export function buildPassReport(input) {
|
|
172
|
+
if (!isRecord(input))
|
|
173
|
+
fail('回执入参必须是对象');
|
|
174
|
+
const unknownKey = Object.keys(input).find((key) => !TOP_LEVEL_KEYS.includes(key));
|
|
175
|
+
if (unknownKey)
|
|
176
|
+
fail(`未知字段:${unknownKey}`);
|
|
177
|
+
const skill = input.skill;
|
|
178
|
+
if (typeof skill !== 'string' || !PASS_SKILLS.includes(skill)) {
|
|
179
|
+
fail(`skill 只能是:${PASS_SKILLS.join('、')}`);
|
|
180
|
+
}
|
|
181
|
+
const typedSkill = skill;
|
|
182
|
+
if (typeof input.executed !== 'boolean')
|
|
183
|
+
fail('executed 必须是布尔值');
|
|
184
|
+
const mode = input.mode;
|
|
185
|
+
if (typeof mode !== 'string' || !PASS_MODES.includes(mode)) {
|
|
186
|
+
fail(`mode 只能是:${PASS_MODES.join('、')}`);
|
|
187
|
+
}
|
|
188
|
+
const typedMode = mode;
|
|
189
|
+
if (!input.executed) {
|
|
190
|
+
if (typedMode !== 'skipped')
|
|
191
|
+
fail('executed 为 false 时 mode 必须是 skipped');
|
|
192
|
+
const reason = input.skip_reason;
|
|
193
|
+
if (typeof reason !== 'string' || reason.trim() === '')
|
|
194
|
+
fail('未执行时必须给出 skip_reason');
|
|
195
|
+
if ([...reason.trim()].length > MAX_SKIP_REASON)
|
|
196
|
+
fail(`skip_reason 最多 ${MAX_SKIP_REASON} 个字`);
|
|
197
|
+
if (input.files !== undefined && Array.isArray(input.files) && input.files.length > 0) {
|
|
198
|
+
fail('未执行时 files 必须为空');
|
|
199
|
+
}
|
|
200
|
+
if (input.steps !== undefined && Array.isArray(input.steps) && input.steps.length > 0) {
|
|
201
|
+
fail('未执行时 steps 必须为空');
|
|
202
|
+
}
|
|
203
|
+
if (input.held !== undefined && Array.isArray(input.held) && input.held.length > 0) {
|
|
204
|
+
fail('未执行时 held 必须为空');
|
|
205
|
+
}
|
|
206
|
+
if (input.checks !== undefined && Array.isArray(input.checks) && input.checks.length > 0) {
|
|
207
|
+
fail('未执行时 checks 必须为空');
|
|
208
|
+
}
|
|
209
|
+
return { receipt: `未执行 ${typedSkill}:${reason.trim()}` };
|
|
210
|
+
}
|
|
211
|
+
if (typedMode === 'skipped')
|
|
212
|
+
fail('executed 为 true 时 mode 不能是 skipped');
|
|
213
|
+
if (typedSkill === 'designing-theme' && typedMode !== 'theme')
|
|
214
|
+
fail('designing-theme 只接受 mode: theme');
|
|
215
|
+
if (typedSkill === 'writing-quarto' && typedMode === 'theme')
|
|
216
|
+
fail('writing-quarto 不接受 mode: theme');
|
|
217
|
+
const files = normalizeFiles(input.files);
|
|
218
|
+
if (files.length === 0)
|
|
219
|
+
fail('已执行时 files 至少给 1 个路径');
|
|
220
|
+
const steps = normalizeSteps(input.steps, typedSkill);
|
|
221
|
+
const held = normalizeHeld(input.held);
|
|
222
|
+
const checks = normalizeChecks(input.checks);
|
|
223
|
+
const fileEdits = files.reduce((sum, file) => sum + file.edits, 0);
|
|
224
|
+
const stepEdits = steps.reduce((sum, step) => sum + step.edits, 0);
|
|
225
|
+
if (fileEdits !== stepEdits)
|
|
226
|
+
fail(`files 的改动合计 ${fileEdits} 与 steps 的合计 ${stepEdits} 不一致`);
|
|
227
|
+
const lines = [`已执行 ${typedSkill} · ${MODE_LABELS[typedMode]}`, files.map((file) => file.path).join('、')];
|
|
228
|
+
if (fileEdits === 0) {
|
|
229
|
+
lines.push('改了 0 处');
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
lines.push(`改了 ${fileEdits} 处:` + steps.map((step) => `${stepLabel(typedSkill, step.name)} ${step.edits}`).join(','));
|
|
233
|
+
}
|
|
234
|
+
if (held.length > 0)
|
|
235
|
+
lines.push('未改:' + held.map((item) => HELD_LABELS[item]).join('、'));
|
|
236
|
+
const checksText = checkLine(checks);
|
|
237
|
+
if (checksText)
|
|
238
|
+
lines.push(checksText);
|
|
239
|
+
return { receipt: lines.join('\n') };
|
|
240
|
+
}
|
|
241
|
+
const RESULT_SCHEMA = {
|
|
242
|
+
type: 'object',
|
|
243
|
+
additionalProperties: false,
|
|
244
|
+
properties: { receipt: { type: 'string' } },
|
|
245
|
+
required: ['receipt'],
|
|
246
|
+
};
|
|
247
|
+
export function registerPassReportTool(tools) {
|
|
248
|
+
tools.register({
|
|
249
|
+
name: 'quarto_pass_report',
|
|
250
|
+
description: '一次写作或主题润色收尾时调用一次,出一张短回执卡片:执行了哪个技能、什么模式、改了哪些文件与步骤、未改项、检查结论。入参是闭集:skill、mode、files(最多 8 个,每条带 path 与 edits)、steps(写作 1-9,主题用定表面/定字体/定尺度/定颜色/定组件/通过检查,各带 edits,合计必须等于 files 的 edits 合计)、held、checks、skip_reason。没改也传 0;不适用时 executed:false、mode:skipped、给 skip_reason。卡片就是回执,不要在回复里再复述。',
|
|
251
|
+
parameters: {
|
|
252
|
+
type: 'object',
|
|
253
|
+
additionalProperties: false,
|
|
254
|
+
properties: {
|
|
255
|
+
skill: { type: 'string', enum: [...PASS_SKILLS] },
|
|
256
|
+
executed: { type: 'boolean' },
|
|
257
|
+
mode: { type: 'string', enum: [...PASS_MODES] },
|
|
258
|
+
files: {
|
|
259
|
+
type: 'array',
|
|
260
|
+
maxItems: MAX_FILES,
|
|
261
|
+
items: {
|
|
262
|
+
type: 'object',
|
|
263
|
+
additionalProperties: false,
|
|
264
|
+
properties: {
|
|
265
|
+
path: { type: 'string' },
|
|
266
|
+
edits: { type: 'integer', minimum: 0 },
|
|
267
|
+
},
|
|
268
|
+
required: ['path', 'edits'],
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
steps: {
|
|
272
|
+
type: 'array',
|
|
273
|
+
items: {
|
|
274
|
+
type: 'object',
|
|
275
|
+
additionalProperties: false,
|
|
276
|
+
properties: {
|
|
277
|
+
name: { type: 'string' },
|
|
278
|
+
edits: { type: 'integer', minimum: 1 },
|
|
279
|
+
},
|
|
280
|
+
required: ['name', 'edits'],
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
held: { type: 'array', items: { type: 'string', enum: [...HELD_ITEMS] } },
|
|
284
|
+
checks: {
|
|
285
|
+
type: 'array',
|
|
286
|
+
items: {
|
|
287
|
+
type: 'object',
|
|
288
|
+
additionalProperties: false,
|
|
289
|
+
properties: {
|
|
290
|
+
name: { type: 'string', enum: [...CHECK_NAMES] },
|
|
291
|
+
status: { type: 'string', enum: [...CHECK_STATUSES] },
|
|
292
|
+
},
|
|
293
|
+
required: ['name', 'status'],
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
skip_reason: { type: 'string', maxLength: MAX_SKIP_REASON },
|
|
297
|
+
},
|
|
298
|
+
required: ['skill', 'executed', 'mode'],
|
|
299
|
+
},
|
|
300
|
+
output: {
|
|
301
|
+
schema: RESULT_SCHEMA,
|
|
302
|
+
render: (_args, value) => [{ type: 'text', text: value.receipt }],
|
|
303
|
+
presentationMeta: (_args, value) => ({ summary: value.receipt.split('\n')[0], lines: value.receipt.split('\n') }),
|
|
304
|
+
},
|
|
305
|
+
async execute(args) {
|
|
306
|
+
return buildPassReport(args);
|
|
307
|
+
},
|
|
308
|
+
presentCall(args) {
|
|
309
|
+
const files = Array.isArray(args.files) ? args.files : [];
|
|
310
|
+
return {
|
|
311
|
+
card: 'generic',
|
|
312
|
+
title: '写作回执',
|
|
313
|
+
kind: 'execute',
|
|
314
|
+
locations: files.map((file) => ({ path: String(file.path) })),
|
|
315
|
+
rawInput: `${args.skill} · ${args.mode}`,
|
|
316
|
+
content: [
|
|
317
|
+
{
|
|
318
|
+
type: 'text',
|
|
319
|
+
text: args.executed === false ? `${args.skill}:未执行` : `${args.skill} · ${args.mode}(${files.length} 个文件)`,
|
|
320
|
+
},
|
|
321
|
+
],
|
|
322
|
+
};
|
|
323
|
+
},
|
|
324
|
+
presentResult(_args, result) {
|
|
325
|
+
return {
|
|
326
|
+
card: 'generic',
|
|
327
|
+
title: '写作回执',
|
|
328
|
+
content: result.content,
|
|
329
|
+
};
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
}
|
package/locale/en.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "Quarto Book Writing",
|
|
4
|
-
"description": "Author
|
|
4
|
+
"description": "Author and check Quarto Book chapters and maintain a reusable theme; no business code, only inside type: book repositories."
|
|
5
5
|
}
|
|
6
6
|
}
|
package/locale/zh.json
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chengzhao-dev/dsh-quarto-book",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Quarto Book
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "为 Quarto Book 写章节、校验结构并维护可复用主题;不改业务代码,只在 type: book 的仓库里出现。 · Author and check Quarto Book chapters and maintain a reusable theme; no business code, only inside `type: book` repositories.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -30,6 +30,14 @@
|
|
|
30
30
|
"dsh": {
|
|
31
31
|
"bundle": {
|
|
32
32
|
"patch": "./cordis.patch.yml"
|
|
33
|
+
},
|
|
34
|
+
"catalog": {
|
|
35
|
+
"category": "workflow",
|
|
36
|
+
"summary": {
|
|
37
|
+
"zh": "为 Quarto Book 写章节、校验结构并维护可复用主题;不改业务代码,只在 type: book 的仓库里出现。",
|
|
38
|
+
"en": "Author and check Quarto Book chapters and maintain a reusable theme; no business code, only inside `type: book` repositories."
|
|
39
|
+
},
|
|
40
|
+
"capabilities": ["skills", "tools"]
|
|
33
41
|
}
|
|
34
42
|
},
|
|
35
43
|
"icon": "./icon.svg",
|
|
@@ -40,3 +40,4 @@ metadata:
|
|
|
40
40
|
- [ ] `quarto render` 通过,明暗主题下页面无错位或对比度问题;`scripts/checking-layout.py` 通过。
|
|
41
41
|
- [ ] 代码块 filename 标题在长路径输入下不撑破容器,窄屏仍可读;超长代码在块内横向滚动,不出整页横向滚动条。
|
|
42
42
|
- [ ] 若项目内有主题副本,已在交付说明中写清改动是否同步,以及以哪一份为准。
|
|
43
|
+
- [ ] 收尾调用一次 `quarto_pass_report`:`skill` 填 `designing-theme`,`mode` 填 `theme`,`steps` 用主题步骤表的名字(`定表面`、`定字体`、`定尺度`、`定颜色`、`定组件`、`通过检查`),检查结论照实填。卡片就是回执,回复里不再复述。
|