@gavinc-cn/touchstone-dsh 0.1.1 → 0.3.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/README.md +39 -11
- package/README.zh.md +25 -10
- package/backend/lib/access.js +44 -0
- package/backend/lib/agents.js +341 -0
- package/backend/lib/args.js +94 -0
- package/backend/lib/auth.js +195 -0
- package/backend/lib/board.js +6154 -0
- package/backend/lib/builtin-assets.js +1177 -0
- package/backend/lib/casescan.js +456 -0
- package/backend/lib/chat.js +2053 -0
- package/backend/lib/cookie.js +76 -0
- package/backend/lib/db.js +2260 -0
- package/backend/lib/dshdriver.js +1148 -0
- package/backend/lib/dshevents.js +851 -0
- package/backend/lib/export-cases.js +261 -0
- package/backend/lib/feishu.js +1738 -0
- package/backend/lib/feishu_cfg.js +228 -0
- package/backend/lib/feishu_conv.js +1484 -0
- package/backend/lib/feishu_device.js +428 -0
- package/backend/lib/feishu_inbound.js +1674 -0
- package/backend/lib/feishu_provision.js +855 -0
- package/backend/lib/feishu_rest.js +398 -0
- package/backend/lib/feishu_slash.js +568 -0
- package/backend/lib/feishu_ws.js +950 -0
- package/backend/lib/http.js +312 -0
- package/backend/lib/lib.js +973 -0
- package/backend/lib/loadcase.js +749 -0
- package/backend/lib/loadgen.js +1407 -0
- package/backend/lib/localbus.js +164 -0
- package/backend/lib/lock.js +405 -0
- package/backend/lib/proc.js +278 -0
- package/backend/lib/prompts.js +1021 -0
- package/backend/lib/pybase64.js +114 -0
- package/backend/lib/pyjson.js +214 -0
- package/backend/lib/query.js +125 -0
- package/backend/lib/rag.js +1310 -0
- package/backend/lib/runner.js +4520 -0
- package/backend/lib/servercaps.js +182 -0
- package/backend/lib/serverstate.js +237 -0
- package/backend/lib/sessparse.js +1176 -0
- package/backend/lib/sse.js +207 -0
- package/backend/lib/static.js +84 -0
- package/backend/lib/taskjson.js +302 -0
- package/backend/lib/waitq.js +2114 -0
- package/backend/lib/worktree.js +588 -0
- package/backend/lib/zstd-frames.js +218 -0
- package/backend/routes/admin.js +553 -0
- package/backend/routes/agents.js +149 -0
- package/backend/routes/assets.js +145 -0
- package/backend/routes/board.js +1220 -0
- package/backend/routes/feishu.js +269 -0
- package/backend/routes/feishu_app.js +215 -0
- package/backend/routes/feishu_hook.js +358 -0
- package/backend/routes/index.js +297 -0
- package/backend/routes/projects.js +1059 -0
- package/backend/routes/sessions.js +946 -0
- package/backend/routes/tasks.js +1530 -0
- package/backend/schema/fixtures/ancient.sql +189 -0
- package/backend/schema/fixtures/pre_alter.sql +177 -0
- package/backend/schema/migrations.json +154 -0
- package/backend/schema/schema.json +1451 -0
- package/backend/schema/schema.sql +216 -0
- package/backend/server.js +498 -0
- package/board.py +805 -72
- package/chat.py +590 -17
- package/db.py +356 -140
- package/dsh-plugin/cordis.patch.yml +7 -3
- package/dsh-plugin/install.sh +28 -11
- package/dsh-plugin/lib/agent-driver.js +710 -89
- package/dsh-plugin/lib/client.bundle.js +94 -9
- package/dsh-plugin/lib/index.js +529 -39
- package/dsh-plugin/lib/pack-closure.mjs +291 -0
- package/dsh-plugin/scripts/prepack-check.mjs +30 -2
- package/dsh-plugin/src/client.js +94 -9
- package/dshdriver.py +74 -5
- package/dshevents.py +65 -3
- package/feishu.py +1197 -34
- package/feishu_conv.py +920 -0
- package/lifecycle.py +116 -13
- package/package.json +6 -1
- package/requirements.txt +2 -1
- package/server.py +218 -26
- package/sessparse.py +150 -44
- package/touchstone.py +6 -3
- package/waitq.py +23 -0
- package/webui/dist-plugin/assets/index-ChP3sg6y.css +1 -0
- package/webui/dist-plugin/assets/index-UrMyeAXF.js +484 -0
- package/webui/dist-plugin/index.html +2 -2
- package/webui/dist-plugin/assets/index-B0WoP1Qy.css +0 -1
- package/webui/dist-plugin/assets/index-DtwKoi1T.js +0 -473
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* zstd **多帧**解压(Node 后端 M2 的第一个交付物,方案 R1b)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么需要自己扫帧边界:`node:zlib` 的 zstd 只解**单帧**——`zstdDecompressSync(整个文件)`
|
|
5
|
+
* 只返回第一帧(真机实测:989 帧 / 5,151,586 字节的会话文件只回 203 字节),流式 API 随后报
|
|
6
|
+
* `ZSTD_error_prefix_unknown`。而 dsh 会话存储就是**多帧**追加写的
|
|
7
|
+
* (`~/.dsh/sessions/<bucket>/<sid>/session*.jsonl.zstd`,一次追加一帧),所以 Node 侧要
|
|
8
|
+
* 自己做「帧边界扫描 + 逐帧解压」,否则 sessparse 直接废掉。
|
|
9
|
+
*
|
|
10
|
+
* 扫描只走**帧头 + 块头**(不做熵解码),规则出自 zstd 规范(RFC 8878):
|
|
11
|
+
* magic(4) → Frame_Header(2..14,由 Frame_Header_Descriptor 决定各字段宽度)
|
|
12
|
+
* → 若干 Block(每块 3 字节头 + 内容;RLE 块内容仅 1 字节)
|
|
13
|
+
* → [Content_Checksum(4)](FHD 的 checksum 位置 1 时)
|
|
14
|
+
* 另有 Skippable Frame(magic 0x184D2A5?):8 字节头 + 声明长度,直接跳过。
|
|
15
|
+
*
|
|
16
|
+
* 与 Python 侧的等价口径(`sessparse._dsh_decompressed`,逐帧 `decompressobj()` +
|
|
17
|
+
* `unused_data`):**坏帧即停,保留此前已解出的帧**(真机踩过:3206 条的会话尾部多 21 字节
|
|
18
|
+
* 垃圾,整段解析曾因此丢成 0 条);且坏帧若是「写了一半的合法帧」,Python 会把其中**已完整
|
|
19
|
+
* 解出的块内容**一并返回 —— 本模块同样(`decompressFramePartial`,`finishFlush: Z_NO_FLUSH`,
|
|
20
|
+
* 与 Python 实测逐字节同值)。因此主入口 `decompressSessionFile()` **绝不抛异常**。
|
|
21
|
+
*/
|
|
22
|
+
import { constants, zstdDecompressSync } from 'node:zlib';
|
|
23
|
+
|
|
24
|
+
/** 标准 zstd 帧 magic(小端读作 uint32) */
|
|
25
|
+
export const ZSTD_MAGIC = 0xfd2fb528;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Skippable frame 的 magic 区间:`0x184D2A50`..`0x184D2A5F`(低 4 位为序号,规范允许 16 个)。
|
|
29
|
+
* 判定用「与 0xFFFFFFF0 相与后等于基址」。
|
|
30
|
+
*/
|
|
31
|
+
export const ZSTD_SKIP_MAGIC_BASE = 0x184d2a50;
|
|
32
|
+
const ZSTD_SKIP_MAGIC_MASK = 0xfffffff0;
|
|
33
|
+
|
|
34
|
+
/** 帧头最多 18 字节(4 magic + 1 FHD + 1 WD + 4 DictID + 8 FCS + …),块头 3 字节 */
|
|
35
|
+
const MAX_HEADER_BYTES = 18;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 扫描**一个** zstd 帧的结束偏移(不含结束位置,即下一帧的起点)。
|
|
39
|
+
*
|
|
40
|
+
* 只解析头与块头,不解熵 ⇒ O(帧数 × 块数) 的纯偏移运算,真机 989 帧文件耗时亚毫秒级。
|
|
41
|
+
* 帧不完整(截断)、magic 不对、块类型保留值 ⇒ 抛错(调用方按「坏帧即停」处理)。
|
|
42
|
+
* @param {Buffer} buf
|
|
43
|
+
* @param {number} offset 帧起始偏移
|
|
44
|
+
* @returns {number} 下一帧的起始偏移
|
|
45
|
+
*/
|
|
46
|
+
export function frameEnd(buf, offset = 0) {
|
|
47
|
+
let p = offset;
|
|
48
|
+
if (p + 4 > buf.length) throw new Error(`帧头截断(offset=${offset})`);
|
|
49
|
+
const magic = buf.readUInt32LE(p);
|
|
50
|
+
if ((magic & ZSTD_SKIP_MAGIC_MASK) === ZSTD_SKIP_MAGIC_BASE) {
|
|
51
|
+
// Skippable frame:magic + 4 字节声明长度 + 该长度的用户数据
|
|
52
|
+
if (p + 8 > buf.length) throw new Error(`skippable 帧头截断(offset=${offset})`);
|
|
53
|
+
const size = buf.readUInt32LE(p + 4);
|
|
54
|
+
const end = p + 8 + size;
|
|
55
|
+
if (end > buf.length) throw new Error(`skippable 帧截断(offset=${offset})`);
|
|
56
|
+
return end;
|
|
57
|
+
}
|
|
58
|
+
if (magic !== ZSTD_MAGIC) {
|
|
59
|
+
throw new Error(`magic 不对(offset=${offset}: 0x${magic.toString(16)})`);
|
|
60
|
+
}
|
|
61
|
+
p += 4;
|
|
62
|
+
if (p >= buf.length) throw new Error(`缺 Frame_Header_Descriptor(offset=${offset})`);
|
|
63
|
+
const fhd = buf[p];
|
|
64
|
+
p += 1;
|
|
65
|
+
const fcsFlag = fhd >> 6; // Frame_Content_Size 的宽度档
|
|
66
|
+
const singleSegment = (fhd >> 5) & 1; // 单段帧(无 Window_Descriptor)
|
|
67
|
+
const dictFlag = fhd & 3; // Dictionary_ID 宽度档
|
|
68
|
+
const hasChecksum = (fhd >> 2) & 1; // 末尾是否有 4 字节 xxhash
|
|
69
|
+
if (!singleSegment) p += 1; // Window_Descriptor
|
|
70
|
+
p += dictFlag === 3 ? 4 : dictFlag; // Dictionary_ID
|
|
71
|
+
// FCS 宽度:flag=0 时「单段帧 1 字节 / 非单段帧 0 字节」,flag=1→2、2→4、3→8
|
|
72
|
+
p += fcsFlag === 0 ? (singleSegment ? 1 : 0) : (fcsFlag === 1 ? 2 : (fcsFlag === 2 ? 4 : 8));
|
|
73
|
+
if (p > buf.length) throw new Error(`帧头截断(offset=${offset})`);
|
|
74
|
+
|
|
75
|
+
for (;;) {
|
|
76
|
+
if (p + 3 > buf.length) throw new Error(`块头截断(offset=${offset})`);
|
|
77
|
+
const blockHeader = buf[p] | (buf[p + 1] << 8) | (buf[p + 2] << 16);
|
|
78
|
+
p += 3;
|
|
79
|
+
const last = blockHeader & 1;
|
|
80
|
+
const type = (blockHeader >> 1) & 3;
|
|
81
|
+
const size = blockHeader >> 3;
|
|
82
|
+
if (type === 3) throw new Error(`保留块类型(offset=${offset})`);
|
|
83
|
+
p += type === 1 ? 1 : size; // RLE 块只有 1 字节内容
|
|
84
|
+
if (p > buf.length) throw new Error(`块内容截断(offset=${offset})`);
|
|
85
|
+
if (last) break;
|
|
86
|
+
}
|
|
87
|
+
if (hasChecksum) p += 4;
|
|
88
|
+
if (p > buf.length) throw new Error(`校验和截断(offset=${offset})`);
|
|
89
|
+
return p;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 扫出**连续有效帧**的 `{start, end, bytes}` 列表(遇到第一个坏帧/半截帧即停)。
|
|
94
|
+
*
|
|
95
|
+
* 「即停」不是偷懒,而是与 Python 逐帧解压的行为对齐:dsh 边写边追加,读侧可能撞上「末帧
|
|
96
|
+
* 写了一半」,此时只应丢那一帧,前面的历史必须留着。
|
|
97
|
+
* @param {Buffer} buf
|
|
98
|
+
* @returns {Array<{start:number,end:number,bytes:number,skippable:boolean}>}
|
|
99
|
+
*/
|
|
100
|
+
export function scanFrames(buf) {
|
|
101
|
+
const out = [];
|
|
102
|
+
let off = 0;
|
|
103
|
+
while (off < buf.length) {
|
|
104
|
+
const start = off;
|
|
105
|
+
let end;
|
|
106
|
+
try {
|
|
107
|
+
end = frameEnd(buf, off);
|
|
108
|
+
} catch {
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
if (end <= start) break; // 防死循环(规范上不可能,保留闸门)
|
|
112
|
+
const magic = buf.readUInt32LE(start);
|
|
113
|
+
out.push({ start, end, bytes: end - start,
|
|
114
|
+
skippable: (magic & ZSTD_SKIP_MAGIC_MASK) === ZSTD_SKIP_MAGIC_BASE });
|
|
115
|
+
off = end;
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 尽力解出**一个不完整帧**里已完整落盘的块内容(Python `decompressobj()` 的部分输出语义)。
|
|
122
|
+
*
|
|
123
|
+
* 为什么需要它:Python 的 `obj.decompress(半截帧)` **不抛错**,而是返回其中已解出的块内容
|
|
124
|
+
* (实测:400 KB 不可压数据的一帧截到 60% ⇒ Python 返回 239,997 B;Node 的
|
|
125
|
+
* `zstdDecompressSync` 默认在截断处抛 `Z_BUF_ERROR`,加 `finishFlush: Z_NO_FLUSH` 后拿到的
|
|
126
|
+
* 字节数与 Python **完全相同**)。dsh 边写边追加,读侧撞上「末帧写了一半」是常态,半帧里若含
|
|
127
|
+
* 完整事件行(标题/提问),丢掉它就会让会话窗口少一条。
|
|
128
|
+
*
|
|
129
|
+
* 纯垃圾 / 坏校验和仍会抛错 ⇒ 返回空 Buffer(与 Python 抛 `ZstdError` 时「本次结果整段丢弃」同效)。
|
|
130
|
+
* @param {Buffer} bytes 从坏帧起点开始的剩余字节
|
|
131
|
+
* @returns {Buffer} 已解出的部分内容(没有则空 Buffer)
|
|
132
|
+
*/
|
|
133
|
+
export function decompressFramePartial(bytes) {
|
|
134
|
+
if (!bytes || bytes.length === 0) return Buffer.alloc(0);
|
|
135
|
+
try {
|
|
136
|
+
return zstdDecompressSync(bytes, { finishFlush: constants.Z_NO_FLUSH });
|
|
137
|
+
} catch {
|
|
138
|
+
return Buffer.alloc(0);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 逐帧解压(与 Python `decompressobj()` 的逐帧语义对齐),返回每一帧的解压结果。
|
|
144
|
+
*
|
|
145
|
+
* 坏帧处**两种口径都要**:
|
|
146
|
+
* - `partialTail: true`(缺省)= **Python 等价**:坏帧里已完整解出的块内容也取回来
|
|
147
|
+
* (`decompressFramePartial`),然后停止——这正是 `decompressobj()` 的实际行为;
|
|
148
|
+
* - `partialTail: false` = 严格口径:坏帧整帧丢弃(只用于对比测试与排障)。
|
|
149
|
+
* 两种口径都**不会**继续解析坏帧之后的字节(Python 在坏帧处即停)。
|
|
150
|
+
* @param {Buffer} buf
|
|
151
|
+
* @param {{partialTail?: boolean}} [options]
|
|
152
|
+
* @returns {{chunks: Buffer[], frames: number, stoppedAt: number|null, reason: string, recovered: boolean}}
|
|
153
|
+
*/
|
|
154
|
+
export function decompressFrames(buf, { partialTail = true } = {}) {
|
|
155
|
+
const chunks = [];
|
|
156
|
+
let off = 0;
|
|
157
|
+
const finish = (reason) => {
|
|
158
|
+
let recovered = false;
|
|
159
|
+
if (partialTail && off < buf.length) {
|
|
160
|
+
const tail = decompressFramePartial(buf.subarray(off));
|
|
161
|
+
if (tail.length > 0) {
|
|
162
|
+
chunks.push(tail);
|
|
163
|
+
recovered = true;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return { chunks, frames: chunks.length, stoppedAt: off, reason, recovered };
|
|
167
|
+
};
|
|
168
|
+
for (;;) {
|
|
169
|
+
if (off >= buf.length) {
|
|
170
|
+
return { chunks, frames: chunks.length, stoppedAt: null, reason: '', recovered: false };
|
|
171
|
+
}
|
|
172
|
+
let end;
|
|
173
|
+
try {
|
|
174
|
+
end = frameEnd(buf, off);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
return finish(`扫帧失败: ${error.message}`);
|
|
177
|
+
}
|
|
178
|
+
let out;
|
|
179
|
+
try {
|
|
180
|
+
out = zstdDecompressSync(buf.subarray(off, end));
|
|
181
|
+
} catch (error) {
|
|
182
|
+
// 扫描过了但解压失败:magic/框架像 zstd 而内容坏了(Python 同样在此停)
|
|
183
|
+
return finish(`解压失败: ${error.message}`);
|
|
184
|
+
}
|
|
185
|
+
chunks.push(out);
|
|
186
|
+
off = end;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* 解压 dsh 会话文件缓冲(**主入口**,逐字对齐 `sessparse._dsh_decompressed`)。
|
|
192
|
+
*
|
|
193
|
+
* 永不抛异常:完全不是 zstd / 首帧即坏 ⇒ 返回空串;尾部坏帧 ⇒ 返回此前帧拼出的文本。
|
|
194
|
+
* 解码用 `errors="replace"` 同款容错(Node 的 utf8 解码本来就把非法序列换成 U+FFFD)。
|
|
195
|
+
* @param {Buffer} buf
|
|
196
|
+
* @returns {string}
|
|
197
|
+
*/
|
|
198
|
+
export function decompressSessionFile(buf) {
|
|
199
|
+
return Buffer.concat(decompressFrames(buf).chunks).toString('utf8');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** 帧头字段自检用:给一个偏移,返回该帧头解出来的字段(测试与排障用) */
|
|
203
|
+
export function parseFrameHeader(buf, offset = 0) {
|
|
204
|
+
if (offset + 5 > buf.length) throw new Error('帧头截断');
|
|
205
|
+
const magic = buf.readUInt32LE(offset);
|
|
206
|
+
if (magic !== ZSTD_MAGIC) throw new Error(`magic 不对: 0x${magic.toString(16)}`);
|
|
207
|
+
const fhd = buf[offset + 4];
|
|
208
|
+
return {
|
|
209
|
+
magic,
|
|
210
|
+
descriptor: fhd,
|
|
211
|
+
fcsFlag: fhd >> 6,
|
|
212
|
+
singleSegment: ((fhd >> 5) & 1) === 1,
|
|
213
|
+
dictFlag: fhd & 3,
|
|
214
|
+
hasChecksum: ((fhd >> 2) & 1) === 1,
|
|
215
|
+
unusedBit: ((fhd >> 3) & 1) === 1, // 规范要求为 0(置位即格式异常,只报告不拦)
|
|
216
|
+
maxHeaderBytes: MAX_HEADER_BYTES,
|
|
217
|
+
};
|
|
218
|
+
}
|