@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,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `export_cases.py`(160 行)的 Node 对应物(Node 后端 M6-2)。
|
|
3
|
+
*
|
|
4
|
+
* 两个子命令:`export`(扫案例库 → `<根>/.live/cases.jsonl` 派生索引)与
|
|
5
|
+
* `retest-candidates`(按变更文件粗筛候选复测用例)。**案例库的 md 是唯一权威**,
|
|
6
|
+
* 本模块只读;索引写进 `.live/`(可随时重建)。
|
|
7
|
+
*
|
|
8
|
+
* 与 Python 的形态差异(逐条给理由):
|
|
9
|
+
* ① CLI 入口是模块底部 `if (import.meta.url === pathToFileURL(process.argv[1]).href)`,
|
|
10
|
+
* 子命令解析手写(Python 用 argparse)——**退出码与 stdout/stderr 分流照 oracle**:
|
|
11
|
+
* 成功 0;未知/缺失子命令 2;`export` 的警告走 stderr、结果行走 stdout。
|
|
12
|
+
* argparse 的用法文案(`usage: …`)**不逐字节复刻**(那是帮助文本,不是行为契约),
|
|
13
|
+
* 但两条错误路径的**退出码**必须一致(对拍用例钉这个)。
|
|
14
|
+
* ② `cmd_retest_candidates` 在 Python 里是同步函数(`--semantic` 用惰性 `import rag`);
|
|
15
|
+
* Node 的 ESM 只能 `await import()`,故本函数是 **async**,`--semantic` 分支的语义
|
|
16
|
+
* (配置不可用即保持原序、异常整体吞掉)照抄。**不传 `--semantic` 时行为与同步版等价。**
|
|
17
|
+
* ③ `--semantic` 的「RAG 可用」方向不纳入本批对拍(见 M6 计划 §六.12);「RAG 不可用 ⇒
|
|
18
|
+
* 退化为原序」方向两版必须一致。
|
|
19
|
+
*
|
|
20
|
+
* 逐字节口径:`cases.jsonl` 每行是 `json.dumps(rec, ensure_ascii=False)`(→ `lib/pyjson.js`
|
|
21
|
+
* 的 `pyJsonDumps`),`--json` 输出是 `json.dumps(..., ensure_ascii=False, indent=2)`
|
|
22
|
+
* (→ `pyJsonDumpsIndent`)。字段**插入序**就是 JSON 键序,故 record 的构造顺序必须照抄。
|
|
23
|
+
*/
|
|
24
|
+
import fs from 'node:fs';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
import { pathToFileURL } from 'node:url';
|
|
27
|
+
|
|
28
|
+
import {
|
|
29
|
+
CASE_DIR_RE, VERIFY_SCRIPT_NAME, isFile, parseFields, parseStatusMd, pyStrCompare, readText,
|
|
30
|
+
} from './lib.js';
|
|
31
|
+
import { pyJsonDumps, pyJsonDumpsIndent } from './pyjson.js';
|
|
32
|
+
|
|
33
|
+
// `VERIFY_SCRIPT_NAME` 归位 `lib.js`(M6-6 前的 lib 补齐),本模块 import 后原样 re-export
|
|
34
|
+
// ——保持既有导入面不变。
|
|
35
|
+
export { VERIFY_SCRIPT_NAME };
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 「依据代码」里的代码文件引用(照 `export_cases.py:21-24`)。
|
|
39
|
+
* 字符集**不含中文**(原文注释:避免吞掉前文);可带 `:行号` 或 `:起-止`。
|
|
40
|
+
*/
|
|
41
|
+
export const CODE_REF_RE = new RegExp(
|
|
42
|
+
'[A-Za-z0-9_./-]+\\.(?:cpp|cc|cxx|hpp|h|java|py|jsx?|tsx?|sql|xml|proto|go|rs)'
|
|
43
|
+
+ '(?::\\d+(?:-\\d+)?)?',
|
|
44
|
+
'g',
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* `export_cases.extract_code_files`:抽出文件路径(去行号、去重、**保留原文写法与出现序**)。
|
|
49
|
+
*
|
|
50
|
+
* Python 用 `if path not in files` 的去重(保序 list,不是 set)——照抄。
|
|
51
|
+
*/
|
|
52
|
+
export function extractCodeFiles(raw) {
|
|
53
|
+
const files = [];
|
|
54
|
+
const text = raw == null ? '' : String(raw);
|
|
55
|
+
CODE_REF_RE.lastIndex = 0;
|
|
56
|
+
let m = CODE_REF_RE.exec(text);
|
|
57
|
+
while (m !== null) {
|
|
58
|
+
const p = m[0].split(':')[0];
|
|
59
|
+
if (!files.includes(p)) files.push(p);
|
|
60
|
+
m = CODE_REF_RE.exec(text);
|
|
61
|
+
}
|
|
62
|
+
return files;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* `export_cases.scan_cases`:遍历案例库,返回用例记录列表(按 id 升序)。
|
|
67
|
+
*
|
|
68
|
+
* `os.walk` 的三条语义逐条对齐:① 跳过隐藏目录(**不深入**);② 目录名匹配
|
|
69
|
+
* `^(FS\d{4})_(.+)$` 才算用例,**不匹配也继续下钻**(子目录里可能有真用例);
|
|
70
|
+
* ③ `dir` 字段是相对根的路径(`os.path.relpath`)。
|
|
71
|
+
*/
|
|
72
|
+
export function scanCases(root) {
|
|
73
|
+
const base = String(root == null ? '' : root);
|
|
74
|
+
const records = [];
|
|
75
|
+
const walk = (dir) => {
|
|
76
|
+
let entries;
|
|
77
|
+
try {
|
|
78
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
79
|
+
} catch {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const m = CASE_DIR_RE.exec(path.basename(dir));
|
|
83
|
+
if (m) {
|
|
84
|
+
const caseMd = parseFields(readText(path.join(dir, 'case.md')));
|
|
85
|
+
const status = parseStatusMd(path.join(dir, 'status.md'));
|
|
86
|
+
const codeRefs = caseMd['依据代码'] || '';
|
|
87
|
+
// 字段顺序照 Python 的 dict 字面量(它同时决定 jsonl 的键序)
|
|
88
|
+
records.push({
|
|
89
|
+
id: m[1],
|
|
90
|
+
name: m[2],
|
|
91
|
+
dir: path.relative(base, dir) || '.',
|
|
92
|
+
interface: caseMd['接口'] || '',
|
|
93
|
+
message_id: caseMd['message_id'] || '',
|
|
94
|
+
module: caseMd['所属模块/功能'] || '',
|
|
95
|
+
status: status.status,
|
|
96
|
+
bug_report: status.bug_report,
|
|
97
|
+
code_files: extractCodeFiles(codeRefs),
|
|
98
|
+
code_refs: codeRefs,
|
|
99
|
+
// 照 Python `os.path.isfile`:**同名目录不算**(`existsSync` 会把目录判成 true)
|
|
100
|
+
has_script: isFile(path.join(dir, VERIFY_SCRIPT_NAME)),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
for (const ent of entries) {
|
|
104
|
+
if (ent.name.startsWith('.')) continue; // 跳过 .live/.web 等机器目录
|
|
105
|
+
if (ent.isDirectory()) walk(path.join(dir, ent.name));
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
walk(base);
|
|
109
|
+
records.sort((a, b) => pyStrCompare(a.id, b.id));
|
|
110
|
+
return records;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** `export_cases._norm`:`path.replace('\\','/').lstrip('./')`(**按字符集剥离前导**) */
|
|
114
|
+
export function norm(filePath) {
|
|
115
|
+
return String(filePath == null ? '' : filePath).replace(/\\/g, '/').replace(/^[./]+/, '');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* `export_cases._match_level`:`path`=路径(互为 `/` 后缀)/ `name`=仅同名(需人工确认)/
|
|
120
|
+
* 无匹配返回 null。
|
|
121
|
+
*/
|
|
122
|
+
export function matchLevel(caseFile, changed) {
|
|
123
|
+
const cf = norm(caseFile);
|
|
124
|
+
const ch = norm(changed);
|
|
125
|
+
if (cf === ch || ch.endsWith(`/${cf}`) || cf.endsWith(`/${ch}`)) return 'path';
|
|
126
|
+
if (path.basename(cf) === path.basename(ch)) return 'name';
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* `export_cases.cmd_export`:导出 cases.jsonl(每行一个用例),返回退出码 0。
|
|
132
|
+
*
|
|
133
|
+
* 落盘走「临时文件 + rename」(照 `os.replace`);stdout 一行结果、stderr 可选警告。
|
|
134
|
+
*/
|
|
135
|
+
export function cmdExport(args) {
|
|
136
|
+
const root = args.root;
|
|
137
|
+
const out = args.out || path.join(root, '.live', 'cases.jsonl');
|
|
138
|
+
const records = scanCases(root);
|
|
139
|
+
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
140
|
+
const tmp = `${out}.tmp`;
|
|
141
|
+
let body = '';
|
|
142
|
+
for (const rec of records) body += `${pyJsonDumps(rec)}\n`;
|
|
143
|
+
fs.writeFileSync(tmp, body, { encoding: 'utf8' });
|
|
144
|
+
fs.renameSync(tmp, out);
|
|
145
|
+
const noCode = records.filter((r) => r.code_files.length === 0).map((r) => r.id);
|
|
146
|
+
process.stdout.write(`导出 ${records.length} 条用例 -> ${out}\n`);
|
|
147
|
+
if (noCode.length) {
|
|
148
|
+
process.stderr.write(`警告:${noCode.length} 条未提取到依据代码文件:${noCode.join(' ')}\n`);
|
|
149
|
+
}
|
|
150
|
+
return 0;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* `export_cases.cmd_retest_candidates`:按变更文件列表粗筛候选复测用例(**async**,见文件头②)。
|
|
155
|
+
*
|
|
156
|
+
* 匹配等级只在「尚未命中 path」时降级(`matched.get(changed) != "path"`)——即同一变更文件
|
|
157
|
+
* 只要有一条 `path` 命中就不被后来的 `name` 覆盖。`--json` 走 `indent=2`;否则逐行打印、
|
|
158
|
+
* 汇总行走 stderr。
|
|
159
|
+
*/
|
|
160
|
+
export async function cmdRetestCandidates(args) {
|
|
161
|
+
const records = scanCases(args.root);
|
|
162
|
+
const hits = [];
|
|
163
|
+
for (const rec of records) {
|
|
164
|
+
const matched = new Map(); // changed_file -> level(插入序)
|
|
165
|
+
for (const caseFile of rec.code_files) {
|
|
166
|
+
for (const changed of args.files) {
|
|
167
|
+
const level = matchLevel(caseFile, changed);
|
|
168
|
+
if (level && matched.get(changed) !== 'path') matched.set(changed, level);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (matched.size) hits.push([rec, matched]);
|
|
172
|
+
}
|
|
173
|
+
if (args.semantic) {
|
|
174
|
+
// RAG 语义重排(可选):配置存在时按混合检索序输出;不可用保持原序(离线默认)。
|
|
175
|
+
// 照 Python:**任何异常整体吞掉**(`except Exception: pass`)。
|
|
176
|
+
try {
|
|
177
|
+
const rag = await import('./rag.js');
|
|
178
|
+
if (rag.loadConfig()) {
|
|
179
|
+
const ranked = new Map();
|
|
180
|
+
const rows = await rag.retrieve(args.root, args.files.join('\n'), 30);
|
|
181
|
+
rows.forEach((r, i) => ranked.set(r.id, i));
|
|
182
|
+
hits.sort((a, b) => (ranked.get(a[0].id) ?? 10 ** 9) - (ranked.get(b[0].id) ?? 10 ** 9));
|
|
183
|
+
}
|
|
184
|
+
} catch {
|
|
185
|
+
/* 离线/未移植:保持原序(与 Python 同) */
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (args.json) {
|
|
189
|
+
// 照 Python:`matched` 是普通 dict,键序 = **首次赋值序**(遍历 code_files × files 的顺序),
|
|
190
|
+
// 不是排序序——只有下面那条「逐行打印」分支才 `sorted(matched.items())`。
|
|
191
|
+
const payload = hits.map(([rec, matched]) => {
|
|
192
|
+
const out = { ...rec, matched: {} };
|
|
193
|
+
for (const [key, level] of matched.entries()) out.matched[key] = level;
|
|
194
|
+
return out;
|
|
195
|
+
});
|
|
196
|
+
process.stdout.write(`${pyJsonDumpsIndent(payload, 2)}\n`);
|
|
197
|
+
} else {
|
|
198
|
+
for (const [rec, matched] of hits) {
|
|
199
|
+
const keys = [...matched.keys()].sort((a, b) => pyStrCompare(a, b));
|
|
200
|
+
const files = keys.map((f) => `${f}(${matched.get(f)})`).join(', ');
|
|
201
|
+
process.stdout.write(`${rec.id} [${rec.status}] ${rec.dir} <- ${files}\n`);
|
|
202
|
+
}
|
|
203
|
+
process.stderr.write(`共 ${hits.length} 条候选(path=路径匹配,name=仅同名需确认)\n`);
|
|
204
|
+
}
|
|
205
|
+
return 0;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** 手写子命令解析(见文件头①)。返回 `{cmd, opts}`;非法用法返回 `{error}`。 */
|
|
209
|
+
export function parseArgv(argv) {
|
|
210
|
+
const [cmd, ...rest] = argv;
|
|
211
|
+
if (cmd === 'export') {
|
|
212
|
+
const opts = { root: '', out: '' };
|
|
213
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
214
|
+
if (rest[i] === '--out') opts.out = rest[++i] ?? '';
|
|
215
|
+
else if (opts.root === '') opts.root = rest[i];
|
|
216
|
+
else return { error: `unrecognized arguments: ${rest[i]}` };
|
|
217
|
+
}
|
|
218
|
+
if (!opts.root) return { error: 'the following arguments are required: root' };
|
|
219
|
+
return { cmd, opts };
|
|
220
|
+
}
|
|
221
|
+
if (cmd === 'retest-candidates') {
|
|
222
|
+
const opts = { root: '', files: [], json: false, semantic: false };
|
|
223
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
224
|
+
if (rest[i] === '--json') opts.json = true;
|
|
225
|
+
else if (rest[i] === '--semantic') opts.semantic = true;
|
|
226
|
+
else if (opts.root === '') opts.root = rest[i];
|
|
227
|
+
else opts.files.push(rest[i]);
|
|
228
|
+
}
|
|
229
|
+
if (!opts.root) return { error: 'the following arguments are required: root' };
|
|
230
|
+
if (!opts.files.length) return { error: 'the following arguments are required: files' };
|
|
231
|
+
return { cmd, opts };
|
|
232
|
+
}
|
|
233
|
+
return { error: 'the following arguments are required: cmd' };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** CLI 主入口(退出码语义照 oracle:成功 0、用法错 2) */
|
|
237
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
238
|
+
const parsed = parseArgv(argv);
|
|
239
|
+
if (parsed.error) {
|
|
240
|
+
process.stderr.write(`usage: export_cases.py {export,retest-candidates} ...\n`);
|
|
241
|
+
process.stderr.write(`export_cases.py: error: ${parsed.error}\n`);
|
|
242
|
+
return 2;
|
|
243
|
+
}
|
|
244
|
+
if (parsed.cmd === 'export') return cmdExport(parsed.opts);
|
|
245
|
+
return cmdRetestCandidates(parsed.opts);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// 直接执行时才跑(被 import 时不动)——`node backend/lib/export-cases.js export <root>`
|
|
249
|
+
//
|
|
250
|
+
// **入口不许用顶层 await**(2026-10-13 M6-3 实测):`main()` 的 `--semantic` 分支会
|
|
251
|
+
// `await import('./rag.js')`,而 `rag.js` 静态 import 回本模块(`scanCases`)——若本模块的
|
|
252
|
+
// 求值被顶层 await 挂起,这条异步环就会死锁:进程报 `Detected unsettled top-level await`、
|
|
253
|
+
// 退出码 **13**(而 Python 侧是「RAG 不可用即优雅降级」)。改成 `.then()` 后求值立即完成,
|
|
254
|
+
// 环变成普通的一前一后。
|
|
255
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
256
|
+
main().then((code) => { process.exitCode = code; },
|
|
257
|
+
(err) => {
|
|
258
|
+
process.stderr.write(`export-cases: ${err && err.stack ? err.stack : err}\n`);
|
|
259
|
+
process.exitCode = 2;
|
|
260
|
+
});
|
|
261
|
+
}
|