@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,1021 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `prompts.py`(666 行,仓库根)的 Node 对应物(Node 后端 M6-4)。
|
|
3
|
+
*
|
|
4
|
+
* Touchstone 每轮 prompt 模板:首轮携带完整约束(案例库根/bug_report 目录/阅读范围/选项/红线),
|
|
5
|
+
* 常规测试轮再注入内置流程文档与文件模板(`builtin_prompts/free_style/`),续轮只发短 prompt。
|
|
6
|
+
* 七类 `task_type` 决定首轮文案:normal / regression / stress / fix / retest_bug / reject / pipeline
|
|
7
|
+
* (空值与未知类型回落 normal)。
|
|
8
|
+
*
|
|
9
|
+
* ── 导出名 ────────────────────────────────────────────────────────────────────
|
|
10
|
+
* 主名**照 Python 名**(`first_round_prompt`/`_load_asset`/…,含私有函数的下划线前缀),文件末尾
|
|
11
|
+
* 再给一套 camelCase 别名(`firstRoundPrompt`/…)供 JS 调用方使用——与 `rag.js`(M6-3)同一条
|
|
12
|
+
* 约定。对拍用例的 `fn` 写 Python 名、`nodeFn` 逐条指向本文件的导出名,不靠命名巧合。
|
|
13
|
+
*
|
|
14
|
+
* ── 与 Python 的四处**形态**差异(都已在调用方或量具里显式对齐) ─────────────────
|
|
15
|
+
* ① **异步**:`_semantic_order` 要调 `rag.retrieve_detail`(Node 侧只有异步的 `fetch`)⇒
|
|
16
|
+
* `_semantic_order` / `retest_range_prompt` / `retest_prompt` / `first_round_prompt`
|
|
17
|
+
* 返回 Promise(其余函数保持同步)。`await` 一个同步值无害,对拍器(`mod-run.mjs`)对两侧
|
|
18
|
+
* 都 `await fn(...)`,故这条不产生可观察差异;**但 JS 调用方必须 `await`**。
|
|
19
|
+
* ② **元组 → 数组**:Python 的 `(目录名, 全文)` 在 JS 侧就是 `[目录名, 全文]`(不用对象),
|
|
20
|
+
* 对拍无需 `shape` 适配;`None` 照旧 `null`。
|
|
21
|
+
* ③ **错误类型**:`RuntimeError` / `NameError` 在本文件有同名 JS 类,`KeyError` / `ValueError` /
|
|
22
|
+
* `AttributeError` 复用 `rag.js` 的同名类(`constructor.name` 与 Python `type(exc).__name__`
|
|
23
|
+
* 逐字相同),故「两侧同抛」的对拍分支能比 `$kind`;**消息也逐字照抄**(含中文文案与
|
|
24
|
+
* `tuple.index(x): x not in tuple`、`'list' object has no attribute 'get'` 这类 Python 原话)。
|
|
25
|
+
* ④ **`_ASSET_DIR` 由 `import.meta.url` 推出**:Python 是
|
|
26
|
+
* `dirname(abspath(__file__)) + builtin_prompts/free_style`;本文件在 `backend/lib/` 下,
|
|
27
|
+
* 故先上溯两级到仓库根再拼同样的两段。两侧最终指向**同一个真实目录**(`builtin_prompts/`
|
|
28
|
+
* 不在 `backend/` 里),所以「资产缺失」的错误文案里那条绝对路径两侧逐字节相同。
|
|
29
|
+
*
|
|
30
|
+
* ── 逐点等价的口径(每条都对着 Python 源码核过) ─────────────────────────────────
|
|
31
|
+
* 1. **`(x or "").strip()`**:一处不落地走 `stripOrEmpty`(`pyTruthy` 决定 `or`,`pyStrip` 是
|
|
32
|
+
* Python 的空白集合)——`env_label`/`guide_text`/`skill_*`/`extra`/`date_from`/`date_to`
|
|
33
|
+
* 共十处,任何一处写成 JS 的 `||`/`.trim()` 都会在「空串 vs 全空白 vs None」上分叉。
|
|
34
|
+
* 2. **真值判定**:`task["auto_fix"]`/`auto_commit`/`only_cases`/`bug_dirs`/`commits`/`files`
|
|
35
|
+
* 一律 `pyTruthy`(JS 的 `[]`/`{}` 是真值,Python 是假值;`0`/`""`/None 同假)。
|
|
36
|
+
* 3. **截断按码点**:`commits[:60]`/`files[:50]` 是**列表**切片(与 JS `slice` 同),而
|
|
37
|
+
* `(c['code_refs'] or '未填写')[:100]` 是**字符串**的码点切片 ⇒ `pySliceCodePoints`
|
|
38
|
+
* (JS `slice` 按 UTF-16 码元,会把边界上的 emoji 劈成半个代理对)。
|
|
39
|
+
* 4. **`task[...]` / `project[...]` 取键**:走 `pyGetItem`(缺键抛 `KeyError`,消息是键的 repr),
|
|
40
|
+
* `.get(k, d)` 走 `pyGet`(键在但为 None **不**走默认值)。生产里 project/task 是
|
|
41
|
+
* `sqlite3.Row`(缺键是 `IndexError`),对拍/单测里是 dict——本文件按 **dict 口径**对齐,
|
|
42
|
+
* 两者只在「字段缺失」时不同,而 prompt 只用 db 视图里必然存在的字段。
|
|
43
|
+
* 5. **`json.loads(payload or "{}")`**:`_jsonLoads` 先严格解析,失败且源码里有
|
|
44
|
+
* `NaN`/`Infinity`/`-Infinity` 字面量时替换后重试(CPython 默认 `allow_nan=True`),
|
|
45
|
+
* 仍失败抛 `SyntaxError`(= Python 的 `ValueError` 分支)。与 `db.js` 的私有 `pyJsonLoads`
|
|
46
|
+
* 同一算法,但那份未导出,不能 import。
|
|
47
|
+
* 6. **`flow.md` 与四份模板的拼接**:`"\n\n---\n\n"` 分隔线逐字节照抄,末尾再补一个 `"\n"`
|
|
48
|
+
* (`normal_prompt`)/ `_stress_doc` 的 `load_test.md` + 四份压测模板同理。
|
|
49
|
+
* 7. **`STAGE_ORDER`**:Python 元组 → JS 冻结数组(对拍探针把元组摊成数组,形状本就一致);
|
|
50
|
+
* `STAGE_ORDER.index(x)` 的 `ValueError` 与 `STAGE_LABELS[x]` 的 `KeyError` **照样抛**,
|
|
51
|
+
* 不兜底(未知 `end_stage` 是调用方的错误,静默回落会把「拼错阶段名」变成「跑了错的阶段」)。
|
|
52
|
+
*
|
|
53
|
+
* ── oracle 的活缺陷(**照抄,不许顺手修好**) ───────────────────────────────────
|
|
54
|
+
* `prompts.py:353` 在 `retest_prompt` 里写的是 `return retest_range_prompt(project, task, log)`,
|
|
55
|
+
* 而该函数作用域内**没有** `log` ⇒ `retest_bug` 未绑定报告且带日期范围时必抛
|
|
56
|
+
* `NameError: name 'log' is not defined`,日期范围复测的这条回落路径实际不可用。
|
|
57
|
+
* `tests/test_prompts_build.py:414-425` 已把它固化成「记录不修」。本文件在**同一位置**抛同名
|
|
58
|
+
* 同文案的 `NameError`(见 `retest_prompt`),对拍用例用 `expect:{expectOk:false}` 钉住。
|
|
59
|
+
* 另外 `first_round_prompt` 的 `log` 形参在 oracle 里就是从始至终没用过的死参数(它没把 log
|
|
60
|
+
* 转给 `retest_prompt`),本文件照旧保留该形参并同样不使用。
|
|
61
|
+
*
|
|
62
|
+
* ── 已知残余差异(备案,别当 bug 修) ───────────────────────────────────────────
|
|
63
|
+
* - **非字符串字段的 `strip()`**:`(x or "").strip()` 在 Python 里若 x 是非字符串真值
|
|
64
|
+
* (如 `5`)抛 `AttributeError: 'int' object has no attribute 'strip'`;JS 侧 `pyStrip`
|
|
65
|
+
* `String()` 化后返回 `"5"`。project/task 来自 sqlite 视图,这些列非 str 即 None,
|
|
66
|
+
* 该形态在生产里不可达。
|
|
67
|
+
* - **非字符串元素的 `join`**:`'、'.join(only_cases)` / `"- {cid}:{d}"` 在 Python 里遇到
|
|
68
|
+
* 非 str 元素抛 `TypeError`,JS 的 `Array.join` 会静默 `String()` 化。payload 里的
|
|
69
|
+
* `only_cases` 由平台写入用例 id 串,该形态不可达。
|
|
70
|
+
* - **`import.meta.url` 已解析符号链接**:Python 的 `abspath(__file__)` 不解符号链接,
|
|
71
|
+
* 若仓库以符号链接路径 import,两版推出的资产目录字符串可能不同(本仓库无此形态)。
|
|
72
|
+
* - **f-string 里的 `None`**:Python `f"{x}"` 把 None 渲染成 `None`、JS 模板串渲染成 `null`。
|
|
73
|
+
* 本文件插值的全部字段(task 的 `retest`/`id`/`end_stage`/`payload`/`extra`/`date_from`/
|
|
74
|
+
* `date_to` 与 project 的各路径列)在 `db.py` 的建表语句里都是 `TEXT/INTEGER NOT NULL`,
|
|
75
|
+
* None 不可达;真出现时两侧会分叉(届时按需引入 `pyStr` 口径)。
|
|
76
|
+
* - **`_semantic_order` 的 `via` 元素**:Python `"vec" in r.get("via", "")` 在 via 非 str 时抛
|
|
77
|
+
* TypeError、进而被外层 `except Exception` 吞成 `[]`;JS 侧 `String(...)` 化后照常判断。
|
|
78
|
+
* `rag.retrieve_detail` 的 `via` 恒为 str(`bm25`/`kw`/`vec`/`bm25+vec`),该形态不可达。
|
|
79
|
+
*
|
|
80
|
+
* 零 npm 依赖:只用 `node:path` / `node:url` 与本仓库内的 `lib.js` / `export-cases.js` / `rag.js`。
|
|
81
|
+
*/
|
|
82
|
+
import path from 'node:path';
|
|
83
|
+
import { fileURLToPath } from 'node:url';
|
|
84
|
+
|
|
85
|
+
// `export_cases.scan_cases`(M6-2 已落)与 `lib` 的读侧子集(含 M6-4 新落的 `gitLogChanges`)
|
|
86
|
+
import { scanCases } from './export-cases.js';
|
|
87
|
+
import { bugCases, findCaseDirs, gitLogChanges, pyJoin, pyStrip, readText } from './lib.js';
|
|
88
|
+
// `rag` 的配置/检索入口(M6-3 已落)+ 它那套 Python 语义小工具(错误类/真值/码点切片)
|
|
89
|
+
import {
|
|
90
|
+
AttributeError, KeyError, ValueError, change_query, load_config, pySliceCodePoints, pyTruthy,
|
|
91
|
+
retrieve_detail,
|
|
92
|
+
} from './rag.js';
|
|
93
|
+
|
|
94
|
+
// --------------------------------------------------------------------------
|
|
95
|
+
// Python 语义小工具(本文件私有;`rag.js` 有同名私有实现,各文件不互相 import 私有名)
|
|
96
|
+
// --------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
/** Python `RuntimeError`(内置资产缺失或为空) */
|
|
99
|
+
export class RuntimeError extends Error {
|
|
100
|
+
constructor(message) {
|
|
101
|
+
super(message);
|
|
102
|
+
this.name = 'RuntimeError';
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Python `NameError`(照抄 `prompts.py:353` 的活缺陷,见文件头) */
|
|
107
|
+
export class NameError extends Error {
|
|
108
|
+
constructor(message) {
|
|
109
|
+
super(message);
|
|
110
|
+
this.name = 'NameError';
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Python 类型名(只用于错误文案;int/float 在 JS 里已不可分,按是否整数猜) */
|
|
115
|
+
function pyTypeName(value) {
|
|
116
|
+
if (value === null || value === undefined) return 'NoneType';
|
|
117
|
+
if (Array.isArray(value)) return 'list';
|
|
118
|
+
switch (typeof value) {
|
|
119
|
+
case 'boolean': return 'bool';
|
|
120
|
+
case 'number': return Number.isInteger(value) ? 'int' : 'float';
|
|
121
|
+
case 'string': return 'str';
|
|
122
|
+
case 'object': return 'dict';
|
|
123
|
+
default: return typeof value;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Python `repr(str)` 的可用子集(单引号,转义反斜杠与单引号)——只用于 `KeyError` 文案 */
|
|
128
|
+
function pyReprString(text) {
|
|
129
|
+
return `'${String(text).replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Python `x or fallback`(`bool(x)` 口径:空 list/dict/""/0/None 皆为假) */
|
|
133
|
+
function pyOr(value, fallback) {
|
|
134
|
+
return pyTruthy(value) ? value : fallback;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Python `(x or "").strip()`——十处调用点共用这一条口径(见文件头 1) */
|
|
138
|
+
function stripOrEmpty(value) {
|
|
139
|
+
return pyStrip(pyOr(value, ''));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Python `d.get(k[, d])`:只有**键不存在**才给默认值(键在但为 None 仍返回 None) */
|
|
143
|
+
function pyGet(value, key, fallback = undefined) {
|
|
144
|
+
if (value === null || value === undefined || typeof value !== 'object' || Array.isArray(value)) {
|
|
145
|
+
throw new AttributeError(`'${pyTypeName(value)}' object has no attribute 'get'`);
|
|
146
|
+
}
|
|
147
|
+
return Object.hasOwn(value, key) ? value[key] : fallback;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Python `d[k]`(键不存在抛 `KeyError`,消息是键的 repr) */
|
|
151
|
+
function pyGetItem(value, key) {
|
|
152
|
+
if (value === null || value === undefined || typeof value !== 'object' || Array.isArray(value)) {
|
|
153
|
+
throw new TypeError(`'${pyTypeName(value)}' object is not subscriptable`);
|
|
154
|
+
}
|
|
155
|
+
if (!Object.hasOwn(value, key)) throw new KeyError(pyReprString(key));
|
|
156
|
+
return value[key];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** `pyJsonLoads` 的哨兵串(同 `db.js`:只在这三个字面量出现处替换,反序列化后还原) */
|
|
160
|
+
const PY_JSON_NAN = '\u0000py-json:NaN\u0000';
|
|
161
|
+
const PY_JSON_INF = '\u0000py-json:Infinity\u0000';
|
|
162
|
+
const PY_JSON_NEG_INF = '\u0000py-json:-Infinity\u0000';
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* 把源码里**字符串之外**的 `NaN`/`Infinity`/`-Infinity` 换成哨兵串(字符串里的同名文本不动)。
|
|
166
|
+
* 一处都没换返回 `null`(没必要重试)。与 `db.js:patchPyJsonSpecials` 同一算法。
|
|
167
|
+
*/
|
|
168
|
+
function patchPyJsonSpecials(src) {
|
|
169
|
+
let out = '';
|
|
170
|
+
let changed = false;
|
|
171
|
+
let inString = false;
|
|
172
|
+
for (let i = 0; i < src.length;) {
|
|
173
|
+
const ch = src[i];
|
|
174
|
+
if (inString) {
|
|
175
|
+
out += ch;
|
|
176
|
+
if (ch === '\\') { // 转义对整体跳过,免得把 \" 当字符串结束
|
|
177
|
+
out += src[i + 1] ?? '';
|
|
178
|
+
i += 2;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (ch === '"') inString = false;
|
|
182
|
+
i += 1;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (ch === '"') {
|
|
186
|
+
inString = true;
|
|
187
|
+
out += ch;
|
|
188
|
+
i += 1;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (src.startsWith('-Infinity', i)) { out += JSON.stringify(PY_JSON_NEG_INF); i += 9; changed = true; continue; }
|
|
192
|
+
if (src.startsWith('Infinity', i)) { out += JSON.stringify(PY_JSON_INF); i += 8; changed = true; continue; }
|
|
193
|
+
if (src.startsWith('NaN', i)) { out += JSON.stringify(PY_JSON_NAN); i += 3; changed = true; continue; }
|
|
194
|
+
out += ch;
|
|
195
|
+
i += 1;
|
|
196
|
+
}
|
|
197
|
+
return changed ? out : null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* CPython `json.loads()` 的读侧对应物(只补齐读路径可观察到的差异,见文件头 5)。
|
|
202
|
+
*
|
|
203
|
+
* 入参口径照抄 Python:只接受 str(Node 里另加 UTF-8 字节,Python 的 `json.loads` 也收 bytes);
|
|
204
|
+
* 其它类型抛 `TypeError`——`_task_payload` 只 catch `ValueError`,与 Python 一样让 TypeError 冒出去。
|
|
205
|
+
*/
|
|
206
|
+
function jsonLoads(text) {
|
|
207
|
+
let src;
|
|
208
|
+
if (typeof text === 'string') src = text;
|
|
209
|
+
else if (text instanceof Uint8Array) src = new TextDecoder('utf-8').decode(text);
|
|
210
|
+
else {
|
|
211
|
+
throw new TypeError('the JSON object must be str, bytes or bytearray, not '
|
|
212
|
+
+ pyTypeName(text));
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
return JSON.parse(src);
|
|
216
|
+
} catch (strictError) {
|
|
217
|
+
const patched = patchPyJsonSpecials(src);
|
|
218
|
+
if (patched === null) throw strictError; // = Python 的 ValueError 分支
|
|
219
|
+
return JSON.parse(patched, (key, value) => {
|
|
220
|
+
if (value === PY_JSON_NAN) return NaN;
|
|
221
|
+
if (value === PY_JSON_INF) return Infinity;
|
|
222
|
+
if (value === PY_JSON_NEG_INF) return -Infinity;
|
|
223
|
+
return value;
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// --------------------------------------------------------------------------
|
|
229
|
+
// 常量与内置资产(逐条对齐 prompts.py:25-36)
|
|
230
|
+
// --------------------------------------------------------------------------
|
|
231
|
+
|
|
232
|
+
/** 本文件所在目录 → 仓库根(`<repo>/backend/lib` 上溯两级;见文件头 ④) */
|
|
233
|
+
const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
234
|
+
const REPO_ROOT = path.resolve(MODULE_DIR, '..', '..');
|
|
235
|
+
|
|
236
|
+
/** 内置提示词资产目录:`builtin_prompts/free_style/`(流程文档 + 文件模板) */
|
|
237
|
+
export const _ASSET_DIR = pyJoin(REPO_ROOT, 'builtin_prompts', 'free_style');
|
|
238
|
+
|
|
239
|
+
/** 资产读取缓存(**进程内只读一次磁盘、永不失效**;键是 `"/".join(names)`) */
|
|
240
|
+
export const _ASSET_CACHE = new Map();
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* 生命周期阶段(与 `server.STAGES` / `runner.STAGES` / 前端 `STAGES` 保持一致;
|
|
244
|
+
* prompts 不 import server 防循环依赖)。
|
|
245
|
+
*/
|
|
246
|
+
export const STAGE_ORDER = Object.freeze(['gen_case', 'execute', 'report', 'analyze', 'fix',
|
|
247
|
+
'deploy', 'retest']);
|
|
248
|
+
|
|
249
|
+
/** 阶段 key → 中文名(键序照 Python dict 字面量) */
|
|
250
|
+
export const STAGE_LABELS = {
|
|
251
|
+
gen_case: '测试用例生成',
|
|
252
|
+
execute: '测试',
|
|
253
|
+
report: '生成报告',
|
|
254
|
+
analyze: '报告分析',
|
|
255
|
+
fix: '问题修复',
|
|
256
|
+
deploy: '重新部署',
|
|
257
|
+
retest: '复测',
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
/** `STAGE_ORDER.index(x)`:不在表内抛 `ValueError`(Python 原话,照抄不兜底) */
|
|
261
|
+
function stageIndex(stage) {
|
|
262
|
+
const i = STAGE_ORDER.indexOf(stage);
|
|
263
|
+
if (i < 0) throw new ValueError('tuple.index(x): x not in tuple');
|
|
264
|
+
return i;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** `STAGE_LABELS[x]`:键不存在抛 `KeyError`(消息是键的 repr,照抄不兜底) */
|
|
268
|
+
function stageLabel(stage) {
|
|
269
|
+
if (!Object.hasOwn(STAGE_LABELS, stage)) throw new KeyError(pyReprString(stage));
|
|
270
|
+
return STAGE_LABELS[stage];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* `prompts._load_asset`:读取 `builtin_prompts/free_style/` 下的内置提示词资产(带缓存)。
|
|
275
|
+
*
|
|
276
|
+
* names 为相对 `_ASSET_DIR` 的路径分段;文件**缺失或为空**时抛 `RuntimeError`——
|
|
277
|
+
* 宁可任务失败留日志,也不给 agent 注入残缺流程。缓存键 `"/".join(names)`,
|
|
278
|
+
* 命中即返回同一份字符串、不再碰磁盘(对拍/单测可据此钉住「只读一次」)。
|
|
279
|
+
*/
|
|
280
|
+
export function _load_asset(...names) {
|
|
281
|
+
const key = names.join('/');
|
|
282
|
+
if (!_ASSET_CACHE.has(key)) {
|
|
283
|
+
const full = pyJoin(_ASSET_DIR, ...names);
|
|
284
|
+
const text = readText(full);
|
|
285
|
+
if (!text) throw new RuntimeError(`内置提示词资产缺失或为空: ${full}`);
|
|
286
|
+
_ASSET_CACHE.set(key, text);
|
|
287
|
+
}
|
|
288
|
+
return _ASSET_CACHE.get(key);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* `prompts._free_style_doc`:拼接自由风格测试流程文档与全部文件模板(首轮 prompt 注入用)。
|
|
293
|
+
*
|
|
294
|
+
* 模板依次附在流程文档之后、用分隔线隔开;`flow.md` 末尾的「附带文件模板」一节与该
|
|
295
|
+
* 拼接顺序一一对应。
|
|
296
|
+
*/
|
|
297
|
+
export function _free_style_doc() {
|
|
298
|
+
const parts = [_load_asset('flow.md')];
|
|
299
|
+
for (const name of ['case_template.md', 'status_template.md', 'index_template.md',
|
|
300
|
+
'bug_report_template.md']) {
|
|
301
|
+
parts.push('\n\n---\n\n' + _load_asset('templates', name));
|
|
302
|
+
}
|
|
303
|
+
return parts.join('');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* `prompts.first_round_prompt`:构建首轮 prompt。project/task 为 db 视图(sqlite Row 或 dict)。
|
|
308
|
+
*
|
|
309
|
+
* `log` 形参在 oracle 里就是**死参数**(没有转给 `retest_prompt`,见文件头「oracle 的活缺陷」),
|
|
310
|
+
* 本文件照旧保留、照旧不用。返回 Promise(见文件头 ①)。
|
|
311
|
+
*/
|
|
312
|
+
export async function first_round_prompt(project, task, log = null) {
|
|
313
|
+
void log; // 照抄 oracle 的死参数(见 docstring)
|
|
314
|
+
const taskType = pyOr(pyGetItem(task, 'task_type'), 'normal');
|
|
315
|
+
let prompt;
|
|
316
|
+
if (taskType === 'fix') {
|
|
317
|
+
prompt = fix_prompt(project, task);
|
|
318
|
+
} else if (taskType === 'retest_bug') {
|
|
319
|
+
prompt = await retest_prompt(project, task);
|
|
320
|
+
} else if (taskType === 'stress') {
|
|
321
|
+
prompt = stress_prompt(project, task);
|
|
322
|
+
} else if (taskType === 'regression') {
|
|
323
|
+
prompt = regression_prompt(project, task);
|
|
324
|
+
} else if (taskType === 'pipeline') {
|
|
325
|
+
prompt = pipeline_prompt(project, task);
|
|
326
|
+
} else if (taskType === 'reject') {
|
|
327
|
+
prompt = reject_prompt(project, task);
|
|
328
|
+
} else {
|
|
329
|
+
prompt = normal_prompt(project, task);
|
|
330
|
+
}
|
|
331
|
+
// 阶段任务:要求 agent 按阶段推进更新 live.json 的 phase/phase_label(仅监控展示,
|
|
332
|
+
// 平台不做硬依赖);探索/压测的 phase 约定由现有流程文档/压测 prompt 承担
|
|
333
|
+
if (['fix', 'retest_bug', 'pipeline', 'regression'].includes(taskType)) {
|
|
334
|
+
prompt += ('\n---\n阶段进度上报(仅监控展示):每完成/进入一个阶段,更新 '
|
|
335
|
+
+ `${pyGetItem(project, 'work_dir')}/.live/live.json 的 phase=done 或当前阶段 key、`
|
|
336
|
+
+ 'phase_label=当前阶段中文名。\n');
|
|
337
|
+
}
|
|
338
|
+
const extra = stripOrEmpty(pyGetItem(task, 'extra'));
|
|
339
|
+
if (extra) {
|
|
340
|
+
prompt += ('\n\n---\n用户附加要求(平台转达,务必遵守;如与上面约束冲突以本段为准):\n'
|
|
341
|
+
+ extra + '\n');
|
|
342
|
+
}
|
|
343
|
+
return prompt;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* `prompts._project_context`:项目配置的上下文段落:环境标签/项目附加提示词/能力 skill。
|
|
348
|
+
*
|
|
349
|
+
* 提交规范与部署说明已并入项目附加提示词(guide_text),不再单独注入;
|
|
350
|
+
* 项目绑定能力 skill(skill_understand/skill_test/skill_cases)时在此追加注入行。
|
|
351
|
+
* 所有任务类型(测试/修复/复测)的首轮 prompt 都附带,供 agent 理解项目;全空返回空串。
|
|
352
|
+
*/
|
|
353
|
+
export function _project_context(project) {
|
|
354
|
+
const parts = [];
|
|
355
|
+
const img = stripOrEmpty(pyGetItem(project, 'env_label'));
|
|
356
|
+
if (img) {
|
|
357
|
+
parts.push(`- 环境标签:${img}(测试/修复/部署针对该环境)`);
|
|
358
|
+
}
|
|
359
|
+
const guide = stripOrEmpty(pyGetItem(project, 'guide_text'));
|
|
360
|
+
if (guide) {
|
|
361
|
+
parts.push(`- 项目附加提示(如何理解项目/写 commit/部署等):\n ${guide}`);
|
|
362
|
+
}
|
|
363
|
+
let skill = stripOrEmpty(pyGetItem(project, 'skill_understand'));
|
|
364
|
+
if (skill) {
|
|
365
|
+
parts.push(`- 理解项目:优先使用 skill「${skill}」分析项目结构与约定`);
|
|
366
|
+
}
|
|
367
|
+
skill = stripOrEmpty(pyGetItem(project, 'skill_test'));
|
|
368
|
+
if (skill) {
|
|
369
|
+
parts.push(`- 测试项目:优先使用 skill「${skill}」了解本项目的测试方式`
|
|
370
|
+
+ '(启动/运行/环境/压测等要求)');
|
|
371
|
+
}
|
|
372
|
+
skill = stripOrEmpty(pyGetItem(project, 'skill_cases'));
|
|
373
|
+
if (skill) {
|
|
374
|
+
parts.push(`- 用例规范:生成用例时遵循 skill「${skill}」的用例命名/结构/登记规范`);
|
|
375
|
+
}
|
|
376
|
+
if (!parts.length) return '';
|
|
377
|
+
return parts.join('\n');
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* `prompts._date_range_ctx`:提交日期范围上下文段:范围说明 + 范围内 git 提交/变更文件摘录。
|
|
382
|
+
*
|
|
383
|
+
* task 带 date_from/date_to(YYYY-MM-DD,空=不限)时返回多行文本,无日期范围返回空串;
|
|
384
|
+
* git 摘录失败时给范围说明并注明原因(`git_log_changes` 只降级不抛)。
|
|
385
|
+
* 三分支:有提交/变更文件、范围内无提交(`ok` 但空)、摘录失败(`ok=False`)。
|
|
386
|
+
*/
|
|
387
|
+
export function _date_range_ctx(project, task) {
|
|
388
|
+
const df = stripOrEmpty(pyGetItem(task, 'date_from'));
|
|
389
|
+
const dt = stripOrEmpty(pyGetItem(task, 'date_to'));
|
|
390
|
+
if (!(df || dt)) return '';
|
|
391
|
+
const log = gitLogChanges(pyGetItem(project, 'project_dir'), df, dt);
|
|
392
|
+
const lines = [`- 提交日期范围:${df || '最早'} .. ${dt || '现在'}(本任务优先覆盖该范围内提交)`];
|
|
393
|
+
const commits = pyTruthy(pyGet(log, 'ok')) ? pyOr(pyGet(log, 'commits'), []) : [];
|
|
394
|
+
const files = pyTruthy(pyGet(log, 'ok')) ? pyOr(pyGet(log, 'files'), []) : [];
|
|
395
|
+
if (commits.length) {
|
|
396
|
+
lines.push(`- 范围内提交(${commits.length} 条):`);
|
|
397
|
+
lines.push(' ' + commits.slice(0, 60).join('\n '));
|
|
398
|
+
}
|
|
399
|
+
if (files.length) {
|
|
400
|
+
const more = files.length > 50 ? ` …共 ${files.length} 个` : '';
|
|
401
|
+
lines.push(`- 变更文件(${files.length} 个,去重):`);
|
|
402
|
+
lines.push(' ' + files.slice(0, 50).join('\n ') + more);
|
|
403
|
+
} else if (pyTruthy(pyGet(log, 'ok'))) {
|
|
404
|
+
lines.push('- 该日期范围内没有提交(非 git 仓库或时间段无提交),按项目当前代码自行判断');
|
|
405
|
+
} else {
|
|
406
|
+
lines.push(`- git 摘录失败(${pyOr(pyGet(log, 'error'), '未知原因')}),按项目当前代码自行判断`);
|
|
407
|
+
}
|
|
408
|
+
return lines.join('\n') + '\n';
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* `prompts._date_range_priority`:探索任务优先范围段——在普通探索约束里插入
|
|
413
|
+
* 「先分析范围内提交、优先相关用例」;无日期范围(`_date_range_ctx` 返回空串)时整段不出现。
|
|
414
|
+
*/
|
|
415
|
+
export function _date_range_priority(project, task) {
|
|
416
|
+
const ctx = _date_range_ctx(project, task);
|
|
417
|
+
if (!ctx) return '';
|
|
418
|
+
return ('- 优先范围:先按下方提交清单分析改动涉及的模块与文件,'
|
|
419
|
+
+ '本轮优先设计并执行与这些改动相关的用例;\n' + ctx);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* `prompts.normal_prompt`:常规测试轮(执行一轮自由风格探索测试,流程文档与模板由平台注入)。
|
|
424
|
+
*
|
|
425
|
+
* `end_stage` 收窄每轮范围:gen_case 只生成用例不执行;execute 执行回状态不写 bug 报告;
|
|
426
|
+
* report 及以后(含空值)为现状完整行为。
|
|
427
|
+
*/
|
|
428
|
+
export function normal_prompt(project, task) {
|
|
429
|
+
const autoFix = pyTruthy(pyGetItem(task, 'auto_fix')) ? '开' : '关';
|
|
430
|
+
const end = pyOr(pyGetItem(task, 'end_stage'), 'report');
|
|
431
|
+
let scope;
|
|
432
|
+
if (end === 'gen_case') {
|
|
433
|
+
scope = '- 任务终点=测试用例生成:每轮只按模板生成新用例并登记各级 INDEX/status,'
|
|
434
|
+
+ '严禁执行用例、严禁发起任何请求;\n';
|
|
435
|
+
} else if (end === 'execute') {
|
|
436
|
+
scope = '- 任务终点=测试:每轮生成新用例并执行,回写 status.md 与 execution 记录;'
|
|
437
|
+
+ '发现失败只记录用例状态,严禁写 bug 报告;\n';
|
|
438
|
+
} else {
|
|
439
|
+
scope = '';
|
|
440
|
+
}
|
|
441
|
+
const ctx = _project_context(project);
|
|
442
|
+
const ctxBlock = ctx ? `- 项目上下文:\n${ctx}\n` : '';
|
|
443
|
+
const datePart = _date_range_priority(project, task);
|
|
444
|
+
const head = (
|
|
445
|
+
'执行一轮自由风格探索测试,严格按下方《流程文档》与《文件模板》执行,约束如下:\n'
|
|
446
|
+
+ `- 案例库根目录:${pyGetItem(project, 'cases_root')}(INDEX.md/ROUNDS.md 不存在则按附带模板初始化)\n`
|
|
447
|
+
+ `- bug_report 输出到:${pyGetItem(project, 'bug_dir')}\n`
|
|
448
|
+
+ `- 阅读范围:当前项目 ${pyGetItem(project, 'project_dir')}\n`
|
|
449
|
+
+ `- 误报教训:生成用例前必读 ${pyGetItem(project, 'cases_root')}/PITFALLS.md`
|
|
450
|
+
+ '(历史被用户否决的报告及原因),避开其中记录的误报模式;文件不存在则跳过\n'
|
|
451
|
+
+ `- 选项:自动修复=${autoFix};复测=${pyGetItem(task, 'retest')}\n`
|
|
452
|
+
+ `${scope}`
|
|
453
|
+
+ `${datePart}`
|
|
454
|
+
+ `${ctxBlock}`
|
|
455
|
+
+ `- 停止条件:只执行这一轮;结束后写 ${pyGetItem(project, 'work_dir')}/.live/live.json 的 `
|
|
456
|
+
+ 'phase=done 并输出本轮摘要\n'
|
|
457
|
+
+ '- 项目红线:不编译、不部署、不 git 提交\n'
|
|
458
|
+
+ "- 最后用一行输出本轮摘要:'ROUND_DONE 轮次N 新增X通过Y失败Z新bug'\n"
|
|
459
|
+
);
|
|
460
|
+
return head + '\n---\n\n' + _free_style_doc() + '\n';
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* `prompts._stress_doc`:拼接压测流程文档与方案包模板(stress 首轮 prompt 注入用)。
|
|
465
|
+
*
|
|
466
|
+
* 模板以 `---` 分隔线跟在流程文档后;顺序与 `load_test.md` 正文的引用一致
|
|
467
|
+
* (方案文档 → 图表声明 → 场景 → INDEX)。
|
|
468
|
+
*/
|
|
469
|
+
export function _stress_doc() {
|
|
470
|
+
const parts = [_load_asset('load_test.md')];
|
|
471
|
+
for (const name of ['load_plan_template.md', 'load_charts_template.md',
|
|
472
|
+
'load_scenario_template.md', 'load_index_template.md']) {
|
|
473
|
+
parts.push('\n\n---\n\n' + _load_asset('templates', name));
|
|
474
|
+
}
|
|
475
|
+
return parts.join('');
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* `prompts.stress_prompt`:压测任务首轮——生成压测方案包(方案文档 + 执行载体 + 图表声明)。
|
|
480
|
+
*
|
|
481
|
+
* 方案包固定落在 `<案例库根>/load/task_<任务id>/`(平台按任务 id 直接定位,不经 INDEX 解析);
|
|
482
|
+
* 用户压测说明(payload.brief)是目标地址与关注接口的权威来源。本轮只写文件不发请求。
|
|
483
|
+
*/
|
|
484
|
+
export function stress_prompt(project, task) {
|
|
485
|
+
const brief = pyOr(pyGet(_task_payload(task), 'brief', ''),
|
|
486
|
+
'(未填写,按案例库与项目信息自行确定)');
|
|
487
|
+
const cdir = `${pyGetItem(project, 'cases_root')}/load/task_${pyGetItem(task, 'id')}`;
|
|
488
|
+
return (
|
|
489
|
+
'压测任务(第 1 步/共 2 步,本轮只生成压测方案包,绝不发任何请求):\n'
|
|
490
|
+
+ `- 方案包目录:${cdir}(本轮只允许在该目录内写文件)\n`
|
|
491
|
+
+ `- 压测说明(用户提供,目标服务地址与关注接口以它为准):\n---\n${brief}\n---\n`
|
|
492
|
+
+ `- 案例库根目录:${pyGetItem(project, 'cases_root')}(用例记录了接口事实;说明未覆盖的`
|
|
493
|
+
+ `接口细节阅读 ${pyGetItem(project, 'project_dir')} 代码确认,不要臆造)\n`
|
|
494
|
+
+ `${_project_context(project)}`
|
|
495
|
+
+ '产出(格式见下方《压测流程文档》与各模板):\n'
|
|
496
|
+
+ `1. ${cdir}/plan.md(必产):压测方案文档,按《方案文档模板》写全`
|
|
497
|
+
+ '(目标/接口/加压模型/指标定义/通过标准);\n'
|
|
498
|
+
+ `2. 执行载体二选一:简单 HTTP 压测写 ${cdir}/scenario.json(按《场景模板》,`
|
|
499
|
+
+ '字段与取值范围不符合会导致平台校验失败);需要多协议/有状态链路/业务自定义'
|
|
500
|
+
+ `度量时写 ${cdir}/run.py(按《流程文档》的脚本契约,用 loadgen.MetricsWriter 上报指标);\n`
|
|
501
|
+
+ `3. ${cdir}/charts.json(建议产):按《图表声明模板》声明本次要看的图`
|
|
502
|
+
+ '(吞吐/延迟/错误率之外的业务量也一视同仁,如委托数、各状态分布);\n'
|
|
503
|
+
+ `4. 同步更新 ${pyGetItem(project, 'cases_root')}/load/INDEX.md(不存在则按附带模板新建),`
|
|
504
|
+
+ '登记本方案一行;\n'
|
|
505
|
+
+ '要求:\n'
|
|
506
|
+
+ '- plan.md 必须是 **Markdown**:`#/##/###` 分节、`|` 管道表格、`-` 与 `1.` 列表、'
|
|
507
|
+
+ '反引号包代码与路径——平台「压测方案」页直接渲染它(标题/表格/列表/引用块都有排版),'
|
|
508
|
+
+ '空格对齐的伪表格与纯文本大段不会成表;开头一段「- 键:值」抬头会渲染成方案抬头栏;\n'
|
|
509
|
+
+ '- 指标命名与图表声明必须一致:charts.json 引用的每个 metric 都要真的上报;\n'
|
|
510
|
+
+ '- 红线:本轮严禁向任何地址发请求、不做压测、不启动/停止服务、不编译、'
|
|
511
|
+
+ '不部署、不 git 提交;不在方案包目录之外写文件;发压由平台在第 2 步自动执行;\n'
|
|
512
|
+
+ `- 写完文件后更新 ${pyGetItem(project, 'work_dir')}/.live/live.json 的 `
|
|
513
|
+
+ "phase=done,并输出一行摘要:'STRESS_DONE 方案要点(目标/驱动/面板数)'。"
|
|
514
|
+
) + '\n---\n\n' + _stress_doc() + '\n';
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* `prompts.fix_prompt`:修复任务首轮——起点固定「报告分析」,按终点拼接 报告分析→(修复→部署→复测)。
|
|
519
|
+
*
|
|
520
|
+
* 未绑定 bug 报告(`_linked_bug` 返回 None)时回落常规探索;存量任务(无 end_stage)回落
|
|
521
|
+
* 终点=问题修复。能力 skill 绑定(skill_commit/skill_deploy 非空时改写对应条款)原样保留;
|
|
522
|
+
* 未知 `end_stage` 在 `STAGE_LABELS[end]` 上抛 `KeyError`(照抄,不兜底)。
|
|
523
|
+
*/
|
|
524
|
+
export function fix_prompt(project, task) {
|
|
525
|
+
const bug = _linked_bug(project, task);
|
|
526
|
+
if (bug === null) {
|
|
527
|
+
return normal_prompt(project, task);
|
|
528
|
+
}
|
|
529
|
+
const [bugDirName, content] = bug;
|
|
530
|
+
const end = pyOr(pyGetItem(task, 'end_stage'), 'fix');
|
|
531
|
+
const autoCommit = pyTruthy(pyGetItem(task, 'auto_commit')) ? '开' : '关';
|
|
532
|
+
const doDeploy = end === 'deploy' || end === 'retest';
|
|
533
|
+
const doRetest = end === 'retest';
|
|
534
|
+
// 复测选项行:终点=复测时注明复测在本任务内执行(平台不再另建复测任务);
|
|
535
|
+
// 终点<复测(仅分析/修复/部署)时只显示「自动复测=关」
|
|
536
|
+
const retestOpt = doRetest
|
|
537
|
+
? '自动复测=开(复测在本任务内执行,平台不再另建复测任务)\n'
|
|
538
|
+
: '自动复测=关\n';
|
|
539
|
+
const ctx = _project_context(project);
|
|
540
|
+
const ctxBlock = ctx ? `\n- 项目上下文:\n${ctx}` : '';
|
|
541
|
+
// 能力 skill 绑定:部署/提交仅在字段非空时改写对应要求文案;红线与开关语义不动
|
|
542
|
+
const skillCommit = stripOrEmpty(pyGetItem(project, 'skill_commit'));
|
|
543
|
+
const skillDeploy = stripOrEmpty(pyGetItem(project, 'skill_deploy'));
|
|
544
|
+
let commitReq;
|
|
545
|
+
if (skillCommit) {
|
|
546
|
+
commitReq = ('修复完成后使用 skill「' + skillCommit + '」完成 git commit'
|
|
547
|
+
+ '(仅 add 本次改动的文件);严禁 git push;'
|
|
548
|
+
+ '若 git 状态/权限不允许提交,在摘要中说明原因');
|
|
549
|
+
} else {
|
|
550
|
+
commitReq = ('修复完成后直接 git commit(仅 add 本次改动的文件,'
|
|
551
|
+
+ 'commit message 需符合项目附加提示词中的 git commit 规范);'
|
|
552
|
+
+ '严禁 git push;若 git 状态/权限不允许提交,在摘要中说明原因');
|
|
553
|
+
}
|
|
554
|
+
const head = (
|
|
555
|
+
`处理以下 bug 报告(任务范围:报告分析 → ${stageLabel(end)}),逐阶段执行:\n`
|
|
556
|
+
+ `- bug 报告目录:${pyGetItem(project, 'bug_dir')}/${bugDirName}/\n`
|
|
557
|
+
+ `- bug_report.md 全文:\n---\n${content}\n---\n`
|
|
558
|
+
+ `- 相关项目代码根:${pyGetItem(project, 'project_dir')}`
|
|
559
|
+
+ `${ctxBlock}\n`
|
|
560
|
+
+ `- 选项:自动提交=${autoCommit};自动部署=${doDeploy ? '开' : '关'};${retestOpt}`
|
|
561
|
+
+ '\n阶段一 报告分析:\n'
|
|
562
|
+
+ '1. 对照报告全文与项目代码核实问题是否成立、确认根因与触发条件;\n'
|
|
563
|
+
+ '2. 把结论写回该报告 bug_report.md 的「## 报告分析」小节'
|
|
564
|
+
+ '(是否成立/根因/涉及文件:行号/建议修复方向)。\n'
|
|
565
|
+
);
|
|
566
|
+
if (end === 'analyze') {
|
|
567
|
+
return head + (
|
|
568
|
+
'终点即报告分析:不修改项目代码、不部署、不 git 提交、不改报告其他小节;\n'
|
|
569
|
+
+ "最后用一行输出摘要:'FIX_DONE 已分析(结论要点)'。"
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
const parts = [
|
|
573
|
+
'\n阶段二 问题修复(修复=直接修改项目代码):\n'
|
|
574
|
+
+ '1. 按报告推荐方案修复:直接修改项目代码(文件:行号 已在报告中给出);\n'
|
|
575
|
+
+ '2. 自动提交=开:' + commitReq + ';\n'
|
|
576
|
+
+ ' 自动提交=关:严禁执行任何形式的 git commit(含经远程/消息通道委托提交),'
|
|
577
|
+
+ '不应为验证修复而提交代码;\n',
|
|
578
|
+
];
|
|
579
|
+
if (doDeploy) {
|
|
580
|
+
let deployReq;
|
|
581
|
+
if (skillDeploy) {
|
|
582
|
+
deployReq = ('使用 skill「' + skillDeploy + '」自动部署到环境标签对应的'
|
|
583
|
+
+ '测试环境并重启服务,完成后给出验证结果;'
|
|
584
|
+
+ '部署失败要说明原因与已完成的步骤');
|
|
585
|
+
} else {
|
|
586
|
+
deployReq = ('按项目附加提示词中的部署说明(指定的部署 skill 或文字步骤)'
|
|
587
|
+
+ '自动部署到环境标签对应的测试环境并重启服务,完成后给出验证结果;'
|
|
588
|
+
+ '部署失败要说明原因与已完成的步骤');
|
|
589
|
+
}
|
|
590
|
+
parts.push(
|
|
591
|
+
'\n阶段三 重新部署:\n'
|
|
592
|
+
+ '自动部署=开:' + deployReq + ';\n'
|
|
593
|
+
+ ' 自动部署=关:严禁执行任何部署/重启服务操作(含经 SSH/消息通道执行),'
|
|
594
|
+
+ '如需部署仅给出命令让用户自行执行;\n');
|
|
595
|
+
}
|
|
596
|
+
if (doRetest) {
|
|
597
|
+
parts.push(
|
|
598
|
+
'\n阶段四 复测(本任务内执行):\n'
|
|
599
|
+
+ '1. 重跑该报告关联的全部用例(按 case 文件执行),通过:status.md 状态改「通过」;'
|
|
600
|
+
+ '失败:改「失败」并写失败原因,失败证据追加到该报告 bug_report.md 的'
|
|
601
|
+
+ '「复测证据」小节;\n'
|
|
602
|
+
+ '2. 不改动案例库用例的断言与步骤;平台将按用例状态自动更新 bug 报告状态;\n');
|
|
603
|
+
}
|
|
604
|
+
let redline = ('红线:自动提交=关时严禁 git commit;不要 git push、不要编译 C++'
|
|
605
|
+
+ '(如需编译仅说明命令)、不要改动案例库与 bug 报告文件本身'
|
|
606
|
+
+ '(「报告分析」「复测证据」小节除外)');
|
|
607
|
+
if (!doDeploy) {
|
|
608
|
+
redline += ';严禁执行任何部署/重启服务操作';
|
|
609
|
+
}
|
|
610
|
+
parts.push(redline + ";\n最后用一行输出摘要:'FIX_DONE 方案要点(已提交:是/否 已部署:"
|
|
611
|
+
+ (doDeploy ? '是/否' : '否') + ' 已复测:' + (doRetest ? '是/否' : '否')
|
|
612
|
+
+ " 结果)'。");
|
|
613
|
+
return head + parts.join('');
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* `prompts.retest_prompt`:复测任务首轮——范围三选一:仅复测 / 重新部署→复测 / 仅重新部署。
|
|
618
|
+
*
|
|
619
|
+
* payload 带 only_cases 时只复核该子集(脚本复测失败升级)。存量任务(start/end 两列均空)
|
|
620
|
+
* 回落「仅复测」;部署是否包含由起点列决定(start=deploy 才含部署),范围含部署时注入部署条款
|
|
621
|
+
* (skill_deploy 非空时改用对应 skill)。未绑定 bug 报告时:带日期范围走 `retest_range_prompt`
|
|
622
|
+
* (**oracle 在这里抛 NameError**,见文件头)、否则回落常规探索。返回 Promise(见文件头 ①)。
|
|
623
|
+
*/
|
|
624
|
+
export async function retest_prompt(project, task) {
|
|
625
|
+
const bug = _linked_bug(project, task);
|
|
626
|
+
if (bug === null) {
|
|
627
|
+
// 未绑定 bug 报告:带日期范围的复测走 retest_range_prompt(无报告复测),
|
|
628
|
+
// 否则回落常规探索(存量行为)。
|
|
629
|
+
// prompts.py:353 的 `log` 在函数作用域内不存在 ⇒ 照抄抛 NameError(不许顺手修好)。
|
|
630
|
+
if (pyTruthy(pyGetItem(task, 'date_from')) || pyTruthy(pyGetItem(task, 'date_to'))) {
|
|
631
|
+
throw new NameError("name 'log' is not defined");
|
|
632
|
+
}
|
|
633
|
+
return normal_prompt(project, task);
|
|
634
|
+
}
|
|
635
|
+
const [bugDirName, content] = bug;
|
|
636
|
+
const end = pyOr(pyGetItem(task, 'end_stage'), 'retest');
|
|
637
|
+
// 部署是否包含由起点决定:仅复测=(retest,retest) 不含部署;重新部署→复测=(deploy,retest)
|
|
638
|
+
// 含部署;存量行(两列均空)回落起点=retest,即现状「不部署直接复测」
|
|
639
|
+
const start = pyOr(pyGetItem(task, 'start_stage'), 'retest');
|
|
640
|
+
const deployFirst = start === 'deploy';
|
|
641
|
+
let caseIds = bugCases(pyJoin(pyGetItem(project, 'bug_dir'), bugDirName)).ids;
|
|
642
|
+
const onlyCases = pyOr(pyGet(_task_payload(task), 'only_cases'), [])
|
|
643
|
+
.filter((c) => new Set(caseIds).has(c));
|
|
644
|
+
if (onlyCases.length) {
|
|
645
|
+
caseIds = onlyCases;
|
|
646
|
+
}
|
|
647
|
+
const caseDirs = findCaseDirs(pyGetItem(project, 'cases_root'), caseIds);
|
|
648
|
+
if (!caseDirs.length) {
|
|
649
|
+
return normal_prompt(project, task);
|
|
650
|
+
}
|
|
651
|
+
const caseLines = caseDirs.map(([cid, d]) => `- ${cid}:${d}`).join('\n');
|
|
652
|
+
const ctx = _project_context(project);
|
|
653
|
+
const ctxBlock = ctx ? `- 项目上下文:\n${ctx}\n` : '';
|
|
654
|
+
let onlyBlock = '';
|
|
655
|
+
if (onlyCases.length) {
|
|
656
|
+
onlyBlock = (
|
|
657
|
+
'\n背景:平台已用各用例固化的 verify.py 脚本做过一轮复测,上述用例被脚本判定失败。\n'
|
|
658
|
+
+ '请逐个人工复核:确认为真回归 → 按要求 2 记失败并补证据;'
|
|
659
|
+
+ '判定脚本过期(接口/数据变化导致断言失效)→ 修正该用例 verify.py 后重跑,\n'
|
|
660
|
+
+ '并在 execution 记录中注明「脚本已修正」。\n'
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const deployBlock = () => {
|
|
665
|
+
const skillDeploy = stripOrEmpty(pyGetItem(project, 'skill_deploy'));
|
|
666
|
+
let body;
|
|
667
|
+
if (skillDeploy) {
|
|
668
|
+
body = `使用 skill「${skillDeploy}」部署到环境标签对应的测试环境并重启服务`;
|
|
669
|
+
} else {
|
|
670
|
+
body = ('按项目附加提示词中的部署说明(指定的部署 skill 或文字步骤)部署到'
|
|
671
|
+
+ '环境标签对应的测试环境并重启服务');
|
|
672
|
+
}
|
|
673
|
+
return ('阶段一 重新部署:\n' + body + ',完成后给出验证结果;'
|
|
674
|
+
+ '部署失败要说明原因与已完成的步骤,并按当前已部署状态继续后续阶段;\n');
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
if (end === 'deploy') {
|
|
678
|
+
return (
|
|
679
|
+
'对以下 bug 报告执行「仅重新部署」(唯一任务:只部署验证,不复测用例):\n'
|
|
680
|
+
+ `- bug 报告目录:${pyGetItem(project, 'bug_dir')}/${bugDirName}/\n`
|
|
681
|
+
+ `- bug_report.md 全文:\n---\n${content}\n---\n`
|
|
682
|
+
+ `${ctxBlock}`
|
|
683
|
+
+ deployBlock()
|
|
684
|
+
+ '红线:不重跑用例、不生成新用例、不修改项目代码与案例库、不 git 提交;\n'
|
|
685
|
+
+ "最后用一行输出摘要:'RETEST_DONE 已部署(验证结果)'。"
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
const deployPart = deployFirst ? deployBlock() : '';
|
|
689
|
+
const redline = ('红线:不生成新用例、不补充新 bug 报告、不编译、不 git 提交'
|
|
690
|
+
+ (deployFirst ? '' : '、不部署')
|
|
691
|
+
+ ';平台会在任务结束时按用例状态自动更新 bug 报告状态');
|
|
692
|
+
return (
|
|
693
|
+
'复测以下 bug 报告关联的全部用例(唯一任务,不要生成新用例):\n'
|
|
694
|
+
+ `- bug 报告目录:${pyGetItem(project, 'bug_dir')}/${bugDirName}/\n`
|
|
695
|
+
+ `- 关联用例(已在平台侧标记「需要复测」):\n${caseLines}\n`
|
|
696
|
+
+ `${ctxBlock}${onlyBlock}`
|
|
697
|
+
+ deployPart
|
|
698
|
+
+ '要求:\n'
|
|
699
|
+
+ '1. 逐个用例按 case 文件(case.md/脚本)执行复测,记录每次请求与返回;\n'
|
|
700
|
+
+ '2. 通过:status.md 状态改「通过」;失败:改「失败」并写失败原因,'
|
|
701
|
+
+ '执行记录文件(execution_*.md)标注复测;\n'
|
|
702
|
+
+ '3. 失败的请求/返回证据追加到该 bug 报告 bug_report.md 的「复测证据」小节;\n'
|
|
703
|
+
+ `4. ${redline};\n`
|
|
704
|
+
+ "5. 最后用一行输出摘要:'RETEST_SUMMARY 通过X失败Y'。"
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* `prompts._semantic_order`:日期范围复测首轮的语义候选 id 序(RAG 未配置/失败返回空,
|
|
710
|
+
* 纯增量增强)。
|
|
711
|
+
*
|
|
712
|
+
* query 与 runner 粗筛兜底同源:变更文件 + 提交摘录(`rag.change_query`)。
|
|
713
|
+
* `log` 可选任务日志回调:检索完成记一行统计(候选数 + via 分布),向量路失败降级 BM25 时
|
|
714
|
+
* 附原因——任务日志可查「这次到底用没用 RAG」。**任何异常都吞掉返回 []**(照 Python 的
|
|
715
|
+
* `except Exception`),故这条路径永不阻塞任务。返回 Promise(见文件头 ①)。
|
|
716
|
+
*/
|
|
717
|
+
export async function _semantic_order(project, task, log = null) {
|
|
718
|
+
try {
|
|
719
|
+
// RAG 未配置:不触发检索(retrieve 会走 BM25 兜底返回非空),保证行为与无 RAG 完全一致
|
|
720
|
+
if (!load_config()) return [];
|
|
721
|
+
const changes = gitLogChanges(pyGetItem(project, 'project_dir'),
|
|
722
|
+
pyGetItem(task, 'date_from'), pyGetItem(task, 'date_to'));
|
|
723
|
+
if (!pyTruthy(pyGet(changes, 'ok'))) return [];
|
|
724
|
+
const [rows, meta] = await retrieve_detail(pyGetItem(project, 'cases_root'),
|
|
725
|
+
change_query(changes), 15);
|
|
726
|
+
if (log) {
|
|
727
|
+
const nVec = rows.filter((r) => String(pyGet(r, 'via', '')).includes('vec')).length;
|
|
728
|
+
let line = (`RAG 语义排序检索:候选 ${rows.length} 条`
|
|
729
|
+
+ `(bm25+vec ${nVec} / 仅bm25 ${rows.length - nVec})`);
|
|
730
|
+
if (pyTruthy(pyGet(meta, 'configured')) && !pyTruthy(pyGet(meta, 'vec_ok'))) {
|
|
731
|
+
line += `;向量路失败已降级 BM25:${pyGet(meta, 'vec_error')}`;
|
|
732
|
+
}
|
|
733
|
+
log(line);
|
|
734
|
+
}
|
|
735
|
+
return rows.map((r) => pyGetItem(r, 'id'));
|
|
736
|
+
} catch {
|
|
737
|
+
return [];
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* `prompts.retest_range_prompt`:日期范围复测首轮——先按范围内提交判断受影响用例,再逐用例
|
|
743
|
+
* 复测(无 bug 报告)。
|
|
744
|
+
*
|
|
745
|
+
* 适用范围:retest_bug 且任务带提交日期范围(date_from/date_to)、未绑定 bug 报告。
|
|
746
|
+
* 要求 agent 以固定行「受影响用例:FS0001、FS0002」输出判断结果;用例清单按案例库扫描注入
|
|
747
|
+
* (上限 150 条,含『依据代码』供影响面映射),超出部分省略。`log` 透传 `_semantic_order`。
|
|
748
|
+
* 返回 Promise(见文件头 ①)。
|
|
749
|
+
*/
|
|
750
|
+
export async function retest_range_prompt(project, task, log = null) {
|
|
751
|
+
const ctx = _project_context(project);
|
|
752
|
+
const ctxBlock = ctx ? `- 项目上下文:\n${ctx}\n` : '';
|
|
753
|
+
let cases = scanCases(pyGetItem(project, 'cases_root'));
|
|
754
|
+
const semIds = await _semantic_order(project, task, log);
|
|
755
|
+
if (semIds.length) {
|
|
756
|
+
const rank = new Map(semIds.map((cid, i) => [cid, i]));
|
|
757
|
+
cases = [...cases].sort((a, b) => {
|
|
758
|
+
const ra = rank.has(a.id) ? rank.get(a.id) : rank.size;
|
|
759
|
+
const rb = rank.has(b.id) ? rank.get(b.id) : rank.size;
|
|
760
|
+
if (ra !== rb) return ra - rb;
|
|
761
|
+
return a.id < b.id ? -1 : (a.id > b.id ? 1 : 0); // Python 元组比较的第二项(id 码点序)
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
const truncated = cases.length > 150;
|
|
765
|
+
const caseLines = cases.slice(0, 150).map((c) => {
|
|
766
|
+
const refs = pyOr(c.code_refs, '未填写');
|
|
767
|
+
return `- ${c.id} ${c.name}(依据代码:${pySliceCodePoints(refs, 100)})`;
|
|
768
|
+
}).join('\n') || '- (案例库暂无用例)';
|
|
769
|
+
const only = pyOr(pyGet(_task_payload(task), 'only_cases'), []);
|
|
770
|
+
let onlyNote;
|
|
771
|
+
if (only.length) {
|
|
772
|
+
onlyNote = (`\n背景:首轮已选出受影响的用例(${only.length} 条:${only.join('、')}),`
|
|
773
|
+
+ '本轮只复测该清单内的用例,不要再重复全库判断;\n');
|
|
774
|
+
} else {
|
|
775
|
+
onlyNote = (
|
|
776
|
+
'\n要求 1:先按范围内提交计算影响面(提交改动文件 → 模块 → 用例『依据代码』'
|
|
777
|
+
+ '映射),只选取受影响的用例进入复测范围;\n'
|
|
778
|
+
+ "要求 2:判断完成后单独输出一行:'受影响用例:FS0001、FS0002'"
|
|
779
|
+
+ "(仅受影响用例 id、顿号分隔;无受影响用例输出'受影响用例:无');\n"
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
const semNote = semIds.length ? `,前 ${Math.min(semIds.length, 150)} 条为语义检索相关候选` : '';
|
|
783
|
+
return (
|
|
784
|
+
'日期范围复测任务(唯一任务,不生成新用例、不写 bug 报告),约束如下:\n'
|
|
785
|
+
+ `- 提交日期范围:${pyOr(pyGetItem(task, 'date_from'), '最早')} .. ${pyOr(pyGetItem(task, 'date_to'), '现在')}\n`
|
|
786
|
+
+ `- 范围内提交与变更文件:\n${_date_range_ctx(project, task)}`
|
|
787
|
+
+ `- 案例库根目录:${pyGetItem(project, 'cases_root')}\n`
|
|
788
|
+
+ `- 用例清单(共 ${cases.length} 条`
|
|
789
|
+
+ `${truncated ? ',仅列前 150 条' : ''}${semNote},`
|
|
790
|
+
+ `按『依据代码』映射影响面):\n${caseLines}\n`
|
|
791
|
+
+ `${ctxBlock}${onlyNote}`
|
|
792
|
+
+ '复测执行要求:\n'
|
|
793
|
+
+ '1. 逐个受影响用例按 case 文件(case.md/脚本)执行复测,记录每次请求与返回;\n'
|
|
794
|
+
+ '2. 通过:status.md 状态改「通过」;失败:改「失败」并写失败原因,'
|
|
795
|
+
+ '执行记录文件(execution_*.md)标注复测;\n'
|
|
796
|
+
+ '3. 红线:不生成新用例、不补充 bug 报告、不编译、不部署、不 git 提交;\n'
|
|
797
|
+
+ "4. 最后用一行输出摘要:'RETEST_SUMMARY 通过X失败Y'。"
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* `prompts.reject_prompt`:修例任务——报告被用户拒绝,按拒绝理由修正关联用例并沉淀教训。
|
|
803
|
+
*
|
|
804
|
+
* 报告状态「已拒绝」与「拒绝记录」小节由平台在创建本任务前写入,agent 只负责案例库侧
|
|
805
|
+
* (修正用例 + PITFALLS.md),不再动报告本身;未绑定报告时回落常规探索。
|
|
806
|
+
*/
|
|
807
|
+
export function reject_prompt(project, task) {
|
|
808
|
+
const bug = _linked_bug(project, task);
|
|
809
|
+
if (bug === null) {
|
|
810
|
+
return normal_prompt(project, task);
|
|
811
|
+
}
|
|
812
|
+
const [bugDirName, content] = bug;
|
|
813
|
+
const reason = _reject_reason(task);
|
|
814
|
+
const caseDirs = findCaseDirs(pyGetItem(project, 'cases_root'),
|
|
815
|
+
bugCases(pyJoin(pyGetItem(project, 'bug_dir'), bugDirName)).ids);
|
|
816
|
+
const caseLines = caseDirs.map(([cid, d]) => `- ${cid}:${d}`).join('\n')
|
|
817
|
+
|| '(未解析到关联用例,跳过用例修正,只沉淀教训)';
|
|
818
|
+
const ctx = _project_context(project);
|
|
819
|
+
const ctxBlock = ctx ? `- 项目上下文:\n${ctx}\n` : '';
|
|
820
|
+
return (
|
|
821
|
+
'以下 bug 报告已被用户拒绝(判定不成立/不接受)。唯一任务:根据拒绝理由'
|
|
822
|
+
+ '修正关联用例,并把教训沉淀到误报教训录,供后续生成用例时避开同类误报:\n'
|
|
823
|
+
+ `- bug 报告目录:${pyGetItem(project, 'bug_dir')}/${bugDirName}/\n`
|
|
824
|
+
+ `- bug_report.md 全文:\n---\n${content}\n---\n`
|
|
825
|
+
+ `- 用户拒绝理由(修例的根本依据):\n---\n${reason}\n---\n`
|
|
826
|
+
+ `- 关联用例(case 目录):\n${caseLines}\n`
|
|
827
|
+
+ `- 案例库根目录:${pyGetItem(project, 'cases_root')}\n`
|
|
828
|
+
+ `${ctxBlock}`
|
|
829
|
+
+ '要求:\n'
|
|
830
|
+
+ '1. 对照报告全文与拒绝理由,判断报告不被接受的原因'
|
|
831
|
+
+ '(误报/理解偏差/断言错误/环境问题等);\n'
|
|
832
|
+
+ '2. 逐个检查关联用例:按理由修正错误的断言与预期;因误报而不成立的用例'
|
|
833
|
+
+ '直接删除(目录连同各级 INDEX.md 中的记录一并移除);修改处在 case 文件中'
|
|
834
|
+
+ '注明「因报告被拒修正」;\n'
|
|
835
|
+
+ `3. 把教训追加到 ${pyGetItem(project, 'cases_root')}/PITFALLS.md(不存在则新建,`
|
|
836
|
+
+ '首行标题「# 误报教训录」),每条一行,格式:\n'
|
|
837
|
+
+ ` '- YYYY-MM-DD ${bugDirName}:一句话教训(拒绝理由摘要)'`
|
|
838
|
+
+ '(日期填当天);\n'
|
|
839
|
+
+ '4. 红线:不生成新用例、不修改 bug 报告文件本身(平台已标记「已拒绝」)、'
|
|
840
|
+
+ '不编译、不部署、不 git 提交;\n'
|
|
841
|
+
+ "5. 最后用一行输出摘要:'REJECT_DONE 修正X删除Y 教训要点'。"
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* `prompts._task_payload`:解析任务 payload JSON;为空或非法时返回 `{}`。
|
|
847
|
+
*
|
|
848
|
+
* 只 catch JSON 解析失败(= Python 的 `ValueError`);非 str/bytes 入参抛 `TypeError`、
|
|
849
|
+
* 解析出的非 dict 值原样返回(调用方 `.get` 时才抛 `AttributeError`)——与 Python 一致。
|
|
850
|
+
*/
|
|
851
|
+
export function _task_payload(task) {
|
|
852
|
+
try {
|
|
853
|
+
return jsonLoads(pyOr(pyGetItem(task, 'payload'), '{}'));
|
|
854
|
+
} catch (exc) {
|
|
855
|
+
if (exc instanceof SyntaxError) return {};
|
|
856
|
+
throw exc;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/** `prompts._reject_reason`:从 payload 解析拒绝理由(创建 reject 任务时由平台写入)。 */
|
|
861
|
+
export function _reject_reason(task) {
|
|
862
|
+
try {
|
|
863
|
+
return pyOr(pyGet(jsonLoads(pyOr(pyGetItem(task, 'payload'), '{}')), 'reason', ''), '(未填写)');
|
|
864
|
+
} catch (exc) {
|
|
865
|
+
if (exc instanceof SyntaxError) return '(未填写)';
|
|
866
|
+
throw exc; // 非 dict 的 AttributeError 照 Python 冒出去
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* `prompts._linked_bug`:从 payload 解析关联 bug 报告,返回 `[目录名, bug_report.md 全文]`,
|
|
872
|
+
* 无效返回 `null`(payload 无 bug_dir / 报告文件缺失或为空)。
|
|
873
|
+
*/
|
|
874
|
+
export function _linked_bug(project, task) {
|
|
875
|
+
const bugDir = pyGet(_task_payload(task), 'bug_dir', '');
|
|
876
|
+
if (!pyTruthy(bugDir)) return null;
|
|
877
|
+
const dirPath = pyJoin(pyGetItem(project, 'bug_dir'), bugDir);
|
|
878
|
+
const content = readText(pyJoin(dirPath, 'bug_report.md'));
|
|
879
|
+
return content ? [bugDir, content] : null;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* `prompts.regression_prompt`:回归轮——按用户指令重跑存量用例(不生成新用例)。
|
|
884
|
+
*
|
|
885
|
+
* 指令存任务 extra(创建时必填),是选取重跑用例的唯一依据;终点=execute 时失败只回状态,
|
|
886
|
+
* ≥report 时失败用例按流程写 bug 报告(不新建用例)。
|
|
887
|
+
*/
|
|
888
|
+
export function regression_prompt(project, task) {
|
|
889
|
+
const end = pyOr(pyGetItem(task, 'end_stage'), 'report');
|
|
890
|
+
const instruction = pyOr(stripOrEmpty(pyGetItem(task, 'extra')), '(未填写)');
|
|
891
|
+
const ctx = _project_context(project);
|
|
892
|
+
const ctxBlock = ctx ? `- 项目上下文:\n${ctx}\n` : '';
|
|
893
|
+
let scope;
|
|
894
|
+
if (end === 'execute') {
|
|
895
|
+
scope = '- 任务终点=测试:失败用例只回写状态与执行记录,严禁写 bug 报告;\n';
|
|
896
|
+
} else {
|
|
897
|
+
scope = ('- 任务终点含「生成报告」:失败用例除回写状态外,按流程文档为该失败'
|
|
898
|
+
+ '创建 bug 报告(关联该用例;不新建用例);\n');
|
|
899
|
+
}
|
|
900
|
+
return (
|
|
901
|
+
'执行一轮存量用例回归(唯一任务:重跑已有用例,严禁生成任何新用例),约束如下:\n'
|
|
902
|
+
+ `- 案例库根目录:${pyGetItem(project, 'cases_root')}(只读取,不登记新用例)\n`
|
|
903
|
+
+ `- 重跑范围指令(用户提供,选取要重跑用例的唯一依据):\n---\n${instruction}\n---\n`
|
|
904
|
+
+ `- 阅读范围:当前项目 ${pyGetItem(project, 'project_dir')}\n`
|
|
905
|
+
+ `${ctxBlock}`
|
|
906
|
+
+ `${scope}`
|
|
907
|
+
+ '- 执行要求:逐个选中用例按 case 文件执行,回写 status.md 状态与执行记录'
|
|
908
|
+
+ '(execution_*.md);\n'
|
|
909
|
+
+ '- 项目红线:不生成新用例、不修改用例的断言与步骤(仅状态/执行记录)、'
|
|
910
|
+
+ '不编译、不部署、不 git 提交;\n'
|
|
911
|
+
+ `- 停止条件:只执行这一轮;结束后更新 ${pyGetItem(project, 'work_dir')}/.live/live.json `
|
|
912
|
+
+ "phase=done 并输出一行摘要:'REGRESSION_DONE 重跑X通过Y失败Z'\n"
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* `prompts.pipeline_prompt`:后段任务首轮——对前段探索/回归产出的 bug 报告清单逐枚执行
|
|
918
|
+
* 报告分析→终点。
|
|
919
|
+
*
|
|
920
|
+
* 清单由平台在任务启动时按「bug 目录时间戳 ≥ 前段任务 created_at」固化进
|
|
921
|
+
* payload.bug_dirs;终点≥fix/部署/复测时追加对应阶段。未知 `end_stage` 在
|
|
922
|
+
* `STAGE_ORDER.index` 上抛 `ValueError`(先于 `STAGE_LABELS[end]` 的 `KeyError`)。
|
|
923
|
+
*/
|
|
924
|
+
export function pipeline_prompt(project, task) {
|
|
925
|
+
const end = pyOr(pyGetItem(task, 'end_stage'), 'report');
|
|
926
|
+
let bugDirs;
|
|
927
|
+
try {
|
|
928
|
+
bugDirs = pyOr(pyGet(jsonLoads(pyOr(pyGetItem(task, 'payload'), '{}')), 'bug_dirs'), []);
|
|
929
|
+
} catch (exc) {
|
|
930
|
+
if (!(exc instanceof SyntaxError)) throw exc;
|
|
931
|
+
bugDirs = [];
|
|
932
|
+
}
|
|
933
|
+
const listing = bugDirs.map((d) => `- ${pyGetItem(project, 'bug_dir')}/${d}/`).join('\n') || '(无)';
|
|
934
|
+
const ctx = _project_context(project);
|
|
935
|
+
const ctxBlock = ctx ? `- 项目上下文:\n${ctx}\n` : '';
|
|
936
|
+
const steps = ['1. 逐枚阅读 bug 报告全文与项目代码,把核实结论写回该报告'
|
|
937
|
+
+ '「## 报告分析」小节(不改代码);\n'];
|
|
938
|
+
const doFix = stageIndex(end) >= stageIndex('fix');
|
|
939
|
+
const doDeploy = stageIndex(end) >= stageIndex('deploy');
|
|
940
|
+
const doRetest = stageIndex(end) >= stageIndex('retest');
|
|
941
|
+
if (doFix) {
|
|
942
|
+
steps.push('2. 对确认成立的问题逐枚修复(直接修改项目代码,按报告推荐方案);\n');
|
|
943
|
+
}
|
|
944
|
+
if (doDeploy) {
|
|
945
|
+
const skillDeploy = stripOrEmpty(pyGetItem(project, 'skill_deploy'));
|
|
946
|
+
const deployBody = skillDeploy
|
|
947
|
+
? `使用 skill「${skillDeploy}」`
|
|
948
|
+
: '按项目附加提示词中的部署说明';
|
|
949
|
+
steps.push(`3. 全部修复完成后,${deployBody}重新部署到环境标签`
|
|
950
|
+
+ '对应的测试环境并重启服务,给出验证结果;\n');
|
|
951
|
+
}
|
|
952
|
+
if (doRetest) {
|
|
953
|
+
steps.push('4. 逐枚重跑各报告关联用例(按 case 文件执行),回写 status.md,'
|
|
954
|
+
+ '失败证据追加到报告「复测证据」小节;平台将按用例状态更新各 bug 报告状态;\n');
|
|
955
|
+
}
|
|
956
|
+
const redline = ('红线:不生成新用例、不改用例断言与步骤、不 git 提交'
|
|
957
|
+
+ (doDeploy ? '' : '、不部署/重启服务') + ';\n');
|
|
958
|
+
return (
|
|
959
|
+
'后段任务:处理前一段探索/回归任务产出的全部 bug 报告(清单如下),'
|
|
960
|
+
+ `任务范围:报告分析 → ${stageLabel(end)},逐枚按顺序执行:\n`
|
|
961
|
+
+ `${listing}\n`
|
|
962
|
+
+ `- 案例库根目录:${pyGetItem(project, 'cases_root')}(复测用例只执行不修改)\n`
|
|
963
|
+
+ `${ctxBlock}`
|
|
964
|
+
+ steps.join('') + redline
|
|
965
|
+
+ "最后用一行输出摘要:'PIPELINE_DONE 分析X修复Y部署:"
|
|
966
|
+
+ (doDeploy ? '是/否' : '否') + "复测通过Z失败W'。"
|
|
967
|
+
);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
/** `prompts.next_round_prompt`:续轮短 prompt(同会话)——探索/回归分版(回归同样不生成新用例)。 */
|
|
971
|
+
export function next_round_prompt(round_no, task = null) {
|
|
972
|
+
if (task != null && pyOr(pyGetItem(task, 'task_type'), 'normal') === 'regression') {
|
|
973
|
+
return (
|
|
974
|
+
`继续执行存量用例回归第 ${round_no} 轮:仍严禁生成任何新用例,按首轮`
|
|
975
|
+
+ '「重跑范围指令」继续选取未重跑的用例执行并回写状态,约束同首轮'
|
|
976
|
+
+ '(重跑指令、终点范围、红线);只执行这一轮,结束后更新工作目录下 '
|
|
977
|
+
+ ".live/live.json phase=done 并输出 'REGRESSION_DONE 重跑X通过Y失败Z' 摘要行。"
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
return (
|
|
981
|
+
`继续执行自由风格探索测试第 ${round_no} 轮:换一个 ROUNDS.md 未记录过的`
|
|
982
|
+
+ '角度生成新 case,并按根 INDEX.md「功能分级」节从高优先级到低优先级推进'
|
|
983
|
+
+ '(高优先级功能域未覆盖时不选边界/异常类细节角度),'
|
|
984
|
+
+ '约束同首轮(案例库根目录、bug_report 目录、阅读范围、'
|
|
985
|
+
+ '选项、红线);只执行这一轮,结束后更新工作目录下 .live/live.json phase=done '
|
|
986
|
+
+ "并输出 'ROUND_DONE ...' 摘要行。"
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/** `prompts.compact_prompt`:轮次开始前的上下文压缩提示(kimi 支持,其余 agent 日志注明跳过)。 */
|
|
991
|
+
export function compact_prompt() {
|
|
992
|
+
return '/compact';
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// --------------------------------------------------------------------------
|
|
996
|
+
// camelCase 别名(供 JS 调用方;对拍用例逐条用 nodeFn 指向**主名**,不依赖别名)
|
|
997
|
+
// --------------------------------------------------------------------------
|
|
998
|
+
|
|
999
|
+
export {
|
|
1000
|
+
_load_asset as loadAsset,
|
|
1001
|
+
_free_style_doc as freeStyleDoc,
|
|
1002
|
+
first_round_prompt as firstRoundPrompt,
|
|
1003
|
+
_project_context as projectContext,
|
|
1004
|
+
_date_range_ctx as dateRangeCtx,
|
|
1005
|
+
_date_range_priority as dateRangePriority,
|
|
1006
|
+
normal_prompt as normalPrompt,
|
|
1007
|
+
_stress_doc as stressDoc,
|
|
1008
|
+
stress_prompt as stressPrompt,
|
|
1009
|
+
fix_prompt as fixPrompt,
|
|
1010
|
+
retest_prompt as retestPrompt,
|
|
1011
|
+
_semantic_order as semanticOrder,
|
|
1012
|
+
retest_range_prompt as retestRangePrompt,
|
|
1013
|
+
reject_prompt as rejectPrompt,
|
|
1014
|
+
_task_payload as taskPayload,
|
|
1015
|
+
_reject_reason as rejectReason,
|
|
1016
|
+
_linked_bug as linkedBug,
|
|
1017
|
+
regression_prompt as regressionPrompt,
|
|
1018
|
+
pipeline_prompt as pipelinePrompt,
|
|
1019
|
+
next_round_prompt as nextRoundPrompt,
|
|
1020
|
+
compact_prompt as compactPrompt,
|
|
1021
|
+
};
|