@chengzhao-dev/dsh-quarto-book 0.3.5 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +33 -180
- package/README.md +33 -180
- package/cordis.patch.yml +2 -2
- package/lib/index.js +3 -1
- package/lib/tools/pass-report.js +332 -0
- package/lib/workspace.js +6 -14
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +12 -5
- package/skills/designing-theme/SKILL.md +1 -0
- package/skills/designing-theme/references/theme-system.md +1 -1
- package/skills/writing-quarto/SKILL.md +2 -1
- package/skills/writing-quarto/steps/09-check.md +16 -2
- package/templates/quarto-book-project/SKILL.md +1 -1
- package/docs/creation-references.md +0 -76
- package/docs/plugin-boundaries.md +0 -94
- package/docs/plugin-flow.md +0 -60
- package/docs/versioning.md +0 -55
- package/docs/writing-quarto-layout.md +0 -36
- package/scripts/checking-skill-links.mjs +0 -249
- package/scripts/embedding-agents.ps1 +0 -113
|
@@ -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/lib/workspace.js
CHANGED
|
@@ -10,17 +10,7 @@ export const BOOK_PROJECT_RE = /^(?![\t ]*#)[\t ]*type:[\t ]*['"]?book['"]?[\t ]
|
|
|
10
10
|
const MAX_SCAN_ENTRIES = 4000;
|
|
11
11
|
const MAX_SCAN_DEPTH = 12;
|
|
12
12
|
const BOOK_SOURCE_EXTENSIONS = new Set(['.qmd', '.md']);
|
|
13
|
-
const
|
|
14
|
-
'readme.md',
|
|
15
|
-
'changelog.md',
|
|
16
|
-
'license.md',
|
|
17
|
-
'contributing.md',
|
|
18
|
-
'code_of_conduct.md',
|
|
19
|
-
'security.md',
|
|
20
|
-
'support.md',
|
|
21
|
-
'authors.md',
|
|
22
|
-
'contributors.md',
|
|
23
|
-
]);
|
|
13
|
+
const NON_CHAPTER_FILE_RE = /^(?:readme|changelog|license|licence|contributing|code[_-]of[_-]conduct|security|support|authors|contributors|agents|claude)(?:[.-][a-z0-9_-]+)*\.(?:md|markdown|qmd)$/i;
|
|
24
14
|
const IGNORED_DIRECTORIES = new Set([
|
|
25
15
|
'.git',
|
|
26
16
|
'.quarto',
|
|
@@ -103,7 +93,7 @@ export async function hasBookSource(root) {
|
|
|
103
93
|
const extension = path.extname(entry.name).toLowerCase();
|
|
104
94
|
if (!BOOK_SOURCE_EXTENSIONS.has(extension))
|
|
105
95
|
continue;
|
|
106
|
-
if (
|
|
96
|
+
if (current.depth === 0 && NON_CHAPTER_FILE_RE.test(entry.name)) {
|
|
107
97
|
continue;
|
|
108
98
|
}
|
|
109
99
|
return true;
|
|
@@ -113,7 +103,7 @@ export async function hasBookSource(root) {
|
|
|
113
103
|
}
|
|
114
104
|
/**
|
|
115
105
|
* 在 cwd 与所属 git 根之间寻找最近的合法书根。
|
|
116
|
-
*
|
|
106
|
+
* 最近一个 `_quarto.yml` 决定结果:它不是书、或没有章节源文件时直接判否,
|
|
117
107
|
* 不再向上爬进无关的父项目。
|
|
118
108
|
*/
|
|
119
109
|
export async function findBookRoot(cwd) {
|
|
@@ -131,7 +121,9 @@ export async function findBookRoot(cwd) {
|
|
|
131
121
|
return undefined;
|
|
132
122
|
let cursor = start;
|
|
133
123
|
for (let depth = 0; depth < MAX_ASCENT; depth += 1) {
|
|
134
|
-
if (await
|
|
124
|
+
if (await exists(path.join(cursor, '_quarto.yml'))) {
|
|
125
|
+
if (!(await declaresBookProject(cursor)))
|
|
126
|
+
return undefined;
|
|
135
127
|
return (await hasBookSource(cursor)) ? cursor : undefined;
|
|
136
128
|
}
|
|
137
129
|
if (cursor === gitRoot)
|
package/locale/en.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "Quarto Book Writing",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "Write Quarto Book chapters, check their structure, and maintain the theme."
|
|
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.1",
|
|
4
|
+
"description": "为 Quarto Book 写章节、检查结构并维护主题。 · Write Quarto Book chapters, check their structure, and maintain the theme.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -19,9 +19,7 @@
|
|
|
19
19
|
"README.md",
|
|
20
20
|
"README.en.md",
|
|
21
21
|
"LICENSE",
|
|
22
|
-
"templates"
|
|
23
|
-
"docs",
|
|
24
|
-
"scripts"
|
|
22
|
+
"templates"
|
|
25
23
|
],
|
|
26
24
|
"engines": {
|
|
27
25
|
"node": ">=22",
|
|
@@ -30,6 +28,14 @@
|
|
|
30
28
|
"dsh": {
|
|
31
29
|
"bundle": {
|
|
32
30
|
"patch": "./cordis.patch.yml"
|
|
31
|
+
},
|
|
32
|
+
"catalog": {
|
|
33
|
+
"category": "skill",
|
|
34
|
+
"summary": {
|
|
35
|
+
"zh": "为 Quarto Book 写章节、检查结构并维护主题。",
|
|
36
|
+
"en": "Write Quarto Book chapters, check their structure, and maintain the theme."
|
|
37
|
+
},
|
|
38
|
+
"capabilities": ["skills", "tools"]
|
|
33
39
|
}
|
|
34
40
|
},
|
|
35
41
|
"icon": "./icon.svg",
|
|
@@ -37,6 +43,7 @@
|
|
|
37
43
|
"build": "tsc -p tsconfig.json",
|
|
38
44
|
"test": "npm run build && node --test tests/*.test.mjs",
|
|
39
45
|
"check": "tsc --noEmit -p tsconfig.json && node scripts/checking-skill-links.mjs",
|
|
46
|
+
"pack": "node -e \"require('fs').mkdirSync('temp',{recursive:true})\" && npm pack --pack-destination temp",
|
|
40
47
|
"pack:check": "npm pack --dry-run"
|
|
41
48
|
},
|
|
42
49
|
"keywords": [
|
|
@@ -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` 用主题步骤表的名字(`定表面`、`定字体`、`定尺度`、`定颜色`、`定组件`、`通过检查`),检查结论照实填。卡片就是回执,回复里不再复述。
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
- 正文与代码行高 1.75(CJK 阅读舒适带);章节分隔靠 H2 的 `margin-top`,无通栏底线。
|
|
60
60
|
- `grid.body-width` 是正文列上限(现 300+872+272+2×16=1476 封顶);Quarto 会从 body-width 中减去两侧各 1rem 的栅格内边距,正文内容列 = body-width − 32px(现 840px)。取 872px 是让 16px Bright Code 下 100 列代码块(834px 内容宽)不换行的最小配置。取值只在 `_quarto.yml`,`--content-width` 只限正文容器;禁止 `75ch` 二次限宽。
|
|
61
61
|
- 代码块不折行:`white-space: pre` + 块内横向滚动(`_quarto.yml` 的 `code-overflow: scroll`);窄视口整页不出横向滚动条,打印时恢复折行(`code.css` 的打印规则)。
|
|
62
|
-
- ≤991.98px
|
|
62
|
+
- ≤991.98px 左栏浮层必须不透明实底(规则在 `assets/theme/css/sidebar.css` 的 991.98px 媒体查询)。
|
|
63
63
|
|
|
64
64
|
## 组件约定(定组件)
|
|
65
65
|
|
|
@@ -39,5 +39,6 @@ metadata:
|
|
|
39
39
|
## 完成判据
|
|
40
40
|
|
|
41
41
|
- [ ] 九步走完,每一步的产出都落在正文里。
|
|
42
|
-
- [ ]
|
|
42
|
+
- [ ] 内容与链接核对通过:项目技能列了检查命令时按它执行,否则跑本技能 `scripts/verifying-content.py` 与 `checking-links.py`。
|
|
43
43
|
- [ ] 调 `quarto_render_check`:`dry_run: true` 最多 2 次,确认 `safe_to_render`;`dry_run: false` 最多 1 次,确认 `html_report.usable` 为 true。不手跑 `quarto render`,也不截图 PNG。
|
|
44
|
+
- [ ] 收尾调用一次 `quarto_pass_report`:`skill` 填 `writing-quarto`,`mode` 填 `polish` 或 `rewrite`,文件与步骤的改动次数、未改项、检查结论照实填。卡片就是回执,回复里不再复述;没改也传 0,当前文件不是章节时传 `executed: false`。
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
产出:内容核对结果、一次渲染检查和 HTML 可用性报告。这一步只跑命令,不按报告改写法。
|
|
4
4
|
|
|
5
|
+
内容与链接检查需要 Python 3;`dry_run: false` 的渲染还需要 Quarto CLI。缺少依赖时停住并报告,不改回手跑 `quarto render`。
|
|
6
|
+
|
|
5
7
|
## 通用情况
|
|
6
8
|
|
|
7
9
|
### 内容与链接
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
项目技能列了内容检查命令时,只跑它列出的命令。没有项目技能时,在书根跑本技能 `scripts/` 下的 `verifying-content.py` 与 `checking-links.py`。
|
|
10
12
|
|
|
11
13
|
`verifying-content.py` 查章节表、YAML 标题、标题层级、句段长度、围栏语言、Mermaid 围栏、占位文字和本地链接。项目专属检查只在书的 `.agents/skills/quarto-book-project/` 存在时启用。
|
|
12
14
|
|
|
@@ -43,6 +45,18 @@
|
|
|
43
45
|
|
|
44
46
|
不手跑 `quarto render`,也不手跑 `checking-output.py`、`checking-callouts.py`、`checking-dom.py`。工具已经按当前书配置调用它们。
|
|
45
47
|
|
|
48
|
+
### 执行回执
|
|
49
|
+
|
|
50
|
+
检查结论确定后,调用一次 `quarto_pass_report`,把这次执行的结论压成一张卡片:
|
|
51
|
+
|
|
52
|
+
- `skill` 填 `writing-quarto`,`mode` 填 `polish` 或 `rewrite`。
|
|
53
|
+
- `files` 写这次动过的章节路径和改动次数(最多 8 个);`steps` 只列有改动的步骤编号(1–9)与次数,两者合计必须一致。
|
|
54
|
+
- `held` 写围栏、事实这类明确没有改动的地方;没改就传 0。
|
|
55
|
+
- `checks` 抄内容与渲染的结论,名字只取 `content`、`html`、`layout`,状态只取 `pass`、`fail`、`skip`。
|
|
56
|
+
- 当前文件不是章节时传 `executed: false`、`mode: skipped`,并给一句 `skip_reason`。
|
|
57
|
+
|
|
58
|
+
卡片就是回执,回复里不再复述;回执不保存到磁盘,也不贴改写后的句子,不根据回执改文章。
|
|
59
|
+
|
|
46
60
|
## 情况
|
|
47
61
|
|
|
48
62
|
### 情况:全仓重构
|
|
@@ -55,7 +69,7 @@
|
|
|
55
69
|
|
|
56
70
|
## 完成判据
|
|
57
71
|
|
|
58
|
-
内容命令通过,`dry_run: false` 返回 `render_result.ok: true`,且 `html_report.usable: true
|
|
72
|
+
内容命令通过,`dry_run: false` 返回 `render_result.ok: true`,且 `html_report.usable: true`,并调用一次 `quarto_pass_report` 出回执,才算完成。任一条件不满足,回到报告指向的步骤。
|
|
59
73
|
|
|
60
74
|
## 正反例
|
|
61
75
|
|
|
@@ -38,7 +38,7 @@ description: 记录某一本 Quarto Book 的项目级约定:书名、读者、
|
|
|
38
38
|
- HTML 可用性:只调 `quarto_render_check`,`qmd_path` 传当前章节;`dry_run: true` 最多 2 次,`dry_run: false` 最多 1 次。
|
|
39
39
|
- 不手跑 `quarto render`,也不手跑 `checking-output.py` 或 `checking-callouts.py`;这些检查由工具按书配置调用。
|
|
40
40
|
- 项目专属词表:同目录 `checks.json`(`forbidden_terms`、`fragment_prefixes`、`chapter_titles`);不写就只跑通用规则。
|
|
41
|
-
-
|
|
41
|
+
- 项目自己的检查命令(如有)与它覆盖的 profile:
|
|
42
42
|
- 命令在哪个环境执行(本机 / WSL / 容器):
|
|
43
43
|
|
|
44
44
|
## 主题与过滤器路径
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
# 创建参考
|
|
2
|
-
|
|
3
|
-
本插件不是从零设计的。它从 DSH 官方文档、下载量靠前的社区插件和六个同类写作参考中取形状;下面记录每个来源被采用的部分,便于后续维护时判断哪些设计是借来的、哪些是本仓自己的约束。
|
|
4
|
-
|
|
5
|
-
## 官方文档
|
|
6
|
-
|
|
7
|
-
- 发布与 bundle 形状:<https://deepseek-harness.github.io/deepseek-harness/en/develop/basic/publish>
|
|
8
|
-
- 采用:`type: module` + `main` 入口、`dsh.bundle.patch`、`engines.dsh`、`files` 白名单、peer/dev 双声明。
|
|
9
|
-
- 采用:入口只导出 `name`、`inject` 与 `apply()`,不 import 宿主实现;补丁只 `insert` 一行。
|
|
10
|
-
- 包展示元数据(中文):<https://deepseek-harness.github.io/deepseek-harness/zh/develop/basic/adding-a-package>
|
|
11
|
-
- 采用:`locale/en.json` 与 `locale/zh.json` 的 `meta.title`、`meta.description`;`icon.svg` 自包含且不超过 256 KiB。
|
|
12
|
-
- 技能子系统:<https://deepseek-harness.github.io/deepseek-harness/en/reference/subsystems/skills>
|
|
13
|
-
- 采用:提供者接口 `{ name, list, get }`、候选字段 `rank` / `locator` / `resourceBase`、目录型资源基址。
|
|
14
|
-
- 采用:包内 rank 550,数字大于项目 `.dsh/skills`(100)与 `.agents/skills`(200),优先级更低,同名项目技能可以覆盖。
|
|
15
|
-
|
|
16
|
-
## 前 20 下载插件的共同形状
|
|
17
|
-
|
|
18
|
-
数据快照:<https://www.dsh.so/data/npm-downloads.json>(2026-10-17 周榜,来源标注 dsh.so,CC-BY-4.0)。对 20 个包逐一读取 npm 元数据与 README 后得到:
|
|
19
|
-
|
|
20
|
-
- 20/20 声明 `dsh` bundle;`lib/index.js` + `cordis.patch.yml` 是主流入口组合。
|
|
21
|
-
- README 以中文为主:17/20 的正文主要是中文,3/20 是英文(`dsh-better-sidebar`、`dsh-context`、`@nanmicoder/dsh-agent-teams`)。
|
|
22
|
-
- 18/20 的 README 提供 `English` 入口,8/20 同时给出 `简体中文` 入口;切换链接通常在标题或 badge 之后,不在首行。
|
|
23
|
-
- 10/20 的包描述含中文,常见写法是中文在前、` · ` 后接英文;其余为纯英文描述。
|
|
24
|
-
- 6/20 声明 `icon`(`dshmarket`、`dsh-better-sidebar`、`dsh-context`、`dsh-plugin-wallpaper-engine`、`dsh-pet`、`@xmanrui/dsh-im`);本插件补上 `icon.svg` 与双语 `locale`,以改善插件市场展示。
|
|
25
|
-
|
|
26
|
-
结论:README 以中文为主、按需提供英文切换;包描述中英并列或纯英文;安装命令只写官方 `dsh plugin add` 一种。
|
|
27
|
-
|
|
28
|
-
## 同类写作插件
|
|
29
|
-
|
|
30
|
-
### lunheng-article-pipeline
|
|
31
|
-
|
|
32
|
-
- 仓库:<https://github.com/zuoyunlai/lunheng-article-pipeline-dsh>,默认分支 `master`
|
|
33
|
-
- npm:`lunheng-article-pipeline@19.18.1`
|
|
34
|
-
- 采用的形状:`skills/<name>/SKILL.md` 一层目录;`files` 同时包含 `skills` 与 `lib`;`exports` 暴露入口、补丁与 `package.json`;入口用 `ctx.skills` 注册并给每个技能独立的目录 `resourceBase`;补丁注释说明技能不再依赖旧的 skill-filesystem mount。
|
|
35
|
-
- 保留的能力:技能正文与参考文件分离、按需加载;未采用其九角色长流程、子代理编排与插图交付。
|
|
36
|
-
|
|
37
|
-
### dsh-plugin-writing-guard
|
|
38
|
-
|
|
39
|
-
- 仓库:<https://github.com/xmutfyh/dsh-plugin-writing-guard>,默认分支 `master`
|
|
40
|
-
- npm:`dsh-plugin-writing-guard@2.0.1`
|
|
41
|
-
- 采用的形状:确定性检查放在独立 Python 脚本中,技能正文只写判断标准与调用方式;`files` 明确列出脚本目录,避免打包遗漏。
|
|
42
|
-
- 保留的能力:把可机器判定的规则(词表、结构、命名)与需要判断的行文规则分开;未采用其论文投稿与 Word 包校验部分。
|
|
43
|
-
|
|
44
|
-
### @waterwx/dsh-novel-forge
|
|
45
|
-
|
|
46
|
-
- 仓库:<https://github.com/watersxya/dsh-novel-forge>,默认分支 `main`
|
|
47
|
-
- npm:`@waterwx/dsh-novel-forge@2.5.1`
|
|
48
|
-
- 采用的形状:`keywords` 同时写 `dsh`、`dsh-plugin`、`deepseek-harness`;样式检查拆成独立脚本;`files` 只列真正要发布的目录。
|
|
49
|
-
- 保留的能力:CSS 变量、对比度、类名与回退检查各自独立;未采用其侧栏、章节生成和 `src/` 运行时。
|
|
50
|
-
|
|
51
|
-
### @max-null/dsh-draft-polish
|
|
52
|
-
|
|
53
|
-
- 仓库:<https://github.com/Max-Null/dsh-draft-polish>,默认分支 `main`
|
|
54
|
-
- npm:`@max-null/dsh-draft-polish@0.2.5`
|
|
55
|
-
- 采用的形状:`README.md` 与 `README_EN.md` 双语镜像;入口与补丁保持最小;设置只存在插件配置里。
|
|
56
|
-
- 保留的能力:双语 README 的交叉链接;未采用其输入框润色按钮与会话草稿回填。
|
|
57
|
-
|
|
58
|
-
### DEEP-IOS/dsh-humanizer
|
|
59
|
-
|
|
60
|
-
- 仓库:<https://github.com/DEEP-IOS/dsh-humanizer>,默认分支 `main`
|
|
61
|
-
- npm:`dsh-humanizer`(`latest` 与 `next` 通道并存,实际版本以仓库 README 为准)
|
|
62
|
-
- 采用的形状:把「保留作者声音、事实与进度」写成技能正文的硬约束;对「为了人味过度改写」用原文锚点对照检查,而不是只做风格评分;理论材料按需返回,不要求每次读完整套。
|
|
63
|
-
- 保留的能力:只取这些约束与对照思路。未采用其 SQLite 记忆、21 章理论、21 个工具和常驻写作引导;本插件不持久化任何写作状态。
|
|
64
|
-
|
|
65
|
-
### op7418/Humanizer-zh
|
|
66
|
-
|
|
67
|
-
- 仓库:<https://github.com/op7418/Humanizer-zh>,默认分支 `main`
|
|
68
|
-
- 来源:blader/humanizer v3.0.0 的汉化版,MIT。
|
|
69
|
-
- 采用的形状:31 个检查点分 A–F 六类的组织方式;把「假对比、起跑式铺垫、意义拔高、空泛高频词、客服腔、编辑过程残留」写进第 5、6 步的「情况:」;文件模式不改代码、命令、路径、链接、YAML、标题与锚点。
|
|
70
|
-
- 保留的能力:每条规则带修改条件、前后示例与保留边界;没有问题的句子不改,不要求每次调用都产生修改。未采用其独立的 `/humanizer-zh` 技能入口和 AI 检测叙述。
|
|
71
|
-
|
|
72
|
-
## 本仓自己的约束
|
|
73
|
-
|
|
74
|
-
- 技能只从 `skills/*/SKILL.md` 发现,不递归扫描;模板放在 `templates/`,不会被注册。
|
|
75
|
-
- 主题资产是本插件的一部分,不声称与任何外部仓库共享;项目若复制主题,以项目 `_quarto.yml` 的路径为准。
|
|
76
|
-
- 所有项目专属内容(章节表、术语、命令、专属校验)留在项目的 `quarto-book-project` 技能里,插件正文不写死。
|