dsh-thesis 1.0.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/DESIGN.md +390 -0
- package/LICENSE +21 -0
- package/NOTICE.md +10 -0
- package/README.md +253 -0
- package/cordis.patch.yml +17 -0
- package/docs/host-api.md +640 -0
- package/lib/codewalk/commands.d.ts +23 -0
- package/lib/codewalk/commands.js +168 -0
- package/lib/codewalk/engine.d.ts +108 -0
- package/lib/codewalk/engine.js +449 -0
- package/lib/codewalk/errors.d.ts +31 -0
- package/lib/codewalk/errors.js +20 -0
- package/lib/codewalk/fs-types.d.ts +34 -0
- package/lib/codewalk/fs-types.js +10 -0
- package/lib/codewalk/gate.d.ts +30 -0
- package/lib/codewalk/gate.js +60 -0
- package/lib/codewalk/host-types.d.ts +198 -0
- package/lib/codewalk/host-types.js +11 -0
- package/lib/codewalk/prestep.d.ts +17 -0
- package/lib/codewalk/prestep.js +62 -0
- package/lib/codewalk/register.d.ts +23 -0
- package/lib/codewalk/register.js +31 -0
- package/lib/codewalk/skill.d.ts +15 -0
- package/lib/codewalk/skill.js +57 -0
- package/lib/codewalk/tools.d.ts +18 -0
- package/lib/codewalk/tools.js +314 -0
- package/lib/codewalk/types.d.ts +151 -0
- package/lib/codewalk/types.js +9 -0
- package/lib/commands.d.ts +33 -0
- package/lib/commands.js +60 -0
- package/lib/config.d.ts +125 -0
- package/lib/config.js +139 -0
- package/lib/dedup/commands.d.ts +21 -0
- package/lib/dedup/commands.js +93 -0
- package/lib/dedup/index.d.ts +75 -0
- package/lib/dedup/index.js +331 -0
- package/lib/dedup/normalize.d.ts +125 -0
- package/lib/dedup/normalize.js +329 -0
- package/lib/dedup/report.d.ts +114 -0
- package/lib/dedup/report.js +309 -0
- package/lib/dedup/rewrite.d.ts +71 -0
- package/lib/dedup/rewrite.js +228 -0
- package/lib/dedup/similarity.d.ts +169 -0
- package/lib/dedup/similarity.js +323 -0
- package/lib/gates/dangerous.d.ts +22 -0
- package/lib/gates/dangerous.js +47 -0
- package/lib/gates/detect.d.ts +54 -0
- package/lib/gates/detect.js +106 -0
- package/lib/gates/messages.d.ts +39 -0
- package/lib/gates/messages.js +74 -0
- package/lib/gates/prompt.d.ts +21 -0
- package/lib/gates/prompt.js +56 -0
- package/lib/gates/writing-intent.d.ts +49 -0
- package/lib/gates/writing-intent.js +68 -0
- package/lib/index.d.ts +36 -0
- package/lib/index.js +76 -0
- package/lib/ingest/code.d.ts +71 -0
- package/lib/ingest/code.js +356 -0
- package/lib/ingest/commands.d.ts +23 -0
- package/lib/ingest/commands.js +74 -0
- package/lib/ingest/docx.d.ts +29 -0
- package/lib/ingest/docx.js +92 -0
- package/lib/ingest/index.d.ts +179 -0
- package/lib/ingest/index.js +640 -0
- package/lib/ingest/ooxml.d.ts +58 -0
- package/lib/ingest/ooxml.js +393 -0
- package/lib/ingest/pdf.d.ts +86 -0
- package/lib/ingest/pdf.js +1016 -0
- package/lib/ingest/pptx.d.ts +32 -0
- package/lib/ingest/pptx.js +116 -0
- package/lib/ingest/text.d.ts +25 -0
- package/lib/ingest/text.js +180 -0
- package/lib/ingest/types.d.ts +48 -0
- package/lib/ingest/types.js +47 -0
- package/lib/ingest/xlsx.d.ts +72 -0
- package/lib/ingest/xlsx.js +411 -0
- package/lib/ingest/zip.d.ts +76 -0
- package/lib/ingest/zip.js +393 -0
- package/lib/intake/definitions.d.ts +65 -0
- package/lib/intake/definitions.js +42 -0
- package/lib/intake/gate-rules.d.ts +57 -0
- package/lib/intake/gate-rules.js +118 -0
- package/lib/intake/gate.d.ts +24 -0
- package/lib/intake/gate.js +86 -0
- package/lib/intake/index.d.ts +76 -0
- package/lib/intake/index.js +468 -0
- package/lib/intake/questions.d.ts +169 -0
- package/lib/intake/questions.js +720 -0
- package/lib/intake/spec.d.ts +66 -0
- package/lib/intake/spec.js +221 -0
- package/lib/intake/state.d.ts +192 -0
- package/lib/intake/state.js +420 -0
- package/lib/memory/recall.d.ts +69 -0
- package/lib/memory/recall.js +177 -0
- package/lib/memory/register.d.ts +37 -0
- package/lib/memory/register.js +52 -0
- package/lib/memory/service.d.ts +39 -0
- package/lib/memory/service.js +39 -0
- package/lib/memory/store.d.ts +45 -0
- package/lib/memory/store.js +123 -0
- package/lib/memory/tools.d.ts +19 -0
- package/lib/memory/tools.js +195 -0
- package/lib/memory/types.d.ts +55 -0
- package/lib/memory/types.js +6 -0
- package/lib/paper/commands.d.ts +13 -0
- package/lib/paper/commands.js +84 -0
- package/lib/paper/lib/aicheck.d.ts +38 -0
- package/lib/paper/lib/aicheck.js +241 -0
- package/lib/paper/lib/bibtex.d.ts +21 -0
- package/lib/paper/lib/bibtex.js +114 -0
- package/lib/paper/lib/check.d.ts +32 -0
- package/lib/paper/lib/check.js +215 -0
- package/lib/paper/lib/defense.d.ts +48 -0
- package/lib/paper/lib/defense.js +220 -0
- package/lib/paper/lib/docx.d.ts +35 -0
- package/lib/paper/lib/docx.js +213 -0
- package/lib/paper/lib/layout.d.ts +41 -0
- package/lib/paper/lib/layout.js +572 -0
- package/lib/paper/lib/ledger.d.ts +112 -0
- package/lib/paper/lib/ledger.js +349 -0
- package/lib/paper/lib/lit-api.d.ts +59 -0
- package/lib/paper/lib/lit-api.js +296 -0
- package/lib/paper/lib/lit-cache.d.ts +32 -0
- package/lib/paper/lib/lit-cache.js +91 -0
- package/lib/paper/lib/project.d.ts +29 -0
- package/lib/paper/lib/project.js +93 -0
- package/lib/paper/lib/review.d.ts +37 -0
- package/lib/paper/lib/review.js +194 -0
- package/lib/paper/lib/zip.d.ts +14 -0
- package/lib/paper/lib/zip.js +124 -0
- package/lib/paper/register.d.ts +20 -0
- package/lib/paper/register.js +183 -0
- package/lib/paper/tools/aicheck.d.ts +6 -0
- package/lib/paper/tools/aicheck.js +38 -0
- package/lib/paper/tools/build.d.ts +27 -0
- package/lib/paper/tools/build.js +166 -0
- package/lib/paper/tools/check.d.ts +7 -0
- package/lib/paper/tools/check.js +79 -0
- package/lib/paper/tools/decision.d.ts +13 -0
- package/lib/paper/tools/decision.js +53 -0
- package/lib/paper/tools/defense.d.ts +11 -0
- package/lib/paper/tools/defense.js +74 -0
- package/lib/paper/tools/init.d.ts +23 -0
- package/lib/paper/tools/init.js +99 -0
- package/lib/paper/tools/lit.d.ts +27 -0
- package/lib/paper/tools/lit.js +236 -0
- package/lib/paper/tools/progress.d.ts +18 -0
- package/lib/paper/tools/progress.js +77 -0
- package/lib/paper/tools/review.d.ts +11 -0
- package/lib/paper/tools/review.js +41 -0
- package/lib/ppt/actions.d.ts +97 -0
- package/lib/ppt/actions.js +263 -0
- package/lib/ppt/check.d.ts +64 -0
- package/lib/ppt/check.js +185 -0
- package/lib/ppt/commands.d.ts +28 -0
- package/lib/ppt/commands.js +71 -0
- package/lib/ppt/convert.d.ts +146 -0
- package/lib/ppt/convert.js +423 -0
- package/lib/ppt/index.d.ts +45 -0
- package/lib/ppt/index.js +57 -0
- package/lib/ppt/marp.d.ts +74 -0
- package/lib/ppt/marp.js +165 -0
- package/lib/ppt/outline.d.ts +206 -0
- package/lib/ppt/outline.js +695 -0
- package/lib/shared/define-tool.d.ts +77 -0
- package/lib/shared/define-tool.js +235 -0
- package/lib/shared/fs-errors.d.ts +50 -0
- package/lib/shared/fs-errors.js +90 -0
- package/lib/shared/index.d.ts +31 -0
- package/lib/shared/index.js +32 -0
- package/lib/shared/spawn.d.ts +38 -0
- package/lib/shared/spawn.js +50 -0
- package/lib/shared/text.d.ts +14 -0
- package/lib/shared/text.js +26 -0
- package/package.json +82 -0
- package/skills/code-walkthrough/SKILL.md +94 -0
- package/skills/code-walkthrough/references/examples.md +129 -0
- package/skills/paper-dedup/SKILL.md +58 -0
- package/skills/paper-intake/SKILL.md +64 -0
- package/skills/thesis-citation/SKILL.md +67 -0
- package/skills/thesis-defense/SKILL.md +87 -0
- package/skills/thesis-eng-design/SKILL.md +97 -0
- package/skills/thesis-inquiry/SKILL.md +55 -0
- package/skills/thesis-inquiry/references/intent-spec-template.md +39 -0
- package/skills/thesis-literature/SKILL.md +56 -0
- package/skills/thesis-opener/SKILL.md +77 -0
- package/skills/thesis-pipeline/SKILL.md +121 -0
- package/skills/thesis-writing/SKILL.md +86 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 文献三工具:thesis_lit_search / thesis_lit_save / thesis_lit_note。
|
|
3
|
+
*
|
|
4
|
+
* 机制保证"零假文献":
|
|
5
|
+
* - search 只返回真实 API 检索结果,并把结果写入检索缓存(.lit-cache.json);
|
|
6
|
+
* - save 只接受缓存中的记录 id,元数据零转述,写 refs.bib 并留审计行;
|
|
7
|
+
* - note 基于缓存或 refs.bib 生成笔记骨架。
|
|
8
|
+
*/
|
|
9
|
+
import * as nodePath from 'node:path';
|
|
10
|
+
import { describeReadFailure, isMissingError } from "../../shared/fs-errors.js";
|
|
11
|
+
import { searchLiterature, LIT_SOURCES, } from "../lib/lit-api.js";
|
|
12
|
+
import { appendLitRecord, loadCache, mergeRecords, NOTES_DIR_REL, REFS_BIB_REL, saveCache, saveRow, searchRow, } from "../lib/lit-cache.js";
|
|
13
|
+
import { bibKeyFor, buildBibtex, dedupeKey, parseBibDois, parseBibKeys } from "../lib/bibtex.js";
|
|
14
|
+
import { findThesisRoot } from "../lib/project.js";
|
|
15
|
+
const BIB_HEADER = `% 文献库(BibTeX)。
|
|
16
|
+
% 红线:本文件只收录真实检索到的文献(由 thesis_lit_search/save 写入)。
|
|
17
|
+
% 每条必须含 DOI 或 URL。引用格式遵循 GB/T 7714-2015(顺序编码制)。
|
|
18
|
+
|
|
19
|
+
`;
|
|
20
|
+
async function thesisRoot(fs, cwd, signal) {
|
|
21
|
+
const root = await findThesisRoot(fs, cwd, signal);
|
|
22
|
+
if (root === null) {
|
|
23
|
+
throw new Error('未找到论文工作区(向上查找 00-管理/进度台账.md 失败)。请先用 thesis_init 创建工作区,或在论文仓库目录内操作。');
|
|
24
|
+
}
|
|
25
|
+
return root;
|
|
26
|
+
}
|
|
27
|
+
function displayAuthors(authors, max = 3) {
|
|
28
|
+
if (authors.length === 0)
|
|
29
|
+
return '(无作者信息)';
|
|
30
|
+
const head = authors.slice(0, max).join(', ');
|
|
31
|
+
return authors.length > max ? `${head} 等` : head;
|
|
32
|
+
}
|
|
33
|
+
function displayRecord(record) {
|
|
34
|
+
const lines = [];
|
|
35
|
+
lines.push(`【${record.id}】${record.title}`);
|
|
36
|
+
lines.push(` 作者:${displayAuthors(record.authors)};年份:${record.year ?? '未知'};出处:${record.venue || '未知'}`);
|
|
37
|
+
if (record.doi !== undefined)
|
|
38
|
+
lines.push(` DOI:${record.doi}`);
|
|
39
|
+
if (record.url !== undefined)
|
|
40
|
+
lines.push(` URL:${record.url}`);
|
|
41
|
+
if (record.citationCount !== undefined)
|
|
42
|
+
lines.push(` 被引:${record.citationCount}`);
|
|
43
|
+
if (record.abstract !== undefined)
|
|
44
|
+
lines.push(` 摘要:${record.abstract}`);
|
|
45
|
+
return lines.join('\n');
|
|
46
|
+
}
|
|
47
|
+
export async function runLitSearch(fs, args, cwd, signal, fetchImpl) {
|
|
48
|
+
if (args.query === undefined || args.query.trim() === '')
|
|
49
|
+
throw new Error('query 必填:检索关键词(中英文均可)。');
|
|
50
|
+
const root = await thesisRoot(fs, cwd, signal);
|
|
51
|
+
const results = await searchLiterature(fetchImpl, {
|
|
52
|
+
query: args.query.trim(),
|
|
53
|
+
...(args.year_from !== undefined ? { yearFrom: args.year_from } : {}),
|
|
54
|
+
...(args.limit !== undefined ? { limit: args.limit } : {}),
|
|
55
|
+
...(args.source !== undefined ? { source: args.source } : {}),
|
|
56
|
+
signal,
|
|
57
|
+
});
|
|
58
|
+
const cache = mergeRecords(await loadCache(fs, root, signal), results);
|
|
59
|
+
await saveCache(fs, root, cache, signal);
|
|
60
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
61
|
+
await appendLitRecord(fs, root, searchRow(date, results[0].source, args.query.trim(), results.length, results.map(r => r.id)), signal);
|
|
62
|
+
const lines = [];
|
|
63
|
+
lines.push(`检索「${args.query.trim()}」命中 ${results.length} 条(来源:${results[0].source}),已写入检索缓存与检索记录。`);
|
|
64
|
+
lines.push('');
|
|
65
|
+
results.slice(0, args.limit ?? 10).forEach(r => {
|
|
66
|
+
lines.push(displayRecord(r));
|
|
67
|
+
lines.push('');
|
|
68
|
+
});
|
|
69
|
+
lines.push('下一步:选定文献后用 thesis_lit_save(ids: [记录 id])收录进 refs.bib;再用 thesis_lit_note 生成阅读笔记。');
|
|
70
|
+
return lines.join('\n');
|
|
71
|
+
}
|
|
72
|
+
export async function runLitSave(fs, args, cwd, signal) {
|
|
73
|
+
if (args.ids === undefined || args.ids.length === 0)
|
|
74
|
+
throw new Error('ids 必填:thesis_lit_search 返回的记录 id 列表。');
|
|
75
|
+
const root = await thesisRoot(fs, cwd, signal);
|
|
76
|
+
const cache = await loadCache(fs, root, signal);
|
|
77
|
+
let bib;
|
|
78
|
+
try {
|
|
79
|
+
bib = await fs.readText(await fs.resolve(nodePath.join(root, REFS_BIB_REL), { signal }), signal);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
// 只有「文献库还没建过」才从表头开始。读失败(权限/IO)时必须停下:
|
|
83
|
+
// 否则会在「假设文献库是空的」前提下写回,等于删掉学生已有的全部文献。
|
|
84
|
+
if (!isMissingError(error)) {
|
|
85
|
+
throw new Error(describeReadFailure('文献库(02-文献/refs.bib)', nodePath.join(root, REFS_BIB_REL), error, signal));
|
|
86
|
+
}
|
|
87
|
+
bib = BIB_HEADER;
|
|
88
|
+
}
|
|
89
|
+
if (bib.trim() === '')
|
|
90
|
+
bib = BIB_HEADER;
|
|
91
|
+
const keys = parseBibKeys(bib);
|
|
92
|
+
const dois = parseBibDois(bib);
|
|
93
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
94
|
+
const outcomes = [];
|
|
95
|
+
let added = 0;
|
|
96
|
+
for (const id of args.ids) {
|
|
97
|
+
const record = cache.records[id];
|
|
98
|
+
if (record === undefined) {
|
|
99
|
+
outcomes.push(`✗ ${id}:不在检索缓存中。缓存只包含真实检索结果;请先用 thesis_lit_search 检索,不要手工构造元数据。`);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (record.doi !== undefined && dois.has(record.doi.toLowerCase())) {
|
|
103
|
+
outcomes.push(`⏭ ${record.title}:DOI 已在 refs.bib 中,跳过。`);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const finalKey = dedupeKey(bibKeyFor(record), keys);
|
|
107
|
+
keys.add(finalKey);
|
|
108
|
+
if (record.doi !== undefined)
|
|
109
|
+
dois.add(record.doi.toLowerCase());
|
|
110
|
+
const { entry } = buildBibtex(record, finalKey);
|
|
111
|
+
bib += `${entry}\n\n`;
|
|
112
|
+
await appendLitRecord(fs, root, saveRow(date, record.source, record.title, finalKey, record.doi), signal);
|
|
113
|
+
outcomes.push(`✓ ${record.title} → refs.bib [${finalKey}]`);
|
|
114
|
+
added += 1;
|
|
115
|
+
}
|
|
116
|
+
await fs.writeText(await fs.resolve(nodePath.join(root, REFS_BIB_REL), { signal }), bib, undefined, signal);
|
|
117
|
+
outcomes.push('', `本次收录 ${added}/${args.ids.length} 条。`);
|
|
118
|
+
return outcomes.join('\n');
|
|
119
|
+
}
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// 笔记
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
function noteTemplate(record, bibKey) {
|
|
124
|
+
const authorText = record.authors.length === 0 ? '(无作者信息)' : displayAuthors(record.authors);
|
|
125
|
+
const meta = [`收录于 refs.bib [${bibKey}]`];
|
|
126
|
+
if (record.doi !== undefined)
|
|
127
|
+
meta.push(`DOI:${record.doi}`);
|
|
128
|
+
if (record.url !== undefined)
|
|
129
|
+
meta.push(`URL:${record.url}`);
|
|
130
|
+
if (record.citationCount !== undefined)
|
|
131
|
+
meta.push(`被引 ${record.citationCount} 次`);
|
|
132
|
+
return `# ${record.title}(${authorText},${record.year ?? '年份未知'},${record.venue || '出处未知'})
|
|
133
|
+
|
|
134
|
+
> ${meta.join(';')}
|
|
135
|
+
> 笔记模板见技能 thesis-literature;写论文时从这里取"可用引用句"。
|
|
136
|
+
|
|
137
|
+
## 这篇解决什么问题
|
|
138
|
+
|
|
139
|
+
(待补)
|
|
140
|
+
|
|
141
|
+
## 方法/技术要点(3-5 条,用自己的话)
|
|
142
|
+
|
|
143
|
+
(待补)
|
|
144
|
+
|
|
145
|
+
## 实验与结论(数据集/指标/结果)
|
|
146
|
+
|
|
147
|
+
(待补)
|
|
148
|
+
|
|
149
|
+
## 与我课题的关系
|
|
150
|
+
|
|
151
|
+
(待补:借鉴什么 / 对比什么 / 差异在哪)
|
|
152
|
+
|
|
153
|
+
## 可用引用句
|
|
154
|
+
|
|
155
|
+
(待补:1-2 句,注明放哪一章)
|
|
156
|
+
|
|
157
|
+
## 摘要(原始)
|
|
158
|
+
|
|
159
|
+
${record.abstract ?? '(检索记录无摘要;请从原文自行摘录)'}
|
|
160
|
+
`;
|
|
161
|
+
}
|
|
162
|
+
/** 从 refs.bib 中按 key 提取最小记录(供 note 在缓存缺失时使用)。 */
|
|
163
|
+
function findRecordByBibKey(bib, key) {
|
|
164
|
+
const m = new RegExp(`^@(\\w+)\\s*\\{\\s*${key}\\s*,\\s*([\\s\\S]*?)\\n\\}`, 'm').exec(bib);
|
|
165
|
+
if (m === null)
|
|
166
|
+
return undefined;
|
|
167
|
+
const body = m[2] ?? '';
|
|
168
|
+
const field = (name) => {
|
|
169
|
+
const fm = new RegExp(`^\\s*${name}\\s*=\\s*\\{([\\s\\S]*?)\\}`, 'm').exec(body);
|
|
170
|
+
return fm?.[1]?.trim();
|
|
171
|
+
};
|
|
172
|
+
const authorRaw = field('author');
|
|
173
|
+
const authors = authorRaw === undefined ? [] : authorRaw.split(/\s+and\s+/).map(a => a.trim()).filter(a => a !== '');
|
|
174
|
+
const title = field('title') ?? '(未知标题)';
|
|
175
|
+
const yearRaw = field('year');
|
|
176
|
+
const year = yearRaw === undefined || yearRaw === '' ? null : Number.parseInt(yearRaw, 10);
|
|
177
|
+
const doi = field('doi');
|
|
178
|
+
const url = field('url');
|
|
179
|
+
const typeMap = { article: 'article', inproceedings: 'inproceedings', book: 'book', phdthesis: 'phdthesis' };
|
|
180
|
+
return {
|
|
181
|
+
id: '',
|
|
182
|
+
title,
|
|
183
|
+
authors,
|
|
184
|
+
year: Number.isNaN(year) ? null : year,
|
|
185
|
+
venue: field('journal') ?? field('booktitle') ?? field('school') ?? field('publisher') ?? field('howpublished') ?? '',
|
|
186
|
+
type: typeMap[m[1] ?? ''] ?? 'misc',
|
|
187
|
+
...(doi !== undefined ? { doi } : {}),
|
|
188
|
+
...(url !== undefined ? { url } : {}),
|
|
189
|
+
source: 'refs.bib',
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
export async function runLitNote(fs, args, cwd, signal) {
|
|
193
|
+
if (args.ref === undefined || args.ref.trim() === '')
|
|
194
|
+
throw new Error('ref 必填:缓存记录 id(thesis_lit_search 返回)或 refs.bib 条目 key。');
|
|
195
|
+
const root = await thesisRoot(fs, cwd, signal);
|
|
196
|
+
const ref = args.ref.trim();
|
|
197
|
+
let record;
|
|
198
|
+
let bibKey;
|
|
199
|
+
const cache = await loadCache(fs, root, signal);
|
|
200
|
+
if (cache.records[ref] !== undefined) {
|
|
201
|
+
record = cache.records[ref];
|
|
202
|
+
bibKey = bibKeyFor(record);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
let bib;
|
|
206
|
+
try {
|
|
207
|
+
bib = await fs.readText(await fs.resolve(nodePath.join(root, REFS_BIB_REL), { signal }), signal);
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
if (!isMissingError(error)) {
|
|
211
|
+
throw new Error(describeReadFailure('文献库(02-文献/refs.bib)', nodePath.join(root, REFS_BIB_REL), error, signal));
|
|
212
|
+
}
|
|
213
|
+
throw new Error(`记录 ${ref} 既不在检索缓存、refs.bib 也不存在。请先用 thesis_lit_search 检索。`);
|
|
214
|
+
}
|
|
215
|
+
const byKey = findRecordByBibKey(bib, ref);
|
|
216
|
+
if (byKey === undefined) {
|
|
217
|
+
throw new Error(`记录 ${ref} 不在检索缓存中,refs.bib 中也没有该 key。请先用 thesis_lit_search 检索。`);
|
|
218
|
+
}
|
|
219
|
+
record = byKey;
|
|
220
|
+
bibKey = ref;
|
|
221
|
+
}
|
|
222
|
+
const notePath = nodePath.join(root, NOTES_DIR_REL, `${bibKey}.md`);
|
|
223
|
+
try {
|
|
224
|
+
await fs.readText(await fs.resolve(notePath, { signal }), signal);
|
|
225
|
+
if (args.force !== true)
|
|
226
|
+
throw new Error(`笔记已存在:${notePath}。如确需覆盖,请设 force: true。`);
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
if (error instanceof Error && error.message.startsWith('笔记已存在'))
|
|
230
|
+
throw error;
|
|
231
|
+
// 不存在:继续创建。
|
|
232
|
+
}
|
|
233
|
+
await fs.writeText(await fs.resolve(notePath, { signal }), noteTemplate(record, bibKey), undefined, signal);
|
|
234
|
+
return `笔记骨架已创建:${notePath}\n下一步:读原文,补全"方法/技术要点"与"与我课题的关系"两节(见技能 thesis-literature)。`;
|
|
235
|
+
}
|
|
236
|
+
export { LIT_SOURCES };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* thesis_progress:进度台账读写 + 人工关卡闸门(G1/G2/G3/G4)。
|
|
3
|
+
*
|
|
4
|
+
* 关卡是工具层强制,不是提示语:阶段入口关卡未通过时,该阶段任务
|
|
5
|
+
* 拒绝进入 doing/done(lib/ledger.ts canUpdateTask)。通过关卡前,
|
|
6
|
+
* 关联任务必须先 done(lib/ledger.ts applyGate)。
|
|
7
|
+
*/
|
|
8
|
+
import type { FileSystem } from '@deepseek-ai/dsh-fs';
|
|
9
|
+
export interface ProgressArgs {
|
|
10
|
+
action: 'report' | 'update' | 'gate';
|
|
11
|
+
task_id?: string;
|
|
12
|
+
status?: 'todo' | 'doing' | 'done';
|
|
13
|
+
note?: string;
|
|
14
|
+
gate?: 'G1' | 'G2' | 'G3' | 'G4';
|
|
15
|
+
pass?: boolean;
|
|
16
|
+
chapter?: string;
|
|
17
|
+
}
|
|
18
|
+
export declare function runProgress(fs: FileSystem, args: ProgressArgs, cwd: string | undefined, signal?: AbortSignal): Promise<string>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* thesis_progress:进度台账读写 + 人工关卡闸门(G1/G2/G3/G4)。
|
|
3
|
+
*
|
|
4
|
+
* 关卡是工具层强制,不是提示语:阶段入口关卡未通过时,该阶段任务
|
|
5
|
+
* 拒绝进入 doing/done(lib/ledger.ts canUpdateTask)。通过关卡前,
|
|
6
|
+
* 关联任务必须先 done(lib/ledger.ts applyGate)。
|
|
7
|
+
*/
|
|
8
|
+
import * as nodePath from 'node:path';
|
|
9
|
+
import { describeReadFailure, isMissingError } from "../../shared/fs-errors.js";
|
|
10
|
+
import { applyGate, applyTaskUpdate, GATE_NAMES, parseLedger, renderLedger, renderReport, } from "../lib/ledger.js";
|
|
11
|
+
import { findThesisRoot, LEDGER_REL } from "../lib/project.js";
|
|
12
|
+
async function loadLedger(fs, cwd, signal) {
|
|
13
|
+
const root = await findThesisRoot(fs, cwd, signal);
|
|
14
|
+
if (root === null) {
|
|
15
|
+
throw new Error('未找到论文工作区(向上查找 00-管理/进度台账.md 失败)。请先用 thesis_init 创建工作区,或在论文仓库目录内操作。');
|
|
16
|
+
}
|
|
17
|
+
const ledgerPath = nodePath.join(root, LEDGER_REL);
|
|
18
|
+
let raw;
|
|
19
|
+
try {
|
|
20
|
+
raw = await fs.readText(await fs.resolve(ledgerPath, { signal }), signal);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
// 只有「台账确实不存在」才可以重建(工作区被手工删掉台账的异常情况)。
|
|
24
|
+
// 读失败(权限/IO)时重建会把 25 条真实任务状态重置为初始态,且报告看起来一切正常——
|
|
25
|
+
// 那是最坏的一种失败:学生以为进度没丢。
|
|
26
|
+
if (!isMissingError(error)) {
|
|
27
|
+
throw new Error(describeReadFailure('进度台账(00-管理/进度台账.md)', ledgerPath, error, signal));
|
|
28
|
+
}
|
|
29
|
+
const fresh = renderLedger(parseLedger('').state);
|
|
30
|
+
await fs.writeText(await fs.resolve(ledgerPath, { signal }), fresh, undefined, signal);
|
|
31
|
+
return { root, state: parseLedger(fresh).state };
|
|
32
|
+
}
|
|
33
|
+
const parsed = parseLedger(raw);
|
|
34
|
+
if (!parsed.found) {
|
|
35
|
+
throw new Error('进度台账的 `<!-- thesis:state -->` 状态块缺失或损坏。请勿手工修改该 JSON 块;如被误改,可让 AI 依据默认任务清单重建。');
|
|
36
|
+
}
|
|
37
|
+
return { root, state: parsed.state };
|
|
38
|
+
}
|
|
39
|
+
async function saveLedger(fs, root, state, signal) {
|
|
40
|
+
const target = await fs.resolve(nodePath.join(root, LEDGER_REL), { signal });
|
|
41
|
+
await fs.writeText(target, renderLedger(state), undefined, signal);
|
|
42
|
+
}
|
|
43
|
+
export async function runProgress(fs, args, cwd, signal) {
|
|
44
|
+
switch (args.action) {
|
|
45
|
+
case 'report': {
|
|
46
|
+
const { state } = await loadLedger(fs, cwd, signal);
|
|
47
|
+
return renderReport(state);
|
|
48
|
+
}
|
|
49
|
+
case 'update': {
|
|
50
|
+
if (args.task_id === undefined || args.status === undefined) {
|
|
51
|
+
throw new Error('update 需要 task_id 与 status(todo/doing/done)。');
|
|
52
|
+
}
|
|
53
|
+
const { root, state } = await loadLedger(fs, cwd, signal);
|
|
54
|
+
const result = applyTaskUpdate(state, args.task_id, args.status, args.note);
|
|
55
|
+
if (!result.ok)
|
|
56
|
+
throw new Error(result.reason);
|
|
57
|
+
await saveLedger(fs, root, state, signal);
|
|
58
|
+
return `已更新:${result.task.id} ${result.task.title} → ${args.status}。\n${renderReport(state)}`;
|
|
59
|
+
}
|
|
60
|
+
case 'gate': {
|
|
61
|
+
if (args.gate === undefined || args.pass === undefined) {
|
|
62
|
+
throw new Error('gate 需要 gate(G1/G2/G3/G4)与 pass(boolean)。');
|
|
63
|
+
}
|
|
64
|
+
const { root, state } = await loadLedger(fs, cwd, signal);
|
|
65
|
+
const result = applyGate(state, args.gate, args.pass, { chapter: args.chapter, note: args.note });
|
|
66
|
+
if (!result.ok)
|
|
67
|
+
throw new Error(result.reason);
|
|
68
|
+
await saveLedger(fs, root, state, signal);
|
|
69
|
+
const name = GATE_NAMES[args.gate] ?? args.gate;
|
|
70
|
+
const extra = args.gate === 'G2' && args.chapter !== undefined ? `(${args.chapter})` : '';
|
|
71
|
+
return `关卡 ${args.gate} ${name}${extra} ${args.pass ? '已通过' : '已重置为未通过'}。\n${renderReport(state)}`;
|
|
72
|
+
}
|
|
73
|
+
default: {
|
|
74
|
+
throw new Error(`未知 action:${String(args.action)}。有效值:report、update、gate。`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* thesis_review:单章确定性评审(字数/大纲/引用/图表/未完成标记/G2 清单)。
|
|
3
|
+
*
|
|
4
|
+
* 评审的是"机器可证明"的部分;语义质量由 AI 依据 thesis-writing 技能完成,
|
|
5
|
+
* 报告结论里会提示这一点。G2 人工验收流程见技能 thesis-pipeline / thesis-writing。
|
|
6
|
+
*/
|
|
7
|
+
import type { FileSystem } from '@deepseek-ai/dsh-fs';
|
|
8
|
+
export interface ReviewArgs {
|
|
9
|
+
chapter: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function runReview(fs: FileSystem, args: ReviewArgs, cwd: string | undefined, signal?: AbortSignal): Promise<string>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* thesis_review:单章确定性评审(字数/大纲/引用/图表/未完成标记/G2 清单)。
|
|
3
|
+
*
|
|
4
|
+
* 评审的是"机器可证明"的部分;语义质量由 AI 依据 thesis-writing 技能完成,
|
|
5
|
+
* 报告结论里会提示这一点。G2 人工验收流程见技能 thesis-pipeline / thesis-writing。
|
|
6
|
+
*/
|
|
7
|
+
import * as nodePath from 'node:path';
|
|
8
|
+
import { chapterMetaFor } from "../lib/layout.js";
|
|
9
|
+
import { REFS_BIB_REL } from "../lib/lit-cache.js";
|
|
10
|
+
import { findThesisRoot } from "../lib/project.js";
|
|
11
|
+
import { renderReviewReport, reviewChapter } from "../lib/review.js";
|
|
12
|
+
export async function runReview(fs, args, cwd, signal) {
|
|
13
|
+
if (args.chapter === undefined || args.chapter.trim() === '') {
|
|
14
|
+
throw new Error('chapter 必填:章节文件名,如 01-绪论(可带 .md)。');
|
|
15
|
+
}
|
|
16
|
+
const root = await findThesisRoot(fs, cwd, signal);
|
|
17
|
+
if (root === null) {
|
|
18
|
+
throw new Error('未找到论文工作区(向上查找 00-管理/进度台账.md 失败)。请先在论文仓库目录内操作。');
|
|
19
|
+
}
|
|
20
|
+
const located = chapterMetaFor(args.chapter.trim());
|
|
21
|
+
if (located === undefined) {
|
|
22
|
+
throw new Error(`未知章节:${args.chapter}。有效章节:01-绪论 至 07-总结与展望。`);
|
|
23
|
+
}
|
|
24
|
+
const chapterPath = nodePath.join(root, '06-论文/章节', `${located.meta.file}.md`);
|
|
25
|
+
let chapterText;
|
|
26
|
+
try {
|
|
27
|
+
chapterText = await fs.readText(await fs.resolve(chapterPath, { signal }), signal);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
throw new Error(`章节文件不存在:${chapterPath}。请先撰写该章(模板已由 thesis_init 生成,可能尚未动笔)。`);
|
|
31
|
+
}
|
|
32
|
+
let bibText = '';
|
|
33
|
+
try {
|
|
34
|
+
bibText = await fs.readText(await fs.resolve(nodePath.join(root, REFS_BIB_REL), { signal }), signal);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// 文献库尚未建立:引用检查会如实报告 0 条。
|
|
38
|
+
}
|
|
39
|
+
const report = reviewChapter({ chapterText, bibText, meta: located.meta, chapterNo: located.chapterNo });
|
|
40
|
+
return renderReviewReport(report);
|
|
41
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `thesis_slides` 的四个动作实现(outline / convert / check / guide)。
|
|
3
|
+
*
|
|
4
|
+
* 与 `src/paper/tools/*.ts` 同一分层思路:**业务逻辑在这里,靠 `defineTool` 的
|
|
5
|
+
* 胶水在 `index.ts`**。本文件只依赖 Node 内置与 `FileSystem` 的**类型**(宿主在
|
|
6
|
+
* 运行期提供真实实现),因此在没有安装宿主包的离线环境里也能被单元测试直接导入。
|
|
7
|
+
*
|
|
8
|
+
* 落盘约定(全部相对论文仓库根):
|
|
9
|
+
* - `07-答辩/PPT.md`(outline;默认不覆盖,force 时先备份为 `PPT-<时间戳>.md`)
|
|
10
|
+
* - `07-答辩/PPT.pptx`(convert 成功时,由外部转换器写出)
|
|
11
|
+
* - `07-答辩/PPT转换说明.md`(convert 无论成败都写:探测结果 + 命令 + 退出码 + 输出摘要)
|
|
12
|
+
*
|
|
13
|
+
* @module dsh-thesis/ppt
|
|
14
|
+
*/
|
|
15
|
+
import type { FileSystem } from '@deepseek-ai/dsh-fs';
|
|
16
|
+
import type { PptOptions } from '../config.ts';
|
|
17
|
+
import { type Audience, type OutlineInputs, type SlidePlan } from './outline.ts';
|
|
18
|
+
import { type CheckResult, type SlideIssue } from './check.ts';
|
|
19
|
+
import { type ConversionResult, type EngineDetection } from './convert.ts';
|
|
20
|
+
/** `thesis_slides` 的参数。 */
|
|
21
|
+
export interface PptArgs {
|
|
22
|
+
readonly action: 'outline' | 'convert' | 'check' | 'guide';
|
|
23
|
+
/** 目标页数(默认 11,会夹到骨架允许区间)。 */
|
|
24
|
+
readonly pages?: number;
|
|
25
|
+
/** 听众档位:undergrad(默认)| master。 */
|
|
26
|
+
readonly audience?: Audience;
|
|
27
|
+
/** outline:已存在 PPT.md 时是否覆盖(默认 false,覆盖前自动备份)。 */
|
|
28
|
+
readonly force?: boolean;
|
|
29
|
+
}
|
|
30
|
+
/** 可注入的依赖(测试用替身;生产用默认实现)。 */
|
|
31
|
+
export interface PptDeps {
|
|
32
|
+
/** 引擎探测替身。 */
|
|
33
|
+
readonly detect?: () => EngineDetection;
|
|
34
|
+
/** 转换替身(测试用它模拟成功/失败)。 */
|
|
35
|
+
readonly convert?: (slidesPath: string, outPath: string, options: {
|
|
36
|
+
engine: PptOptions['engine'];
|
|
37
|
+
timeoutMs: number;
|
|
38
|
+
}) => ConversionResult;
|
|
39
|
+
/** 二进制落盘钩子(默认不做事:真实转换器已把 pptx 写在目标路径上)。 */
|
|
40
|
+
readonly writeBinary?: (absPath: string, data: Uint8Array) => Promise<void>;
|
|
41
|
+
/** 时间源(测试固定备份文件名)。 */
|
|
42
|
+
readonly now?: () => Date;
|
|
43
|
+
}
|
|
44
|
+
/** PPT 工具在论文工作区里的所有落盘位置。 */
|
|
45
|
+
export declare const PPT_RELS: {
|
|
46
|
+
/** 幻灯 Markdown(唯一真源)。 */
|
|
47
|
+
readonly slides: "07-答辩/PPT.md";
|
|
48
|
+
/** 外部转换器产出的 pptx。 */
|
|
49
|
+
readonly pptx: "07-答辩/PPT.pptx";
|
|
50
|
+
/** 转换说明(探测结果 + 确切命令,任何情况都写)。 */
|
|
51
|
+
readonly notes: "07-答辩/PPT转换说明.md";
|
|
52
|
+
/** 素材(输入,由 thesis_defense 生成)。 */
|
|
53
|
+
readonly materials: "07-答辩/答辩素材.md";
|
|
54
|
+
};
|
|
55
|
+
/** 动作返回:有界文本 + 结构化关键事实(测试与上层复用)。 */
|
|
56
|
+
export interface PptActionResult {
|
|
57
|
+
readonly text: string;
|
|
58
|
+
readonly plan?: SlidePlan;
|
|
59
|
+
readonly detection?: EngineDetection;
|
|
60
|
+
readonly conversion?: ConversionResult;
|
|
61
|
+
readonly error?: string;
|
|
62
|
+
}
|
|
63
|
+
/** 时间戳(本地时间,文件名安全:20260912-184800)。 */
|
|
64
|
+
export declare function timestamp(now: Date): string;
|
|
65
|
+
/** 由已装载的素材构建幻灯计划(工具与测试共用)。 */
|
|
66
|
+
export declare function planFromInputs(inputs: OutlineInputs, audience: Audience, pages: number): SlidePlan;
|
|
67
|
+
/** 定位论文仓库根;找不到即抛错(所有动作都要求在工作区内)。 */
|
|
68
|
+
export declare function requireRoot(fs: FileSystem, cwd: string | undefined, signal?: AbortSignal): Promise<string>;
|
|
69
|
+
/** outline 的文本摘要(有界)。 */
|
|
70
|
+
export declare function outlineSummary(plan: SlidePlan, slidesRel: string, notesRel: string, backupRel?: string): string;
|
|
71
|
+
/** `action=outline`:生成/刷新 `07-答辩/PPT.md`。 */
|
|
72
|
+
export declare function runOutline(fs: FileSystem, root: string, args: PptArgs, options: {
|
|
73
|
+
ppt: PptOptions;
|
|
74
|
+
}, signal?: AbortSignal, deps?: PptDeps): Promise<PptActionResult>;
|
|
75
|
+
/** check 的完整结果(含锚点落盘校验)。 */
|
|
76
|
+
export interface DeckCheck {
|
|
77
|
+
readonly result: CheckResult;
|
|
78
|
+
readonly anchorIssues: readonly SlideIssue[];
|
|
79
|
+
readonly text: string;
|
|
80
|
+
}
|
|
81
|
+
/** `action=check`:对已有 `07-答辩/PPT.md` 做确定性质量检查。 */
|
|
82
|
+
export declare function runCheckAction(fs: FileSystem, root: string, args: PptArgs, signal?: AbortSignal): Promise<DeckCheck>;
|
|
83
|
+
/** convert 的文本摘要(有界)。 */
|
|
84
|
+
export declare function convertSummary(slidesRel: string, pptxRel: string, notesRel: string, detection: EngineDetection, engine: PptOptions['engine'], result: ConversionResult | undefined, error: string | undefined): string;
|
|
85
|
+
/** `action=convert`:执行外部转换;失败时**先写说明再抛错**(绝不假装成功)。 */
|
|
86
|
+
export declare function runConvertAction(fs: FileSystem, root: string, options: {
|
|
87
|
+
ppt: PptOptions;
|
|
88
|
+
}, signal?: AbortSignal, deps?: PptDeps): Promise<PptActionResult>;
|
|
89
|
+
/** `action=guide`:只输出可复制执行的转换指引,不写盘、不执行。 */
|
|
90
|
+
export declare function runGuideAction(deps?: PptDeps): PptActionResult;
|
|
91
|
+
/**
|
|
92
|
+
* 按 action 分发。`convert` 失败时抛出的错误里**带上已经写好的说明**,
|
|
93
|
+
* 让模型看到的是一次真实失败 + 下一步命令,而不是沉默。
|
|
94
|
+
*/
|
|
95
|
+
export declare function runPptAction(fs: FileSystem, cwd: string | undefined, args: PptArgs, options: {
|
|
96
|
+
ppt: PptOptions;
|
|
97
|
+
}, signal?: AbortSignal, deps?: PptDeps): Promise<PptActionResult>;
|