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,449 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 代码演练引擎:一个普通类,由 `register.ts` 里的 `registerCodewalk`
|
|
3
|
+
* 注册为 `paperLearn` 服务。
|
|
4
|
+
*
|
|
5
|
+
* 引擎负责守住流程,模型负责执行流程。所有非法迁移都在这里被拒绝:没有
|
|
6
|
+
* 真实跑过一次验证门,模型就不能把某个里程碑标成 verified;不能修改已冻结
|
|
7
|
+
* (verified)的里程碑;不能跳阶段。状态落在 `<cwd>/.paper/state.json`,
|
|
8
|
+
* 因此跨会话存活,并随演练骨架仓库一起走。
|
|
9
|
+
*
|
|
10
|
+
* 这个类刻意**不**继承 Cordis 的 `Service`:装进 profile 的 bundle 会从自己的
|
|
11
|
+
* 依赖树里解析 `@deepseek-ai/cordis`,两份模块副本会破坏框架以 Symbol 为键的
|
|
12
|
+
* 生命周期钩子。注册一律走 `ctx.provide` —— 与生产 boot 插件用的是同一个接缝。
|
|
13
|
+
*
|
|
14
|
+
* @module dsh-thesis/codewalk
|
|
15
|
+
*/
|
|
16
|
+
import { LearningError } from "./errors.js";
|
|
17
|
+
const PHASES = ['analyzing', 'skeletonizing', 'learning', 'complete'];
|
|
18
|
+
const PHASE_NEXT = {
|
|
19
|
+
analyzing: 'skeletonizing',
|
|
20
|
+
skeletonizing: 'learning',
|
|
21
|
+
learning: 'complete',
|
|
22
|
+
complete: null,
|
|
23
|
+
};
|
|
24
|
+
const TODO_STATUS = ['pending', 'in_progress', 'done'];
|
|
25
|
+
const MILESTONE_STATUS = ['pending', 'in_progress', 'verified', 'failed'];
|
|
26
|
+
const QUESTION_STATUS = ['unasked', 'asked', 'passed', 'failed'];
|
|
27
|
+
const DIFFICULTIES = [1, 2, 3, 4];
|
|
28
|
+
/** 合法的 todo 状态迁移:允许返工(done → in_progress)。 */
|
|
29
|
+
const TODO_MOVES = {
|
|
30
|
+
pending: ['in_progress', 'done'],
|
|
31
|
+
in_progress: ['done'],
|
|
32
|
+
done: ['in_progress'],
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* 代码演练引擎。
|
|
36
|
+
*
|
|
37
|
+
* 所有状态机方法都直接修改传入的 `state` 对象,并用 `LearningError` 拒绝非法
|
|
38
|
+
* 迁移。持久化方法一律走注入的文件系统,因此本地与远端后端行为一致。
|
|
39
|
+
*/
|
|
40
|
+
export class AiLearningEngine {
|
|
41
|
+
ctx;
|
|
42
|
+
resolved;
|
|
43
|
+
constructor(ctx, config = {}) {
|
|
44
|
+
this.ctx = ctx;
|
|
45
|
+
this.resolved = {
|
|
46
|
+
stateDir: config.stateDir ?? '.paper',
|
|
47
|
+
gates: config.gates ?? { go: { build: ['go', 'build', './...'] } },
|
|
48
|
+
maxCapturedOutput: config.maxCapturedOutput ?? 8000,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** 解析后的配置(默认值已物化)。 */
|
|
52
|
+
get config() {
|
|
53
|
+
return this.resolved;
|
|
54
|
+
}
|
|
55
|
+
/** 宿主文件系统接缝;`inject: ['fs']` 保证其存在。 */
|
|
56
|
+
get fs() {
|
|
57
|
+
return this.ctx.get('fs');
|
|
58
|
+
}
|
|
59
|
+
stateFilePath() {
|
|
60
|
+
return `${this.resolved.stateDir}/state.json`;
|
|
61
|
+
}
|
|
62
|
+
// ── 持久化 ───────────────────────────────────────────────────────────
|
|
63
|
+
/** 相对会话 cwd 解析状态文件目标。 */
|
|
64
|
+
async stateTarget(cwd) {
|
|
65
|
+
return this.fs.resolve(this.stateFilePath(), { cwd });
|
|
66
|
+
}
|
|
67
|
+
/** `cwd` 下是否已存在演练状态。 */
|
|
68
|
+
async hasState(cwd) {
|
|
69
|
+
const target = await this.stateTarget(cwd);
|
|
70
|
+
return (await this.fs.stat(target)) !== undefined;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* 读取并校验 `cwd` 下的状态;不存在时返回 `undefined`。
|
|
74
|
+
* 内容损坏时以 `STATE_INVALID` 拒绝。
|
|
75
|
+
*/
|
|
76
|
+
async load(cwd) {
|
|
77
|
+
const target = await this.stateTarget(cwd);
|
|
78
|
+
const info = await this.fs.stat(target);
|
|
79
|
+
if (info === undefined)
|
|
80
|
+
return undefined;
|
|
81
|
+
const text = await this.fs.readText(target);
|
|
82
|
+
return parseState(text);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* 为一次新的代码演练创建初始状态。
|
|
86
|
+
*
|
|
87
|
+
* 已存在状态时以 `STATE_EXISTS` 拒绝(经 `createIfAbsent` 保证竞态安全);
|
|
88
|
+
* 源语言没有配置验证门时以 `GATE_UNKNOWN` 拒绝。
|
|
89
|
+
*/
|
|
90
|
+
async create(cwd, input) {
|
|
91
|
+
const gate = this.resolved.gates[input.origin.language];
|
|
92
|
+
if (gate === undefined) {
|
|
93
|
+
throw new LearningError('GATE_UNKNOWN', `语言 "${input.origin.language}" 没有配置验证门(已配置:${Object.keys(this.resolved.gates).join(', ') || '无'})`);
|
|
94
|
+
}
|
|
95
|
+
const target = await this.stateTarget(cwd);
|
|
96
|
+
if ((await this.fs.stat(target)) !== undefined) {
|
|
97
|
+
throw new LearningError('STATE_EXISTS', `${cwd} 下已存在演练状态`);
|
|
98
|
+
}
|
|
99
|
+
const state = {
|
|
100
|
+
schemaVersion: 1,
|
|
101
|
+
phase: 'analyzing',
|
|
102
|
+
origin: { path: input.origin.path, language: input.origin.language },
|
|
103
|
+
scope: { ...input.scope },
|
|
104
|
+
milestones: [],
|
|
105
|
+
todos: [],
|
|
106
|
+
gates: this.resolved.gates,
|
|
107
|
+
records: [],
|
|
108
|
+
updatedAt: Date.now(),
|
|
109
|
+
};
|
|
110
|
+
const content = serializeState(state);
|
|
111
|
+
try {
|
|
112
|
+
await this.fs.writeText(target, content, { createIfAbsent: true });
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
if (errorCode(error) === 'FS_NOT_OBSERVED') {
|
|
116
|
+
throw new LearningError('STATE_EXISTS', `${cwd} 下已存在演练状态`);
|
|
117
|
+
}
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
return state;
|
|
121
|
+
}
|
|
122
|
+
/** 持久化当前状态,并刷新 `updatedAt`。 */
|
|
123
|
+
async save(cwd, state) {
|
|
124
|
+
validateStateShape(state);
|
|
125
|
+
state.updatedAt = Date.now();
|
|
126
|
+
const target = await this.stateTarget(cwd);
|
|
127
|
+
await this.fs.writeText(target, serializeState(state));
|
|
128
|
+
}
|
|
129
|
+
// ── 阶段状态机 ───────────────────────────────────────────────────────
|
|
130
|
+
/**
|
|
131
|
+
* 推进工作流阶段。只允许沿
|
|
132
|
+
* analyzing → skeletonizing → learning → complete 向前走。
|
|
133
|
+
* 进入 `learning` 至少需要一个带 todo 的里程碑;
|
|
134
|
+
* 进入 `complete` 需要每个里程碑都已 verified。
|
|
135
|
+
*/
|
|
136
|
+
advancePhase(state, next) {
|
|
137
|
+
if (next === state.phase)
|
|
138
|
+
return;
|
|
139
|
+
if (PHASE_NEXT[state.phase] !== next) {
|
|
140
|
+
throw new LearningError('ILLEGAL_TRANSITION', `不能从阶段 "${state.phase}" 迁移到 "${next}"`);
|
|
141
|
+
}
|
|
142
|
+
if (next === 'learning') {
|
|
143
|
+
if (state.milestones.length === 0) {
|
|
144
|
+
throw new LearningError('ILLEGAL_TRANSITION', '不能进入 learning 阶段:尚未定义任何里程碑');
|
|
145
|
+
}
|
|
146
|
+
for (const milestone of state.milestones) {
|
|
147
|
+
if (milestone.todos.length === 0) {
|
|
148
|
+
throw new LearningError('ILLEGAL_TRANSITION', `里程碑 "${milestone.id}" 没有任何 todo`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (next === 'complete') {
|
|
153
|
+
const open = state.milestones.filter((milestone) => milestone.status !== 'verified');
|
|
154
|
+
if (open.length > 0) {
|
|
155
|
+
throw new LearningError('ILLEGAL_TRANSITION', `不能完成:仍有未关闭的里程碑:${open.map((milestone) => milestone.id).join(', ')}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
state.phase = next;
|
|
159
|
+
if (next === 'learning')
|
|
160
|
+
this.startNextMilestone(state);
|
|
161
|
+
}
|
|
162
|
+
// ── todo 状态机 ──────────────────────────────────────────────────────
|
|
163
|
+
/** 新增或替换一个 todo。被 verified 里程碑引用的 todo 已冻结。 */
|
|
164
|
+
upsertTodo(state, todo) {
|
|
165
|
+
if (!DIFFICULTIES.includes(todo.difficulty)) {
|
|
166
|
+
throw new LearningError('ILLEGAL_TRANSITION', `todo "${todo.id}" 的难度值非法`);
|
|
167
|
+
}
|
|
168
|
+
if (!TODO_STATUS.includes(todo.status)) {
|
|
169
|
+
throw new LearningError('ILLEGAL_TRANSITION', `todo "${todo.id}" 的状态值非法`);
|
|
170
|
+
}
|
|
171
|
+
this.assertTodoEditable(state, todo.id);
|
|
172
|
+
const index = state.todos.findIndex((entry) => entry.id === todo.id);
|
|
173
|
+
if (index >= 0)
|
|
174
|
+
state.todos[index] = todo;
|
|
175
|
+
else
|
|
176
|
+
state.todos.push(todo);
|
|
177
|
+
}
|
|
178
|
+
/** 沿 pending → in_progress → done 迁移一个 todo(允许返工)。 */
|
|
179
|
+
setTodoStatus(state, todoId, status) {
|
|
180
|
+
const todo = state.todos.find((entry) => entry.id === todoId);
|
|
181
|
+
if (todo === undefined)
|
|
182
|
+
throw new LearningError('TODO_UNKNOWN', `未知 todo "${todoId}"`);
|
|
183
|
+
if (status === todo.status)
|
|
184
|
+
return;
|
|
185
|
+
if (!TODO_MOVES[todo.status].includes(status)) {
|
|
186
|
+
throw new LearningError('ILLEGAL_TRANSITION', `不能把 todo "${todoId}" 从 "${todo.status}" 迁移到 "${status}"`);
|
|
187
|
+
}
|
|
188
|
+
todo.status = status;
|
|
189
|
+
}
|
|
190
|
+
assertTodoEditable(state, todoId) {
|
|
191
|
+
const frozen = state.milestones.some((milestone) => milestone.status === 'verified' && milestone.todos.includes(todoId));
|
|
192
|
+
if (frozen) {
|
|
193
|
+
throw new LearningError('ILLEGAL_TRANSITION', `todo "${todoId}" 已被 verified 里程碑冻结`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// ── 里程碑状态机 ─────────────────────────────────────────────────────
|
|
197
|
+
/**
|
|
198
|
+
* 新增或替换一个里程碑。verified 里程碑已冻结。每个 todo 引用都必须能解析。
|
|
199
|
+
* 新里程碑从 `pending` 开始;对未关闭里程碑做内容编辑会保留其当前状态。
|
|
200
|
+
*/
|
|
201
|
+
upsertMilestone(state, milestone) {
|
|
202
|
+
const existing = state.milestones.find((entry) => entry.id === milestone.id);
|
|
203
|
+
if (existing?.status === 'verified') {
|
|
204
|
+
throw new LearningError('ILLEGAL_TRANSITION', `里程碑 "${milestone.id}" 已 verified,处于冻结状态`);
|
|
205
|
+
}
|
|
206
|
+
for (const todoId of milestone.todos) {
|
|
207
|
+
if (!state.todos.some((todo) => todo.id === todoId)) {
|
|
208
|
+
throw new LearningError('TODO_UNKNOWN', `里程碑 "${milestone.id}" 引用了未知 todo "${todoId}"`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
for (const [index, question] of milestone.questions.entries()) {
|
|
212
|
+
if (question.ask.trim() === '') {
|
|
213
|
+
throw new LearningError('ILLEGAL_TRANSITION', `里程碑 "${milestone.id}" 的第 ${index} 个问题缺少提问文本`);
|
|
214
|
+
}
|
|
215
|
+
if (!QUESTION_STATUS.includes(question.status)) {
|
|
216
|
+
throw new LearningError('ILLEGAL_TRANSITION', `里程碑 "${milestone.id}" 的第 ${index} 个问题状态非法`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const normalized = {
|
|
220
|
+
...milestone,
|
|
221
|
+
status: existing === undefined ? 'pending' : existing.status,
|
|
222
|
+
questions: milestone.questions.map((question) => ({ ...question })),
|
|
223
|
+
};
|
|
224
|
+
if (existing === undefined)
|
|
225
|
+
state.milestones.push(normalized);
|
|
226
|
+
else {
|
|
227
|
+
const index = state.milestones.indexOf(existing);
|
|
228
|
+
if (index >= 0)
|
|
229
|
+
state.milestones.splice(index, 1, normalized);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/** 启动第一个 pending 里程碑。要求处于 learning 阶段。 */
|
|
233
|
+
startNextMilestone(state) {
|
|
234
|
+
if (state.phase !== 'learning') {
|
|
235
|
+
throw new LearningError('ILLEGAL_TRANSITION', `阶段 "${state.phase}" 下不能启动里程碑`);
|
|
236
|
+
}
|
|
237
|
+
const next = state.milestones.find((milestone) => milestone.status === 'pending');
|
|
238
|
+
if (next !== undefined)
|
|
239
|
+
next.status = 'in_progress';
|
|
240
|
+
}
|
|
241
|
+
/** 重新打开一个 failed 里程碑,允许再试一次。 */
|
|
242
|
+
retryMilestone(state, milestoneId) {
|
|
243
|
+
const milestone = this.milestone(state, milestoneId);
|
|
244
|
+
if (milestone.status !== 'failed') {
|
|
245
|
+
throw new LearningError('ILLEGAL_TRANSITION', `里程碑 "${milestoneId}" 当前是 "${milestone.status}",不是 failed`);
|
|
246
|
+
}
|
|
247
|
+
milestone.status = 'in_progress';
|
|
248
|
+
}
|
|
249
|
+
milestone(state, milestoneId) {
|
|
250
|
+
const milestone = state.milestones.find((entry) => entry.id === milestoneId);
|
|
251
|
+
if (milestone === undefined)
|
|
252
|
+
throw new LearningError('MILESTONE_UNKNOWN', `未知里程碑 "${milestoneId}"`);
|
|
253
|
+
return milestone;
|
|
254
|
+
}
|
|
255
|
+
// ── 问答状态机 ───────────────────────────────────────────────────────
|
|
256
|
+
/** 提出(或在失分后重提)某个里程碑问题。 */
|
|
257
|
+
askQuestion(state, milestoneId, questionIndex) {
|
|
258
|
+
const question = this.question(state, milestoneId, questionIndex);
|
|
259
|
+
if (question.status === 'asked')
|
|
260
|
+
return;
|
|
261
|
+
if (question.status !== 'unasked' && question.status !== 'failed') {
|
|
262
|
+
throw new LearningError('ILLEGAL_TRANSITION', `第 ${questionIndex} 个问题当前是 "${question.status}",不可提问`);
|
|
263
|
+
}
|
|
264
|
+
question.status = 'asked';
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* 为一个已提出的问题给出下一级升级提示。返回提示文本;提示阶梯用尽时返回
|
|
268
|
+
* `null` —— 此时必须公布答案,并要求学生用自己的话复述,再走 `assessAnswer`。
|
|
269
|
+
*/
|
|
270
|
+
giveHint(state, milestoneId, questionIndex) {
|
|
271
|
+
const question = this.question(state, milestoneId, questionIndex);
|
|
272
|
+
if (question.status !== 'asked') {
|
|
273
|
+
throw new LearningError('ILLEGAL_TRANSITION', `第 ${questionIndex} 个问题必须先提出才能给提示`);
|
|
274
|
+
}
|
|
275
|
+
if (question.hintLevel >= question.hints.length)
|
|
276
|
+
return null;
|
|
277
|
+
const hint = question.hints[question.hintLevel];
|
|
278
|
+
question.hintLevel += 1;
|
|
279
|
+
return hint ?? null;
|
|
280
|
+
}
|
|
281
|
+
/** 用评分结论(可选评分备注)给一个已提出的问题定分。 */
|
|
282
|
+
assessAnswer(state, milestoneId, questionIndex, passed, note) {
|
|
283
|
+
const question = this.question(state, milestoneId, questionIndex);
|
|
284
|
+
if (question.status !== 'asked') {
|
|
285
|
+
throw new LearningError('ILLEGAL_TRANSITION', `第 ${questionIndex} 个问题必须先提出才能评分`);
|
|
286
|
+
}
|
|
287
|
+
question.status = passed ? 'passed' : 'failed';
|
|
288
|
+
if (note !== undefined)
|
|
289
|
+
question.note = note;
|
|
290
|
+
}
|
|
291
|
+
question(state, milestoneId, questionIndex) {
|
|
292
|
+
const milestone = this.milestone(state, milestoneId);
|
|
293
|
+
const question = milestone.questions[questionIndex];
|
|
294
|
+
if (question === undefined) {
|
|
295
|
+
throw new LearningError('QUESTION_UNKNOWN', `里程碑 "${milestoneId}" 没有第 ${questionIndex} 个问题`);
|
|
296
|
+
}
|
|
297
|
+
return question;
|
|
298
|
+
}
|
|
299
|
+
// ── 验证 ─────────────────────────────────────────────────────────────
|
|
300
|
+
/**
|
|
301
|
+
* 记录一次实际执行的验证门,并把结论折进里程碑。
|
|
302
|
+
*
|
|
303
|
+
* - 退出码 0 且所有问题已 passed → `verified`,随后启动下一个 pending
|
|
304
|
+
* 里程碑(或整个流程完成);
|
|
305
|
+
* - 退出码 0 但仍有未通过的问题 → 保持 in_progress(`pending-questions`);
|
|
306
|
+
* - 退出码非 0 → `failed`;用 `retryMilestone` 重新打开。
|
|
307
|
+
*/
|
|
308
|
+
recordGate(state, milestoneId, input) {
|
|
309
|
+
const milestone = this.milestone(state, milestoneId);
|
|
310
|
+
if (milestone.status !== 'in_progress') {
|
|
311
|
+
throw new LearningError('ILLEGAL_TRANSITION', `里程碑 "${milestoneId}" 当前是 "${milestone.status}",不是 in_progress`);
|
|
312
|
+
}
|
|
313
|
+
const record = {
|
|
314
|
+
milestone: milestoneId,
|
|
315
|
+
command: [...input.command],
|
|
316
|
+
exitCode: input.exitCode,
|
|
317
|
+
stdout: truncate(input.stdout, this.resolved.maxCapturedOutput),
|
|
318
|
+
stderr: truncate(input.stderr, this.resolved.maxCapturedOutput),
|
|
319
|
+
durationMs: input.durationMs,
|
|
320
|
+
at: Date.now(),
|
|
321
|
+
};
|
|
322
|
+
state.records.push(record);
|
|
323
|
+
if (input.exitCode !== 0) {
|
|
324
|
+
milestone.status = 'failed';
|
|
325
|
+
return { kind: 'failed' };
|
|
326
|
+
}
|
|
327
|
+
const openQuestions = milestone.questions.filter((question) => question.status !== 'passed');
|
|
328
|
+
if (openQuestions.length > 0) {
|
|
329
|
+
return { kind: 'pending-questions' };
|
|
330
|
+
}
|
|
331
|
+
milestone.status = 'verified';
|
|
332
|
+
const open = state.milestones.filter((entry) => entry.status === 'pending');
|
|
333
|
+
if (open.length === 0) {
|
|
334
|
+
state.phase = 'complete';
|
|
335
|
+
return { kind: 'verified' };
|
|
336
|
+
}
|
|
337
|
+
open[0].status = 'in_progress';
|
|
338
|
+
return { kind: 'verified' };
|
|
339
|
+
}
|
|
340
|
+
// ── 视图 ─────────────────────────────────────────────────────────────
|
|
341
|
+
/** 只读快照,供状态视图、模型工具与注入使用。 */
|
|
342
|
+
snapshot(state) {
|
|
343
|
+
const current = state.milestones.find((milestone) => milestone.status === 'in_progress');
|
|
344
|
+
const next = state.milestones.find((milestone) => milestone.status === 'pending');
|
|
345
|
+
let questionCount = 0;
|
|
346
|
+
let doneQuestionCount = 0;
|
|
347
|
+
for (const milestone of state.milestones) {
|
|
348
|
+
questionCount += milestone.questions.length;
|
|
349
|
+
doneQuestionCount += milestone.questions.filter((question) => question.status === 'passed').length;
|
|
350
|
+
}
|
|
351
|
+
return {
|
|
352
|
+
state,
|
|
353
|
+
phase: state.phase,
|
|
354
|
+
current,
|
|
355
|
+
next,
|
|
356
|
+
gateCount: state.records.length,
|
|
357
|
+
questionCount,
|
|
358
|
+
doneQuestionCount,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
/** `/thesis-defense status` 用的人类可读状态文本。 */
|
|
362
|
+
describe(state) {
|
|
363
|
+
const view = this.snapshot(state);
|
|
364
|
+
const verified = state.milestones.filter((milestone) => milestone.status === 'verified').length;
|
|
365
|
+
const lines = [
|
|
366
|
+
`代码演练状态 — phase: ${view.phase}`,
|
|
367
|
+
` origin: ${state.origin.path} (${state.origin.language})`,
|
|
368
|
+
` level: ${state.scope.level}${state.scope.module === undefined ? '' : `, module: ${state.scope.module}`}`,
|
|
369
|
+
` milestones: ${verified}/${state.milestones.length} verified`,
|
|
370
|
+
` todos: ${state.todos.filter((todo) => todo.status === 'done').length}/${state.todos.length} done`,
|
|
371
|
+
` questions: ${view.doneQuestionCount}/${view.questionCount} passed`,
|
|
372
|
+
` gate runs: ${view.gateCount}`,
|
|
373
|
+
];
|
|
374
|
+
if (view.current !== undefined) {
|
|
375
|
+
const todos = view.current.todos
|
|
376
|
+
.map((id) => state.todos.find((todo) => todo.id === id))
|
|
377
|
+
.filter((todo) => todo !== undefined);
|
|
378
|
+
const doneCount = todos.filter((todo) => todo.status === 'done').length;
|
|
379
|
+
lines.push(` current: ${view.current.title} (todos ${doneCount}/${todos.length} done, questions ${view.current.questions.length})`);
|
|
380
|
+
}
|
|
381
|
+
else if (view.next !== undefined) {
|
|
382
|
+
lines.push(` next: ${view.next.title}`);
|
|
383
|
+
}
|
|
384
|
+
return lines.join('\n');
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
// ── 纯函数辅助 ───────────────────────────────────────────────────────────
|
|
388
|
+
function serializeState(state) {
|
|
389
|
+
return `${JSON.stringify(state, null, 2)}\n`;
|
|
390
|
+
}
|
|
391
|
+
function parseState(text) {
|
|
392
|
+
let value;
|
|
393
|
+
try {
|
|
394
|
+
value = JSON.parse(text);
|
|
395
|
+
}
|
|
396
|
+
catch {
|
|
397
|
+
throw new LearningError('STATE_INVALID', '状态文件不是合法 JSON');
|
|
398
|
+
}
|
|
399
|
+
const state = value;
|
|
400
|
+
validateStateShape(state);
|
|
401
|
+
return state;
|
|
402
|
+
}
|
|
403
|
+
/** 对已加载(或即将保存)状态做结构校验。 */
|
|
404
|
+
function validateStateShape(value) {
|
|
405
|
+
if (typeof value !== 'object' || value === null) {
|
|
406
|
+
throw new LearningError('STATE_INVALID', '状态必须是对象');
|
|
407
|
+
}
|
|
408
|
+
const state = value;
|
|
409
|
+
if (state.schemaVersion !== 1) {
|
|
410
|
+
throw new LearningError('STATE_INVALID', `不支持的 schemaVersion ${String(state.schemaVersion)}`);
|
|
411
|
+
}
|
|
412
|
+
if (!PHASES.includes(state.phase)) {
|
|
413
|
+
throw new LearningError('STATE_INVALID', `非法阶段 ${String(state.phase)}`);
|
|
414
|
+
}
|
|
415
|
+
if (typeof state.origin?.path !== 'string' ||
|
|
416
|
+
typeof state.origin.language !== 'string' ||
|
|
417
|
+
typeof state.scope?.level !== 'string') {
|
|
418
|
+
throw new LearningError('STATE_INVALID', 'origin 或 scope 字段格式错误');
|
|
419
|
+
}
|
|
420
|
+
if (state.gates === undefined || typeof state.gates[state.origin.language] !== 'object') {
|
|
421
|
+
throw new LearningError('STATE_INVALID', `语言 "${state.origin.language}" 没有记录验证门`);
|
|
422
|
+
}
|
|
423
|
+
if (!Array.isArray(state.milestones) || !Array.isArray(state.todos) || !Array.isArray(state.records)) {
|
|
424
|
+
throw new LearningError('STATE_INVALID', 'milestones、todos 或 records 不是数组');
|
|
425
|
+
}
|
|
426
|
+
for (const milestone of state.milestones) {
|
|
427
|
+
if (!MILESTONE_STATUS.includes(milestone.status)) {
|
|
428
|
+
throw new LearningError('STATE_INVALID', `里程碑 "${milestone.id}" 的状态非法`);
|
|
429
|
+
}
|
|
430
|
+
for (const todoId of milestone.todos) {
|
|
431
|
+
if (!state.todos.some((todo) => todo.id === todoId)) {
|
|
432
|
+
throw new LearningError('STATE_INVALID', `里程碑 "${milestone.id}" 引用了未知 todo "${todoId}"`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
function errorCode(error) {
|
|
438
|
+
if (typeof error === 'object' && error !== null && 'code' in error) {
|
|
439
|
+
const code = error.code;
|
|
440
|
+
if (typeof code === 'string')
|
|
441
|
+
return code;
|
|
442
|
+
}
|
|
443
|
+
return undefined;
|
|
444
|
+
}
|
|
445
|
+
function truncate(text, max) {
|
|
446
|
+
if (text.length <= max)
|
|
447
|
+
return text;
|
|
448
|
+
return `\u2026${text.slice(-(max - 1))}`;
|
|
449
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 演练引擎的结构化错误。每一次拒绝都携带稳定的 `code`,调用方(命令、工具)
|
|
3
|
+
* 因此可以按 code 分支,而不必去匹配文案。
|
|
4
|
+
*
|
|
5
|
+
* @module dsh-thesis/codewalk
|
|
6
|
+
*/
|
|
7
|
+
export type LearningErrorCode =
|
|
8
|
+
/** create 要求目标不存在,但已有状态文件。 */
|
|
9
|
+
'STATE_EXISTS'
|
|
10
|
+
/** 状态文件存在但无法解析或校验失败。 */
|
|
11
|
+
| 'STATE_INVALID'
|
|
12
|
+
/** 请求的迁移违反状态机。 */
|
|
13
|
+
| 'ILLEGAL_TRANSITION'
|
|
14
|
+
/** 里程碑 id 未知。 */
|
|
15
|
+
| 'MILESTONE_UNKNOWN'
|
|
16
|
+
/** todo id 未知。 */
|
|
17
|
+
| 'TODO_UNKNOWN'
|
|
18
|
+
/** 问题下标越界。 */
|
|
19
|
+
| 'QUESTION_UNKNOWN'
|
|
20
|
+
/** 该语言在解析后的配置里没有验证门。 */
|
|
21
|
+
| 'GATE_UNKNOWN'
|
|
22
|
+
/** 没有组合 shell 执行器,验证门无法运行。 */
|
|
23
|
+
| 'SHELL_MISSING';
|
|
24
|
+
/** 一次被拒绝的演练引擎操作。 */
|
|
25
|
+
export declare class LearningError extends Error {
|
|
26
|
+
/** 稳定的机器可读 code。 */
|
|
27
|
+
readonly code: LearningErrorCode;
|
|
28
|
+
constructor(code: LearningErrorCode, message: string);
|
|
29
|
+
}
|
|
30
|
+
/** 任意拒绝是否为 LearningError。 */
|
|
31
|
+
export declare function isLearningError(error: unknown): error is LearningError;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 演练引擎的结构化错误。每一次拒绝都携带稳定的 `code`,调用方(命令、工具)
|
|
3
|
+
* 因此可以按 code 分支,而不必去匹配文案。
|
|
4
|
+
*
|
|
5
|
+
* @module dsh-thesis/codewalk
|
|
6
|
+
*/
|
|
7
|
+
/** 一次被拒绝的演练引擎操作。 */
|
|
8
|
+
export class LearningError extends Error {
|
|
9
|
+
/** 稳定的机器可读 code。 */
|
|
10
|
+
code;
|
|
11
|
+
constructor(code, message) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = 'LearningError';
|
|
14
|
+
this.code = code;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** 任意拒绝是否为 LearningError。 */
|
|
18
|
+
export function isLearningError(error) {
|
|
19
|
+
return error instanceof LearningError;
|
|
20
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 本插件需要的那一小片宿主文件系统接缝。
|
|
3
|
+
*
|
|
4
|
+
* 刻意**不**从 `@deepseek-ai/dsh-fs` 导入:已发布的 rc.1 依赖树引用了未发布的包,
|
|
5
|
+
* 因此外部 bundle 不能安装 dsh 包。宿主真实的 `ctx.fs` 在运行期满足这个结构化
|
|
6
|
+
* 接口;引擎只通过这份声明的契约与它对话。
|
|
7
|
+
*
|
|
8
|
+
* @module dsh-thesis/codewalk
|
|
9
|
+
*/
|
|
10
|
+
/** 不透明的已解析文件句柄;只有 `displayPath` 可以对外展示。 */
|
|
11
|
+
export interface LearningFsTarget {
|
|
12
|
+
readonly targetKey: unknown;
|
|
13
|
+
readonly displayPath: string;
|
|
14
|
+
}
|
|
15
|
+
/** `stat` 返回的元信息。 */
|
|
16
|
+
export interface LearningFsInfo {
|
|
17
|
+
readonly version: unknown;
|
|
18
|
+
readonly type: string;
|
|
19
|
+
}
|
|
20
|
+
/** 受保护的写入意图:仅当目标不存在时创建。 */
|
|
21
|
+
export interface CreateIfAbsentIntent {
|
|
22
|
+
readonly createIfAbsent: true;
|
|
23
|
+
}
|
|
24
|
+
/** 引擎传给 `writeText` 的写入意图联合。 */
|
|
25
|
+
export type LearningWriteIntent = CreateIfAbsentIntent;
|
|
26
|
+
/** 引擎使用的文件系统表面;由宿主 `ctx.fs` 满足。 */
|
|
27
|
+
export interface StateFileSystem {
|
|
28
|
+
resolve(path: string, opts?: {
|
|
29
|
+
cwd?: string;
|
|
30
|
+
}): Promise<LearningFsTarget>;
|
|
31
|
+
stat(target: LearningFsTarget, signal?: AbortSignal): Promise<LearningFsInfo | undefined>;
|
|
32
|
+
readText(target: LearningFsTarget, signal?: AbortSignal): Promise<string>;
|
|
33
|
+
writeText(target: LearningFsTarget, content: string, expected?: LearningWriteIntent, signal?: AbortSignal): Promise<void>;
|
|
34
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 共享的验证门执行逻辑:通过 `ctx.shell` 跑一个里程碑的验证命令,把结论折进
|
|
3
|
+
* 引擎(记录 + 状态 + 落盘),再把结算后的结果交回给调用方渲染。
|
|
4
|
+
*
|
|
5
|
+
* 人类的 `/thesis-defense check` 命令与模型侧的 `defense_code_update` `check_gate` 动作共用这段
|
|
6
|
+
* 逻辑,因此两个表面永远不会对「什么叫 verified」产生分歧。
|
|
7
|
+
*
|
|
8
|
+
* @module dsh-thesis/codewalk
|
|
9
|
+
*/
|
|
10
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
11
|
+
import { AiLearningEngine } from './engine.ts';
|
|
12
|
+
import type { GateOutcome, LearningState, Milestone } from './types.ts';
|
|
13
|
+
export declare const GATE_TIMEOUT_MS = 120000;
|
|
14
|
+
/** 一次验证门执行结算后的结果。 */
|
|
15
|
+
export interface MilestoneGateOutcome {
|
|
16
|
+
readonly outcome: GateOutcome;
|
|
17
|
+
readonly command: readonly string[];
|
|
18
|
+
readonly exitCode: number;
|
|
19
|
+
readonly interrupted: boolean;
|
|
20
|
+
readonly stdout: string;
|
|
21
|
+
readonly stderr: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* 跑 `milestone` 的有效验证门,记录它,持久化状态,并返回结算后的结论。
|
|
25
|
+
* 验证门失败会自动重新打开该里程碑 —— 修完再跑一次检查就是预期的重试回路。
|
|
26
|
+
*
|
|
27
|
+
* `session`(调用方 agent 的会话,若有)会喂给宿主的逐调用沙箱策略解析,因此
|
|
28
|
+
* 带沙箱的 shell 执行器能拿到它需要的策略,而不是退回到部署默认值。
|
|
29
|
+
*/
|
|
30
|
+
export declare function runMilestoneGate(ctx: Context, engine: AiLearningEngine, cwd: string, state: LearningState, milestone: Milestone, signal: AbortSignal, session?: unknown): Promise<MilestoneGateOutcome>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 共享的验证门执行逻辑:通过 `ctx.shell` 跑一个里程碑的验证命令,把结论折进
|
|
3
|
+
* 引擎(记录 + 状态 + 落盘),再把结算后的结果交回给调用方渲染。
|
|
4
|
+
*
|
|
5
|
+
* 人类的 `/thesis-defense check` 命令与模型侧的 `defense_code_update` `check_gate` 动作共用这段
|
|
6
|
+
* 逻辑,因此两个表面永远不会对「什么叫 verified」产生分歧。
|
|
7
|
+
*
|
|
8
|
+
* @module dsh-thesis/codewalk
|
|
9
|
+
*/
|
|
10
|
+
import { AiLearningEngine } from "./engine.js";
|
|
11
|
+
import { LearningError } from "./errors.js";
|
|
12
|
+
export const GATE_TIMEOUT_MS = 120_000;
|
|
13
|
+
/**
|
|
14
|
+
* 跑 `milestone` 的有效验证门,记录它,持久化状态,并返回结算后的结论。
|
|
15
|
+
* 验证门失败会自动重新打开该里程碑 —— 修完再跑一次检查就是预期的重试回路。
|
|
16
|
+
*
|
|
17
|
+
* `session`(调用方 agent 的会话,若有)会喂给宿主的逐调用沙箱策略解析,因此
|
|
18
|
+
* 带沙箱的 shell 执行器能拿到它需要的策略,而不是退回到部署默认值。
|
|
19
|
+
*/
|
|
20
|
+
export async function runMilestoneGate(ctx, engine, cwd, state, milestone, signal, session) {
|
|
21
|
+
const shell = ctx.get('shell');
|
|
22
|
+
if (shell === undefined) {
|
|
23
|
+
throw new LearningError('SHELL_MISSING', '没有组合 shell 执行器,无法运行验证门');
|
|
24
|
+
}
|
|
25
|
+
const gate = milestone.gate ?? state.gates[state.origin.language];
|
|
26
|
+
if (gate === undefined) {
|
|
27
|
+
throw new LearningError('GATE_UNKNOWN', `语言 "${state.origin.language}" 没有记录验证门`);
|
|
28
|
+
}
|
|
29
|
+
if (milestone.status === 'failed')
|
|
30
|
+
engine.retryMilestone(state, milestone.id);
|
|
31
|
+
const command = [...gate.build];
|
|
32
|
+
const started = Date.now();
|
|
33
|
+
const result = await shell.run({
|
|
34
|
+
command: command.join(' '),
|
|
35
|
+
workdir: cwd,
|
|
36
|
+
timeoutMs: GATE_TIMEOUT_MS,
|
|
37
|
+
stdoutMaxBytes: engine.config.maxCapturedOutput,
|
|
38
|
+
signal,
|
|
39
|
+
sandboxPolicy: resolveSandboxPolicy(ctx, session),
|
|
40
|
+
});
|
|
41
|
+
const durationMs = Date.now() - started;
|
|
42
|
+
const interrupted = result.timedOut || result.aborted;
|
|
43
|
+
const exitCode = interrupted ? 1 : (result.exitCode ?? 1);
|
|
44
|
+
const outcome = engine.recordGate(state, milestone.id, {
|
|
45
|
+
command,
|
|
46
|
+
exitCode,
|
|
47
|
+
stdout: result.stdout.text,
|
|
48
|
+
stderr: result.stderr.text,
|
|
49
|
+
durationMs,
|
|
50
|
+
});
|
|
51
|
+
await engine.save(cwd, state);
|
|
52
|
+
return { outcome, command, exitCode, interrupted, stdout: result.stdout.text, stderr: result.stderr.text };
|
|
53
|
+
}
|
|
54
|
+
/** 宿主组合了沙箱策略服务时,解析本次调用的策略。 */
|
|
55
|
+
function resolveSandboxPolicy(ctx, session) {
|
|
56
|
+
const service = ctx.get('sandboxPolicy');
|
|
57
|
+
if (service === undefined)
|
|
58
|
+
return undefined;
|
|
59
|
+
return service.resolve(session === undefined ? {} : { session });
|
|
60
|
+
}
|