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,720 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 材料到手后的逐题追问:问题库、排序裁剪、材料感知。
|
|
3
|
+
*
|
|
4
|
+
* 设计要点(与 `gates/detect.ts` 同一套「便宜、面向人、有终止条件」):
|
|
5
|
+
* 1. **一次只问一个**:本模块只回答「下一个该问什么」,绝不在一次返回里塞多个问题;
|
|
6
|
+
* 2. **可证伪**:每个问题都写清 `why`(不答会返什么工)、`affects`(影响哪个产出物)、
|
|
7
|
+
* `answerShape`(什么样的答案算合格),使回答能被机器写进意图规格并被后续阶段校验;
|
|
8
|
+
* 3. **材料感知**:`thesis_ingest` 已经把材料读进工作区,凡是材料里已经有的答案就
|
|
9
|
+
* **不再重复问**——能自动回答的标为「待用户确认」,能安全推断的直接免问。
|
|
10
|
+
* 这是「一次到位」的关键:问用户之前先问工作区。
|
|
11
|
+
*
|
|
12
|
+
* 本文件是纯函数模块:零 I/O、零宿主依赖,输入 `IntakeState` + {@link IntakeContext},
|
|
13
|
+
* 输出结构化的下一个问题。所有排序都是全序(priority → 问题库顺序),结果确定。
|
|
14
|
+
*
|
|
15
|
+
* @module dsh-thesis/intake
|
|
16
|
+
*/
|
|
17
|
+
/** 每个产出物由哪些工具兑现(工具名是契约,见 DESIGN.md §5)。 */
|
|
18
|
+
export const AFFECT_TOOLS = {
|
|
19
|
+
格式检查: ['thesis_check'],
|
|
20
|
+
引用规范: ['thesis_lit_search', 'thesis_lit_save', 'thesis_lit_note', 'thesis_check'],
|
|
21
|
+
字数分配: ['thesis_review', 'thesis_check'],
|
|
22
|
+
写作规范: ['thesis_stylecheck'],
|
|
23
|
+
原创性: ['thesis_originality'],
|
|
24
|
+
答案PPT: ['thesis_slides'],
|
|
25
|
+
答辩问答: ['thesis_defense', 'defense_code_status', 'defense_code_next', 'defense_code_update'],
|
|
26
|
+
文献检索: ['thesis_lit_search'],
|
|
27
|
+
};
|
|
28
|
+
/** 读不到台账时的默认阶段(任务约定:按「开题前」处理)。 */
|
|
29
|
+
export const DEFAULT_STAGE = '开题前';
|
|
30
|
+
/** 空材料摘要:没有清单时的默认值。 */
|
|
31
|
+
export const EMPTY_MATERIALS = { found: false, items: [] };
|
|
32
|
+
/** 全部规格分节(渲染顺序即此顺序,保证幂等)。 */
|
|
33
|
+
export const INTAKE_SECTIONS = [
|
|
34
|
+
'学校与规范',
|
|
35
|
+
'课题与目标',
|
|
36
|
+
'时间线',
|
|
37
|
+
'系统与数据',
|
|
38
|
+
'文献与引用',
|
|
39
|
+
'交付形式',
|
|
40
|
+
];
|
|
41
|
+
/** 阶段 → 优先命中的产出物;用于把「当前阶段最要紧的问题」排前面。 */
|
|
42
|
+
export const STAGE_AFFECTS = {
|
|
43
|
+
选题: ['字数分配', '答辩问答'],
|
|
44
|
+
开题: ['引用规范', '字数分配'],
|
|
45
|
+
文献调研: ['引用规范'],
|
|
46
|
+
系统设计: ['答辩问答'],
|
|
47
|
+
系统实现: ['答辩问答'],
|
|
48
|
+
系统测试: ['答辩问答'],
|
|
49
|
+
论文撰写: ['字数分配', '原创性', '格式检查'],
|
|
50
|
+
定稿与合规: ['格式检查', '原创性', '引用规范'],
|
|
51
|
+
答辩: ['答案PPT', '答辩问答'],
|
|
52
|
+
};
|
|
53
|
+
/** 按当前阶段给 `affects` 加权重(分数越小越先问)。 */
|
|
54
|
+
function affectRank(question, stage) {
|
|
55
|
+
const wanted = STAGE_AFFECTS[stage];
|
|
56
|
+
if (wanted === undefined)
|
|
57
|
+
return 0;
|
|
58
|
+
for (const affect of question.affects) {
|
|
59
|
+
if (wanted.includes(affect))
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
return 1;
|
|
63
|
+
}
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// 轻校验工具(全部可证伪:错就给一句具体的话,不阻止用户)
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
function text(answer, min = 2) {
|
|
68
|
+
const trimmed = answer.trim();
|
|
69
|
+
if (trimmed.length === 0)
|
|
70
|
+
return { ok: false, note: '答案为空。' };
|
|
71
|
+
if (trimmed.length < min)
|
|
72
|
+
return { ok: false, note: `「${trimmed}」太短,请写完整名称。` };
|
|
73
|
+
return { ok: true };
|
|
74
|
+
}
|
|
75
|
+
/** 从答案里取第一个整数("3 万字" → 3;"12000" → 12000)。 */
|
|
76
|
+
export function firstNumber(answer) {
|
|
77
|
+
const m = /\d+(?:\.\d+)?/.exec(answer.replace(/,/g, ''));
|
|
78
|
+
if (m === null)
|
|
79
|
+
return undefined;
|
|
80
|
+
const n = Number(m[0]);
|
|
81
|
+
return Number.isFinite(n) ? n : undefined;
|
|
82
|
+
}
|
|
83
|
+
function positiveNumber(answer, min, max, what) {
|
|
84
|
+
const n = firstNumber(answer);
|
|
85
|
+
if (n === undefined)
|
|
86
|
+
return { ok: false, note: `没看到数字。「${what}」请给一个数值,例:${what} 15000。` };
|
|
87
|
+
if (n < min || n > max)
|
|
88
|
+
return { ok: false, note: `${n} 不像合理的${what}(预期 ${min}–${max})。请核对后重答。` };
|
|
89
|
+
return { ok: true };
|
|
90
|
+
}
|
|
91
|
+
function ratio(answer) {
|
|
92
|
+
const n = firstNumber(answer);
|
|
93
|
+
if (n === undefined)
|
|
94
|
+
return { ok: false, note: '没看到数字。重复率阈值请给百分数,例:15%。' };
|
|
95
|
+
if (n <= 0 || n > 60)
|
|
96
|
+
return { ok: false, note: `${n}% 不像合理的重复率阈值(预期 0–60%)。请核对学校文件。` };
|
|
97
|
+
return { ok: true };
|
|
98
|
+
}
|
|
99
|
+
/** 解析 `2026-03-01` / `2026/3/1` / `2026年3月1日` / `3月1日`。 */
|
|
100
|
+
function parseDate(answer) {
|
|
101
|
+
const withYear = /(\d{4})\s*[-/.年]\s*(\d{1,2})\s*[-/.月]\s*(\d{1,2})\s*日?/.exec(answer);
|
|
102
|
+
if (withYear !== null) {
|
|
103
|
+
return { year: Number(withYear[1]), month: Number(withYear[2]), day: Number(withYear[3]) };
|
|
104
|
+
}
|
|
105
|
+
const noYear = /(\d{1,2})\s*月\s*(\d{1,2})\s*日?/.exec(answer);
|
|
106
|
+
if (noYear !== null)
|
|
107
|
+
return { month: Number(noYear[1]), day: Number(noYear[2]) };
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* 日期合理性检查。只拦「明显不可能」的日期(如 2026-13-45),
|
|
112
|
+
* 不判断日期是否已过——过期提醒是台账的职责,不是追问的职责。
|
|
113
|
+
*/
|
|
114
|
+
export function validateDeadline(answer, now = new Date()) {
|
|
115
|
+
const d = parseDate(answer);
|
|
116
|
+
if (d === undefined) {
|
|
117
|
+
return { ok: false, note: '没看到日期。请用 YYYY-MM-DD 或「2026年3月1日」这样的写法。' };
|
|
118
|
+
}
|
|
119
|
+
if (d.month < 1 || d.month > 12 || d.day < 1 || d.day > 31) {
|
|
120
|
+
return { ok: false, note: `「${d.month} 月 ${d.day} 日」不是合法日期,请核对。` };
|
|
121
|
+
}
|
|
122
|
+
if (d.year !== undefined) {
|
|
123
|
+
const thisYear = now.getFullYear();
|
|
124
|
+
if (d.year < thisYear - 1 || d.year > thisYear + 6) {
|
|
125
|
+
return { ok: false, note: `年份 ${d.year} 超出合理区间(${thisYear - 1}–${thisYear + 6}),请核对。` };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
return { ok: true, note: '已记录月日;未写年份,请确认是否为本学年日期。' };
|
|
130
|
+
}
|
|
131
|
+
return { ok: true };
|
|
132
|
+
}
|
|
133
|
+
function yearRange(answer) {
|
|
134
|
+
const years = (answer.match(/\d{4}/g) ?? []).map(Number);
|
|
135
|
+
if (years.length === 0)
|
|
136
|
+
return { ok: true, note: '未写年限;建议补一句「近 5 年文献不少于一半」,引用检查时要用。' };
|
|
137
|
+
const from = years[0];
|
|
138
|
+
if (from < 1950 || from > 2100)
|
|
139
|
+
return { ok: false, note: `年份 ${from} 不像合理区间下限,请核对。` };
|
|
140
|
+
return { ok: true };
|
|
141
|
+
}
|
|
142
|
+
function citationFormat(answer) {
|
|
143
|
+
const t = answer.trim();
|
|
144
|
+
if (t.length === 0)
|
|
145
|
+
return { ok: false, note: '答案为空。' };
|
|
146
|
+
const hit = /7714|GB|APA|MLA|IEEE|ACM|哈佛|学校模板|按学校|模板/.test(t);
|
|
147
|
+
if (!hit)
|
|
148
|
+
return { ok: true, note: '无法从答案判定标准号。请确认是「GB/T 7714」还是学校模板里的自有格式——这决定引用列表的排版。' };
|
|
149
|
+
return { ok: true };
|
|
150
|
+
}
|
|
151
|
+
function fileOrNone(answer) {
|
|
152
|
+
const t = answer.trim();
|
|
153
|
+
if (t.length === 0)
|
|
154
|
+
return { ok: false, note: '答案为空。' };
|
|
155
|
+
if (/^(无|没有|暂无|未提供|未拿到|找不到|不知道|不清楚|无模板|没有模板)/.test(t)) {
|
|
156
|
+
return { ok: true, note: '按「无学校模板」处理:构建论文时走通用过渡模板,并在产出里显式标注。若之后拿到模板,用同一问题重答一次即可。' };
|
|
157
|
+
}
|
|
158
|
+
if (!/\.(docx?|dotx?|pdf|md|txt)\b/i.test(t) && !/[\\/]/.test(t)) {
|
|
159
|
+
return { ok: false, note: `「${t}」看起来不是文件路径。请给出文件在工作区里的相对路径(例:00-管理/学校模板.docx),或回「没有」。` };
|
|
160
|
+
}
|
|
161
|
+
return { ok: true };
|
|
162
|
+
}
|
|
163
|
+
function pathOrNone(answer) {
|
|
164
|
+
const t = answer.trim();
|
|
165
|
+
if (t.length === 0)
|
|
166
|
+
return { ok: false, note: '答案为空。' };
|
|
167
|
+
if (/^(无|没有|暂无|还没有|未开始|从零|待开发)/.test(t)) {
|
|
168
|
+
return { ok: true, note: '按「尚无代码」处理:实现阶段从零开始,进度台账里的实现任务按新项目排。' };
|
|
169
|
+
}
|
|
170
|
+
if (!/[\\/]/.test(t) && !/\.(zip|tar|gz)\b/i.test(t)) {
|
|
171
|
+
return { ok: false, note: `「${t}」不像路径或压缩包。请给出目录(例:01-材料/代码/),或回「还没有」。` };
|
|
172
|
+
}
|
|
173
|
+
return { ok: true };
|
|
174
|
+
}
|
|
175
|
+
function yesNo(answer) {
|
|
176
|
+
const t = answer.trim();
|
|
177
|
+
if (/^(有|是|已|已经|yes|y|没有|无|否|还没|尚未|未|no|n|部分)/i.test(t))
|
|
178
|
+
return { ok: true };
|
|
179
|
+
return { ok: true, note: '未识别为「有/没有」。请直接回答「有」或「没有」,并补一句现状——实验结论只能来自真实运行结果。' };
|
|
180
|
+
}
|
|
181
|
+
function topicSource(answer) {
|
|
182
|
+
const t = answer.trim();
|
|
183
|
+
const fromAdvisor = /(导师|老师|指导教师|课题|给定|指定|分配)/.test(t);
|
|
184
|
+
const selfPicked = /(自选|自己|自主|我定|自拟)/.test(t);
|
|
185
|
+
if (!fromAdvisor && !selfPicked) {
|
|
186
|
+
return { ok: true, note: '没看出题目是导师给定还是自选。这不影响记录,但答辩常问「为什么做这个题」,建议写清来源。' };
|
|
187
|
+
}
|
|
188
|
+
if (selfPicked && !fromAdvisor) {
|
|
189
|
+
return { ok: true, note: '判定为自选题目:请把最终题目原文补全(含副标题),它是后续所有产出的标题来源。' };
|
|
190
|
+
}
|
|
191
|
+
return { ok: true };
|
|
192
|
+
}
|
|
193
|
+
function paperCount(answer) {
|
|
194
|
+
const n = firstNumber(answer);
|
|
195
|
+
if (n === undefined)
|
|
196
|
+
return { ok: false, note: '没看到数字。请给中文文献与外文文献的最小篇数,例:中文 15 篇、外文 5 篇。' };
|
|
197
|
+
if (n < 3 || n > 500)
|
|
198
|
+
return { ok: false, note: `${n} 篇不像合理的文献数量下限(预期 3–500)。请核对学校要求。` };
|
|
199
|
+
return { ok: true };
|
|
200
|
+
}
|
|
201
|
+
function ratioSum(answer) {
|
|
202
|
+
const nums = (answer.match(/\d+/g) ?? []).map(Number);
|
|
203
|
+
const counts = nums.filter(n => n >= 500);
|
|
204
|
+
if (counts.length === 0)
|
|
205
|
+
return { ok: false, note: '没看到字数。请给全文总字数与各章分配,例:全文 15000,绪论 2000。' };
|
|
206
|
+
const major = counts.filter(n => n >= 3000);
|
|
207
|
+
const sum = major.reduce((a, b) => a + b, 0);
|
|
208
|
+
const total = counts[0];
|
|
209
|
+
if (major.length >= 3 && sum > total * 1.5) {
|
|
210
|
+
return { ok: false, note: `各章字数合计(约 ${sum})明显超过总字数(${total})。请核对分配,避免评审时对不上。` };
|
|
211
|
+
}
|
|
212
|
+
return { ok: true };
|
|
213
|
+
}
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
// 问题库
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
/**
|
|
218
|
+
* 追问问题库。分节顺序即 {@link INTAKE_SECTIONS} 的顺序。
|
|
219
|
+
*
|
|
220
|
+
* `required: true` 的问题不答就无法 `done`;其余问题可以 `skip`(须给理由)。
|
|
221
|
+
*/
|
|
222
|
+
export const QUESTIONS = [
|
|
223
|
+
// ---- 学校与规范 ----
|
|
224
|
+
{
|
|
225
|
+
id: 'school.name',
|
|
226
|
+
section: '学校与规范',
|
|
227
|
+
ask: '你的学校、学院和专业全称是什么?(例:XX大学 计算机学院 软件工程)',
|
|
228
|
+
why: '封面、页眉、开题报告抬头都要逐字一致;写错一个字就要重新生成全套 docx,格式检查也过不了。',
|
|
229
|
+
affects: ['格式检查', '答案PPT'],
|
|
230
|
+
answerShape: '学校 + 学院 + 专业全称,例:XX大学 计算机学院 软件工程',
|
|
231
|
+
required: true,
|
|
232
|
+
hint: '看学生证、教务系统个人信息页,或往届同专业论文封面的写法(连"学院"和"系"的区别都要一致)。',
|
|
233
|
+
validate: a => text(a, 4),
|
|
234
|
+
priority: 1,
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
id: 'school.degree',
|
|
238
|
+
section: '学校与规范',
|
|
239
|
+
ask: '你的学位类型和层次是什么?(本科/硕士/博士,学术型还是专业型)',
|
|
240
|
+
why: '本科毕设与硕士学位论文的字数下限、章节结构、封面模板都不同;判错层会把整篇论文的结构带偏。',
|
|
241
|
+
affects: ['格式检查', '字数分配', '答辩问答'],
|
|
242
|
+
answerShape: '层次 + 类型,例:本科(工学学士)/ 硕士(专业型)',
|
|
243
|
+
required: true,
|
|
244
|
+
hint: '问导师最准;也可看培养方案里的"学位授予"一栏。',
|
|
245
|
+
validate: a => text(a, 2),
|
|
246
|
+
priority: 1,
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
id: 'school.template',
|
|
250
|
+
section: '学校与规范',
|
|
251
|
+
ask: '学校/学院的论文格式模板文件拿到了吗?拿到就给我它在工作区里的路径;没有就回「没有」。',
|
|
252
|
+
why: '没有模板就只能用通用过渡排版,最终仍需按学校模板重排一遍封面、标题层级和参考文献样式——这一步返工量最大。',
|
|
253
|
+
affects: ['格式检查', '引用规范'],
|
|
254
|
+
answerShape: '文件相对路径(例:00-管理/学校模板.docx),或「没有」',
|
|
255
|
+
required: true,
|
|
256
|
+
hint: '教务处/研究生院网站"下载专区"、学院群文件、导师发的压缩包;学长学姐的模板有时更新更及时。',
|
|
257
|
+
validate: fileOrNone,
|
|
258
|
+
priority: 1,
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
id: 'school.rule.ai',
|
|
262
|
+
section: '学校与规范',
|
|
263
|
+
ask: '学校对 AI 使用有明文规定吗?原文怎么说的?(没有明文规定也请说明)',
|
|
264
|
+
why: '决定能写到什么程度、哪些段落必须完全手写、自查报告要怎么写;踩线会直接判定学术不端,不是格式问题。',
|
|
265
|
+
affects: ['原创性', '格式检查', '答辩问答'],
|
|
266
|
+
answerShape: '「有:<规定要点原文摘录>」或「没有明文规定」,附出处(文件/通知名)',
|
|
267
|
+
required: true,
|
|
268
|
+
hint: '找《毕业设计(论文)工作管理办法》《学术道德规范》和导师转发的通知,摘录原文更好。',
|
|
269
|
+
validate: a => text(a, 4),
|
|
270
|
+
priority: 1,
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
id: 'school.rule.numbering',
|
|
274
|
+
section: '学校与规范',
|
|
275
|
+
ask: '图、表、公式的编号规范有要求吗?(例:图 3-1 / 图 3.1;公式按章编号)',
|
|
276
|
+
why: '编号体系一旦定了就不能中途改,否则全文交叉引用、图表清单一并作废;格式检查会逐条比对。',
|
|
277
|
+
affects: ['格式检查'],
|
|
278
|
+
answerShape: '编号写法 + 是否有章号,例:图 3-1、表 3-1、公式 (3-1);没有规定就说「没有」',
|
|
279
|
+
required: false,
|
|
280
|
+
hint: '翻模板正文里的示例图注;模板没有就按学院常见做法(图 章-序)并在规格里记下来。',
|
|
281
|
+
priority: 2,
|
|
282
|
+
},
|
|
283
|
+
// ---- 课题与目标 ----
|
|
284
|
+
{
|
|
285
|
+
id: 'topic.title',
|
|
286
|
+
section: '课题与目标',
|
|
287
|
+
ask: '论文题目(或课题方向)的原文是什么?有副标题也一并写上。',
|
|
288
|
+
why: '题目是所有产出的标题来源:封面、页眉、开题报告、PPT、答辩问题都从它派生,改了题目等于全套返工。',
|
|
289
|
+
affects: ['格式检查', '答案PPT', '答辩问答'],
|
|
290
|
+
answerShape: '题目原文,例:基于知识图谱的校园问答系统设计与实现',
|
|
291
|
+
required: true,
|
|
292
|
+
hint: '看选题确认书/任务书;还没定的先说方向,如"想做推荐系统",我按方向记录并标注待定。',
|
|
293
|
+
validate: a => text(a, 4),
|
|
294
|
+
priority: 1,
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
id: 'topic.source',
|
|
298
|
+
section: '课题与目标',
|
|
299
|
+
ask: '这个题目是导师给定的,还是你自己选的?',
|
|
300
|
+
why: '自选题目要额外准备"为什么选它/工作量够不够"的论证(G1 选题拍板必问);导师给定的题则以任务书为准,改动要先报备。',
|
|
301
|
+
affects: ['答辩问答'],
|
|
302
|
+
answerShape: '「导师给定」或「自选」,自选请补一句选题理由',
|
|
303
|
+
required: false,
|
|
304
|
+
hint: '看任务书是谁填的、有没有"自拟题目"字样。',
|
|
305
|
+
validate: topicSource,
|
|
306
|
+
priority: 2,
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
id: 'scope.deliverable',
|
|
310
|
+
section: '课题与目标',
|
|
311
|
+
ask: '最终交付物是什么?(只要论文,还是论文 + 可运行系统 + 演示?)',
|
|
312
|
+
why: '决定工作量口径与章节结构:纯论文型不需要系统实现章,而"系统 + 论文"必须留可运行证据,否则测试章无据可写。',
|
|
313
|
+
affects: ['答辩问答', '答案PPT'],
|
|
314
|
+
answerShape: '交付物清单,例:论文 + 可运行 Web 系统 + 答辩 PPT + 演示视频',
|
|
315
|
+
required: false,
|
|
316
|
+
hint: '看任务书"成果形式"一栏。',
|
|
317
|
+
priority: 2,
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
id: 'scope.wordcount',
|
|
321
|
+
section: '课题与目标',
|
|
322
|
+
ask: '正文(不含参考文献附录)要求多少字?如果各章有分配也一并说。',
|
|
323
|
+
why: '字数决定章节篇幅与文献数量:不够会被扣分,超了评审会挑"注水";各章分配还是逐章验收(G2)的量化标准。',
|
|
324
|
+
affects: ['字数分配', '格式检查'],
|
|
325
|
+
answerShape: '总量 + 可选各章,例:全文 15000 字,绪论 2000、测试 1500',
|
|
326
|
+
required: false,
|
|
327
|
+
hint: '任务书/学院文件里通常有"不少于 1.5 万字";没有分配就按七章比例先定,再让导师确认。',
|
|
328
|
+
validate: ratioSum,
|
|
329
|
+
priority: 1,
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
id: 'scope.core',
|
|
333
|
+
section: '课题与目标',
|
|
334
|
+
ask: '这个系统最核心的功能是什么?一句话说清它解决什么问题。',
|
|
335
|
+
why: '核心功能决定设计章的主线、测试章的用例和答辩的第一问;说不清就会出现"功能很多但都不深"的典型扣分点。',
|
|
336
|
+
affects: ['答辩问答'],
|
|
337
|
+
answerShape: '一句话:「给谁用 + 解决什么问题 + 关键手段」',
|
|
338
|
+
required: false,
|
|
339
|
+
hint: '问自己:如果只能保留一个功能,是哪个?',
|
|
340
|
+
priority: 2,
|
|
341
|
+
},
|
|
342
|
+
// ---- 时间线 ----
|
|
343
|
+
{
|
|
344
|
+
id: 'deliver.timeline',
|
|
345
|
+
section: '时间线',
|
|
346
|
+
ask: '开题、中期、查重截止、答辩这四个时间点分别是什么时候?说不出的写「不知道」。',
|
|
347
|
+
why: '时间线决定每周该做什么:没有截止日期就无法判断"能不能按时定稿",也无法在逾期时提醒你。',
|
|
348
|
+
affects: ['答辩问答'],
|
|
349
|
+
answerShape: '尽量给日期,例:开题 2026-01-10,中期 2026-03-20,查重 2026-05-01,答辩 2026-05-20',
|
|
350
|
+
required: true,
|
|
351
|
+
hint: '学院发的《毕业设计日程安排》表;找不到就先把已知的一个日期写上,其余写"不知道"。',
|
|
352
|
+
validate: a => {
|
|
353
|
+
const known = /不知道|不确定|待定|未定|说不清/.test(a);
|
|
354
|
+
if (known)
|
|
355
|
+
return { ok: true, note: '已记录「部分日期未知」。请尽快补齐——没有截止日期就无法排出每周计划。' };
|
|
356
|
+
return validateDeadline(a);
|
|
357
|
+
},
|
|
358
|
+
priority: 1,
|
|
359
|
+
},
|
|
360
|
+
// ---- 系统与数据 ----
|
|
361
|
+
{
|
|
362
|
+
id: 'tech.stack',
|
|
363
|
+
section: '系统与数据',
|
|
364
|
+
ask: '技术栈定了吗?大概用了哪些语言、框架、数据库?',
|
|
365
|
+
why: '技术栈决定设计章怎么写、环境怎么搭、答辩会被问哪些取舍("为什么用它不用它"是必问题)。',
|
|
366
|
+
affects: ['答辩问答'],
|
|
367
|
+
answerShape: '语言 + 主要框架 + 数据库,例:Python + Django + MySQL;没定就说「没定」',
|
|
368
|
+
required: false,
|
|
369
|
+
hint: '看已有代码的依赖文件(package.json / requirements.txt / pom.xml)。',
|
|
370
|
+
priority: 2,
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
id: 'tech.code',
|
|
374
|
+
section: '系统与数据',
|
|
375
|
+
ask: '已有代码在哪里?(给出目录或压缩包路径;还没写就回「还没有」)',
|
|
376
|
+
why: '有代码就能直接对齐实现章与测试章的写法;不知道代码在哪,实现章只能凭空描述,最容易被判定造假。',
|
|
377
|
+
affects: ['答辩问答'],
|
|
378
|
+
answerShape: '相对路径,例:01-材料/代码/ 或 01-材料/源码.zip;没有就回「还没有」',
|
|
379
|
+
required: false,
|
|
380
|
+
hint: 'git 仓库地址、压缩包、导师给的基础工程都算;注意别把 .git 目录整个塞进论文仓库。',
|
|
381
|
+
validate: pathOrNone,
|
|
382
|
+
priority: 2,
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
id: 'data.source',
|
|
386
|
+
section: '系统与数据',
|
|
387
|
+
ask: '实验/测试数据从哪来?(自己采集、公开数据集、模拟生成、还是系统运行日志)',
|
|
388
|
+
why: '数据来源决定测试章能不能写"可复现":公开数据集要给名称版本,自采集要说明采集过程,否则结论无法被验证。',
|
|
389
|
+
affects: ['答辩问答'],
|
|
390
|
+
answerShape: '来源 + 名称/规模,例:公开数据集 MovieLens-1M;或自采集 500 条问卷',
|
|
391
|
+
required: false,
|
|
392
|
+
hint: '数据集官网的引用格式也一并记下来,引用章要用。',
|
|
393
|
+
priority: 2,
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
id: 'data.real',
|
|
397
|
+
section: '系统与数据',
|
|
398
|
+
ask: '现在已经有真实运行/测试结果了吗?还是数据还没跑出来?',
|
|
399
|
+
why: '结论章与测试章只能写真实跑出来的数;没有结果就先占位标注"待补",绝不允许编造——编造数据是学术不端红线。',
|
|
400
|
+
affects: ['答辩问答'],
|
|
401
|
+
answerShape: '「有:<结果在哪>」或「没有:<计划什么时候跑>」',
|
|
402
|
+
required: true,
|
|
403
|
+
hint: '有截图/日志/CSV 就在答案里给出路径,测试章直接引用。',
|
|
404
|
+
validate: yesNo,
|
|
405
|
+
priority: 1,
|
|
406
|
+
},
|
|
407
|
+
// ---- 文献与引用 ----
|
|
408
|
+
{
|
|
409
|
+
id: 'ref.requirement',
|
|
410
|
+
section: '文献与引用',
|
|
411
|
+
ask: '参考文献有数量要求吗?对年份有要求吗?(例:中文 15 篇、外文 5 篇,近五年不少于一半)',
|
|
412
|
+
why: '数量不够或年份太旧会在文献综述和答辩上直接扣分;这些要求是筛选文献的硬条件,不是写的时候再补。',
|
|
413
|
+
affects: ['引用规范'],
|
|
414
|
+
answerShape: '篇数下限 + 年份要求,例:至少 20 篇,其中外文 5 篇、近五年 10 篇',
|
|
415
|
+
required: false,
|
|
416
|
+
hint: '任务书/学院文件;没有明确要求就按"本科≥15 篇、外文≥3 篇、近五年≥1/3"先定,并让导师确认。',
|
|
417
|
+
validate: paperCount,
|
|
418
|
+
priority: 2,
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
id: 'ref.format',
|
|
422
|
+
section: '文献与引用',
|
|
423
|
+
ask: '引用格式要求是哪一种?(GB/T 7714 还是学校模板里的自有格式)',
|
|
424
|
+
why: '引用格式决定 refs.bib 的排版与正文标注方式(顺序编码 vs 著者-出版年);中途换格式等于全文引用重排。',
|
|
425
|
+
affects: ['引用规范', '格式检查'],
|
|
426
|
+
answerShape: '标准号 + 标注方式,例:GB/T 7714-2015 顺序编码制(上标数字)',
|
|
427
|
+
required: true,
|
|
428
|
+
hint: '看学校模板里"参考文献"一节的示例;问导师或学长最省事。',
|
|
429
|
+
validate: citationFormat,
|
|
430
|
+
priority: 2,
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
id: 'ref.samples',
|
|
434
|
+
section: '文献与引用',
|
|
435
|
+
ask: '有往届同专业学长的论文可以给我看看吗?(给路径或说「没有」)',
|
|
436
|
+
why: '同校同专业的往届论文是格式与篇幅的现实基准:章节怎么分、图表多少张、评审口味,比任何说明文件都准。',
|
|
437
|
+
affects: ['格式检查', '引用规范', '答辩问答'],
|
|
438
|
+
answerShape: '文件路径(例:01-材料/往届论文/)或「没有」',
|
|
439
|
+
required: false,
|
|
440
|
+
hint: '知网/万方按"作者单位 + 专业"检索学位论文;学院资料室、导师手里通常也有。',
|
|
441
|
+
priority: 3,
|
|
442
|
+
},
|
|
443
|
+
// ---- 交付形式 ----
|
|
444
|
+
{
|
|
445
|
+
id: 'plag.system',
|
|
446
|
+
section: '交付形式',
|
|
447
|
+
ask: '学校用哪个查重系统?重复率红线是多少?',
|
|
448
|
+
why: '不同系统(知网/维普/PaperPass)对同一段文字的判定不同,阈值决定降重的目标线;线不清楚,降重就是白改。',
|
|
449
|
+
affects: ['原创性'],
|
|
450
|
+
answerShape: '系统名 + 阈值,例:知网 PMLC,全文重复率 ≤15%',
|
|
451
|
+
required: false,
|
|
452
|
+
hint: '学院通知里通常写明"知网查重,≤15%";不确定就先问导师助理。',
|
|
453
|
+
validate: ratio,
|
|
454
|
+
priority: 3,
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
id: 'plag.rule',
|
|
458
|
+
section: '交付形式',
|
|
459
|
+
ask: '重复率怎么算、有没有特殊规定?(例:全文 vs 正文、单章是否也查、是否允许引用自己以前的论文)',
|
|
460
|
+
why: '同一篇论文按全文算和按章节算结论可能相反;不搞清口径就会把时间花在不需要改的章节上。',
|
|
461
|
+
affects: ['原创性'],
|
|
462
|
+
answerShape: '计算口径 + 特殊规定,例:按全文算,致谢与参考文献不计入',
|
|
463
|
+
required: false,
|
|
464
|
+
priority: 3,
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
id: 'defense.format',
|
|
468
|
+
section: '交付形式',
|
|
469
|
+
ask: '答辩什么形式?PPT 有没有页数或时长限制,是不是盲审?',
|
|
470
|
+
why: '时长决定 PPT 页数与内容密度(10 分钟约 12–15 页),盲审则要删掉所有可识别身份的信息;这两点会整体改变 PPT 与讲稿。',
|
|
471
|
+
affects: ['答案PPT', '答辩问答'],
|
|
472
|
+
answerShape: '形式 + 页数/时长 + 是否盲审,例:现场答辩,PPT ≤15 页,8 分钟,非盲审',
|
|
473
|
+
required: false,
|
|
474
|
+
hint: '学院答辩通知;没有明确要求就按"12 页 + 8 分钟"起稿,再按实际调整。',
|
|
475
|
+
priority: 3,
|
|
476
|
+
},
|
|
477
|
+
];
|
|
478
|
+
// ---------------------------------------------------------------------------
|
|
479
|
+
// 查询与排序
|
|
480
|
+
// ---------------------------------------------------------------------------
|
|
481
|
+
const BY_ID = new Map(QUESTIONS.map(q => [q.id, q]));
|
|
482
|
+
export function questionById(id) {
|
|
483
|
+
return BY_ID.get(id);
|
|
484
|
+
}
|
|
485
|
+
export function listRequired() {
|
|
486
|
+
return QUESTIONS.filter(q => q.required);
|
|
487
|
+
}
|
|
488
|
+
/** 全部问题 id(现状报告用)。 */
|
|
489
|
+
export function allQuestionIds() {
|
|
490
|
+
return QUESTIONS.map(q => q.id);
|
|
491
|
+
}
|
|
492
|
+
/** 某问题是否已被回答(跳过不算回答)。 */
|
|
493
|
+
export function isAnswered(state, q) {
|
|
494
|
+
return state.answers[q.id] !== undefined;
|
|
495
|
+
}
|
|
496
|
+
/** 某回答是否还需要用户确认(材料推断出来的答案)。 */
|
|
497
|
+
export function needsConfirm(state, q) {
|
|
498
|
+
const rec = state.answers[q.id];
|
|
499
|
+
return rec !== undefined && !rec.confirmed;
|
|
500
|
+
}
|
|
501
|
+
// ---------------------------------------------------------------------------
|
|
502
|
+
// 材料感知
|
|
503
|
+
// ---------------------------------------------------------------------------
|
|
504
|
+
function entryPath(e) {
|
|
505
|
+
return e.path.replace(/\\/g, '/');
|
|
506
|
+
}
|
|
507
|
+
function entryLabel(e) {
|
|
508
|
+
return entryPath(e).replace(/^.*\//, '');
|
|
509
|
+
}
|
|
510
|
+
/** 材料条目的类型/文件名里是否命中任一关键词(大小写不敏感)。 */
|
|
511
|
+
function matches(e, words) {
|
|
512
|
+
const hay = `${e.kind} ${entryPath(e)}`.toLowerCase();
|
|
513
|
+
return words.some(w => hay.includes(w.toLowerCase()));
|
|
514
|
+
}
|
|
515
|
+
/** 找第一份学校格式模板:文件名/类型里出现"模板/格式/规范",或 kind 明确为学校模板。 */
|
|
516
|
+
export function findTemplateMaterial(materials) {
|
|
517
|
+
const strong = materials.items.find(e => matches(e, ['学校模板', '格式模板', '论文模板', '模板', '格式要求', '规范']));
|
|
518
|
+
if (strong !== undefined)
|
|
519
|
+
return strong;
|
|
520
|
+
// 次优:上传了 docx,但没明说是模板——只在文件名像规范时才认。
|
|
521
|
+
return materials.items.find(e => /\.(docx?|dotx?)$/i.test(entryPath(e)) && matches(e, ['要求', '说明', '手册', '文件']));
|
|
522
|
+
}
|
|
523
|
+
function findMaterial(materials, words) {
|
|
524
|
+
return materials.items.find(e => matches(e, words));
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* 从材料清单摘要推断哪些问题可以免问 / 自动回答。
|
|
528
|
+
*
|
|
529
|
+
* 纯函数:同样的清单与状态永远给出同样结果。清单缺失(`found:false`)时
|
|
530
|
+
* 什么都不推断,一切照常提问——**宽容优先**,不猜。
|
|
531
|
+
*/
|
|
532
|
+
export function materialInsight(materials) {
|
|
533
|
+
const autoAnswers = new Map();
|
|
534
|
+
const suppress = new Set();
|
|
535
|
+
if (!materials.found || materials.items.length === 0)
|
|
536
|
+
return { autoAnswers, suppress };
|
|
537
|
+
const template = findTemplateMaterial(materials);
|
|
538
|
+
if (template !== undefined) {
|
|
539
|
+
autoAnswers.set('school.template', {
|
|
540
|
+
value: entryPath(template),
|
|
541
|
+
evidence: `材料清单里的「${entryLabel(template)}」(类型:${template.kind || '未标注'})看起来就是学校格式模板`,
|
|
542
|
+
needsConfirm: true,
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
const aiRule = findMaterial(materials, ['AI使用', 'AI规定', 'ai规定', '学术道德', '学术诚信', '学校规定', '管理办法']);
|
|
546
|
+
if (aiRule !== undefined) {
|
|
547
|
+
autoAnswers.set('school.rule.ai', {
|
|
548
|
+
value: `见材料:${entryPath(aiRule)}`,
|
|
549
|
+
evidence: `材料清单里的「${entryLabel(aiRule)}」属于学校规定类文件,其中可能含 AI 使用条款`,
|
|
550
|
+
needsConfirm: true,
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
const samples = materials.items.filter(e => matches(e, ['往届', '学长', '学姐', '参考论文', '学位论文', '范文']));
|
|
554
|
+
if (samples.length > 0) {
|
|
555
|
+
autoAnswers.set('ref.samples', {
|
|
556
|
+
value: samples.map(e => entryPath(e)).join('、'),
|
|
557
|
+
evidence: `材料清单里有 ${samples.length} 份往届/参考论文(如「${entryLabel(samples[0])}」)`,
|
|
558
|
+
needsConfirm: true,
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
// 已从材料安全推断的:数据类问题有数据文件即视为"已有",写作阶段自然会用到具体文件。
|
|
562
|
+
const dataFiles = materials.items.filter(e => /\.(xlsx?|csv|json|txt|db|sqlite)$/i.test(entryPath(e)));
|
|
563
|
+
if (dataFiles.length > 0) {
|
|
564
|
+
autoAnswers.set('data.source', {
|
|
565
|
+
value: `${dataFiles.map(e => entryPath(e)).join('、')}(导入工作区的数据文件)`,
|
|
566
|
+
evidence: `材料清单里有 ${dataFiles.length} 个数据文件`,
|
|
567
|
+
needsConfirm: true,
|
|
568
|
+
});
|
|
569
|
+
suppress.add('data.source');
|
|
570
|
+
}
|
|
571
|
+
const codeFiles = materials.items.filter(e => /\.(zip|tar|gz|jar|whl)$/i.test(entryPath(e)));
|
|
572
|
+
const codeDir = materials.items.find(e => matches(e, ['代码', '源码', 'source', 'src']));
|
|
573
|
+
const codeHit = codeDir ?? codeFiles[0];
|
|
574
|
+
if (codeHit !== undefined) {
|
|
575
|
+
autoAnswers.set('tech.code', {
|
|
576
|
+
value: entryPath(codeHit),
|
|
577
|
+
evidence: `材料清单里的「${entryLabel(codeHit)}」是代码/源码类材料`,
|
|
578
|
+
needsConfirm: true,
|
|
579
|
+
});
|
|
580
|
+
suppress.add('tech.code');
|
|
581
|
+
}
|
|
582
|
+
const stackFile = materials.items.find(e => /(package\.json|requirements\.txt|pom\.xml|build\.gradle|cargo\.toml|go\.mod|\.csproj)$/i.test(entryPath(e)));
|
|
583
|
+
if (stackFile !== undefined) {
|
|
584
|
+
autoAnswers.set('tech.stack', {
|
|
585
|
+
value: `见依赖清单:${entryPath(stackFile)}(尚未解析,需用户确认后补全技术栈)`,
|
|
586
|
+
evidence: `材料清单里有依赖清单文件「${entryLabel(stackFile)}」`,
|
|
587
|
+
needsConfirm: true,
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
return { autoAnswers, suppress };
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* 材料自动回答里的「确认清单」:`autoAnswers` 中尚未被用户确认的部分。
|
|
594
|
+
* 这些是让「一次到位」落地的关键——不重复问,但也不替用户拍板。
|
|
595
|
+
*/
|
|
596
|
+
export function pendingConfirmations(state, insight) {
|
|
597
|
+
const out = [];
|
|
598
|
+
for (const id of insight.autoAnswers.keys()) {
|
|
599
|
+
const q = questionById(id);
|
|
600
|
+
if (q === undefined)
|
|
601
|
+
continue;
|
|
602
|
+
if (!state.answers[id]?.confirmed)
|
|
603
|
+
out.push(q);
|
|
604
|
+
}
|
|
605
|
+
return out;
|
|
606
|
+
}
|
|
607
|
+
// ---------------------------------------------------------------------------
|
|
608
|
+
// 裁剪与选取
|
|
609
|
+
// ---------------------------------------------------------------------------
|
|
610
|
+
/** 排序用的一个排名元组:越小越先问。 */
|
|
611
|
+
function rankTuple(q, ctx, state, insight, index) {
|
|
612
|
+
const rec = state.answers[q.id];
|
|
613
|
+
const auto = insight.autoAnswers.has(q.id);
|
|
614
|
+
// 材料已经给出答案、只差用户点头(含尚未落盘的自动回答)——最便宜,先问。
|
|
615
|
+
const mustConfirm = auto && (rec === undefined || !rec.confirmed);
|
|
616
|
+
const blockingRequired = q.required && (rec === undefined || rec.valid === false);
|
|
617
|
+
let bucket;
|
|
618
|
+
if (mustConfirm)
|
|
619
|
+
bucket = 0;
|
|
620
|
+
else if (blockingRequired)
|
|
621
|
+
bucket = 1; // 拦路的必答项
|
|
622
|
+
else if (rec === undefined)
|
|
623
|
+
bucket = 2; // 普通未答
|
|
624
|
+
else
|
|
625
|
+
bucket = 3; // 已答待补(非阻塞)
|
|
626
|
+
return [bucket, affectRank(q, ctx.stage), q.priority, index];
|
|
627
|
+
}
|
|
628
|
+
function compareRank(a, b) {
|
|
629
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
630
|
+
const x = a[i];
|
|
631
|
+
const y = b[i];
|
|
632
|
+
if (x !== y)
|
|
633
|
+
return x - y;
|
|
634
|
+
}
|
|
635
|
+
return 0;
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* 当前应该问的下一个问题;没有可问的返回 undefined。
|
|
639
|
+
*
|
|
640
|
+
* 规则(按优先级):
|
|
641
|
+
* 1. 材料已给出答案、只差用户确认的;
|
|
642
|
+
* 2. `required` 且未答(或格式不合格)的;
|
|
643
|
+
* 3. 其余未答的,按「当前阶段命中的产出物 → priority → 问题库顺序」;
|
|
644
|
+
* 4. 已跳过的不再问(要改就显式重答)。
|
|
645
|
+
*/
|
|
646
|
+
export function nextQuestion(state, ctx) {
|
|
647
|
+
const insight = materialInsight(ctx.materials);
|
|
648
|
+
let best;
|
|
649
|
+
let bestRank;
|
|
650
|
+
let index = 0;
|
|
651
|
+
for (const q of QUESTIONS) {
|
|
652
|
+
const current = index;
|
|
653
|
+
index += 1;
|
|
654
|
+
if (insight.suppress.has(q.id))
|
|
655
|
+
continue;
|
|
656
|
+
const rec = state.answers[q.id];
|
|
657
|
+
if (rec !== undefined && rec.confirmed)
|
|
658
|
+
continue;
|
|
659
|
+
if (rec === undefined && state.skipped.includes(q.id))
|
|
660
|
+
continue;
|
|
661
|
+
const rank = rankTuple(q, ctx, state, insight, current);
|
|
662
|
+
if (rank[0] === 3)
|
|
663
|
+
continue; // 已答且合格的非必答项:不再打扰
|
|
664
|
+
if (bestRank === undefined || compareRank(rank, bestRank) < 0) {
|
|
665
|
+
best = q;
|
|
666
|
+
bestRank = rank;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
return best;
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* 问题队列快照(不含已确认的答案):用于报告「还差什么」。
|
|
673
|
+
* 返回顺序与 {@link nextQuestion} 的选取顺序一致,但不做 maxQuestions 裁剪。
|
|
674
|
+
*/
|
|
675
|
+
export function questionQueue(state, ctx) {
|
|
676
|
+
const insight = materialInsight(ctx.materials);
|
|
677
|
+
const entries = [];
|
|
678
|
+
let index = 0;
|
|
679
|
+
for (const q of QUESTIONS) {
|
|
680
|
+
const current = index;
|
|
681
|
+
index += 1;
|
|
682
|
+
if (insight.suppress.has(q.id))
|
|
683
|
+
continue;
|
|
684
|
+
const rec = state.answers[q.id];
|
|
685
|
+
if (rec !== undefined && rec.confirmed)
|
|
686
|
+
continue;
|
|
687
|
+
if (rec === undefined && state.skipped.includes(q.id))
|
|
688
|
+
continue;
|
|
689
|
+
const rank = rankTuple(q, ctx, state, insight, current);
|
|
690
|
+
if (rank[0] === 3)
|
|
691
|
+
continue;
|
|
692
|
+
entries.push({ q, rank });
|
|
693
|
+
}
|
|
694
|
+
entries.sort((a, b) => compareRank(a.rank, b.rank));
|
|
695
|
+
return entries.map(e => e.q);
|
|
696
|
+
}
|
|
697
|
+
/** 阻塞项:`required` 但未答、或已答但格式不合格(`valid === false`)。 */
|
|
698
|
+
export function blockingRequired(state) {
|
|
699
|
+
return listRequired().filter(q => {
|
|
700
|
+
const rec = state.answers[q.id];
|
|
701
|
+
return rec === undefined || rec.valid === false;
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
/** 进度:已答(含未确认)/ 必答总数。 */
|
|
705
|
+
export function progress(state) {
|
|
706
|
+
const required = listRequired();
|
|
707
|
+
const answered = QUESTIONS.filter(q => state.answers[q.id] !== undefined).length;
|
|
708
|
+
const confirmed = QUESTIONS.filter(q => state.answers[q.id]?.confirmed === true).length;
|
|
709
|
+
const doneRequired = required.filter(q => state.answers[q.id] !== undefined).length;
|
|
710
|
+
return { answered: doneRequired, total: required.length, required: required.length, confirmed };
|
|
711
|
+
}
|
|
712
|
+
/** 一次问答循环可推进的问题数(`maxQuestions` 上限的裁剪视图)。 */
|
|
713
|
+
export function clampQueue(queue, maxQuestions) {
|
|
714
|
+
const limit = Math.max(1, Math.floor(maxQuestions));
|
|
715
|
+
return queue.slice(0, limit);
|
|
716
|
+
}
|
|
717
|
+
/** 组装一个规格上下文:材料感知 + 阶段。 */
|
|
718
|
+
export function makeContext(materials, stage) {
|
|
719
|
+
return { materials, stage: stage === undefined || stage === '' ? DEFAULT_STAGE : stage };
|
|
720
|
+
}
|