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.
Files changed (188) hide show
  1. package/DESIGN.md +390 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE.md +10 -0
  4. package/README.md +253 -0
  5. package/cordis.patch.yml +17 -0
  6. package/docs/host-api.md +640 -0
  7. package/lib/codewalk/commands.d.ts +23 -0
  8. package/lib/codewalk/commands.js +168 -0
  9. package/lib/codewalk/engine.d.ts +108 -0
  10. package/lib/codewalk/engine.js +449 -0
  11. package/lib/codewalk/errors.d.ts +31 -0
  12. package/lib/codewalk/errors.js +20 -0
  13. package/lib/codewalk/fs-types.d.ts +34 -0
  14. package/lib/codewalk/fs-types.js +10 -0
  15. package/lib/codewalk/gate.d.ts +30 -0
  16. package/lib/codewalk/gate.js +60 -0
  17. package/lib/codewalk/host-types.d.ts +198 -0
  18. package/lib/codewalk/host-types.js +11 -0
  19. package/lib/codewalk/prestep.d.ts +17 -0
  20. package/lib/codewalk/prestep.js +62 -0
  21. package/lib/codewalk/register.d.ts +23 -0
  22. package/lib/codewalk/register.js +31 -0
  23. package/lib/codewalk/skill.d.ts +15 -0
  24. package/lib/codewalk/skill.js +57 -0
  25. package/lib/codewalk/tools.d.ts +18 -0
  26. package/lib/codewalk/tools.js +314 -0
  27. package/lib/codewalk/types.d.ts +151 -0
  28. package/lib/codewalk/types.js +9 -0
  29. package/lib/commands.d.ts +33 -0
  30. package/lib/commands.js +60 -0
  31. package/lib/config.d.ts +125 -0
  32. package/lib/config.js +139 -0
  33. package/lib/dedup/commands.d.ts +21 -0
  34. package/lib/dedup/commands.js +93 -0
  35. package/lib/dedup/index.d.ts +75 -0
  36. package/lib/dedup/index.js +331 -0
  37. package/lib/dedup/normalize.d.ts +125 -0
  38. package/lib/dedup/normalize.js +329 -0
  39. package/lib/dedup/report.d.ts +114 -0
  40. package/lib/dedup/report.js +309 -0
  41. package/lib/dedup/rewrite.d.ts +71 -0
  42. package/lib/dedup/rewrite.js +228 -0
  43. package/lib/dedup/similarity.d.ts +169 -0
  44. package/lib/dedup/similarity.js +323 -0
  45. package/lib/gates/dangerous.d.ts +22 -0
  46. package/lib/gates/dangerous.js +47 -0
  47. package/lib/gates/detect.d.ts +54 -0
  48. package/lib/gates/detect.js +106 -0
  49. package/lib/gates/messages.d.ts +39 -0
  50. package/lib/gates/messages.js +74 -0
  51. package/lib/gates/prompt.d.ts +21 -0
  52. package/lib/gates/prompt.js +56 -0
  53. package/lib/gates/writing-intent.d.ts +49 -0
  54. package/lib/gates/writing-intent.js +68 -0
  55. package/lib/index.d.ts +36 -0
  56. package/lib/index.js +76 -0
  57. package/lib/ingest/code.d.ts +71 -0
  58. package/lib/ingest/code.js +356 -0
  59. package/lib/ingest/commands.d.ts +23 -0
  60. package/lib/ingest/commands.js +74 -0
  61. package/lib/ingest/docx.d.ts +29 -0
  62. package/lib/ingest/docx.js +92 -0
  63. package/lib/ingest/index.d.ts +179 -0
  64. package/lib/ingest/index.js +640 -0
  65. package/lib/ingest/ooxml.d.ts +58 -0
  66. package/lib/ingest/ooxml.js +393 -0
  67. package/lib/ingest/pdf.d.ts +86 -0
  68. package/lib/ingest/pdf.js +1016 -0
  69. package/lib/ingest/pptx.d.ts +32 -0
  70. package/lib/ingest/pptx.js +116 -0
  71. package/lib/ingest/text.d.ts +25 -0
  72. package/lib/ingest/text.js +180 -0
  73. package/lib/ingest/types.d.ts +48 -0
  74. package/lib/ingest/types.js +47 -0
  75. package/lib/ingest/xlsx.d.ts +72 -0
  76. package/lib/ingest/xlsx.js +411 -0
  77. package/lib/ingest/zip.d.ts +76 -0
  78. package/lib/ingest/zip.js +393 -0
  79. package/lib/intake/definitions.d.ts +65 -0
  80. package/lib/intake/definitions.js +42 -0
  81. package/lib/intake/gate-rules.d.ts +57 -0
  82. package/lib/intake/gate-rules.js +118 -0
  83. package/lib/intake/gate.d.ts +24 -0
  84. package/lib/intake/gate.js +86 -0
  85. package/lib/intake/index.d.ts +76 -0
  86. package/lib/intake/index.js +468 -0
  87. package/lib/intake/questions.d.ts +169 -0
  88. package/lib/intake/questions.js +720 -0
  89. package/lib/intake/spec.d.ts +66 -0
  90. package/lib/intake/spec.js +221 -0
  91. package/lib/intake/state.d.ts +192 -0
  92. package/lib/intake/state.js +420 -0
  93. package/lib/memory/recall.d.ts +69 -0
  94. package/lib/memory/recall.js +177 -0
  95. package/lib/memory/register.d.ts +37 -0
  96. package/lib/memory/register.js +52 -0
  97. package/lib/memory/service.d.ts +39 -0
  98. package/lib/memory/service.js +39 -0
  99. package/lib/memory/store.d.ts +45 -0
  100. package/lib/memory/store.js +123 -0
  101. package/lib/memory/tools.d.ts +19 -0
  102. package/lib/memory/tools.js +195 -0
  103. package/lib/memory/types.d.ts +55 -0
  104. package/lib/memory/types.js +6 -0
  105. package/lib/paper/commands.d.ts +13 -0
  106. package/lib/paper/commands.js +84 -0
  107. package/lib/paper/lib/aicheck.d.ts +38 -0
  108. package/lib/paper/lib/aicheck.js +241 -0
  109. package/lib/paper/lib/bibtex.d.ts +21 -0
  110. package/lib/paper/lib/bibtex.js +114 -0
  111. package/lib/paper/lib/check.d.ts +32 -0
  112. package/lib/paper/lib/check.js +215 -0
  113. package/lib/paper/lib/defense.d.ts +48 -0
  114. package/lib/paper/lib/defense.js +220 -0
  115. package/lib/paper/lib/docx.d.ts +35 -0
  116. package/lib/paper/lib/docx.js +213 -0
  117. package/lib/paper/lib/layout.d.ts +41 -0
  118. package/lib/paper/lib/layout.js +572 -0
  119. package/lib/paper/lib/ledger.d.ts +112 -0
  120. package/lib/paper/lib/ledger.js +349 -0
  121. package/lib/paper/lib/lit-api.d.ts +59 -0
  122. package/lib/paper/lib/lit-api.js +296 -0
  123. package/lib/paper/lib/lit-cache.d.ts +32 -0
  124. package/lib/paper/lib/lit-cache.js +91 -0
  125. package/lib/paper/lib/project.d.ts +29 -0
  126. package/lib/paper/lib/project.js +93 -0
  127. package/lib/paper/lib/review.d.ts +37 -0
  128. package/lib/paper/lib/review.js +194 -0
  129. package/lib/paper/lib/zip.d.ts +14 -0
  130. package/lib/paper/lib/zip.js +124 -0
  131. package/lib/paper/register.d.ts +20 -0
  132. package/lib/paper/register.js +183 -0
  133. package/lib/paper/tools/aicheck.d.ts +6 -0
  134. package/lib/paper/tools/aicheck.js +38 -0
  135. package/lib/paper/tools/build.d.ts +27 -0
  136. package/lib/paper/tools/build.js +166 -0
  137. package/lib/paper/tools/check.d.ts +7 -0
  138. package/lib/paper/tools/check.js +79 -0
  139. package/lib/paper/tools/decision.d.ts +13 -0
  140. package/lib/paper/tools/decision.js +53 -0
  141. package/lib/paper/tools/defense.d.ts +11 -0
  142. package/lib/paper/tools/defense.js +74 -0
  143. package/lib/paper/tools/init.d.ts +23 -0
  144. package/lib/paper/tools/init.js +99 -0
  145. package/lib/paper/tools/lit.d.ts +27 -0
  146. package/lib/paper/tools/lit.js +236 -0
  147. package/lib/paper/tools/progress.d.ts +18 -0
  148. package/lib/paper/tools/progress.js +77 -0
  149. package/lib/paper/tools/review.d.ts +11 -0
  150. package/lib/paper/tools/review.js +41 -0
  151. package/lib/ppt/actions.d.ts +97 -0
  152. package/lib/ppt/actions.js +263 -0
  153. package/lib/ppt/check.d.ts +64 -0
  154. package/lib/ppt/check.js +185 -0
  155. package/lib/ppt/commands.d.ts +28 -0
  156. package/lib/ppt/commands.js +71 -0
  157. package/lib/ppt/convert.d.ts +146 -0
  158. package/lib/ppt/convert.js +423 -0
  159. package/lib/ppt/index.d.ts +45 -0
  160. package/lib/ppt/index.js +57 -0
  161. package/lib/ppt/marp.d.ts +74 -0
  162. package/lib/ppt/marp.js +165 -0
  163. package/lib/ppt/outline.d.ts +206 -0
  164. package/lib/ppt/outline.js +695 -0
  165. package/lib/shared/define-tool.d.ts +77 -0
  166. package/lib/shared/define-tool.js +235 -0
  167. package/lib/shared/fs-errors.d.ts +50 -0
  168. package/lib/shared/fs-errors.js +90 -0
  169. package/lib/shared/index.d.ts +31 -0
  170. package/lib/shared/index.js +32 -0
  171. package/lib/shared/spawn.d.ts +38 -0
  172. package/lib/shared/spawn.js +50 -0
  173. package/lib/shared/text.d.ts +14 -0
  174. package/lib/shared/text.js +26 -0
  175. package/package.json +82 -0
  176. package/skills/code-walkthrough/SKILL.md +94 -0
  177. package/skills/code-walkthrough/references/examples.md +129 -0
  178. package/skills/paper-dedup/SKILL.md +58 -0
  179. package/skills/paper-intake/SKILL.md +64 -0
  180. package/skills/thesis-citation/SKILL.md +67 -0
  181. package/skills/thesis-defense/SKILL.md +87 -0
  182. package/skills/thesis-eng-design/SKILL.md +97 -0
  183. package/skills/thesis-inquiry/SKILL.md +55 -0
  184. package/skills/thesis-inquiry/references/intent-spec-template.md +39 -0
  185. package/skills/thesis-literature/SKILL.md +56 -0
  186. package/skills/thesis-opener/SKILL.md +77 -0
  187. package/skills/thesis-pipeline/SKILL.md +121 -0
  188. package/skills/thesis-writing/SKILL.md +86 -0
@@ -0,0 +1,77 @@
1
+ /**
2
+ * 工具定义器:**生产用宿主的 `defineTool`,离线用本地等价实现**。
3
+ *
4
+ * 为什么需要它:本插件的工具定义用的是 DSH 的作者侧 schema DSL(`required: true`
5
+ * 是**每个属性**的注解,不是 JSON Schema 的 `required` 数组),必须编译成宿主
6
+ * 认可的 JSON Schema 子集后才能交给 `ctx.tools.register`。这个编译步骤由
7
+ * `@deepseek-ai/dsh-tools` 的 `defineTool` 完成——但它是**运行时**依赖:
8
+ * 在没有宿主的离线环境(`npm test`、评审、CI)里 import 它会直接 ERR_MODULE_NOT_FOUND,
9
+ * 于是所有注册工具的模块都无法被单测加载,装配测试也写不出来。
10
+ *
11
+ * 解法是双模:
12
+ * - **生产**:动态 import 宿主的 `defineTool`,行为与 DSH 第一方插件完全一致
13
+ * (含宿主自己的参数校验与 `ToolArgsError`);
14
+ * - **离线**:退回本文件里的等价实现,覆盖本插件实际使用的 DSL 子集
15
+ * (string/number/integer/boolean/array/object/json/oneOf/enum/const + 注解),
16
+ * 并提供同样的"参数违规 → `INVALID_ARGS`"行为。
17
+ *
18
+ * 编译结果的一致性由两件事兜底:① `tests/shared-define-tool.test.ts` 逐例断言
19
+ * 编译产物;② `scripts/host-smoke.mjs` 在真实 DSH 宿主上注册并执行工具(见 README「开发」)。
20
+ *
21
+ * @module dsh-thesis/shared/define-tool
22
+ */
23
+ import type { ToolExecution } from '@deepseek-ai/dsh-tools';
24
+ /** 作者侧值 schema(本插件用到的子集)。 */
25
+ interface ValueSpec {
26
+ type?: string;
27
+ description?: string;
28
+ title?: string;
29
+ default?: unknown;
30
+ examples?: unknown;
31
+ enum?: readonly unknown[];
32
+ const?: unknown;
33
+ items?: ValueSpec;
34
+ properties?: Record<string, ValueSpec & {
35
+ required?: true;
36
+ }>;
37
+ additionalProperties?: boolean;
38
+ oneOf?: readonly ValueSpec[];
39
+ required?: true;
40
+ }
41
+ /** 工具定义(作者侧形态)。 */
42
+ export interface PaperToolOptions {
43
+ readonly name: string;
44
+ readonly description: string;
45
+ readonly parameters: Record<string, ValueSpec & {
46
+ required?: true;
47
+ }>;
48
+ readonly output: {
49
+ readonly schema: ValueSpec;
50
+ render(args: any, value: any): Array<{
51
+ type: 'text';
52
+ text: string;
53
+ }>;
54
+ };
55
+ execute(args: any, exec: ToolExecution): Promise<unknown>;
56
+ }
57
+ /** 把作者侧的隐式开放参数对象编译成根级 JSON Schema。 */
58
+ export declare function compileParameterSchema(spec: Record<string, ValueSpec & {
59
+ required?: true;
60
+ }>): Record<string, unknown>;
61
+ /** 编译输出 schema。 */
62
+ export declare function compileValueSchema(spec: ValueSpec): Record<string, unknown>;
63
+ /** 参数违规:与宿主同名的错误码,便于 UI/日志统一识别。 */
64
+ export declare class PaperToolArgsError extends Error {
65
+ readonly code = "INVALID_ARGS";
66
+ readonly violations: string[];
67
+ constructor(violations: string[]);
68
+ }
69
+ /** 是否运行在真实宿主内(供工具返回值与测试断言使用)。 */
70
+ export declare const usingHostDefineTool: boolean;
71
+ /**
72
+ * 定义一个模型工具。
73
+ *
74
+ * 优先宿主实现(与 DSH 第一方插件完全一致),离线退回本地等价实现。
75
+ */
76
+ export declare function definePaperTool(options: PaperToolOptions): unknown;
77
+ export {};
@@ -0,0 +1,235 @@
1
+ /**
2
+ * 工具定义器:**生产用宿主的 `defineTool`,离线用本地等价实现**。
3
+ *
4
+ * 为什么需要它:本插件的工具定义用的是 DSH 的作者侧 schema DSL(`required: true`
5
+ * 是**每个属性**的注解,不是 JSON Schema 的 `required` 数组),必须编译成宿主
6
+ * 认可的 JSON Schema 子集后才能交给 `ctx.tools.register`。这个编译步骤由
7
+ * `@deepseek-ai/dsh-tools` 的 `defineTool` 完成——但它是**运行时**依赖:
8
+ * 在没有宿主的离线环境(`npm test`、评审、CI)里 import 它会直接 ERR_MODULE_NOT_FOUND,
9
+ * 于是所有注册工具的模块都无法被单测加载,装配测试也写不出来。
10
+ *
11
+ * 解法是双模:
12
+ * - **生产**:动态 import 宿主的 `defineTool`,行为与 DSH 第一方插件完全一致
13
+ * (含宿主自己的参数校验与 `ToolArgsError`);
14
+ * - **离线**:退回本文件里的等价实现,覆盖本插件实际使用的 DSL 子集
15
+ * (string/number/integer/boolean/array/object/json/oneOf/enum/const + 注解),
16
+ * 并提供同样的"参数违规 → `INVALID_ARGS`"行为。
17
+ *
18
+ * 编译结果的一致性由两件事兜底:① `tests/shared-define-tool.test.ts` 逐例断言
19
+ * 编译产物;② `scripts/host-smoke.mjs` 在真实 DSH 宿主上注册并执行工具(见 README「开发」)。
20
+ *
21
+ * @module dsh-thesis/shared/define-tool
22
+ */
23
+ /** 编译期错误:作者写错了 schema(与宿主一样,装配时大声失败)。 */
24
+ function authorError(message) {
25
+ throw new Error(`dsh-thesis defineTool: ${message}`);
26
+ }
27
+ function copyAnnotations(from, to) {
28
+ for (const key of ['description', 'title', 'default', 'examples']) {
29
+ if (from[key] !== undefined)
30
+ to[key] = from[key];
31
+ }
32
+ }
33
+ /** 把「属性映射 + 逐属性 required」编译成 `{properties, required}` 形式。 */
34
+ function compilePropertyMap(spec, path) {
35
+ const properties = {};
36
+ const required = [];
37
+ for (const [name, raw] of Object.entries(spec ?? {})) {
38
+ if (raw === null || typeof raw !== 'object')
39
+ authorError(`${path}.${name} 必须是对象`);
40
+ const { required: isRequired, ...rest } = raw;
41
+ if (isRequired === true)
42
+ required.push(name);
43
+ properties[name] = compileValue(rest, `${path}.${name}`);
44
+ }
45
+ return {
46
+ properties: Object.keys(properties).length > 0 ? properties : undefined,
47
+ required: required.length > 0 ? required : undefined,
48
+ };
49
+ }
50
+ /** 编译一个值 schema 节点。 */
51
+ function compileValue(spec, path) {
52
+ if (spec === null || typeof spec !== 'object')
53
+ authorError(`${path} 必须是对象`);
54
+ const out = {};
55
+ copyAnnotations(spec, out);
56
+ if (spec.oneOf !== undefined) {
57
+ if (!Array.isArray(spec.oneOf) || spec.oneOf.length < 2)
58
+ authorError(`${path}.oneOf 至少需要两个分支`);
59
+ out.oneOf = spec.oneOf.map((branch, index) => compileValue(branch, `${path}.oneOf[${index}]`));
60
+ return out;
61
+ }
62
+ const type = spec.type;
63
+ if (type === undefined)
64
+ authorError(`${path} 缺少 type`);
65
+ // 作者专用:只作注解,不施加约束(对应宿主把 json 节点编译为注解式 schema)。
66
+ if (type === 'json')
67
+ return out;
68
+ out.type = type;
69
+ if (spec.enum !== undefined)
70
+ out.enum = spec.enum;
71
+ if (spec.const !== undefined)
72
+ out.const = spec.const;
73
+ if (type === 'object') {
74
+ // 与宿主作者侧契约一致:object 必须显式声明开放性(additionalProperties: boolean),
75
+ // 避免"忘了写"被静默解释成任一种语义。
76
+ if (typeof spec.additionalProperties !== 'boolean') {
77
+ authorError(`${path}: type 'object' 必须显式声明 additionalProperties(true=开放 / false=封闭)`);
78
+ }
79
+ const compiled = compilePropertyMap(spec.properties ?? {}, path);
80
+ if (compiled.properties !== undefined)
81
+ out.properties = compiled.properties;
82
+ if (compiled.required !== undefined)
83
+ out.required = compiled.required;
84
+ out.additionalProperties = spec.additionalProperties;
85
+ }
86
+ else if (type === 'array') {
87
+ if (spec.items !== undefined)
88
+ out.items = compileValue(spec.items, `${path}.items`);
89
+ }
90
+ return out;
91
+ }
92
+ /** 把作者侧的隐式开放参数对象编译成根级 JSON Schema。 */
93
+ export function compileParameterSchema(spec) {
94
+ const compiled = compilePropertyMap(spec, 'parameters');
95
+ return {
96
+ type: 'object',
97
+ ...compiled.properties !== undefined ? { properties: compiled.properties } : {},
98
+ ...compiled.required !== undefined ? { required: compiled.required } : {},
99
+ };
100
+ }
101
+ /** 编译输出 schema。 */
102
+ export function compileValueSchema(spec) {
103
+ return compileValue(spec, 'schema');
104
+ }
105
+ // ---------------------------------------------------------------------------
106
+ // 离线参数校验(等价于宿主 defineTool 内部对模型实参的校验)
107
+ // ---------------------------------------------------------------------------
108
+ function describe(value) {
109
+ if (value === null)
110
+ return 'null';
111
+ if (Array.isArray(value))
112
+ return 'array';
113
+ return typeof value;
114
+ }
115
+ function jsonTypeOf(value) {
116
+ if (value === null)
117
+ return 'null';
118
+ if (Array.isArray(value))
119
+ return 'array';
120
+ if (typeof value === 'number')
121
+ return Number.isInteger(value) ? 'integer' : 'number';
122
+ return typeof value;
123
+ }
124
+ function typeMatches(schema, value) {
125
+ const expected = schema.type;
126
+ if (expected === undefined)
127
+ return true;
128
+ const actual = jsonTypeOf(value);
129
+ if (expected === 'number')
130
+ return actual === 'number' || actual === 'integer';
131
+ if (expected === 'integer')
132
+ return actual === 'integer';
133
+ if (expected === 'null')
134
+ return actual === 'null';
135
+ // 注解式 json 节点不约束类型。
136
+ if (expected === 'json')
137
+ return true;
138
+ return actual === expected;
139
+ }
140
+ /** 递归校验一个值;返回路径限定的违规列表(空数组 = 通过)。 */
141
+ function validateValue(schema, value, path) {
142
+ const violations = [];
143
+ const oneOf = schema.oneOf;
144
+ if (oneOf !== undefined) {
145
+ const ok = oneOf.some(branch => validateValue(branch, value, path).length === 0);
146
+ if (!ok)
147
+ violations.push(`${path || '(root)'} 不满足 oneOf 的任何一个分支`);
148
+ return violations;
149
+ }
150
+ if (!typeMatches(schema, value)) {
151
+ violations.push(`${path || '(root)'} 期望 ${String(schema.type)},实际 ${describe(value)}`);
152
+ return violations;
153
+ }
154
+ const enumValues = schema.enum;
155
+ if (enumValues !== undefined && !enumValues.some(candidate => Object.is(candidate, value))) {
156
+ violations.push(`${path || '(root)'} 取值不在枚举内(${enumValues.map(v => JSON.stringify(v)).join(' / ')})`);
157
+ }
158
+ if (schema.type === 'object' && value !== null && typeof value === 'object' && !Array.isArray(value)) {
159
+ const record = value;
160
+ const required = schema.required ?? [];
161
+ for (const name of required) {
162
+ if (record[name] === undefined)
163
+ violations.push(`${path ? `${path}.` : ''}${name} 为必填参数`);
164
+ }
165
+ const properties = schema.properties ?? {};
166
+ for (const [name, childSchema] of Object.entries(properties)) {
167
+ const child = record[name];
168
+ if (child === undefined)
169
+ continue;
170
+ violations.push(...validateValue(childSchema, child, path ? `${path}.${name}` : name));
171
+ }
172
+ }
173
+ if (schema.type === 'array' && Array.isArray(value)) {
174
+ const items = schema.items;
175
+ if (items !== undefined) {
176
+ value.forEach((item, index) => {
177
+ violations.push(...validateValue(items, item, `${path}[${index}]`));
178
+ });
179
+ }
180
+ }
181
+ return violations;
182
+ }
183
+ /** 参数违规:与宿主同名的错误码,便于 UI/日志统一识别。 */
184
+ export class PaperToolArgsError extends Error {
185
+ code = 'INVALID_ARGS';
186
+ violations;
187
+ constructor(violations) {
188
+ super(`invalid arguments: ${violations.join('; ')}`);
189
+ this.name = 'PaperToolArgsError';
190
+ this.violations = violations;
191
+ }
192
+ }
193
+ /** 离线实现:编译 + 校验 + 返回注册就绪的定义。 */
194
+ function localDefineTool(options) {
195
+ const parameters = compileParameterSchema(options.parameters);
196
+ const outputSchema = compileValueSchema(options.output.schema);
197
+ const render = options.output.render;
198
+ return {
199
+ name: options.name,
200
+ description: options.description,
201
+ parameters,
202
+ output: { schema: outputSchema, render: (args, value) => render(args, value) },
203
+ async execute(args, exec) {
204
+ const violations = validateValue(parameters, args, '');
205
+ if (violations.length > 0)
206
+ throw new PaperToolArgsError(violations);
207
+ return options.execute(args, exec);
208
+ },
209
+ };
210
+ }
211
+ /**
212
+ * 生产用宿主 `defineTool`,离线为 `undefined`。
213
+ *
214
+ * 顶层 await 的代价是一次模块加载;收益是「离线可测」与「生产同源」两者兼得。
215
+ */
216
+ let hostDefineTool;
217
+ try {
218
+ const host = await import('@deepseek-ai/dsh-tools');
219
+ if (typeof host.defineTool === 'function') {
220
+ hostDefineTool = host.defineTool;
221
+ }
222
+ }
223
+ catch {
224
+ hostDefineTool = undefined;
225
+ }
226
+ /** 是否运行在真实宿主内(供工具返回值与测试断言使用)。 */
227
+ export const usingHostDefineTool = hostDefineTool !== undefined;
228
+ /**
229
+ * 定义一个模型工具。
230
+ *
231
+ * 优先宿主实现(与 DSH 第一方插件完全一致),离线退回本地等价实现。
232
+ */
233
+ export function definePaperTool(options) {
234
+ return hostDefineTool !== undefined ? hostDefineTool(options) : localDefineTool(options);
235
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * 文件系统错误分类:**区分「不存在」与「读失败」**。
3
+ *
4
+ * 这是本插件最容易出人命的一处细节。工作区里的文件都是学生的真实资产
5
+ * (追问答案、进度台账、文献库、检索缓存),而几乎所有读路径的写法都是:
6
+ *
7
+ * ```ts
8
+ * try { return await fs.readText(...) } catch { return undefined } // ✗ 危险
9
+ * ```
10
+ *
11
+ * 这种写法把「文件不存在(第一次运行,正常)」和「文件存在但读不出来
12
+ * (权限、IO 错误、句柄失效)」折叠成同一个 `undefined`,调用方于是认为
13
+ * 「还没建过」,转身用默认值把它**覆盖写回**——学生的数据就没了。
14
+ *
15
+ * 正确做法:只有**确认不存在**才当作 `undefined`,其余错误一律上抛,
16
+ * 让工具以可读错误告知用户。宿主与各测试夹具的错误码不同,故在此集中判定。
17
+ *
18
+ * @module dsh-thesis/shared/fs-errors
19
+ */
20
+ /** 各实现使用的「目标不存在」错误码。 */
21
+ export declare const MISSING_CODES: readonly string[];
22
+ /**
23
+ * 该错误是否表示「目标不存在」?
24
+ *
25
+ * 判据按可靠性排序:错误码 → 消息里的 `ENOENT` / `not found` / `不存在`。
26
+ * 消息回退是必要的:某些后端(含部分测试替身)不带错误码。
27
+ */
28
+ export declare function isMissingError(error: unknown): boolean;
29
+ /**
30
+ * 该错误是否表示「调用被取消」。
31
+ *
32
+ * 取消与失败必须分开处理:把 `AbortError` 当成「文件不存在」会让取消变成静默继续,
33
+ * 当成「检索源失败」又会让取消变成一条误导性的网络错误。`fetch` 与宿主 fs 在取消时
34
+ * 抛的都是 `name === 'AbortError'` 的错误。
35
+ */
36
+ export declare function isAbortError(error: unknown): boolean;
37
+ /**
38
+ * 读取一个「存在则读、不存在则 undefined」的文件;**其余错误原样抛出**。
39
+ *
40
+ * @param read 实际的读取动作(通常包住 `fs.resolve` + `fs.readText`)。
41
+ * @param onMissing 命中「不存在」时的返回值,缺省 `undefined`。
42
+ */
43
+ export declare function readOrUndefined<T>(read: () => Promise<T>, onMissing?: T | undefined): Promise<T | undefined>;
44
+ /**
45
+ * 把读取错误包装成给用户看的可读消息(工具层用)。
46
+ *
47
+ * 关键是把「读不出来」和「还没建过」在**文案上**也区分开,否则用户会以为
48
+ * 自己的文件真的不存在,从而接受一次错误的覆盖。
49
+ */
50
+ export declare function describeReadFailure(what: string, path: string, error: unknown, signal?: AbortSignal): string;
@@ -0,0 +1,90 @@
1
+ /**
2
+ * 文件系统错误分类:**区分「不存在」与「读失败」**。
3
+ *
4
+ * 这是本插件最容易出人命的一处细节。工作区里的文件都是学生的真实资产
5
+ * (追问答案、进度台账、文献库、检索缓存),而几乎所有读路径的写法都是:
6
+ *
7
+ * ```ts
8
+ * try { return await fs.readText(...) } catch { return undefined } // ✗ 危险
9
+ * ```
10
+ *
11
+ * 这种写法把「文件不存在(第一次运行,正常)」和「文件存在但读不出来
12
+ * (权限、IO 错误、句柄失效)」折叠成同一个 `undefined`,调用方于是认为
13
+ * 「还没建过」,转身用默认值把它**覆盖写回**——学生的数据就没了。
14
+ *
15
+ * 正确做法:只有**确认不存在**才当作 `undefined`,其余错误一律上抛,
16
+ * 让工具以可读错误告知用户。宿主与各测试夹具的错误码不同,故在此集中判定。
17
+ *
18
+ * @module dsh-thesis/shared/fs-errors
19
+ */
20
+ /** 各实现使用的「目标不存在」错误码。 */
21
+ export const MISSING_CODES = ['ENOENT', 'FS_NOT_FOUND'];
22
+ function codeOf(error) {
23
+ if (error === null || typeof error !== 'object')
24
+ return undefined;
25
+ const code = error.code;
26
+ return typeof code === 'string' ? code : undefined;
27
+ }
28
+ function messageOf(error) {
29
+ if (error instanceof Error)
30
+ return error.message;
31
+ return typeof error === 'string' ? error : '';
32
+ }
33
+ /**
34
+ * 该错误是否表示「目标不存在」?
35
+ *
36
+ * 判据按可靠性排序:错误码 → 消息里的 `ENOENT` / `not found` / `不存在`。
37
+ * 消息回退是必要的:某些后端(含部分测试替身)不带错误码。
38
+ */
39
+ export function isMissingError(error) {
40
+ const code = codeOf(error);
41
+ if (code !== undefined)
42
+ return MISSING_CODES.includes(code);
43
+ const message = messageOf(error);
44
+ if (message === '')
45
+ return false;
46
+ return /\bENOENT\b|FS_NOT_FOUND|\bnot found\b|不存在|no such file/i.test(message);
47
+ }
48
+ /**
49
+ * 该错误是否表示「调用被取消」。
50
+ *
51
+ * 取消与失败必须分开处理:把 `AbortError` 当成「文件不存在」会让取消变成静默继续,
52
+ * 当成「检索源失败」又会让取消变成一条误导性的网络错误。`fetch` 与宿主 fs 在取消时
53
+ * 抛的都是 `name === 'AbortError'` 的错误。
54
+ */
55
+ export function isAbortError(error) {
56
+ if (error === null || typeof error !== 'object')
57
+ return false;
58
+ return error.name === 'AbortError';
59
+ }
60
+ /**
61
+ * 读取一个「存在则读、不存在则 undefined」的文件;**其余错误原样抛出**。
62
+ *
63
+ * @param read 实际的读取动作(通常包住 `fs.resolve` + `fs.readText`)。
64
+ * @param onMissing 命中「不存在」时的返回值,缺省 `undefined`。
65
+ */
66
+ export async function readOrUndefined(read, onMissing = undefined) {
67
+ try {
68
+ return await read();
69
+ }
70
+ catch (error) {
71
+ if (isMissingError(error))
72
+ return onMissing;
73
+ throw error;
74
+ }
75
+ }
76
+ /**
77
+ * 把读取错误包装成给用户看的可读消息(工具层用)。
78
+ *
79
+ * 关键是把「读不出来」和「还没建过」在**文案上**也区分开,否则用户会以为
80
+ * 自己的文件真的不存在,从而接受一次错误的覆盖。
81
+ */
82
+ export function describeReadFailure(what, path, error, signal) {
83
+ if (signal?.aborted === true)
84
+ return `${what}读取被取消(${path})。`;
85
+ const code = codeOf(error);
86
+ const suffix = code !== undefined ? `(错误码 ${code})` : '';
87
+ const detail = messageOf(error);
88
+ return `${what}存在但读不出来:${path}${suffix}${detail === '' ? '' : ` —— ${detail}`}。可能是权限问题或文件损坏;`
89
+ + '已停止本次操作,未覆盖任何已有内容。请检查该文件后重试。';
90
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * 跨模块共用的工具胶水:输出形状、会话定位、二进制落盘。
3
+ *
4
+ * 三个模块(论文流水线 / 记忆 / 新能力)都通过这里拿同一套约定,
5
+ * 避免每个模块各写一份 `textOutput`/`sessionCwd` 造成的漂移。
6
+ *
7
+ * @module dsh-thesis/shared
8
+ */
9
+ import type { ToolExecution } from '@deepseek-ai/dsh-tools';
10
+ export { definePaperTool, usingHostDefineTool, compileParameterSchema, compileValueSchema, type PaperToolOptions } from './define-tool.ts';
11
+ /** 统一的纯文本工具输出(本插件所有工具都返回有界字符串)。 */
12
+ export declare function textOutput(): {
13
+ schema: {
14
+ type: "string";
15
+ };
16
+ render: (_args: unknown, value: unknown) => {
17
+ type: "text";
18
+ text: string;
19
+ }[];
20
+ };
21
+ /** 当前会话的工作目录;无 agent 的执行体(测试/程序化调用)返回 undefined。 */
22
+ export declare function sessionCwd(exec: ToolExecution): string | undefined;
23
+ /** 二进制落盘签名(docx 等产出)。 */
24
+ export type WriteBinary = (absPath: string, data: Uint8Array) => Promise<void>;
25
+ /**
26
+ * 二进制落盘:`ctx.fs` 没有二进制写入能力,docx/pptx 由插件经 node:fs 写。
27
+ * 这是文档化的例外,全插件只有这一处。
28
+ */
29
+ export declare const writeBinary: WriteBinary;
30
+ /** 缩进渲染一个字符串列表,用于工具返回值里的人类可读清单。 */
31
+ export declare function bulletList(items: readonly string[], marker?: string): string;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * 跨模块共用的工具胶水:输出形状、会话定位、二进制落盘。
3
+ *
4
+ * 三个模块(论文流水线 / 记忆 / 新能力)都通过这里拿同一套约定,
5
+ * 避免每个模块各写一份 `textOutput`/`sessionCwd` 造成的漂移。
6
+ *
7
+ * @module dsh-thesis/shared
8
+ */
9
+ import { writeFile } from 'node:fs/promises';
10
+ export { definePaperTool, usingHostDefineTool, compileParameterSchema, compileValueSchema } from "./define-tool.js";
11
+ /** 统一的纯文本工具输出(本插件所有工具都返回有界字符串)。 */
12
+ export function textOutput() {
13
+ return {
14
+ schema: { type: 'string' },
15
+ render: (_args, value) => [{ type: 'text', text: String(value) }],
16
+ };
17
+ }
18
+ /** 当前会话的工作目录;无 agent 的执行体(测试/程序化调用)返回 undefined。 */
19
+ export function sessionCwd(exec) {
20
+ return exec.agent?.session.header.cwd;
21
+ }
22
+ /**
23
+ * 二进制落盘:`ctx.fs` 没有二进制写入能力,docx/pptx 由插件经 node:fs 写。
24
+ * 这是文档化的例外,全插件只有这一处。
25
+ */
26
+ export const writeBinary = async (absPath, data) => {
27
+ await writeFile(absPath, data);
28
+ };
29
+ /** 缩进渲染一个字符串列表,用于工具返回值里的人类可读清单。 */
30
+ export function bulletList(items, marker = '- ') {
31
+ return items.map(item => `${marker}${item}`).join('\n');
32
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * 子进程调用的统一助手:**退出码必须等于 0 才算成功**,且不依赖被限制的管道 stdio。
3
+ *
4
+ * 两个真实教训:
5
+ *
6
+ * 1. **`status === null` 不是成功**。子进程从未启动(`EPERM`)、被信号杀死或被
7
+ * `timeout` 掐断时,`spawnSync` 返回 `status: null`。原先的写法是
8
+ * `if (status !== 0) 失败`,`null !== 0` 成立所以能挡住——但 `if (status === 0) 成功`
9
+ * 这类写法会把「没跑起来」当成成功。本模块统一用 {@link spawnOk} 判定,
10
+ * 并把 `error`(含 `EPERM`)与退出码一起回报,绝不静默。
11
+ *
12
+ * 2. **管道 stdio 在某些受限环境被拒**。沙箱/受限宿主下 `stdio: 'pipe'` 会直接
13
+ * `EPERM`,此时 `git init` 之类的调用一律失败。因此默认用 `'ignore'`:
14
+ * 退出码仍可用,但拿不到 stdout/stderr——需要输出内容的调用(如 `git log`)
15
+ * 必须显式传 `capture: true` 并在无输出时如实降级,而不是假装拿到结果。
16
+ *
17
+ * @module dsh-thesis/shared/spawn
18
+ */
19
+ export interface SpawnOptions {
20
+ readonly cwd?: string;
21
+ readonly timeoutMs?: number;
22
+ /** 是否需要 stdout/stderr 内容(默认 false;受限环境下管道 stdio 可能被拒)。 */
23
+ readonly capture?: boolean;
24
+ }
25
+ export interface SpawnOutcome {
26
+ /** 退出码为 0 时为 true;子进程未启动/被杀死(status=null)一律为 false。 */
27
+ readonly ok: boolean;
28
+ readonly status: number | null;
29
+ readonly stdout: string;
30
+ readonly stderr: string;
31
+ /** 子进程未能启动时的原因(如 `Error: spawnSync git EPERM`)。 */
32
+ readonly error: string | undefined;
33
+ readonly timedOut: boolean;
34
+ }
35
+ /** 运行一条命令并给出**可靠的成功判定**与尽可能完整的诊断信息。 */
36
+ export declare function spawnCommand(command: string, args: readonly string[], options?: SpawnOptions): SpawnOutcome;
37
+ /** 失败原因的可读描述(写进工具返回值,绝不出现「失败但什么都不说」)。 */
38
+ export declare function describeFailure(command: string, outcome: SpawnOutcome): string;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * 子进程调用的统一助手:**退出码必须等于 0 才算成功**,且不依赖被限制的管道 stdio。
3
+ *
4
+ * 两个真实教训:
5
+ *
6
+ * 1. **`status === null` 不是成功**。子进程从未启动(`EPERM`)、被信号杀死或被
7
+ * `timeout` 掐断时,`spawnSync` 返回 `status: null`。原先的写法是
8
+ * `if (status !== 0) 失败`,`null !== 0` 成立所以能挡住——但 `if (status === 0) 成功`
9
+ * 这类写法会把「没跑起来」当成成功。本模块统一用 {@link spawnOk} 判定,
10
+ * 并把 `error`(含 `EPERM`)与退出码一起回报,绝不静默。
11
+ *
12
+ * 2. **管道 stdio 在某些受限环境被拒**。沙箱/受限宿主下 `stdio: 'pipe'` 会直接
13
+ * `EPERM`,此时 `git init` 之类的调用一律失败。因此默认用 `'ignore'`:
14
+ * 退出码仍可用,但拿不到 stdout/stderr——需要输出内容的调用(如 `git log`)
15
+ * 必须显式传 `capture: true` 并在无输出时如实降级,而不是假装拿到结果。
16
+ *
17
+ * @module dsh-thesis/shared/spawn
18
+ */
19
+ import { spawnSync } from 'node:child_process';
20
+ /** 运行一条命令并给出**可靠的成功判定**与尽可能完整的诊断信息。 */
21
+ export function spawnCommand(command, args, options = {}) {
22
+ const capture = options.capture === true;
23
+ const result = spawnSync(command, [...args], {
24
+ ...(options.cwd !== undefined ? { cwd: options.cwd } : {}),
25
+ encoding: 'utf8',
26
+ stdio: capture ? 'pipe' : 'ignore',
27
+ ...(options.timeoutMs !== undefined ? { timeout: options.timeoutMs } : {}),
28
+ });
29
+ const error = result.error === undefined ? undefined : String(result.error);
30
+ // Node 在 timeout 触发时会设置 error.code === 'ETIMEDOUT'(或 signal 为 SIGTERM)。
31
+ const timedOut = error !== undefined && /ETIMEDOUT|timed out/i.test(error);
32
+ return {
33
+ ok: result.status === 0,
34
+ status: result.status,
35
+ stdout: result.stdout ?? '',
36
+ stderr: result.stderr ?? '',
37
+ error,
38
+ timedOut,
39
+ };
40
+ }
41
+ /** 失败原因的可读描述(写进工具返回值,绝不出现「失败但什么都不说」)。 */
42
+ export function describeFailure(command, outcome) {
43
+ const head = `${command} 未能完成`;
44
+ if (outcome.error !== undefined)
45
+ return `${head}:${outcome.error}`;
46
+ if (outcome.timedOut)
47
+ return `${head}:超时被终止`;
48
+ const detail = (outcome.stderr.trim() || outcome.stdout.trim()).split('\n')[0] ?? '';
49
+ return `${head}:退出码 ${outcome.status ?? 'null'}${detail === '' ? '' : ` —— ${detail}`}`;
50
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * 纯文本启发式工具:零依赖、纯函数,被闸门、追问与跨会话事实回灌共用。
3
+ *
4
+ * 这里的函数只回答「这段文本里有什么」——**不含任何产品判断**。
5
+ * 「什么样的文本应当触发闸门」属于 `gates/detect.ts`。
6
+ *
7
+ * @module dsh-thesis/shared/text
8
+ */
9
+ /** 命令摘录:截断到 max 字符,用于决策理由展示(绝不无限引用命令全文)。 */
10
+ export declare function excerpt(text: string, max?: number): string;
11
+ /** 从消息内容块中提取纯文本(只取 text 块)。 */
12
+ export declare function textOfBlocks(blocks: readonly {
13
+ type: string;
14
+ }[]): string;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * 纯文本启发式工具:零依赖、纯函数,被闸门、追问与跨会话事实回灌共用。
3
+ *
4
+ * 这里的函数只回答「这段文本里有什么」——**不含任何产品判断**。
5
+ * 「什么样的文本应当触发闸门」属于 `gates/detect.ts`。
6
+ *
7
+ * @module dsh-thesis/shared/text
8
+ */
9
+ /** 命令摘录:截断到 max 字符,用于决策理由展示(绝不无限引用命令全文)。 */
10
+ export function excerpt(text, max = 200) {
11
+ const single = text.replace(/\s+/g, ' ').trim();
12
+ if (single.length <= max)
13
+ return single;
14
+ return `${single.slice(0, max)}…(已截断,共 ${single.length} 字符)`;
15
+ }
16
+ /** 从消息内容块中提取纯文本(只取 text 块)。 */
17
+ export function textOfBlocks(blocks) {
18
+ let out = '';
19
+ for (const block of blocks) {
20
+ const text = block.text;
21
+ if (block.type === 'text' && typeof text === 'string')
22
+ out += text;
23
+ out += '\n';
24
+ }
25
+ return out;
26
+ }