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,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* thesis_build:产出论文 docx(+ 可选 pdf)。
|
|
3
|
+
*
|
|
4
|
+
* 引擎选择:
|
|
5
|
+
* - pandoc 存在时优先(--reference-doc 使用学校模板);
|
|
6
|
+
* - 无 pandoc 时使用内置零依赖转换器(lib/docx.ts),并在构建说明中
|
|
7
|
+
* 显式标注"内置过渡引擎,非学校模板排版"。
|
|
8
|
+
*
|
|
9
|
+
* 二进制例外(文档化):ctx.fs 无二进制写入能力,docx/pdf 字节由本工具
|
|
10
|
+
* 通过 node:fs 直接写入论文仓库 06-论文/产出/ 目录。文本工作全部走 ctx.fs。
|
|
11
|
+
*/
|
|
12
|
+
import * as nodePath from 'node:path';
|
|
13
|
+
import { readFile, rm, writeFile } from 'node:fs/promises';
|
|
14
|
+
import { tmpdir } from 'node:os';
|
|
15
|
+
import { describeFailure, spawnCommand } from "../../shared/spawn.js";
|
|
16
|
+
import { buildDocx } from "../lib/docx.js";
|
|
17
|
+
import { CHAPTER_META, isWrittenChapter } from "../lib/layout.js";
|
|
18
|
+
import { parseLedger } from "../lib/ledger.js";
|
|
19
|
+
import { findThesisRoot } from "../lib/project.js";
|
|
20
|
+
const OUT_DIR = '06-论文/产出';
|
|
21
|
+
const TEMPLATE_DIR = '06-论文/assets/学校模板';
|
|
22
|
+
async function readOptional(fs, absPath, signal) {
|
|
23
|
+
try {
|
|
24
|
+
return await fs.readText(await fs.resolve(absPath, { signal }), signal);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function listOptional(fs, absPath, signal) {
|
|
31
|
+
try {
|
|
32
|
+
const entries = await fs.listDir(await fs.resolve(absPath, { signal }), signal);
|
|
33
|
+
return entries.map(e => e.name);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function pandocAvailable() {
|
|
40
|
+
return spawnCommand('pandoc', ['--version'], { timeoutMs: PROBE_TIMEOUT_MS }).ok;
|
|
41
|
+
}
|
|
42
|
+
/** 引擎探测的超时(毫秒):探测不该拖住工具调用。 */
|
|
43
|
+
const PROBE_TIMEOUT_MS = 10_000;
|
|
44
|
+
/** 单次 pandoc 转换的超时(毫秒):大型论文 + 模板可能慢,但必须有上限。 */
|
|
45
|
+
const CONVERT_TIMEOUT_MS = 120_000;
|
|
46
|
+
export async function runBuild(fs, args, cwd, signal, writeBinary) {
|
|
47
|
+
const root = await findThesisRoot(fs, cwd, signal);
|
|
48
|
+
if (root === null)
|
|
49
|
+
throw new Error('未找到论文工作区。请先在论文仓库目录内操作。');
|
|
50
|
+
const format = args.format ?? 'docx';
|
|
51
|
+
if (format === 'pdf' && (args.engine ?? 'auto') === 'internal') {
|
|
52
|
+
throw new Error('内置引擎只能产出 docx。PDF 需要 pandoc(并安装 LaTeX 引擎)。');
|
|
53
|
+
}
|
|
54
|
+
// 章节收集(按模板顺序;只收录"已撰写"章节)
|
|
55
|
+
const chapterParts = [];
|
|
56
|
+
const included = [];
|
|
57
|
+
const missing = [];
|
|
58
|
+
for (const [index, meta] of CHAPTER_META.entries()) {
|
|
59
|
+
const text = await readOptional(fs, nodePath.join(root, '06-论文/章节', `${meta.file}.md`), signal);
|
|
60
|
+
if (text === undefined || !isWrittenChapter(text, index + 1, meta)) {
|
|
61
|
+
missing.push(meta.file);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
included.push(meta.file);
|
|
65
|
+
chapterParts.push(text);
|
|
66
|
+
}
|
|
67
|
+
if (included.length === 0)
|
|
68
|
+
throw new Error('没有任何已撰写章节(06-论文/章节/*.md 均为空)。');
|
|
69
|
+
// 标题:台账 state.title,缺省通用
|
|
70
|
+
let title = '本科毕业论文(设计)';
|
|
71
|
+
const ledgerText = await readOptional(fs, nodePath.join(root, '00-管理/进度台账.md'), signal);
|
|
72
|
+
if (ledgerText !== undefined) {
|
|
73
|
+
const parsed = parseLedger(ledgerText);
|
|
74
|
+
if (parsed.found && parsed.state.title !== undefined && parsed.state.title !== '')
|
|
75
|
+
title = parsed.state.title;
|
|
76
|
+
}
|
|
77
|
+
const markdown = chapterParts.join('\n\n');
|
|
78
|
+
// 模板探测
|
|
79
|
+
const templateFiles = await listOptional(fs, nodePath.join(root, TEMPLATE_DIR), signal);
|
|
80
|
+
const template = templateFiles.find(f => /\.(docx?|dotx?)$/i.test(f));
|
|
81
|
+
// 引擎选择
|
|
82
|
+
const requested = args.engine ?? 'auto';
|
|
83
|
+
const hasPandoc = pandocAvailable();
|
|
84
|
+
let engine;
|
|
85
|
+
if (requested === 'pandoc') {
|
|
86
|
+
if (!hasPandoc)
|
|
87
|
+
throw new Error('请求 pandoc 引擎但本机未安装 pandoc。请安装 pandoc 或改用 internal 引擎。');
|
|
88
|
+
engine = 'pandoc';
|
|
89
|
+
}
|
|
90
|
+
else if (requested === 'internal') {
|
|
91
|
+
engine = 'internal';
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
engine = hasPandoc ? 'pandoc' : 'internal';
|
|
95
|
+
}
|
|
96
|
+
// docx 字节
|
|
97
|
+
const docxPath = nodePath.join(root, OUT_DIR, '论文.docx');
|
|
98
|
+
let docxBytes;
|
|
99
|
+
if (engine === 'internal') {
|
|
100
|
+
docxBytes = buildDocx({ title, markdown });
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
// pandoc:合并内容写临时文件后转换
|
|
104
|
+
const tmpMd = nodePath.join(tmpdir(), `dsh-thesis-build-${process.pid}-${Date.now()}.md`);
|
|
105
|
+
await writeFile(tmpMd, `${title}\n\n${'='.repeat(Math.max(4, title.length))}\n\n${markdown}`, 'utf8');
|
|
106
|
+
try {
|
|
107
|
+
const argv = ['-f', 'markdown', '-t', 'docx', '-o', docxPath];
|
|
108
|
+
if (template !== undefined)
|
|
109
|
+
argv.push('--reference-doc', nodePath.join(root, TEMPLATE_DIR, template));
|
|
110
|
+
argv.push(tmpMd);
|
|
111
|
+
const result = spawnCommand('pandoc', argv, { timeoutMs: CONVERT_TIMEOUT_MS });
|
|
112
|
+
if (!result.ok)
|
|
113
|
+
throw new Error(`pandoc 转换失败:${describeFailure('pandoc', result)}`);
|
|
114
|
+
docxBytes = new Uint8Array(await readFile(docxPath));
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
await rm(tmpMd, { force: true }).catch(() => { });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
await writeBinary(docxPath, docxBytes);
|
|
121
|
+
// pdf(仅 pandoc)
|
|
122
|
+
let pdfPath;
|
|
123
|
+
if (format === 'pdf' || format === 'both') {
|
|
124
|
+
if (engine !== 'pandoc')
|
|
125
|
+
throw new Error('PDF 需要 pandoc。');
|
|
126
|
+
const out = nodePath.join(root, OUT_DIR, '论文.pdf');
|
|
127
|
+
const tmpMd = nodePath.join(tmpdir(), `dsh-thesis-build-${process.pid}-${Date.now()}.md`);
|
|
128
|
+
await writeFile(tmpMd, `${title}\n\n${'='.repeat(Math.max(4, title.length))}\n\n${markdown}`, 'utf8');
|
|
129
|
+
try {
|
|
130
|
+
const result = spawnCommand('pandoc', ['-f', 'markdown', '-t', 'pdf', '-o', out, tmpMd], { timeoutMs: CONVERT_TIMEOUT_MS });
|
|
131
|
+
if (!result.ok)
|
|
132
|
+
throw new Error(`PDF 转换失败(通常缺 LaTeX 引擎):${describeFailure('pandoc', result)}`);
|
|
133
|
+
pdfPath = out;
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
await rm(tmpMd, { force: true }).catch(() => { });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// 构建说明(文本,走 ctx.fs)
|
|
140
|
+
const note = [
|
|
141
|
+
'# 构建说明',
|
|
142
|
+
'',
|
|
143
|
+
`- 时间:${new Date().toISOString()}`,
|
|
144
|
+
`- 引擎:${engine === 'pandoc' ? 'pandoc' : '内置过渡引擎(非学校模板排版)'}`,
|
|
145
|
+
`- 学校模板:${template ?? '未发现(请将学校模板放入 06-论文/assets/学校模板/ 后重建)'}`,
|
|
146
|
+
`- 收录章节:${included.join('、') || '无'}`,
|
|
147
|
+
`- 未收录章节(空或缺失):${missing.join('、') || '无'}`,
|
|
148
|
+
`- 产物:论文.docx${pdfPath !== undefined ? '、论文.pdf' : ''}`,
|
|
149
|
+
'',
|
|
150
|
+
].join('\n');
|
|
151
|
+
await fs.writeText(await fs.resolve(nodePath.join(root, OUT_DIR, '构建说明.md'), { signal }), note, undefined, signal);
|
|
152
|
+
return { docxPath, ...(pdfPath !== undefined ? { pdfPath } : {}), engine, template, chapters: included, missing };
|
|
153
|
+
}
|
|
154
|
+
export function buildSummary(outcome) {
|
|
155
|
+
const lines = [
|
|
156
|
+
`构建完成:${outcome.docxPath}`,
|
|
157
|
+
...(outcome.pdfPath !== undefined ? [`PDF:${outcome.pdfPath}`] : []),
|
|
158
|
+
`- 引擎:${outcome.engine === 'pandoc' ? 'pandoc' : '内置过渡引擎(非学校模板排版,提交前请获取学校模板后用 pandoc 重建)'}`,
|
|
159
|
+
`- 学校模板:${outcome.template ?? '未发现'}`,
|
|
160
|
+
`- 收录章节:${outcome.chapters.join('、')}`,
|
|
161
|
+
...(outcome.missing.length > 0 ? [`- 未收录(空/缺失):${outcome.missing.join('、')}`] : []),
|
|
162
|
+
'',
|
|
163
|
+
'打开 论文.docx 检查排版;构建细节见 06-论文/产出/构建说明.md。',
|
|
164
|
+
];
|
|
165
|
+
return lines.join('\n');
|
|
166
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* thesis_check:全文综合检查(五项),产出两份报告:
|
|
3
|
+
* - 08-合规/引用检查报告/检查-<date>.md(引用双向一致)
|
|
4
|
+
* - 08-合规/格式检查报告/检查-<date>.md(字数/图表/术语/模板)
|
|
5
|
+
*/
|
|
6
|
+
import type { FileSystem } from '@deepseek-ai/dsh-fs';
|
|
7
|
+
export declare function runCheck(fs: FileSystem, cwd: string | undefined, signal?: AbortSignal): Promise<string>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* thesis_check:全文综合检查(五项),产出两份报告:
|
|
3
|
+
* - 08-合规/引用检查报告/检查-<date>.md(引用双向一致)
|
|
4
|
+
* - 08-合规/格式检查报告/检查-<date>.md(字数/图表/术语/模板)
|
|
5
|
+
*/
|
|
6
|
+
import * as nodePath from 'node:path';
|
|
7
|
+
import { runGlobalCheck, renderCheckReport } from "../lib/check.js";
|
|
8
|
+
import { CHAPTER_META, isWrittenChapter } from "../lib/layout.js";
|
|
9
|
+
import { REFS_BIB_REL } from "../lib/lit-cache.js";
|
|
10
|
+
import { findThesisRoot } from "../lib/project.js";
|
|
11
|
+
export async function runCheck(fs, cwd, signal) {
|
|
12
|
+
const root = await findThesisRoot(fs, cwd, signal);
|
|
13
|
+
if (root === null)
|
|
14
|
+
throw new Error('未找到论文工作区。请先在论文仓库目录内操作。');
|
|
15
|
+
// 章节收集(只检查"已撰写"章节,脚手架列入未撰写清单)
|
|
16
|
+
const chapters = [];
|
|
17
|
+
const missing = [];
|
|
18
|
+
for (const [index, meta] of CHAPTER_META.entries()) {
|
|
19
|
+
let text;
|
|
20
|
+
try {
|
|
21
|
+
text = await fs.readText(await fs.resolve(nodePath.join(root, '06-论文/章节', `${meta.file}.md`), { signal }), signal);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
text = undefined;
|
|
25
|
+
}
|
|
26
|
+
if (text === undefined || !isWrittenChapter(text, index + 1, meta)) {
|
|
27
|
+
missing.push(meta.file);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
chapters.push({ name: meta.file, no: index + 1, text });
|
|
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
|
+
// 文献库缺失:引用检查会如实报告。
|
|
38
|
+
}
|
|
39
|
+
let templateFiles = [];
|
|
40
|
+
try {
|
|
41
|
+
const entries = await fs.listDir(await fs.resolve(nodePath.join(root, '06-论文/assets/学校模板'), { signal }), signal);
|
|
42
|
+
templateFiles = entries.map(e => e.name);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// 目录缺失:模板检查会报告。
|
|
46
|
+
}
|
|
47
|
+
const report = runGlobalCheck({ chapters, bibText, templateFiles });
|
|
48
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
49
|
+
// 引用检查报告
|
|
50
|
+
const citationSection = report.sections.find(s => s.id === 'citations');
|
|
51
|
+
const citationReport = [
|
|
52
|
+
`# 引用检查报告(${date})`,
|
|
53
|
+
'',
|
|
54
|
+
'> 由 thesis_check 自动生成。检查:正文引用 ↔ refs.bib 双向一致。',
|
|
55
|
+
'',
|
|
56
|
+
`## ${citationSection.title}`,
|
|
57
|
+
'',
|
|
58
|
+
...citationSection.lines,
|
|
59
|
+
'',
|
|
60
|
+
].join('\n');
|
|
61
|
+
await fs.writeText(await fs.resolve(nodePath.join(root, '08-合规/引用检查报告', `检查-${date}.md`), { signal }), citationReport, undefined, signal);
|
|
62
|
+
// 格式检查报告
|
|
63
|
+
const formatSections = report.sections.filter(s => s.id !== 'citations');
|
|
64
|
+
const formatReport = [
|
|
65
|
+
`# 格式检查报告(${date})`,
|
|
66
|
+
'',
|
|
67
|
+
'> 由 thesis_check 自动生成。检查:字数、图/表编号、术语定义顺序、学校模板。',
|
|
68
|
+
'',
|
|
69
|
+
...(missing.length > 0 ? [`⚠ 未撰写章节:${missing.join('、')}(不计入检查)。`, ''] : []),
|
|
70
|
+
...formatSections.flatMap(section => [`## ${section.title}`, '', ...section.lines, '']),
|
|
71
|
+
].join('\n');
|
|
72
|
+
await fs.writeText(await fs.resolve(nodePath.join(root, '08-合规/格式检查报告', `检查-${date}.md`), { signal }), formatReport, undefined, signal);
|
|
73
|
+
return [
|
|
74
|
+
...(missing.length > 0 ? [`⚠ 未撰写章节:${missing.join('、')}(不计入检查)。`, ''] : []),
|
|
75
|
+
renderCheckReport(report),
|
|
76
|
+
'',
|
|
77
|
+
`报告已写入:08-合规/引用检查报告/检查-${date}.md、08-合规/格式检查报告/检查-${date}.md`,
|
|
78
|
+
].join('\n');
|
|
79
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* thesis_decide:决定日志追加(00-管理/决定日志.md)。
|
|
3
|
+
*
|
|
4
|
+
* 决定日志是答辩留痕的核心证据:每个关键决定记录内容、理由、备选。
|
|
5
|
+
*/
|
|
6
|
+
import type { FileSystem } from '@deepseek-ai/dsh-fs';
|
|
7
|
+
export interface DecisionArgs {
|
|
8
|
+
title?: string;
|
|
9
|
+
content: string;
|
|
10
|
+
reason?: string;
|
|
11
|
+
alternatives?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function runDecision(fs: FileSystem, args: DecisionArgs, cwd: string | undefined, signal?: AbortSignal): Promise<string>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* thesis_decide:决定日志追加(00-管理/决定日志.md)。
|
|
3
|
+
*
|
|
4
|
+
* 决定日志是答辩留痕的核心证据:每个关键决定记录内容、理由、备选。
|
|
5
|
+
*/
|
|
6
|
+
import * as nodePath from 'node:path';
|
|
7
|
+
import { describeReadFailure, isMissingError } from "../../shared/fs-errors.js";
|
|
8
|
+
import { appendDecision, countDecisions } from "../lib/ledger.js";
|
|
9
|
+
import { DECISION_REL, findThesisRoot } from "../lib/project.js";
|
|
10
|
+
const DECISION_HEADER = `# 决定日志
|
|
11
|
+
|
|
12
|
+
> 每个关键决定的留痕:内容、理由、备选。答辩时它是"我为什么这么写"的证据。
|
|
13
|
+
> 由 thesis_decide 工具或 /thesis-decide 命令追加。
|
|
14
|
+
|
|
15
|
+
`;
|
|
16
|
+
function deriveTitle(content) {
|
|
17
|
+
const firstLine = content.split('\n')[0].trim();
|
|
18
|
+
const base = firstLine.length > 0 ? firstLine : '未命名决定';
|
|
19
|
+
return base.length > 24 ? `${base.slice(0, 24)}…` : base;
|
|
20
|
+
}
|
|
21
|
+
export async function runDecision(fs, args, cwd, signal) {
|
|
22
|
+
if (args.content === undefined || args.content.trim() === '') {
|
|
23
|
+
throw new Error('content 必填:决定的实际内容。');
|
|
24
|
+
}
|
|
25
|
+
const root = await findThesisRoot(fs, cwd, signal);
|
|
26
|
+
if (root === null) {
|
|
27
|
+
throw new Error('未找到论文工作区(向上查找 00-管理/进度台账.md 失败)。请先用 thesis_init 创建工作区,或在论文仓库目录内操作。');
|
|
28
|
+
}
|
|
29
|
+
const path = nodePath.join(root, DECISION_REL);
|
|
30
|
+
let raw;
|
|
31
|
+
try {
|
|
32
|
+
raw = await fs.readText(await fs.resolve(path, { signal }), signal);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
// 只有「确实还没有决定日志」才用表头开始:读失败时若继续,会把已有的决定全部覆盖掉。
|
|
36
|
+
if (!isMissingError(error)) {
|
|
37
|
+
throw new Error(describeReadFailure('决定日志(00-管理/决定日志.md)', path, error, signal));
|
|
38
|
+
}
|
|
39
|
+
raw = DECISION_HEADER;
|
|
40
|
+
}
|
|
41
|
+
if (raw.trim() === '')
|
|
42
|
+
raw = DECISION_HEADER;
|
|
43
|
+
const entry = {
|
|
44
|
+
date: new Date().toISOString().slice(0, 10),
|
|
45
|
+
title: args.title?.trim() || deriveTitle(args.content),
|
|
46
|
+
content: args.content.trim(),
|
|
47
|
+
...(args.reason?.trim() ? { reason: args.reason.trim() } : {}),
|
|
48
|
+
...(args.alternatives?.trim() ? { alternatives: args.alternatives.trim() } : {}),
|
|
49
|
+
};
|
|
50
|
+
const next = appendDecision(raw, entry);
|
|
51
|
+
await fs.writeText(await fs.resolve(path, { signal }), next, undefined, signal);
|
|
52
|
+
return `已记录决定 #${countDecisions(next)}「${entry.title}」(${entry.date})。决定日志:${path}`;
|
|
53
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* thesis_defense:提取答辩素材 + 生成预答辩问题库骨架。
|
|
3
|
+
*
|
|
4
|
+
* 产出(写入 07-答辩/):
|
|
5
|
+
* - 答辩素材.md —— 课题/章节概览/技术选型/关键决定/测试数据/git 工作量/文献
|
|
6
|
+
* - 预答辩问题库.md —— 六类必问问题模板 + 证据锚点
|
|
7
|
+
* 具体问题生成与模拟问答由 AI(依据素材)与用户完成,流程见技能 thesis-defense。
|
|
8
|
+
*/
|
|
9
|
+
import type { FileSystem } from '@deepseek-ai/dsh-fs';
|
|
10
|
+
import { type GitRunner } from '../lib/defense.ts';
|
|
11
|
+
export declare function runDefensePrep(fs: FileSystem, cwd: string | undefined, signal: AbortSignal | undefined, gitRunner?: GitRunner): Promise<string>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* thesis_defense:提取答辩素材 + 生成预答辩问题库骨架。
|
|
3
|
+
*
|
|
4
|
+
* 产出(写入 07-答辩/):
|
|
5
|
+
* - 答辩素材.md —— 课题/章节概览/技术选型/关键决定/测试数据/git 工作量/文献
|
|
6
|
+
* - 预答辩问题库.md —— 六类必问问题模板 + 证据锚点
|
|
7
|
+
* 具体问题生成与模拟问答由 AI(依据素材)与用户完成,流程见技能 thesis-defense。
|
|
8
|
+
*/
|
|
9
|
+
import * as nodePath from 'node:path';
|
|
10
|
+
import { parseBibKeysOrdered } from "../lib/bibtex.js";
|
|
11
|
+
import { countTestCases, extractDecisionTitles, extractHeadings, extractTechChoices, renderDefenseMaterials, renderQuestionBank, spawnGitStats, } from "../lib/defense.js";
|
|
12
|
+
import { CHAPTER_META, isWrittenChapter } from "../lib/layout.js";
|
|
13
|
+
import { parseLedger } from "../lib/ledger.js";
|
|
14
|
+
import { findThesisRoot } from "../lib/project.js";
|
|
15
|
+
export async function runDefensePrep(fs, cwd, signal, gitRunner = spawnGitStats) {
|
|
16
|
+
const root = await findThesisRoot(fs, cwd, signal);
|
|
17
|
+
if (root === null)
|
|
18
|
+
throw new Error('未找到论文工作区。请先在论文仓库目录内操作。');
|
|
19
|
+
const rootDir = root;
|
|
20
|
+
async function readOptional(rel) {
|
|
21
|
+
try {
|
|
22
|
+
return await fs.readText(await fs.resolve(nodePath.join(rootDir, rel), { signal }), signal);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return '';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
let title = '本科毕业论文(设计)';
|
|
29
|
+
const ledger = parseLedger(await readOptional('00-管理/进度台账.md'));
|
|
30
|
+
if (ledger.found && ledger.state.title !== undefined && ledger.state.title !== '')
|
|
31
|
+
title = ledger.state.title;
|
|
32
|
+
// 章节概览(只统计已撰写章节)
|
|
33
|
+
const chapters = [];
|
|
34
|
+
for (const [index, meta] of CHAPTER_META.entries()) {
|
|
35
|
+
const text = await readOptional(nodePath.join('06-论文/章节', `${meta.file}.md`));
|
|
36
|
+
if (!isWrittenChapter(text, index + 1, meta))
|
|
37
|
+
continue;
|
|
38
|
+
chapters.push({
|
|
39
|
+
name: meta.file,
|
|
40
|
+
no: index + 1,
|
|
41
|
+
title: meta.title,
|
|
42
|
+
cjk: (text.match(/[\u4e00-\u9fff]/g) ?? []).length,
|
|
43
|
+
headings: extractHeadings(text),
|
|
44
|
+
citations: (text.match(/\[\d+(?:[-,,]\d+)*\]/g) ?? []).length,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
const techChoices = extractTechChoices(await readOptional('03-设计/技术选型论证.md'));
|
|
48
|
+
const decisions = extractDecisionTitles(await readOptional('00-管理/决定日志.md'));
|
|
49
|
+
const testCases = countTestCases(await readOptional('05-实验测试/测试计划.md'));
|
|
50
|
+
let resultFiles = [];
|
|
51
|
+
try {
|
|
52
|
+
const entries = await fs.listDir(await fs.resolve(nodePath.join(root, '05-实验测试/结果'), { signal }), signal);
|
|
53
|
+
resultFiles = entries.map(e => e.name).filter(n => !n.startsWith('.'));
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// 结果目录缺失:素材里如实提示。
|
|
57
|
+
}
|
|
58
|
+
const bibCount = parseBibKeysOrdered(await readOptional('02-文献/refs.bib')).length;
|
|
59
|
+
const git = gitRunner(root);
|
|
60
|
+
const materials = renderDefenseMaterials({ title, chapters, techChoices, decisions, testCases, resultFiles, bibCount, git });
|
|
61
|
+
const questions = renderQuestionBank(title);
|
|
62
|
+
await fs.writeText(await fs.resolve(nodePath.join(root, '07-答辩/答辩素材.md'), { signal }), materials, undefined, signal);
|
|
63
|
+
await fs.writeText(await fs.resolve(nodePath.join(root, '07-答辩/预答辩问题库.md'), { signal }), questions, undefined, signal);
|
|
64
|
+
return [
|
|
65
|
+
`答辩素材与问题库已生成:`,
|
|
66
|
+
`- 07-答辩/答辩素材.md(课题"${title}";已撰写 ${chapters.length} 章;技术选型 ${techChoices.length} 项;决定 ${decisions.length} 条;测试用例 ${testCases} 个;git ${git.ok ? `${git.commits} 次提交` : '统计不可用'})`,
|
|
67
|
+
`- 07-答辩/预答辩问题库.md(六类 ${questions.match(/- \[ \]/g)?.length ?? 0} 个问题骨架)`,
|
|
68
|
+
'',
|
|
69
|
+
'下一步(流程见技能 thesis-defense):',
|
|
70
|
+
'1. AI 依据《答辩素材》把问题库的 <> 占位替换为你的系统实际名称,生成 15-20 个具体问题',
|
|
71
|
+
'2. 你逐题回答,答不上的回对应材料补学;至少完整演练 2 轮,第 2 轮随机抽题',
|
|
72
|
+
'3. 演练完成后更新 07-答辩/问答演练.md,并用 thesis_progress 推进阶段 9 任务',
|
|
73
|
+
].join('\n');
|
|
74
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* thesis_init:一键生成论文工作区(DESIGN.md §5 目录结构 + 技能包 + git 初始化)。
|
|
3
|
+
*
|
|
4
|
+
* 校验规则:
|
|
5
|
+
* - root 必须是绝对路径;
|
|
6
|
+
* - 目标目录存在且非空时拒绝(除非 force),绝不覆盖已有文件。
|
|
7
|
+
*/
|
|
8
|
+
import type { FileSystem } from '@deepseek-ai/dsh-fs';
|
|
9
|
+
export interface InitArgs {
|
|
10
|
+
root: string;
|
|
11
|
+
title?: string;
|
|
12
|
+
git?: boolean;
|
|
13
|
+
force?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface InitOutcome {
|
|
16
|
+
readonly root: string;
|
|
17
|
+
readonly files: number;
|
|
18
|
+
readonly skills: number;
|
|
19
|
+
readonly git: 'ok' | 'skipped' | 'failed';
|
|
20
|
+
readonly gitDetail?: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function runInit(fs: FileSystem, args: InitArgs, skillsDir: string, signal?: AbortSignal): Promise<InitOutcome>;
|
|
23
|
+
export declare function initSummary(outcome: InitOutcome): string;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* thesis_init:一键生成论文工作区(DESIGN.md §5 目录结构 + 技能包 + git 初始化)。
|
|
3
|
+
*
|
|
4
|
+
* 校验规则:
|
|
5
|
+
* - root 必须是绝对路径;
|
|
6
|
+
* - 目标目录存在且非空时拒绝(除非 force),绝不覆盖已有文件。
|
|
7
|
+
*/
|
|
8
|
+
import * as nodePath from 'node:path';
|
|
9
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
10
|
+
import { describeFailure, spawnCommand } from "../../shared/spawn.js";
|
|
11
|
+
import { buildLayout } from "../lib/layout.js";
|
|
12
|
+
async function dirEntries(fs, dir) {
|
|
13
|
+
try {
|
|
14
|
+
const target = await fs.resolve(dir);
|
|
15
|
+
const entries = await fs.listDir(target);
|
|
16
|
+
return entries.map(e => e.name);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async function copySkills(fs, skillsDir, root) {
|
|
23
|
+
let count = 0;
|
|
24
|
+
let names = [];
|
|
25
|
+
try {
|
|
26
|
+
names = await readdir(skillsDir);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return count;
|
|
30
|
+
}
|
|
31
|
+
for (const name of names) {
|
|
32
|
+
if (!/^[a-z0-9-]+$/.test(name))
|
|
33
|
+
continue;
|
|
34
|
+
try {
|
|
35
|
+
const body = await readFile(nodePath.join(skillsDir, name, 'SKILL.md'), 'utf8');
|
|
36
|
+
const target = await fs.resolve(nodePath.join(root, '.dsh', 'skills', name, 'SKILL.md'));
|
|
37
|
+
await fs.writeText(target, body);
|
|
38
|
+
count += 1;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// 该目录不是技能包,跳过。
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return count;
|
|
45
|
+
}
|
|
46
|
+
function initGit(root) {
|
|
47
|
+
const init = spawnCommand('git', ['init', '-q'], { cwd: root });
|
|
48
|
+
if (!init.ok)
|
|
49
|
+
return { git: 'failed', gitDetail: describeFailure('git init', init) };
|
|
50
|
+
const add = spawnCommand('git', ['add', '-A'], { cwd: root });
|
|
51
|
+
if (!add.ok)
|
|
52
|
+
return { git: 'failed', gitDetail: describeFailure('git add', add) };
|
|
53
|
+
const commit = spawnCommand('git', ['-c', 'user.name=dsh-thesis', '-c', 'user.email=dsh-thesis@local', 'commit', '-q', '-m', 'chore: dsh-thesis 初始化论文工作区'], { cwd: root });
|
|
54
|
+
if (!commit.ok)
|
|
55
|
+
return { git: 'failed', gitDetail: describeFailure('git commit', commit) };
|
|
56
|
+
return { git: 'ok' };
|
|
57
|
+
}
|
|
58
|
+
export async function runInit(fs, args, skillsDir, signal) {
|
|
59
|
+
if (args.root === undefined || args.root.trim() === '') {
|
|
60
|
+
throw new Error('root 参数必填:论文工作区根目录的绝对路径。');
|
|
61
|
+
}
|
|
62
|
+
if (!nodePath.isAbsolute(args.root)) {
|
|
63
|
+
throw new Error(`root 必须是绝对路径,收到:${args.root}。`);
|
|
64
|
+
}
|
|
65
|
+
const root = nodePath.normalize(args.root);
|
|
66
|
+
const existing = await dirEntries(fs, root);
|
|
67
|
+
if (existing !== null && existing.length > 0 && args.force !== true) {
|
|
68
|
+
throw new Error(`目录 ${root} 非空(已有 ${existing.length} 项)。为避免覆盖数据,请选择空目录或新目录;确认要写入现有目录请设 force: true(不会覆盖同名文件,仍会失败于冲突)。`);
|
|
69
|
+
}
|
|
70
|
+
const now = new Date();
|
|
71
|
+
const date = now.toISOString().slice(0, 10);
|
|
72
|
+
const title = args.title?.trim() || '本科毕业设计';
|
|
73
|
+
const files = buildLayout({ title, date });
|
|
74
|
+
let written = 0;
|
|
75
|
+
for (const file of files) {
|
|
76
|
+
const target = await fs.resolve(nodePath.join(root, file.path), { signal });
|
|
77
|
+
await fs.writeText(target, file.content, undefined, signal);
|
|
78
|
+
written += 1;
|
|
79
|
+
}
|
|
80
|
+
const skills = await copySkills(fs, skillsDir, root);
|
|
81
|
+
const gitResult = args.git === false ? { git: 'skipped' } : initGit(root);
|
|
82
|
+
return { root, files: written, skills, ...gitResult };
|
|
83
|
+
}
|
|
84
|
+
export function initSummary(outcome) {
|
|
85
|
+
const lines = [
|
|
86
|
+
`论文工作区已创建:${outcome.root}`,
|
|
87
|
+
`- 模板文件:${outcome.files} 个(含目录结构、进度台账、决定日志、章节模板)`,
|
|
88
|
+
`- 技能包:${outcome.skills} 个(.dsh/skills/,随仓库版本化)`,
|
|
89
|
+
];
|
|
90
|
+
if (outcome.git === 'ok')
|
|
91
|
+
lines.push('- git:已初始化并完成首次提交');
|
|
92
|
+
else if (outcome.git === 'skipped')
|
|
93
|
+
lines.push('- git:按请求跳过初始化');
|
|
94
|
+
else
|
|
95
|
+
lines.push(`- git:初始化失败(${outcome.gitDetail ?? '未知原因'}),请手动执行 git init`);
|
|
96
|
+
lines.push('');
|
|
97
|
+
lines.push('下一步:让 AI 运行"选题工作坊"(thesis-opener 技能),从阶段 1 开始。');
|
|
98
|
+
return lines.join('\n');
|
|
99
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
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 type { FileSystem } from '@deepseek-ai/dsh-fs';
|
|
10
|
+
import { LIT_SOURCES, type FetchLike, type LitSource } from '../lib/lit-api.ts';
|
|
11
|
+
export interface LitSearchArgs {
|
|
12
|
+
query: string;
|
|
13
|
+
year_from?: number;
|
|
14
|
+
limit?: number;
|
|
15
|
+
source?: LitSource;
|
|
16
|
+
}
|
|
17
|
+
export interface LitSaveArgs {
|
|
18
|
+
ids: string[];
|
|
19
|
+
}
|
|
20
|
+
export interface LitNoteArgs {
|
|
21
|
+
ref: string;
|
|
22
|
+
force?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export declare function runLitSearch(fs: FileSystem, args: LitSearchArgs, cwd: string | undefined, signal: AbortSignal | undefined, fetchImpl: FetchLike): Promise<string>;
|
|
25
|
+
export declare function runLitSave(fs: FileSystem, args: LitSaveArgs, cwd: string | undefined, signal?: AbortSignal): Promise<string>;
|
|
26
|
+
export declare function runLitNote(fs: FileSystem, args: LitNoteArgs, cwd: string | undefined, signal?: AbortSignal): Promise<string>;
|
|
27
|
+
export { LIT_SOURCES };
|