@gavinc-cn/touchstone-dsh 0.2.0 → 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/LICENSE +0 -0
- package/README.md +23 -15
- package/README.zh.md +17 -12
- package/auth.py +0 -0
- 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 +30 -1
- package/builtin_assets.py +0 -0
- package/builtin_prompts/free_style/flow.md +0 -0
- package/builtin_prompts/free_style/load_test.md +0 -0
- package/builtin_prompts/free_style/templates/bug_report_template.md +0 -0
- package/builtin_prompts/free_style/templates/case_template.md +0 -0
- package/builtin_prompts/free_style/templates/index_template.md +0 -0
- package/builtin_prompts/free_style/templates/load_charts_template.md +0 -0
- package/builtin_prompts/free_style/templates/load_index_template.md +0 -0
- package/builtin_prompts/free_style/templates/load_plan_template.md +0 -0
- package/builtin_prompts/free_style/templates/load_scenario_template.md +0 -0
- package/builtin_prompts/free_style/templates/status_template.md +0 -0
- package/db.py +294 -129
- package/dsh-plugin/cordis.patch.yml +0 -0
- package/dsh-plugin/install.sh +22 -8
- package/dsh-plugin/lib/client.bundle.js +0 -0
- package/dsh-plugin/lib/index.js +299 -29
- package/dsh-plugin/lib/pack-closure.mjs +291 -0
- package/dsh-plugin/scripts/build.mjs +0 -0
- package/dsh-plugin/scripts/prepack-check.mjs +30 -2
- package/dsh-plugin/scripts/select-bundle.mjs +0 -0
- package/dsh-plugin/src/client.js +0 -0
- package/dshevents.py +0 -0
- package/export_cases.py +0 -0
- package/extensions/dsh-autocommit/asset.json +0 -0
- package/extensions/dsh-autocommit/pkg/lib/index.mjs +0 -0
- package/extensions/dsh-autocommit/pkg/package.json +0 -0
- package/feishu_conv.py +0 -0
- package/lib.py +0 -0
- package/lifecycle.py +116 -13
- package/loadcase.py +0 -0
- package/loadgen.py +0 -0
- package/localbus.py +0 -0
- package/migrate_p7b.py +0 -0
- package/package.json +6 -1
- package/platcompat.py +0 -0
- package/prompts.py +0 -0
- package/rag.py +0 -0
- package/requirements.txt +0 -0
- package/touchstone.py +0 -0
- package/waitq.py +0 -0
- package/webui/dist-plugin/assets/index-ChP3sg6y.css +1 -0
- package/webui/dist-plugin/assets/{index-JLwyZZxC.js → index-UrMyeAXF.js} +110 -110
- package/webui/dist-plugin/index.html +2 -2
- package/worktree.py +0 -0
- package/webui/dist-plugin/assets/index-tJ0TlvQ3.css +0 -1
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 发包闭包计算 —— 「npm 包里必须有哪些文件」的**唯一一份**算法(零依赖,只用 `node:`)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么要有它:`package.json` 的 `files` 是**白名单**,漏一条就是「发出去的包起不来」,
|
|
5
|
+
* 而漏的形态是静默的 —— 2026-10-15 实测 `@gavinc-cn/touchstone-dsh@0.1.1` 的 105 个文件里
|
|
6
|
+
* **没有 `backend/routes/` 的 11 个文件**(白名单只写了 `backend/server.js` / `backend/lib` /
|
|
7
|
+
* `backend/schema`),解包后 `node backend/server.js` 当场
|
|
8
|
+
* `ERR_MODULE_NOT_FOUND: .../backend/routes/index.js`(`backend/server.js:61` 就 import 它)
|
|
9
|
+
* 并 exit 1。手写清单(`dsh-plugin/scripts/prepack-check.mjs` 的 `REQUIRED`)挡不住这种漏,
|
|
10
|
+
* 因为它只查「我记得的那些」;这里改成**从入口正推**:包内必需 = 两个入口的**静态 import
|
|
11
|
+
* 闭包** + 运行时时按路径读的数据根。新增一个 `backend/routes/xxx.js` 而忘了改白名单 ⇒
|
|
12
|
+
* 闭包自动长大 ⇒ 判据变红。
|
|
13
|
+
*
|
|
14
|
+
* 两个入口(`package.json` 的 `main` + Node 后端入口,见 `dsh-plugin/lib/index.js:117-118`):
|
|
15
|
+
* - `dsh-plugin/lib/index.js` —— dsh 插件壳,宿主 `require`/`import` 的就是它;
|
|
16
|
+
* - `backend/server.js` —— 薄壳 spawn 的 Node 后端进程。
|
|
17
|
+
* 只解析**相对说明符**(`./` / `../`):`node:*` 内建与裸包名不是包内文件,一律记为
|
|
18
|
+
* `unresolved` 供人看(不参与判据 —— 把裸包名当必需会把判据搞成假红)。
|
|
19
|
+
*
|
|
20
|
+
* 数据根(`RUNTIME_RESOURCES`)是**手写的**,条目都带「谁在哪一行读它」的证据;递归 import
|
|
21
|
+
* 扫描器看不到这些「按路径拼出来的读」,所以只能人工登记 —— 但每条都写清理由,不写「大概
|
|
22
|
+
* 需要」。注意 `backend/schema/fixtures/*` **不在**闭包里:它只被 `backend/test/db.test.mjs`、
|
|
23
|
+
* `tools/parity/db-run.mjs`、`tools/export_schema.py` 读(测试/量具),运行时没人读
|
|
24
|
+
* (实测 `grep -rn fixtures backend/lib backend/routes` 无命中)—— 别凭印象往里塞。
|
|
25
|
+
*
|
|
26
|
+
* 白名单展开(`expandFilesEntries`)按 npm 的 `files` 语义实现最小规则,行为经实测校准
|
|
27
|
+
* (`npm pack --dry-run --json`,见下),不是照文档猜的:
|
|
28
|
+
* - 条目相对包根;`*` 不跨 `/`(实测 `files:["*.py","*.md"]` 只收根级的 a.py/top.md,
|
|
29
|
+
* 子目录 `sub/b.py` 不收 ⇒ **没有** matchBase 语义),`**` 跨目录;
|
|
30
|
+
* - 条目指到目录 ⇒ 递归含其内容(`backend/lib`、`webui/dist-plugin` 都是这个形态);
|
|
31
|
+
* - `package.json` / `README*` / `LICENSE*`(含 `LICENCE*`)恒含。
|
|
32
|
+
* 有意**不**建模 `.gitignore`:本仓 `.gitignore` 里有 `webui/dist-plugin/`,但它照进包
|
|
33
|
+
* (实测 3 个文件都在)—— 说明显式 `files` 条目压过忽略规则。副作用写在文件末尾「已知缺口」。
|
|
34
|
+
*/
|
|
35
|
+
import fs from 'node:fs';
|
|
36
|
+
import path from 'node:path';
|
|
37
|
+
import { fileURLToPath } from 'node:url';
|
|
38
|
+
|
|
39
|
+
/** 包根(本文件在 `<包根>/dsh-plugin/lib/` 下, 上溯三级) */
|
|
40
|
+
export const PKG_ROOT = path.dirname(path.dirname(path.dirname(fileURLToPath(import.meta.url))));
|
|
41
|
+
|
|
42
|
+
/** 闭包的两个入口(相对包根,posix 分隔) */
|
|
43
|
+
export const ENTRIES = Object.freeze([
|
|
44
|
+
'dsh-plugin/lib/index.js',
|
|
45
|
+
'backend/server.js',
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 运行时时**按路径读**的数据根(静态扫描看不到;每条都要有证据行号)。
|
|
50
|
+
* `files` 列出「精确文件」(测试夹具不算,别往里塞);`dirs` 列出「整个目录都要」——
|
|
51
|
+
* 目录内容在判据里**当场递归枚举**,所以往里新增数据文件也自动进入闭包。
|
|
52
|
+
*/
|
|
53
|
+
export const RUNTIME_RESOURCES = Object.freeze({
|
|
54
|
+
files: Object.freeze([
|
|
55
|
+
{ rel: 'backend/schema/schema.sql', why: 'backend/lib/db.js:59 initDb 启动即读(目标 DDL)' },
|
|
56
|
+
{ rel: 'backend/schema/schema.json', why: 'backend/lib/db.js:60 收敛器按它补列/建索引' },
|
|
57
|
+
{ rel: 'backend/schema/migrations.json', why: 'backend/lib/db.js:61 声明式迁移清单,启动即执行' },
|
|
58
|
+
]),
|
|
59
|
+
dirs: Object.freeze([
|
|
60
|
+
{ rel: 'builtin_prompts/free_style', why: 'backend/lib/prompts.js:237 _ASSET_DIR,运行时读流程文档与文件模板' },
|
|
61
|
+
{ rel: 'extensions', why: 'backend/lib/builtin-assets.js:262 DEFAULT_EXT_DIR,资产清单扫描根(routes/assets.js 的端点)' },
|
|
62
|
+
{ rel: 'webui/dist-plugin', why: 'server 静态服务目录(薄壳启动参数 --web-dir 传的就是它,dsh-plugin/lib/index.js)' },
|
|
63
|
+
]),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
/** 目录递归枚举时跳过的东西(不进包,也不需要进闭包) */
|
|
67
|
+
const SKIP_DIRS = new Set(['.git', 'node_modules', '.run', '__pycache__']);
|
|
68
|
+
|
|
69
|
+
/** 相对说明符扫描(见文件头:锚在行首,避开注释里的散文;多行 import 靠 `[^'"]` 跨行) */
|
|
70
|
+
const IMPORT_FROM_RE = /^[ \t]*import\s+[^'"]*?\bfrom\s*['"]([^'"]+)['"]/gm;
|
|
71
|
+
const IMPORT_SIDE_EFFECT_RE = /^[ \t]*import\s*['"]([^'"]+)['"]/gm;
|
|
72
|
+
const EXPORT_FROM_RE = /^[ \t]*export\s+(?:\*|\{[^}]*\})\s*from\s*['"]([^'"]+)['"]/gm;
|
|
73
|
+
const DYNAMIC_IMPORT_RE = /\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
74
|
+
|
|
75
|
+
/** posix 化(判据字符串一律用 `/`,Windows 上跑出来的也必须一样) */
|
|
76
|
+
export function toPosix(rel) {
|
|
77
|
+
return rel.split(path.sep).join('/');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** 目录清单缓存:同一进程内按 root 复用(判据是只读的静态检查,进程内不假设文件树会变) */
|
|
81
|
+
const LIST_CACHE = new Map();
|
|
82
|
+
|
|
83
|
+
/** 递归列出 root 下所有**文件**(相对包根,posix 分隔,排序);结果按 root 缓存 */
|
|
84
|
+
export function listFiles(root = PKG_ROOT) {
|
|
85
|
+
const cached = LIST_CACHE.get(root);
|
|
86
|
+
if (cached) return cached;
|
|
87
|
+
const out = [];
|
|
88
|
+
const walk = (dir) => {
|
|
89
|
+
let entries;
|
|
90
|
+
try {
|
|
91
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
92
|
+
} catch {
|
|
93
|
+
return; // 读不动就当不存在(判据自己会报缺)
|
|
94
|
+
}
|
|
95
|
+
for (const entry of entries) {
|
|
96
|
+
const abs = path.join(dir, entry.name);
|
|
97
|
+
if (entry.isDirectory()) {
|
|
98
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
99
|
+
walk(abs);
|
|
100
|
+
} else if (entry.isFile()) {
|
|
101
|
+
out.push(toPosix(path.relative(root, abs)));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
walk(root);
|
|
106
|
+
out.sort();
|
|
107
|
+
LIST_CACHE.set(root, out);
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 抽一个源文件里的相对说明符(去重、保持出现顺序) */
|
|
112
|
+
export function scanRelativeSpecifiers(source) {
|
|
113
|
+
const found = [];
|
|
114
|
+
const add = (spec) => {
|
|
115
|
+
if (!spec) return;
|
|
116
|
+
if (!spec.startsWith('./') && !spec.startsWith('../')) return;
|
|
117
|
+
if (!found.includes(spec)) found.push(spec);
|
|
118
|
+
};
|
|
119
|
+
for (const re of [IMPORT_FROM_RE, IMPORT_SIDE_EFFECT_RE, EXPORT_FROM_RE, DYNAMIC_IMPORT_RE]) {
|
|
120
|
+
re.lastIndex = 0;
|
|
121
|
+
for (const m of source.matchAll(re)) add(m[1]);
|
|
122
|
+
}
|
|
123
|
+
return found;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 解析相对说明符 → 包内文件(posix 相对路径);解析不到返回 null。
|
|
128
|
+
* 支持无扩展名(补 `.js`、`/index.js`),与 Node 的 ESM 解析口径近似(本仓无扩展名导入)。
|
|
129
|
+
*/
|
|
130
|
+
export function resolveSpecifier(fromRel, spec, root = PKG_ROOT) {
|
|
131
|
+
const baseDir = path.posix.dirname(fromRel);
|
|
132
|
+
const joined = path.posix.normalize(path.posix.join(baseDir, spec));
|
|
133
|
+
const candidates = [joined, `${joined}.js`, `${joined}.mjs`, path.posix.join(joined, 'index.js')];
|
|
134
|
+
for (const rel of candidates) {
|
|
135
|
+
if (rel.startsWith('..')) continue; // 跑出包根:不是包内文件
|
|
136
|
+
const abs = path.join(root, rel);
|
|
137
|
+
try {
|
|
138
|
+
if (fs.statSync(abs).isFile()) return rel;
|
|
139
|
+
} catch { /* 不存在的候选,继续试下一个 */ }
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 计算运行时闭包:两个入口的静态 import 闭包 + `RUNTIME_RESOURCES`。
|
|
146
|
+
* @returns {{files: string[], unresolved: Array<{from: string, spec: string}>,
|
|
147
|
+
* entries: string[], resources: string[]}}
|
|
148
|
+
* `files` 排序去重;`unresolved` 是相对说明符解析不到(或裸包名/`node:`)的清单,
|
|
149
|
+
* 只报不用(避免把裸包名当必需导致假红)。
|
|
150
|
+
*/
|
|
151
|
+
export function computeClosure(root = PKG_ROOT) {
|
|
152
|
+
const files = new Set();
|
|
153
|
+
const unresolved = [];
|
|
154
|
+
const queue = [...ENTRIES];
|
|
155
|
+
const seen = new Set();
|
|
156
|
+
while (queue.length > 0) {
|
|
157
|
+
const rel = queue.shift();
|
|
158
|
+
if (seen.has(rel)) continue;
|
|
159
|
+
seen.add(rel);
|
|
160
|
+
const abs = path.join(root, rel);
|
|
161
|
+
let source;
|
|
162
|
+
try {
|
|
163
|
+
source = fs.readFileSync(abs, 'utf8');
|
|
164
|
+
} catch {
|
|
165
|
+
unresolved.push({ from: rel, spec: '(入口本身读不到)' });
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
files.add(rel);
|
|
169
|
+
for (const spec of scanRelativeSpecifiers(source)) {
|
|
170
|
+
const resolved = resolveSpecifier(rel, spec, root);
|
|
171
|
+
if (resolved === null) unresolved.push({ from: rel, spec });
|
|
172
|
+
else queue.push(resolved);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const resources = [];
|
|
176
|
+
for (const item of RUNTIME_RESOURCES.files) {
|
|
177
|
+
files.add(item.rel);
|
|
178
|
+
resources.push(item.rel);
|
|
179
|
+
}
|
|
180
|
+
for (const item of RUNTIME_RESOURCES.dirs) {
|
|
181
|
+
const abs = path.join(root, item.rel);
|
|
182
|
+
if (!fs.existsSync(abs)) { unresolved.push({ from: '(资源根)', spec: item.rel }); continue; }
|
|
183
|
+
for (const rel of listFiles(abs)) {
|
|
184
|
+
const full = `${item.rel}/${rel}`;
|
|
185
|
+
files.add(full);
|
|
186
|
+
resources.push(full);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
files: [...files].sort(),
|
|
191
|
+
unresolved,
|
|
192
|
+
entries: [...ENTRIES],
|
|
193
|
+
resources: resources.sort(),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** glob → 正则(npm `files` 最小语义:`**` 跨 `/`,`*`/`?` 不跨 `/`) */
|
|
198
|
+
function globToRegExp(pattern) {
|
|
199
|
+
let re = '^';
|
|
200
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
201
|
+
const ch = pattern[i];
|
|
202
|
+
if (ch === '*') {
|
|
203
|
+
if (pattern[i + 1] === '*') {
|
|
204
|
+
i++;
|
|
205
|
+
if (pattern[i + 1] === '/') { i++; re += '(?:.*/)?'; } // `**/` 也匹配零层
|
|
206
|
+
else re += '.*';
|
|
207
|
+
} else {
|
|
208
|
+
re += '[^/]*';
|
|
209
|
+
}
|
|
210
|
+
} else if (ch === '?') {
|
|
211
|
+
re += '[^/]';
|
|
212
|
+
} else if ('\\^$.|+()[]{}'.includes(ch)) {
|
|
213
|
+
re += `\\${ch}`;
|
|
214
|
+
} else {
|
|
215
|
+
re += ch;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return new RegExp(`${re}$`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* npm 恒含的文件(`files` 白名单管不着)—— **只在包根**,实测校准过:
|
|
223
|
+
* `files:["a.txt"]` 时 `sub/README.md` / `sub/deep/LICENSE` 都不进包,
|
|
224
|
+
* 而根级 `package.json` 恒在。别把这条写成「任意层级的 README/LICENSE」(会假绿:
|
|
225
|
+
* 例如 `tests/README.md` 会被误判成包内 —— 实测它不在 105 个文件里)。
|
|
226
|
+
*/
|
|
227
|
+
function isAlwaysIncluded(rel) {
|
|
228
|
+
if (rel === 'package.json') return true;
|
|
229
|
+
if (rel.includes('/')) return false;
|
|
230
|
+
return /^(README|LICENSE|LICENCE)(\..*)?$/i.test(rel);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* 展开 `files` 白名单 → 包内文件集合(最小 npm 语义,见文件头)。
|
|
235
|
+
* @param {string[]} entries package.json 的 files 数组
|
|
236
|
+
* @param {{root?: string, allFiles?: string[]}} [opts] 便于测试注入(负对照就在内存里换 entries)
|
|
237
|
+
* @returns {Set<string>} posix 相对路径集合
|
|
238
|
+
*/
|
|
239
|
+
export function expandFilesEntries(entries, opts = {}) {
|
|
240
|
+
const root = opts.root || PKG_ROOT;
|
|
241
|
+
const all = opts.allFiles || listFiles(root);
|
|
242
|
+
const packed = new Set();
|
|
243
|
+
for (const rel of all) if (isAlwaysIncluded(rel)) packed.add(rel);
|
|
244
|
+
for (const raw of entries) {
|
|
245
|
+
const entry = path.posix.normalize(String(raw).replace(/^\.\//, '').replace(/\/+$/, ''));
|
|
246
|
+
if (!entry || entry.startsWith('..')) continue;
|
|
247
|
+
const abs = path.join(root, entry);
|
|
248
|
+
let stat = null;
|
|
249
|
+
try { stat = fs.statSync(abs); } catch { /* 不存在:当 glob 处理 */ }
|
|
250
|
+
if (stat && stat.isDirectory()) {
|
|
251
|
+
for (const rel of listFiles(abs)) packed.add(`${entry}/${rel}`);
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (stat && stat.isFile()) { packed.add(entry); continue; }
|
|
255
|
+
const re = globToRegExp(entry);
|
|
256
|
+
for (const rel of all) if (re.test(rel)) packed.add(rel);
|
|
257
|
+
}
|
|
258
|
+
return packed;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* 判据本体:闭包里但包内没有的文件(空数组 = 过)。
|
|
263
|
+
* @param {string[]} closureFiles `computeClosure().files`
|
|
264
|
+
* @param {Iterable<string>} packedFiles `expandFilesEntries(...)`
|
|
265
|
+
*/
|
|
266
|
+
export function findUnpacked(closureFiles, packedFiles) {
|
|
267
|
+
const packed = packedFiles instanceof Set ? packedFiles : new Set(packedFiles);
|
|
268
|
+
return closureFiles.filter((rel) => !packed.has(rel));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* 读仓库 `package.json` 的 `files` 并跑完整判据(`prepack-check.mjs` 与单测共用)。
|
|
273
|
+
* @returns {{ok: boolean, missing: string[], closure: object, packedCount: number, files: string[]}}
|
|
274
|
+
*/
|
|
275
|
+
export function checkPackClosure(root = PKG_ROOT) {
|
|
276
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
277
|
+
const entries = Array.isArray(pkg.files) ? pkg.files : [];
|
|
278
|
+
const closure = computeClosure(root);
|
|
279
|
+
const packed = expandFilesEntries(entries, { root });
|
|
280
|
+
const missing = findUnpacked(closure.files, packed);
|
|
281
|
+
return { ok: missing.length === 0, missing, closure, packedCount: packed.size, files: entries };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* 已知缺口(写在这里,不装作没有):
|
|
286
|
+
* ① `.gitignore` / `.npmignore` 未建模 —— 若将来某个运行时文件恰好落在被忽略的目录里、
|
|
287
|
+
* 而白名单**没有**显式列出它,本判据会假绿(今天不会:闭包里的路径全部由显式条目覆盖)。
|
|
288
|
+
* ② npm 的 `files` 对**符号链接**、`.npmrc`、`bundledDependencies` 有额外规则,本判据不管。
|
|
289
|
+
* ③ 运行时**动态拼**出来的路径(例如按用户输入拼文件名)静态看不到,只能靠人工登记进
|
|
290
|
+
* `RUNTIME_RESOURCES`;登记漏了就是假绿。
|
|
291
|
+
*/
|
|
File without changes
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 发包前自检 —— package.json 的 `prepack` 钩子(`npm pack` / `npm publish` 前自动执行)。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 三道检查:
|
|
5
5
|
* ① **必需文件在不在**:本包是自包含的(插件壳 + Python 平台 + 预构建 webui/dist-plugin +
|
|
6
6
|
* 内置提示词/内置资产),少任何一块装到 profile 里都跑不起来;
|
|
7
7
|
* ② **三处「包名」逐字一致**:包根 package.json 的 `name`、`dsh-plugin/cordis.patch.yml`
|
|
8
8
|
* 里 insert 行的 `name`、`dsh-plugin/lib/client.bundle.js` 里 `__ModuleLoader__.load({id})`
|
|
9
9
|
* 的 id —— dsh 宿主按注册行的包名给浏览器半预留模块槽,三者不一致时页面报
|
|
10
10
|
* "loaded without registering"(2026-10-07 改名 @gavinc-cn/touchstone-dsh 时踩过)。
|
|
11
|
+
* ③ **闭包 ⊆ 白名单**(2026-10-15 新增):从两个入口(`backend/server.js`、
|
|
12
|
+
* `dsh-plugin/lib/index.js`)正推运行时闭包(静态 import 图 + 运行时按路径读的数据根),
|
|
13
|
+
* 与 `files` 展开结果比对。①的手写清单挡不住「新加了一个文件、忘了改白名单」——
|
|
14
|
+
* 实测 0.1.1 发出去的包里 `backend/routes/` 11 个文件全缺,解包后
|
|
15
|
+
* `node backend/server.js` 直接 ERR_MODULE_NOT_FOUND(`backend/server.js:61` import 它)。
|
|
16
|
+
* 算法在 `dsh-plugin/lib/pack-closure.mjs`(与 `tools/parity/test/pack-closure.test.mjs`
|
|
17
|
+
* **同一份**,不是一个抄一个);这里只读文件、**绝不递归调用 `npm pack`**(会被自己再触发)。
|
|
11
18
|
*
|
|
12
19
|
* 任一项不过就非 0 退出,让 `npm pack` / `npm publish` 直接中断:宁可发不出去,不可发出坏包。
|
|
13
20
|
* 用法: node dsh-plugin/scripts/prepack-check.mjs
|
|
@@ -16,6 +23,8 @@ import { readFileSync, existsSync } from 'node:fs';
|
|
|
16
23
|
import { dirname, join } from 'node:path';
|
|
17
24
|
import { fileURLToPath } from 'node:url';
|
|
18
25
|
|
|
26
|
+
import { checkPackClosure } from '../lib/pack-closure.mjs';
|
|
27
|
+
|
|
19
28
|
/** 包根(本脚本在 <包根>/dsh-plugin/scripts/ 下, 上溯三级) */
|
|
20
29
|
const PKG_ROOT = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
|
|
21
30
|
|
|
@@ -24,6 +33,15 @@ const REQUIRED = [
|
|
|
24
33
|
'package.json',
|
|
25
34
|
'server.py',
|
|
26
35
|
'requirements.txt',
|
|
36
|
+
// Node 后端骨架(2026-10-11 M0):profile 里配 `backend: node` 时薄壳拉起的就是它,
|
|
37
|
+
// 缺了等于「开关一打开就起不来」——必须在发包自检里挡住。
|
|
38
|
+
'backend/server.js',
|
|
39
|
+
'backend/lib',
|
|
40
|
+
// 数据层共享契约(2026-10-11 M1):`backend/lib/db.js` 启动即读 schema.sql /
|
|
41
|
+
// schema.json / migrations.json,缺了就是「Node 后端起不来」——同样必须在发包自检里挡住。
|
|
42
|
+
'backend/schema/schema.sql',
|
|
43
|
+
'backend/schema/schema.json',
|
|
44
|
+
'backend/schema/migrations.json',
|
|
27
45
|
'builtin_prompts',
|
|
28
46
|
'extensions/dsh-autocommit/asset.json',
|
|
29
47
|
'webui/dist-plugin/index.html',
|
|
@@ -53,9 +71,19 @@ if (!bundle.includes(`id: ${JSON.stringify(pkg.name)}`)) {
|
|
|
53
71
|
}
|
|
54
72
|
if (pkg.private === true) failures.push('package.json 里 private: true, 发不出去(发布前删掉)');
|
|
55
73
|
|
|
74
|
+
// 闭包 ⊆ 白名单: 从入口正推运行时必需文件, 逐条要求在 files 展开结果里
|
|
75
|
+
// (算法见 dsh-plugin/lib/pack-closure.mjs;只读文件, 不跑 npm pack ⇒ 可以安全地待在 prepack 里)
|
|
76
|
+
const closure = checkPackClosure(PKG_ROOT);
|
|
77
|
+
if (!closure.ok) {
|
|
78
|
+
failures.push(`files 白名单漏了 ${closure.missing.length} 个运行时必需文件`
|
|
79
|
+
+ `(新增运行时文件/目录后忘了改 package.json 的 files 就会这样):`);
|
|
80
|
+
for (const rel of closure.missing) failures.push(`files 缺: ${rel}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
56
83
|
if (failures.length > 0) {
|
|
57
84
|
console.error(`prepack 自检未通过(${failures.length} 项):`);
|
|
58
85
|
for (const line of failures) console.error(` - ${line}`);
|
|
59
86
|
process.exit(1);
|
|
60
87
|
}
|
|
61
|
-
console.log(`prepack 自检通过: ${pkg.name}@${pkg.version}, ${REQUIRED.length} 个必需路径齐全,
|
|
88
|
+
console.log(`prepack 自检通过: ${pkg.name}@${pkg.version}, ${REQUIRED.length} 个必需路径齐全, 包名三处一致, `
|
|
89
|
+
+ `运行时闭包 ${closure.closure.files.length} 个文件全在白名单内`);
|
|
File without changes
|
package/dsh-plugin/src/client.js
CHANGED
|
File without changes
|
package/dshevents.py
CHANGED
|
File without changes
|
package/export_cases.py
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/feishu_conv.py
CHANGED
|
File without changes
|
package/lib.py
CHANGED
|
File without changes
|
package/lifecycle.py
CHANGED
|
@@ -20,8 +20,14 @@ Python 后端是 dsh 宿主进程 spawn 的子进程。宿主退出/崩溃/被 `
|
|
|
20
20
|
父还活着(例如 stdin 不是管道、或管道被无关方关闭)只记日志不自杀。
|
|
21
21
|
|
|
22
22
|
2. **同库单实例** `SingleInstance`
|
|
23
|
-
- `<db>.lock`
|
|
24
|
-
`
|
|
23
|
+
- `<db>.lock` 上**两道锁**:`flock` 排它(`platcompat.lock_fd`)+ 同一文件上的
|
|
24
|
+
**SQLite 排它锁**(`locking_mode=EXCLUSIVE` + 一次写事务,持锁到进程结束);
|
|
25
|
+
再写 `<db>.pid` 记录 `{pid, ppid, repo_dir, parent_watch, started_at}`。
|
|
26
|
+
- 为什么要两道(2026-10-11,Node 后端 M1):flock 只有 Python 侧有(Node 无
|
|
27
|
+
stdlib flock),Node 版改用 SQLite 自身的排它锁,两种锁**互不相干**
|
|
28
|
+
⇒ 只靠 flock 挡不住 Node 版实例(实测:Node 持锁时 Python 的 flock 照样
|
|
29
|
+
成功)。两边都取 SQLite 锁后,同语言与跨语言互斥都成立,且 `kill -9` 后
|
|
30
|
+
由内核/文件锁自动释放,无需清理残留。
|
|
25
31
|
- 锁被占时:读 pid 记录——若记录进程**由父死感知托管**(`parent_watch=true`)
|
|
26
32
|
且其父已死 ⇒ 判定为**存量孤儿**(升级前的强杀残留),SIGTERM 后重试拿锁;
|
|
27
33
|
否则视为真·另一实例,明确报错退出(防双写同库)。
|
|
@@ -39,6 +45,7 @@ Python 后端是 dsh 宿主进程 spawn 的子进程。宿主退出/崩溃/被 `
|
|
|
39
45
|
import json
|
|
40
46
|
import os
|
|
41
47
|
import signal
|
|
48
|
+
import sqlite3
|
|
42
49
|
import sys
|
|
43
50
|
import threading
|
|
44
51
|
import time
|
|
@@ -225,6 +232,8 @@ class SingleInstance:
|
|
|
225
232
|
self.reason = ""
|
|
226
233
|
self.reaped_pid = 0
|
|
227
234
|
self._fd = None
|
|
235
|
+
# SQLite 排它锁的连接(第二道闸,M1);持锁期间常驻,release 时关闭
|
|
236
|
+
self._lock_conn = None
|
|
228
237
|
|
|
229
238
|
# ---------- 记录读写 ----------
|
|
230
239
|
|
|
@@ -259,17 +268,21 @@ class SingleInstance:
|
|
|
259
268
|
|
|
260
269
|
@staticmethod
|
|
261
270
|
def _looks_like_server(pid):
|
|
262
|
-
"""粗校验 pid 是否是 touchstone
|
|
271
|
+
"""粗校验 pid 是否是 touchstone 的后端(防 PID 复用误杀)。
|
|
263
272
|
|
|
273
|
+
两版后端的命令行都要认:Python 是 `server.py`,Node 是 `backend/server.js`
|
|
274
|
+
(2026-10-11 M1 起 Node 版也走同一套 `.pid` 记录与孤儿收口)。
|
|
264
275
|
读不到命令行时返回 True(宁可不杀——见 acquire 的调用点:仅在
|
|
265
276
|
「pid 记录声明 parent_watch 且其父已死」时才走到这里)。
|
|
266
277
|
"""
|
|
267
278
|
try:
|
|
268
279
|
if sys.platform.startswith("linux"):
|
|
269
280
|
with open(f"/proc/{int(pid)}/cmdline", "rb") as f:
|
|
270
|
-
|
|
281
|
+
cmdline = f.read()
|
|
282
|
+
return b"server.py" in cmdline or b"server.js" in cmdline
|
|
271
283
|
import psutil
|
|
272
|
-
|
|
284
|
+
argv = [str(a) for a in psutil.Process(int(pid)).cmdline()]
|
|
285
|
+
return any("server.py" in a or "server.js" in a for a in argv)
|
|
273
286
|
except Exception: # noqa: BLE001
|
|
274
287
|
return True
|
|
275
288
|
|
|
@@ -308,12 +321,93 @@ class SingleInstance:
|
|
|
308
321
|
|
|
309
322
|
# ---------- 加锁 ----------
|
|
310
323
|
|
|
324
|
+
def _acquire_sqlite_lock(self):
|
|
325
|
+
"""第二道闸:同一锁文件上的 SQLite 排它锁(M1,2026-10-11)。
|
|
326
|
+
|
|
327
|
+
Node 版没有 stdlib `flock`,改用 SQLite 自身的排它锁:在 `<db>.lock` 上
|
|
328
|
+
`PRAGMA locking_mode=EXCLUSIVE` + 一次写事务,锁持有到进程/连接结束
|
|
329
|
+
(`kill -9` 由内核释放,无残留文件要清理)。两种锁机制**互不相干**,所以
|
|
330
|
+
两边都要取这一道,跨语言互斥才成立(实测:Node 持锁时 Python 的 flock
|
|
331
|
+
照样能拿到)。
|
|
332
|
+
|
|
333
|
+
返回 `(状态, 文案)`:
|
|
334
|
+
- `("ok", "")` 拿到(连接挂在 `self._lock_conn`)
|
|
335
|
+
- `("busy", "")` 被占用(调用方报「另一实例」并退出)
|
|
336
|
+
- `("unusable", 文案)` 锁文件本身不可用(损坏/是目录/打不开)——
|
|
337
|
+
**不再降级放行**(2026-10-11 评审 P1):降级等于互斥无声消失,
|
|
338
|
+
实测「先起 Python 再起 Node」双方都能就绪 ⇒ 同库双写。
|
|
339
|
+
"""
|
|
340
|
+
try:
|
|
341
|
+
conn = sqlite3.connect(self.lock_path, timeout=0)
|
|
342
|
+
except sqlite3.Error as e:
|
|
343
|
+
return "unusable", self._unusable_reason(e)
|
|
344
|
+
try:
|
|
345
|
+
conn.execute("PRAGMA busy_timeout=0")
|
|
346
|
+
conn.execute("PRAGMA locking_mode=EXCLUSIVE")
|
|
347
|
+
# 必须真写一次:EXCLUSIVE 模式在首个写事务时取锁并一直持有
|
|
348
|
+
conn.execute("CREATE TABLE IF NOT EXISTS lock_holder ("
|
|
349
|
+
" id INTEGER PRIMARY KEY CHECK (id = 1),"
|
|
350
|
+
" pid INTEGER NOT NULL, started_at INTEGER NOT NULL)")
|
|
351
|
+
conn.execute("INSERT OR REPLACE INTO lock_holder(id, pid, started_at)"
|
|
352
|
+
" VALUES(1, ?, ?)", (os.getpid(), int(time.time())))
|
|
353
|
+
conn.commit()
|
|
354
|
+
except sqlite3.Error as e:
|
|
355
|
+
try:
|
|
356
|
+
conn.close()
|
|
357
|
+
except sqlite3.Error:
|
|
358
|
+
pass
|
|
359
|
+
if "locked" in str(e) or "busy" in str(e):
|
|
360
|
+
return "busy", ""
|
|
361
|
+
return "unusable", self._unusable_reason(e)
|
|
362
|
+
self._lock_conn = conn
|
|
363
|
+
return "ok", ""
|
|
364
|
+
|
|
365
|
+
def _unusable_reason(self, error):
|
|
366
|
+
"""锁文件不可用的报错文案(与 Node `lock.js:unusableLockReason` 同义)。"""
|
|
367
|
+
return (f"单实例锁文件不可用:{self.lock_path}(库 {self.db_file})。原因:{error}。"
|
|
368
|
+
f"该文件应为 SQLite 库或 0 字节空文件;内容损坏/被换成目录时无法建立互斥,"
|
|
369
|
+
f"继续启动会与另一版后端同库双写(任务重复执行、队列行被抢)。"
|
|
370
|
+
f"修复:确认没有实例在跑之后把它移走再启动"
|
|
371
|
+
f"(mv {self.lock_path} {self.lock_path}.broken);"
|
|
372
|
+
f"或显式 --allow-shared-db 自担双写风险。")
|
|
373
|
+
|
|
374
|
+
def _release_sqlite_lock(self):
|
|
375
|
+
"""释放 SQLite 排它锁(幂等)。"""
|
|
376
|
+
conn, self._lock_conn = self._lock_conn, None
|
|
377
|
+
if conn is None:
|
|
378
|
+
return
|
|
379
|
+
try:
|
|
380
|
+
conn.close()
|
|
381
|
+
except sqlite3.Error:
|
|
382
|
+
pass
|
|
383
|
+
|
|
384
|
+
def _try_take_locks(self):
|
|
385
|
+
"""两道锁的状态:`("ok"|"busy"|"unusable", 文案)`。
|
|
386
|
+
|
|
387
|
+
flock 拿到但 SQLite 被占/不可用时**回退 flock**:留着不放会挡住下一个 Python
|
|
388
|
+
实例去走「孤儿收口 → 重试」的路径(它只看到 flock 被占),把可恢复的孤儿变成死锁。
|
|
389
|
+
"""
|
|
390
|
+
if not platcompat.lock_fd(self._fd, blocking=False):
|
|
391
|
+
return "busy", ""
|
|
392
|
+
state, detail = self._acquire_sqlite_lock()
|
|
393
|
+
if state == "ok":
|
|
394
|
+
return "ok", ""
|
|
395
|
+
try:
|
|
396
|
+
platcompat.unlock_fd(self._fd)
|
|
397
|
+
except OSError:
|
|
398
|
+
pass
|
|
399
|
+
return state, detail
|
|
400
|
+
|
|
311
401
|
def acquire(self):
|
|
312
|
-
"""拿同库单实例锁。True=拿到(已写 pid 记录);False
|
|
402
|
+
"""拿同库单实例锁。True=拿到(已写 pid 记录);False=不能独占(`reason` 说明原因)。
|
|
313
403
|
|
|
314
404
|
非阻塞试锁 + 存量孤儿收口:拿到即写记录返回;拿不到先读 pid 记录——
|
|
315
405
|
若记录进程由父死感知托管且其父已死,判为存量孤儿,SIGTERM 后在
|
|
316
406
|
`stale_wait` 内重试;否则立即返回 False 并给出可执行的报错文案。
|
|
407
|
+
|
|
408
|
+
**不再有「降级为无锁运行」分支**(2026-10-11 评审 P1):锁文件打不开/损坏时
|
|
409
|
+
返回 False(调用方 exit 3)并给修复步骤——静默降级会让互斥无声消失、同库双写,
|
|
410
|
+
那比「启动被拒」严重得多。
|
|
317
411
|
"""
|
|
318
412
|
os.makedirs(os.path.dirname(self.lock_path), exist_ok=True)
|
|
319
413
|
if self.allow_shared:
|
|
@@ -322,15 +416,18 @@ class SingleInstance:
|
|
|
322
416
|
try:
|
|
323
417
|
fd = os.open(self.lock_path, os.O_RDWR | os.O_CREAT, 0o644)
|
|
324
418
|
except OSError as e:
|
|
325
|
-
self.
|
|
326
|
-
return
|
|
419
|
+
self.reason = self._unusable_reason(e)
|
|
420
|
+
return False
|
|
327
421
|
self._fd = fd
|
|
328
|
-
deadline = time.time() + max(self.stale_wait, 0.5) + 1.0
|
|
329
422
|
reaped = False
|
|
330
423
|
while True:
|
|
331
|
-
|
|
424
|
+
state, detail = self._try_take_locks()
|
|
425
|
+
if state == "ok":
|
|
332
426
|
self._write_record(os.getpid())
|
|
333
427
|
return True
|
|
428
|
+
if state == "unusable":
|
|
429
|
+
self.reason = detail
|
|
430
|
+
return False
|
|
334
431
|
rec = self._read_record()
|
|
335
432
|
if rec and not reaped and self._reap_stale(rec):
|
|
336
433
|
reaped = True # 孤儿已收口:其锁随进程退出释放,再试
|
|
@@ -343,9 +440,14 @@ class SingleInstance:
|
|
|
343
440
|
f"(TOUCHSTONE_DB / 插件 extraEnv.TOUCHSTONE_DB),"
|
|
344
441
|
f"或显式 --allow-shared-db 自担双写风险。")
|
|
345
442
|
else:
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
443
|
+
# 无归属记录时**不要**教用户删锁文件:删掉后新实例会在新 inode 上取锁,
|
|
444
|
+
# 与仍在运行的占用者双写(flock 与 SQLite 锁同理)。给安全的两个出口。
|
|
445
|
+
self.reason = (
|
|
446
|
+
f"锁文件被占用且无归属记录:{self.lock_path}"
|
|
447
|
+
f"(库 {self.db_file})。占用者可能是另一版后端的异常窗口"
|
|
448
|
+
f"(Node 版取锁后、写 .pid 前被杀)或残留的第三方进程;"
|
|
449
|
+
f"如需并行请用独立库(TOUCHSTONE_DB / 插件 extraEnv.TOUCHSTONE_DB),"
|
|
450
|
+
f"或显式 --allow-shared-db 自担双写风险。")
|
|
349
451
|
return False
|
|
350
452
|
# 不可达:while True 内所有分支都 return(保静态检查可读性)
|
|
351
453
|
|
|
@@ -354,6 +456,7 @@ class SingleInstance:
|
|
|
354
456
|
rec = self._read_record()
|
|
355
457
|
if rec and int(rec.get("pid") or 0) == os.getpid():
|
|
356
458
|
self._remove_record()
|
|
459
|
+
self._release_sqlite_lock()
|
|
357
460
|
if self._fd is not None:
|
|
358
461
|
platcompat.unlock_fd(self._fd)
|
|
359
462
|
try:
|
package/loadcase.py
CHANGED
|
File without changes
|
package/loadgen.py
CHANGED
|
File without changes
|
package/localbus.py
CHANGED
|
File without changes
|
package/migrate_p7b.py
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gavinc-cn/touchstone-dsh",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "AI-agent-driven automated testing platform for DeepSeek Harness (dsh): Markdown case library, bug reports, per-project serial queue, development board and built-in load testing.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Touchstone contributors",
|
|
@@ -27,6 +27,10 @@
|
|
|
27
27
|
"files": [
|
|
28
28
|
"*.py",
|
|
29
29
|
"requirements.txt",
|
|
30
|
+
"backend/server.js",
|
|
31
|
+
"backend/lib",
|
|
32
|
+
"backend/routes",
|
|
33
|
+
"backend/schema",
|
|
30
34
|
"builtin_prompts",
|
|
31
35
|
"extensions",
|
|
32
36
|
"dsh-plugin/cordis.patch.yml",
|
|
@@ -55,6 +59,7 @@
|
|
|
55
59
|
},
|
|
56
60
|
"scripts": {
|
|
57
61
|
"build:client": "node dsh-plugin/scripts/build.mjs",
|
|
62
|
+
"test:backend": "node --test \"backend/test/**/*.test.mjs\"",
|
|
58
63
|
"prepack": "node dsh-plugin/scripts/prepack-check.mjs"
|
|
59
64
|
},
|
|
60
65
|
"repository": {
|
package/platcompat.py
CHANGED
|
File without changes
|
package/prompts.py
CHANGED
|
File without changes
|
package/rag.py
CHANGED
|
File without changes
|
package/requirements.txt
CHANGED
|
File without changes
|
package/touchstone.py
CHANGED
|
File without changes
|
package/waitq.py
CHANGED
|
File without changes
|