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,393 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 零依赖 ZIP **读取器**(与 `paper/lib/zip.ts` 的写入器配对)。
|
|
3
|
+
*
|
|
4
|
+
* .docx/.xlsx/.pptx 都是 ZIP 容器,本文件负责把里面的条目安全地取出来:
|
|
5
|
+
* - 从尾部反向定位 EOCD(`0x06054b50`),再解析中央目录项(`0x02014b50`);
|
|
6
|
+
* - 只支持 method 0(STORE)与 method 8(DEFLATE,用 `node:zlib` 的
|
|
7
|
+
* `inflateRawSync`);其余压缩方式显式报错,绝不猜;
|
|
8
|
+
* - 文件名:general purpose flag bit 11 = UTF-8;否则按 CP437 解码(ZIP 规范
|
|
9
|
+
* 的历史默认),再退到 `TextDecoder('gbk')`——中文 Windows 的压缩工具常
|
|
10
|
+
* 直接用 GBK 写条目名;
|
|
11
|
+
* - **防御损坏输入**:截断、越界、ZIP64、条目数/单条目大小超限一律抛
|
|
12
|
+
* {@link ZipError}(可读中文原因),不返回半截垃圾;可选 CRC32 校验。
|
|
13
|
+
*
|
|
14
|
+
* 为什么不引 fflate/yauzl:本插件要求零第三方依赖(与写入器一致),
|
|
15
|
+
* OOXML 用到的两个压缩方法用一个 `inflateRawSync` 就够。
|
|
16
|
+
*
|
|
17
|
+
* @module dsh-thesis/ingest
|
|
18
|
+
*/
|
|
19
|
+
import { inflateRawSync } from 'node:zlib';
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// 常量与上限
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
const SIG_LOCAL = 0x04034b50;
|
|
24
|
+
const SIG_CENTRAL = 0x02014b50;
|
|
25
|
+
const SIG_EOCD = 0x06054b50;
|
|
26
|
+
const SIG_ZIP64_EOCD = 0x06064b50;
|
|
27
|
+
const SIG_ZIP64_LOCATOR = 0x07064b50;
|
|
28
|
+
const SIG_DATA_DESCRIPTOR = 0x08074b50;
|
|
29
|
+
/** EOCD 固定 22 字节;注释上限 65535 字节。 */
|
|
30
|
+
const EOCD_MIN = 22;
|
|
31
|
+
const MAX_COMMENT = 0xffff;
|
|
32
|
+
/** 中央目录条目数上限(OOXML 文档远小于此;防超大数字导致长时间循环)。 */
|
|
33
|
+
const MAX_ENTRIES = 4096;
|
|
34
|
+
/** 单条目解压后上限(32MB;docx 正文 XML 通常只有几百 KB)。 */
|
|
35
|
+
const MAX_ENTRY_BYTES = 32 * 1024 * 1024;
|
|
36
|
+
/** 中央目录总字节上限(防止伪造的目录长度让我们扫完整个文件)。 */
|
|
37
|
+
const MAX_CENTRAL_BYTES = 64 * 1024 * 1024;
|
|
38
|
+
const encoder = new TextEncoder();
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// CRC32(IEEE 802.3,多项式 0xEDB88320,表驱动)——与写入器同表,独立实现
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
const CRC_TABLE = (() => {
|
|
43
|
+
const table = new Uint32Array(256);
|
|
44
|
+
for (let n = 0; n < 256; n += 1) {
|
|
45
|
+
let c = n;
|
|
46
|
+
for (let k = 0; k < 8; k += 1) {
|
|
47
|
+
c = (c & 1) !== 0 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
48
|
+
}
|
|
49
|
+
table[n] = c >>> 0;
|
|
50
|
+
}
|
|
51
|
+
return table;
|
|
52
|
+
})();
|
|
53
|
+
/** 计算 CRC32(校验 STORE/DEFLATE 条目完整性)。 */
|
|
54
|
+
export function crc32(data) {
|
|
55
|
+
let c = 0xffffffff;
|
|
56
|
+
for (let i = 0; i < data.length; i += 1) {
|
|
57
|
+
c = CRC_TABLE[(c ^ data[i]) & 0xff] ^ (c >>> 8);
|
|
58
|
+
}
|
|
59
|
+
return (c ^ 0xffffffff) >>> 0;
|
|
60
|
+
}
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// 错误类型
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
/** ZIP 解析/解压错误:message 是给人看的中文原因,code 便于测试断言。 */
|
|
65
|
+
export class ZipError extends Error {
|
|
66
|
+
code;
|
|
67
|
+
constructor(code, message) {
|
|
68
|
+
super(message);
|
|
69
|
+
this.name = 'ZipError';
|
|
70
|
+
this.code = code;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// 字节读取(全部做边界检查,越界即抛 ZipError)
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
function requireRange(view, offset, length, what) {
|
|
77
|
+
if (offset < 0 || length < 0 || offset + length > view.byteLength) {
|
|
78
|
+
throw new ZipError('out-of-range', `ZIP 结构越界:${what} 需要 [${offset}, ${offset + length}),文件只有 ${view.byteLength} 字节(文件被截断或已损坏)。`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function u16(view, offset, what) {
|
|
82
|
+
requireRange(view, offset, 2, what);
|
|
83
|
+
return view.getUint16(offset, true);
|
|
84
|
+
}
|
|
85
|
+
function u32(view, offset, what) {
|
|
86
|
+
requireRange(view, offset, 4, what);
|
|
87
|
+
return view.getUint32(offset, true);
|
|
88
|
+
}
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// 文件名编码:UTF-8 / CP437 / GBK
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
/** CP437 码位表(0x80–0xFF)——ZIP 规范里非 UTF-8 条目名的默认编码。 */
|
|
93
|
+
const CP437 = 'ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜ¢£¥₧ƒ' +
|
|
94
|
+
'áíóúñѪº¿⌐¬½¼¡«»░▒▓│┤╡╢╖╕╣║╗╝╜╛┐' +
|
|
95
|
+
'└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀' +
|
|
96
|
+
'αßΓπΣσµτΦΘΩδ∞φε∩≡±≥≤⌠⌡÷≈°∙·√ⁿ²■\u00a0';
|
|
97
|
+
/** CP437 解码(逐码位查表;非法字节按 U+FFFD 处理,绝不抛错)。 */
|
|
98
|
+
function decodeCp437(bytes) {
|
|
99
|
+
let out = '';
|
|
100
|
+
for (let i = 0; i < bytes.length; i += 1) {
|
|
101
|
+
const b = bytes[i];
|
|
102
|
+
out += b < 0x80 ? String.fromCharCode(b) : CP437[b - 0x80] ?? '\ufffd';
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
/** UTF-8 解码(宽松模式:非法序列变 U+FFFD,不抛错)。 */
|
|
107
|
+
function decodeUtf8(bytes) {
|
|
108
|
+
try {
|
|
109
|
+
return new TextDecoder('utf-8').decode(bytes);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return decodeCp437(bytes);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/** GBK 解码尝试结果:false = 环境不支持 GBK(缺 ICU),不必再试。 */
|
|
116
|
+
let gbkSupported = true;
|
|
117
|
+
/**
|
|
118
|
+
* GBK 解码器(懒构造)。Node 的 TextDecoder 需要 ICU 才认识 'gbk';
|
|
119
|
+
* 构造或解码失败时返回 undefined,由调用方回退 CP437。
|
|
120
|
+
*/
|
|
121
|
+
function decodeGbk(bytes) {
|
|
122
|
+
if (!gbkSupported)
|
|
123
|
+
return undefined;
|
|
124
|
+
try {
|
|
125
|
+
return new TextDecoder('gbk').decode(bytes);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
gbkSupported = false;
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* 解出条目名。
|
|
134
|
+
* 顺序:flag bit 11 → UTF-8;否则先按 CP437(规范默认),若解出的名字全是
|
|
135
|
+
* 控制字符/替换符再试 GBK。
|
|
136
|
+
*/
|
|
137
|
+
function decodeName(raw, flags) {
|
|
138
|
+
if ((flags & 0x0800) !== 0)
|
|
139
|
+
return decodeUtf8(raw);
|
|
140
|
+
const cp437 = decodeCp437(raw);
|
|
141
|
+
const hasHighByte = raw.some(b => b >= 0x80);
|
|
142
|
+
if (!hasHighByte)
|
|
143
|
+
return cp437;
|
|
144
|
+
// 高位字节:CP437 会解出「┐├╢」这类框线/生僻字符,GBK 更可能是真名。
|
|
145
|
+
const gbk = decodeGbk(raw);
|
|
146
|
+
if (gbk !== undefined && gbk !== cp437 && !gbk.includes('\ufffd'))
|
|
147
|
+
return gbk;
|
|
148
|
+
return cp437;
|
|
149
|
+
}
|
|
150
|
+
/** ZIP 容器读取器:构造时即解析中央目录,条目数据懒解压并缓存。 */
|
|
151
|
+
export class ZipReader {
|
|
152
|
+
bytes;
|
|
153
|
+
view;
|
|
154
|
+
entries;
|
|
155
|
+
byName;
|
|
156
|
+
cache = new Map();
|
|
157
|
+
constructor(bytes) {
|
|
158
|
+
this.bytes = bytes;
|
|
159
|
+
this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
160
|
+
this.entries = this.parseCentralDirectory();
|
|
161
|
+
this.byName = new Map();
|
|
162
|
+
for (const entry of this.entries) {
|
|
163
|
+
// 重名条目(合法但罕见):先出现的胜出,与常见解压器一致。
|
|
164
|
+
if (!this.byName.has(entry.name))
|
|
165
|
+
this.byName.set(entry.name, entry);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/** 全部条目(含目录条目),按中央目录顺序。 */
|
|
169
|
+
list() {
|
|
170
|
+
return this.entries;
|
|
171
|
+
}
|
|
172
|
+
has(name) {
|
|
173
|
+
return this.byName.has(name);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* 按名字取条目字节;不存在返回 undefined。
|
|
177
|
+
* 解压失败/校验失败抛 {@link ZipError}(不返回半截数据)。
|
|
178
|
+
*/
|
|
179
|
+
read(name) {
|
|
180
|
+
const entry = this.byName.get(name);
|
|
181
|
+
if (entry === undefined)
|
|
182
|
+
return undefined;
|
|
183
|
+
return this.readEntry(entry);
|
|
184
|
+
}
|
|
185
|
+
/** 必须存在的条目;缺失抛 ZipError(给出「可能是别的格式」的提示)。 */
|
|
186
|
+
require(name) {
|
|
187
|
+
const data = this.read(name);
|
|
188
|
+
if (data === undefined) {
|
|
189
|
+
throw new ZipError('missing-entry', `ZIP 中找不到必需条目 ${name}(该文件可能不是有效的 OOXML 文档,或已损坏)。`);
|
|
190
|
+
}
|
|
191
|
+
return data;
|
|
192
|
+
}
|
|
193
|
+
/** 条目名里所有以 `prefix` 开头、以 `suffix` 结尾的名字(升序)。 */
|
|
194
|
+
names(prefix = '', suffix = '') {
|
|
195
|
+
const out = [];
|
|
196
|
+
for (const entry of this.entries) {
|
|
197
|
+
if (entry.directory)
|
|
198
|
+
continue;
|
|
199
|
+
if (!entry.name.startsWith(prefix))
|
|
200
|
+
continue;
|
|
201
|
+
if (!entry.name.endsWith(suffix))
|
|
202
|
+
continue;
|
|
203
|
+
out.push(entry.name);
|
|
204
|
+
}
|
|
205
|
+
return out.sort();
|
|
206
|
+
}
|
|
207
|
+
// ---- 内部:EOCD 与中央目录 ----
|
|
208
|
+
/** 从文件尾反向定位 EOCD 签名(最多回退 64KB+22)。 */
|
|
209
|
+
findEocd() {
|
|
210
|
+
const total = this.view.byteLength;
|
|
211
|
+
if (total < EOCD_MIN) {
|
|
212
|
+
throw new ZipError('truncated', `文件只有 ${total} 字节,连 ZIP 尾部记录(22 字节)都不够:不是有效的 ZIP/OOXML 文件。`);
|
|
213
|
+
}
|
|
214
|
+
const lowest = Math.max(0, total - EOCD_MIN - MAX_COMMENT);
|
|
215
|
+
for (let i = total - EOCD_MIN; i >= lowest; i -= 1) {
|
|
216
|
+
if (this.view.getUint32(i, true) !== SIG_EOCD)
|
|
217
|
+
continue;
|
|
218
|
+
// 注释长度必须与剩余字节数吻合,否则是数据里碰巧出现的同名字节。
|
|
219
|
+
const commentLength = this.view.getUint16(i + 20, true);
|
|
220
|
+
if (i + EOCD_MIN + commentLength === total)
|
|
221
|
+
return i;
|
|
222
|
+
}
|
|
223
|
+
throw new ZipError('no-eocd', '找不到 ZIP 尾部记录(EOCD):文件不是 ZIP/OOXML 格式,或已被截断/损坏。');
|
|
224
|
+
}
|
|
225
|
+
parseCentralDirectory() {
|
|
226
|
+
const eocd = this.findEocd();
|
|
227
|
+
// ZIP64:本模块不实现,但必须显式报错(否则会静默读出错数据)。
|
|
228
|
+
const locator = eocd - 20;
|
|
229
|
+
if (locator >= 0 && this.view.getUint32(locator, true) === SIG_ZIP64_LOCATOR) {
|
|
230
|
+
throw new ZipError('zip64', '这是 ZIP64 格式的压缩包(条目数或大小超过 4GB):本模块不支持 ZIP64,请先用普通方式重新保存该文档。');
|
|
231
|
+
}
|
|
232
|
+
if (eocd >= 4 && this.view.getUint32(eocd - 4, true) === SIG_ZIP64_EOCD) {
|
|
233
|
+
throw new ZipError('zip64', '这是 ZIP64 格式的压缩包:本模块不支持 ZIP64,请重新保存该文档。');
|
|
234
|
+
}
|
|
235
|
+
const total = this.view.byteLength;
|
|
236
|
+
const entryCount = u16(this.view, eocd + 10, 'EOCD 条目数');
|
|
237
|
+
const centralSize = u32(this.view, eocd + 12, 'EOCD 中央目录大小');
|
|
238
|
+
const centralOffset = u32(this.view, eocd + 16, 'EOCD 中央目录偏移');
|
|
239
|
+
if (entryCount === 0xffff || centralOffset === 0xffffffff || centralSize === 0xffffffff) {
|
|
240
|
+
throw new ZipError('zip64', '压缩包使用了 ZIP64 扩展字段:本模块不支持,请重新保存该文档。');
|
|
241
|
+
}
|
|
242
|
+
if (entryCount > MAX_ENTRIES) {
|
|
243
|
+
throw new ZipError('too-many-entries', `压缩包条目数 ${entryCount} 超过上限 ${MAX_ENTRIES}:拒绝解析(可能是恶意构造的文件)。`);
|
|
244
|
+
}
|
|
245
|
+
if (centralSize > MAX_CENTRAL_BYTES) {
|
|
246
|
+
throw new ZipError('central-too-large', `中央目录 ${centralSize} 字节超过上限 ${MAX_CENTRAL_BYTES}:拒绝解析。`);
|
|
247
|
+
}
|
|
248
|
+
if (centralOffset + centralSize > total) {
|
|
249
|
+
throw new ZipError('truncated', `中央目录区间 [${centralOffset}, ${centralOffset + centralSize}) 越出文件末尾 ${total} 字节:文件被截断。`);
|
|
250
|
+
}
|
|
251
|
+
const entries = [];
|
|
252
|
+
let offset = centralOffset;
|
|
253
|
+
for (let i = 0; i < entryCount; i += 1) {
|
|
254
|
+
const signature = u32(this.view, offset, `第 ${i + 1} 个中央目录项签名`);
|
|
255
|
+
if (signature !== SIG_CENTRAL) {
|
|
256
|
+
throw new ZipError('bad-central-entry', `第 ${i + 1} 个中央目录项签名异常(0x${signature.toString(16)},应为 0x02014b50):中央目录已损坏。`);
|
|
257
|
+
}
|
|
258
|
+
const flags = u16(this.view, offset + 8, '条目 flags');
|
|
259
|
+
const method = u16(this.view, offset + 10, '条目压缩方法');
|
|
260
|
+
const crc = u32(this.view, offset + 16, '条目 CRC32');
|
|
261
|
+
const compressedSize = u32(this.view, offset + 20, '条目压缩大小');
|
|
262
|
+
const size = u32(this.view, offset + 24, '条目解压大小');
|
|
263
|
+
const nameLength = u16(this.view, offset + 28, '条目名长度');
|
|
264
|
+
const extraLength = u16(this.view, offset + 30, '条目 extra 长度');
|
|
265
|
+
const commentLength = u16(this.view, offset + 32, '条目注释长度');
|
|
266
|
+
const localHeaderOffset = u32(this.view, offset + 42, '条目本地头偏移');
|
|
267
|
+
requireRange(this.view, offset + 46, nameLength + extraLength + commentLength, `第 ${i + 1} 个中央目录项正文`);
|
|
268
|
+
const nameBytes = this.bytes.subarray(offset + 46, offset + 46 + nameLength);
|
|
269
|
+
const name = decodeName(nameBytes, flags);
|
|
270
|
+
if (size === 0xffffffff || compressedSize === 0xffffffff || localHeaderOffset === 0xffffffff) {
|
|
271
|
+
throw new ZipError('zip64', `条目 ${name || '(未命名)'} 使用 ZIP64 扩展字段:本模块不支持,请重新保存该文档。`);
|
|
272
|
+
}
|
|
273
|
+
if (size > MAX_ENTRY_BYTES) {
|
|
274
|
+
throw new ZipError('entry-too-large', `条目 ${name} 解压后 ${size} 字节,超过上限 ${MAX_ENTRY_BYTES}:拒绝解压。`);
|
|
275
|
+
}
|
|
276
|
+
if (method !== 0 && method !== 8) {
|
|
277
|
+
throw new ZipError('unsupported-method', `条目 ${name} 使用压缩方法 ${method}(仅支持 0=STORE、8=DEFLATE):无法读取。`);
|
|
278
|
+
}
|
|
279
|
+
entries.push({
|
|
280
|
+
name,
|
|
281
|
+
method,
|
|
282
|
+
compressedSize,
|
|
283
|
+
size,
|
|
284
|
+
directory: name.endsWith('/'),
|
|
285
|
+
localHeaderOffset,
|
|
286
|
+
crc,
|
|
287
|
+
flags,
|
|
288
|
+
});
|
|
289
|
+
offset += 46 + nameLength + extraLength + commentLength;
|
|
290
|
+
}
|
|
291
|
+
// 中央目录的字节数与条目数应当自洽;不一致只影响我们已解析的条目,
|
|
292
|
+
// 不做强制校验(部分压缩工具写的 centralSize 略大于实际,属良性偏差)。
|
|
293
|
+
void centralSize;
|
|
294
|
+
return entries;
|
|
295
|
+
}
|
|
296
|
+
// ---- 内部:解压单条目 ----
|
|
297
|
+
readEntry(entry) {
|
|
298
|
+
const cached = this.cache.get(entry.name);
|
|
299
|
+
if (cached !== undefined)
|
|
300
|
+
return cached;
|
|
301
|
+
const headerOffset = entry.localHeaderOffset;
|
|
302
|
+
const signature = u32(this.view, headerOffset, `条目 ${entry.name} 本地头签名`);
|
|
303
|
+
if (signature !== SIG_LOCAL) {
|
|
304
|
+
throw new ZipError('bad-local-header', `条目 ${entry.name} 的本地文件头签名异常(0x${signature.toString(16)},应为 0x04034b50):文件已损坏。`);
|
|
305
|
+
}
|
|
306
|
+
const localNameLength = u16(this.view, headerOffset + 26, `条目 ${entry.name} 本地头名长度`);
|
|
307
|
+
const localExtraLength = u16(this.view, headerOffset + 28, `条目 ${entry.name} 本地头 extra 长度`);
|
|
308
|
+
const dataStart = headerOffset + 30 + localNameLength + localExtraLength;
|
|
309
|
+
const dataEnd = dataStart + entry.compressedSize;
|
|
310
|
+
requireRange(this.view, dataStart, entry.compressedSize, `条目 ${entry.name} 压缩数据`);
|
|
311
|
+
const raw = this.bytes.subarray(dataStart, dataEnd);
|
|
312
|
+
let output;
|
|
313
|
+
if (entry.method === 0) {
|
|
314
|
+
if (raw.length !== entry.size) {
|
|
315
|
+
throw new ZipError('size-mismatch', `条目 ${entry.name} 声明 ${entry.size} 字节但实际存了 ${raw.length} 字节:文件已损坏。`);
|
|
316
|
+
}
|
|
317
|
+
output = raw.slice();
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
output = this.inflate(entry, raw);
|
|
321
|
+
}
|
|
322
|
+
if (output.length !== entry.size) {
|
|
323
|
+
throw new ZipError('size-mismatch', `条目 ${entry.name} 解压得到 ${output.length} 字节,与目录声明的 ${entry.size} 字节不符:文件已损坏。`);
|
|
324
|
+
}
|
|
325
|
+
// flag bit 3:大小/CRC 写在数据描述符里,中央目录的值仍可信,故照样校验。
|
|
326
|
+
const actual = crc32(output);
|
|
327
|
+
if (actual !== entry.crc) {
|
|
328
|
+
throw new ZipError('crc-mismatch', `条目 ${entry.name} CRC32 校验失败(期望 ${entry.crc.toString(16)},实际 ${actual.toString(16)}):文件已损坏。`);
|
|
329
|
+
}
|
|
330
|
+
this.cache.set(entry.name, output);
|
|
331
|
+
return output;
|
|
332
|
+
}
|
|
333
|
+
inflate(entry, raw) {
|
|
334
|
+
try {
|
|
335
|
+
// 上限取「声明大小」与硬上限的较小者:声明值可能被伪造,硬上限兜底防 zip bomb;
|
|
336
|
+
// 真正的大小一致性由 readEntry 的解压后长度比对保证。
|
|
337
|
+
return inflateRawSync(raw, { maxOutputLength: Math.min(MAX_ENTRY_BYTES, Math.max(entry.size, 1024)) });
|
|
338
|
+
}
|
|
339
|
+
catch (error) {
|
|
340
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
341
|
+
if (/maxOutputLength|output length|larger than/i.test(message)) {
|
|
342
|
+
throw new ZipError('entry-too-large', `条目 ${entry.name} 解压结果超过声明大小 ${entry.size} 字节:疑似 zip bomb,已拒绝。`);
|
|
343
|
+
}
|
|
344
|
+
throw new ZipError('inflate-failed', `条目 ${entry.name} DEFLATE 解压失败:${message}(文件已损坏)。`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
// ---------------------------------------------------------------------------
|
|
349
|
+
// 公开 API
|
|
350
|
+
// ---------------------------------------------------------------------------
|
|
351
|
+
/** 打开一个 ZIP 容器(解析失败抛 {@link ZipError})。 */
|
|
352
|
+
export function openZip(bytes) {
|
|
353
|
+
return new ZipReader(bytes);
|
|
354
|
+
}
|
|
355
|
+
/** 按名字读一个条目;容器损坏抛 {@link ZipError},条目缺失返回 undefined。 */
|
|
356
|
+
export function readZipEntry(bytes, name) {
|
|
357
|
+
return openZip(bytes).read(name);
|
|
358
|
+
}
|
|
359
|
+
/** 条目名清单(便捷函数,跳过目录条目)。 */
|
|
360
|
+
export function zipEntryNames(bytes) {
|
|
361
|
+
return openZip(bytes).names();
|
|
362
|
+
}
|
|
363
|
+
/** 把条目字节按 UTF-8 解码成文本(XML/文本条目用)。 */
|
|
364
|
+
export function zipText(bytes, name) {
|
|
365
|
+
const data = openZip(bytes).read(name);
|
|
366
|
+
if (data === undefined)
|
|
367
|
+
return undefined;
|
|
368
|
+
return decodeUtf8(data);
|
|
369
|
+
}
|
|
370
|
+
/** 测试/调试辅助:条目名在字节流里是否存在(不改动输入)。 */
|
|
371
|
+
export function zipContainsName(zip, name) {
|
|
372
|
+
const needle = encoder.encode(name);
|
|
373
|
+
outer: for (let i = 0; i + needle.length <= zip.length; i += 1) {
|
|
374
|
+
for (let j = 0; j < needle.length; j += 1) {
|
|
375
|
+
if (zip[i + j] !== needle[j])
|
|
376
|
+
continue outer;
|
|
377
|
+
}
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
/** 内部:给 ooxml 的其余部分复用的 UTF-8/GBK 解码。 */
|
|
383
|
+
export function decodeBytes(bytes) {
|
|
384
|
+
const utf8 = decodeUtf8(bytes);
|
|
385
|
+
if (!utf8.includes('\ufffd'))
|
|
386
|
+
return utf8;
|
|
387
|
+
const gbk = decodeGbk(bytes);
|
|
388
|
+
if (gbk !== undefined && !gbk.includes('\ufffd'))
|
|
389
|
+
return gbk;
|
|
390
|
+
return utf8;
|
|
391
|
+
}
|
|
392
|
+
// 保留:数据描述符签名在本模块只用于文档说明(我们不依赖它定位数据)。
|
|
393
|
+
void SIG_DATA_DESCRIPTOR;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `thesis_intake` 的工具定义与参数类型。
|
|
3
|
+
*
|
|
4
|
+
* 单独成文件的理由:定义(参数表 + execute)必须是**纯数据**,不能把
|
|
5
|
+
* `definePaperTool` / `ctx.tools.register` 这类宿主能力带进来——否则离线测试
|
|
6
|
+
* 加载 `questions/state/spec` 时会连带 import 宿主包。装配入口在 `./index.ts` 的
|
|
7
|
+
* {@link registerIntake}。
|
|
8
|
+
*
|
|
9
|
+
* @module dsh-thesis/intake
|
|
10
|
+
*/
|
|
11
|
+
import type { FileSystemLike } from './state.ts';
|
|
12
|
+
/** `thesis_intake` 的参数(`action` 之外的字段按 action 取用)。 */
|
|
13
|
+
export interface IntakeArgs {
|
|
14
|
+
action: string;
|
|
15
|
+
/** 回答哪个问题(稳定 id,如 `school.template`)。 */
|
|
16
|
+
question_id?: string;
|
|
17
|
+
/** 用户的答案原文。 */
|
|
18
|
+
answer?: string;
|
|
19
|
+
/** 跳过理由(`action=skip` 必填)。 */
|
|
20
|
+
reason?: string;
|
|
21
|
+
/** 本次会话已经问过几个问题(执行 `maxQuestions` 上限)。 */
|
|
22
|
+
session_asked?: number;
|
|
23
|
+
/** 未知字段原样保留,便于回显与排错。 */
|
|
24
|
+
[key: string]: unknown;
|
|
25
|
+
}
|
|
26
|
+
/** 装配所需的追问选项(来自 `ResolvedPaperConfig.intake`)。 */
|
|
27
|
+
export interface IntakeSubOptions {
|
|
28
|
+
readonly maxQuestions: number;
|
|
29
|
+
readonly specRel: string;
|
|
30
|
+
}
|
|
31
|
+
/** 追问模块能做的全部动作。 */
|
|
32
|
+
export declare const INTAKE_ACTIONS: readonly string[];
|
|
33
|
+
/** 工具执行上下文的最小面:只需要会话 cwd 与中断信号(结构上兼容宿主 `ToolExecution`)。 */
|
|
34
|
+
export interface IntakeExec {
|
|
35
|
+
readonly signal?: AbortSignal;
|
|
36
|
+
readonly agent?: {
|
|
37
|
+
readonly session: {
|
|
38
|
+
readonly header: {
|
|
39
|
+
readonly cwd?: string;
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** 参数表里的一项(与 `shared/define-tool.ts` 的作者侧 DSL 一致)。 */
|
|
45
|
+
export interface IntakeParameterSpec {
|
|
46
|
+
readonly type: string;
|
|
47
|
+
readonly description: string;
|
|
48
|
+
readonly required?: true;
|
|
49
|
+
}
|
|
50
|
+
/** 工具定义(零宿主运行时依赖;装配入口与测试共用同一份)。 */
|
|
51
|
+
export interface IntakeToolDefinition {
|
|
52
|
+
readonly name: string;
|
|
53
|
+
readonly description: string;
|
|
54
|
+
readonly parameters: Record<string, IntakeParameterSpec>;
|
|
55
|
+
execute(args: IntakeArgs, exec: IntakeExec): Promise<string>;
|
|
56
|
+
}
|
|
57
|
+
/** 执行层:由 `./index.ts` 注入,避免定义文件反向依赖执行文件(循环导入)。 */
|
|
58
|
+
export type IntakeRunner = (fs: FileSystemLike, intake: IntakeSubOptions, args: IntakeArgs, cwd: string | undefined, signal?: AbortSignal, sessionAsked?: number) => Promise<string>;
|
|
59
|
+
/**
|
|
60
|
+
* `thesis_intake` 的工具定义:参数表 + execute。
|
|
61
|
+
*
|
|
62
|
+
* 这是模型真正看到的接口;`execute` 把 `session_asked` 搬到位置参数后交给
|
|
63
|
+
* `run()`(即 `runIntake`),保证「工具调用」与「直接调用执行层」行为完全一致。
|
|
64
|
+
*/
|
|
65
|
+
export declare function intakeToolDefinition(fs: FileSystemLike, intake: IntakeSubOptions, run: IntakeRunner): IntakeToolDefinition;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `thesis_intake` 的工具定义与参数类型。
|
|
3
|
+
*
|
|
4
|
+
* 单独成文件的理由:定义(参数表 + execute)必须是**纯数据**,不能把
|
|
5
|
+
* `definePaperTool` / `ctx.tools.register` 这类宿主能力带进来——否则离线测试
|
|
6
|
+
* 加载 `questions/state/spec` 时会连带 import 宿主包。装配入口在 `./index.ts` 的
|
|
7
|
+
* {@link registerIntake}。
|
|
8
|
+
*
|
|
9
|
+
* @module dsh-thesis/intake
|
|
10
|
+
*/
|
|
11
|
+
/** 追问模块能做的全部动作。 */
|
|
12
|
+
export const INTAKE_ACTIONS = ['start', 'ask', 'answer', 'skip', 'status', 'spec', 'done'];
|
|
13
|
+
/**
|
|
14
|
+
* `thesis_intake` 的工具定义:参数表 + execute。
|
|
15
|
+
*
|
|
16
|
+
* 这是模型真正看到的接口;`execute` 把 `session_asked` 搬到位置参数后交给
|
|
17
|
+
* `run()`(即 `runIntake`),保证「工具调用」与「直接调用执行层」行为完全一致。
|
|
18
|
+
*/
|
|
19
|
+
export function intakeToolDefinition(fs, intake, run) {
|
|
20
|
+
return {
|
|
21
|
+
name: 'thesis_intake',
|
|
22
|
+
description: '材料到手后的逐题追问(一次只问一个问题),把学校模板/论文要求/实验数据/代码/参考文献里的模糊要求变成可证伪的意图规格,' +
|
|
23
|
+
`并把"学校格式模板是否已拿到"这类材料已回答的问题标为待确认,不重复问。规格落盘到 ${intake.specRel},每次回答后自动刷新,` +
|
|
24
|
+
'作为后续写作/构建/降重/答辩的唯一依据。支持断点续问。' +
|
|
25
|
+
'action:start(初始化或读取状态,返回第一个问题与进度);ask(取下一个问题);answer(question_id + answer,记录后返回下一个问题与进度);' +
|
|
26
|
+
'skip(跳过,必须带 reason);status(进度/已答/未答/阻塞项);spec(重绘并落盘规格,返回路径与阻塞项);' +
|
|
27
|
+
'done(校验必答项;不齐时拒绝并列出缺口,齐全时标记阶段完成)。返回内容有界且一次最多一个问题。',
|
|
28
|
+
parameters: {
|
|
29
|
+
action: { type: 'string', required: true, description: `必填:${INTAKE_ACTIONS.join(' | ')}` },
|
|
30
|
+
question_id: { type: 'string', description: 'answer/skip 时必填:问题 id(如 school.template、scope.wordcount)' },
|
|
31
|
+
answer: { type: 'string', description: 'answer 时必填:用户答案原文(原样记录,不做改写)' },
|
|
32
|
+
reason: { type: 'string', description: 'skip 时必填:跳过理由(会写进规格的阻塞项与备注)' },
|
|
33
|
+
session_asked: { type: 'number', description: `本次会话已经问过几个问题(用于执行 maxQuestions=${intake.maxQuestions} 的每轮上限;缺省 0)` },
|
|
34
|
+
},
|
|
35
|
+
async execute(args, exec) {
|
|
36
|
+
const sessionAsked = typeof args.session_asked === 'number' && Number.isFinite(args.session_asked)
|
|
37
|
+
? Math.max(0, Math.floor(args.session_asked))
|
|
38
|
+
: 0;
|
|
39
|
+
return await run(fs, intake, args, exec.agent?.session.header.cwd, exec.signal, sessionAsked);
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 入口闸门的判定规则(纯函数,可独立单测)。
|
|
3
|
+
*
|
|
4
|
+
* 闸门要回答两个问题:
|
|
5
|
+
* 1. 用户这句话是不是「要开始写论文」?(而不是在聊天、问问题、改格式)
|
|
6
|
+
* 2. 论文工作区里的意图规格立住了没有?(缺失 / 仍有阻塞项 / 已就绪)
|
|
7
|
+
*
|
|
8
|
+
* 只有 1 为真且 2 为「未就绪」时才注入追问指令——宁漏勿错:闸门的误报代价是
|
|
9
|
+
* 多问一句「要不要先确认要求」,漏报代价是整章按错误要求重写。
|
|
10
|
+
*
|
|
11
|
+
* @module dsh-thesis/intake/gate-rules
|
|
12
|
+
*/
|
|
13
|
+
/** 是否在要求「开始写」。 */
|
|
14
|
+
export declare function isWritingIntent(text: string): boolean;
|
|
15
|
+
/** 用户是否显式授权跳过追问。 */
|
|
16
|
+
export declare function isBypassed(text: string): boolean;
|
|
17
|
+
/** 规格就绪状态的判定结果。 */
|
|
18
|
+
export type SpecStatus = {
|
|
19
|
+
readonly kind: 'missing';
|
|
20
|
+
} | {
|
|
21
|
+
readonly kind: 'blocked';
|
|
22
|
+
readonly reason: string;
|
|
23
|
+
} | {
|
|
24
|
+
readonly kind: 'ready';
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* 判定意图规格的状态。
|
|
28
|
+
*
|
|
29
|
+
* 判据顺序(越靠前越权威):
|
|
30
|
+
* 1. **结构判据**:`## 阻塞项…` 小节的第一行内容——`无。…` 表示已收敛,
|
|
31
|
+
* 是表格/其它内容则表示仍有必答项未答或格式待修正。这条与 `intake/spec.ts`
|
|
32
|
+
* 的渲染契约绑定,并且由 `tests/intake-convergence.test.ts` 端到端守住。
|
|
33
|
+
* 2. **文本回退**:`未知(需补)` / 未勾选项 `- [ ]`(用户手写或旧版规格)。
|
|
34
|
+
*
|
|
35
|
+
* @param specText - 规格文件内容;`undefined` 表示文件不存在。
|
|
36
|
+
*/
|
|
37
|
+
export declare function judgeSpec(specText: string | undefined): SpecStatus;
|
|
38
|
+
/** 闸门注入给模型的指令(自包含:即使 intake 技能没加载也能执行最小流程)。 */
|
|
39
|
+
export declare function intakeGateInstruction(status: SpecStatus, specRel: string): string;
|
|
40
|
+
/** 构造注入消息(user 角色、插件来源;与 learn 的进度卡同一形态)。 */
|
|
41
|
+
export declare function makeIntakeGateMessage(status: SpecStatus, specRel: string, id: string): {
|
|
42
|
+
id: string;
|
|
43
|
+
role: 'user';
|
|
44
|
+
content: Array<{
|
|
45
|
+
type: 'text';
|
|
46
|
+
text: string;
|
|
47
|
+
}>;
|
|
48
|
+
source: {
|
|
49
|
+
kind: string;
|
|
50
|
+
plugin: string;
|
|
51
|
+
form: string;
|
|
52
|
+
sections: Array<{
|
|
53
|
+
name: string;
|
|
54
|
+
text: string;
|
|
55
|
+
}>;
|
|
56
|
+
};
|
|
57
|
+
};
|