@y9931666/dsh-plugin-crossfire 0.1.0 → 0.1.1
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/lib/client.js +486 -194
- package/lib/index.js +339 -165
- package/lib/{state-CXNvzj_T.js → state-B2anbn4l.js} +2 -2
- package/lib/{state-DzrgVdrB.js → state-DvF_Jqcg.js} +13 -1
- package/lib/types/client/defectTitle.d.ts +23 -0
- package/lib/types/client/locales.d.ts +1 -1
- package/lib/types/client/promptView.d.ts +63 -0
- package/lib/types/client/session-cache.d.ts +49 -0
- package/lib/types/engine.d.ts +20 -1
- package/lib/types/index.d.ts +13 -0
- package/lib/types/members.d.ts +67 -0
- package/lib/types/review.d.ts +8 -0
- package/lib/types/split.d.ts +15 -1
- package/lib/types/state.d.ts +10 -0
- package/lib/types/tools.d.ts +8 -0
- package/lib/types/types.d.ts +21 -0
- package/lib/types/version.d.ts +2 -2
- package/package.json +56 -20
- package/release-notes/v0.1.1.md +58 -0
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as newReviewId, c as reviewDirOf, d as withCrossfireLock, f as writeJsonAtomic, i as listReviewIds, l as reviewLockKey, n as appendJsonLine, o as readJsonFile, p as writeTextAtomic, s as readJsonLines, u as stateRootOf } from "./state-
|
|
1
|
+
import { a as newReviewId, c as reviewDirOf, d as withCrossfireLock, f as writeJsonAtomic, i as listReviewIds, l as reviewLockKey, m as writeTextAtomicWithBom, n as appendJsonLine, o as readJsonFile, p as writeTextAtomic, s as readJsonLines, u as stateRootOf } from "./state-DvF_Jqcg.js";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
@@ -157,6 +157,16 @@ function transcriptFile(stateRoot, reviewId) {
|
|
|
157
157
|
function exportFile(stateRoot, reviewId) {
|
|
158
158
|
return join(reviewDirOf(stateRoot, reviewId), "export.md");
|
|
159
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* `<reviewDir>/export.txt` —— 同一份内容的**人读副本**(UTF-8 带 BOM)。
|
|
162
|
+
*
|
|
163
|
+
* 为什么导出两份:`.md` 交给 Markdown 渲染器,但 Windows 上双击 `.md` 常被别的程序
|
|
164
|
+
* 抢走,加上无 BOM 的 UTF-8 会被记事本误判成 ANSI —— 于是"导出物打不开"。`.txt` 带
|
|
165
|
+
* BOM 后双击直接进记事本,中文也不乱码(用户反馈的正是这一点)。
|
|
166
|
+
*/
|
|
167
|
+
function exportTextFile(stateRoot, reviewId) {
|
|
168
|
+
return join(reviewDirOf(stateRoot, reviewId), "export.txt");
|
|
169
|
+
}
|
|
160
170
|
/** 通用:取一个非空字符串,否则返回 undefined。 */
|
|
161
171
|
function asText(value, max = 4096) {
|
|
162
172
|
if (typeof value !== "string") return void 0;
|
|
@@ -250,6 +260,8 @@ function sanitizeExpert(value) {
|
|
|
250
260
|
if (model !== void 0) expert.model = model;
|
|
251
261
|
const role = asText(raw.role, 2e3);
|
|
252
262
|
if (role !== void 0) expert.role = role;
|
|
263
|
+
const persona = asText(raw.persona, 32e3);
|
|
264
|
+
if (persona !== void 0) expert.persona = persona;
|
|
253
265
|
const note = asText(raw.note, 500);
|
|
254
266
|
if (note !== void 0) expert.note = note;
|
|
255
267
|
return expert;
|
|
@@ -1478,6 +1490,29 @@ const TAIL_SECTION = /\[(?:核心产出|下一步建议)[^\]]*\]/;
|
|
|
1478
1490
|
/** 括号里可能出现的维度引导词,去掉它们才剩下维度名。 */
|
|
1479
1491
|
const DIMENSION_LABEL_WORD = /^(维度|方面|角度|类别|类型)/;
|
|
1480
1492
|
/**
|
|
1493
|
+
* 专家报告里的「针对立场」标记(`redteam.ts` 的 `reportEnvelopeSpec` 规定每条缺陷里
|
|
1494
|
+
* 恰好有一行)。`未` 只允许出现在 `打穿` 之前 —— 见 {@link stanceMarkerStrikes}。
|
|
1495
|
+
*/
|
|
1496
|
+
const STANCE_MARKER = /针对\s*立场[^\n]{0,20}?[::]\s*(未\s*)?打穿/;
|
|
1497
|
+
/**
|
|
1498
|
+
* 判定这段文字里专家自己写的「针对立场」结论是不是**打穿**。
|
|
1499
|
+
*
|
|
1500
|
+
* ## 为什么不能只写 `includes('打穿')`(这是真实存在的坑)
|
|
1501
|
+
*
|
|
1502
|
+
* `未打穿` 的后两个字就是 `打穿`,纯子串匹配会把"没打穿"读成"打穿了",方向整个反过来。
|
|
1503
|
+
* 所以这里匹配有界的标记(`针对立场…:打穿|未打穿`)并**显式取反**:只有 `未` 缺席时
|
|
1504
|
+
* 才算打穿;标记压根不存在(含 `undefined`)也一律算没打穿。
|
|
1505
|
+
*
|
|
1506
|
+
* 之所以要有这个本地判定:专家的信封格式是**固定**的,报告正文里本来就写着结论。
|
|
1507
|
+
* 模型拆分失败退到本地启发式({@link splitByMarkers})时不该把这个信号丢掉 ——
|
|
1508
|
+
* 那正是"打穿立场"标签永远显示不出来的原因之一。
|
|
1509
|
+
*/
|
|
1510
|
+
function stanceMarkerStrikes(text$1) {
|
|
1511
|
+
if (text$1 === void 0) return false;
|
|
1512
|
+
const found = STANCE_MARKER.exec(text$1);
|
|
1513
|
+
return found !== null && found[1] === void 0;
|
|
1514
|
+
}
|
|
1515
|
+
/**
|
|
1481
1516
|
* 从段首括号的文字里提取维度,并交给 `resolveDimension` 归一化。
|
|
1482
1517
|
*
|
|
1483
1518
|
* 上一版的兜底把括号里的圆圈数字(①)直接当成了维度名,产出 `维度②`
|
|
@@ -1527,10 +1562,12 @@ function splitByMarkers(content, allowed) {
|
|
|
1527
1562
|
const text$1 = raw.replace(/^[\s*::]+/, "").replace(/\s+/g, " ").trim();
|
|
1528
1563
|
if (text$1 === "") continue;
|
|
1529
1564
|
const { dimension, fallback } = dimensionOfSegmentHead(mark.head, allowed);
|
|
1565
|
+
const strikes = stanceMarkerStrikes(mark.head) || stanceMarkerStrikes(text$1.slice(0, 160));
|
|
1530
1566
|
lines.push({
|
|
1531
1567
|
content: `观点 ${mark.label}(${mark.head}):${text$1}`,
|
|
1532
1568
|
dimension,
|
|
1533
1569
|
...fallback ? { dimensionFallback: true } : {},
|
|
1570
|
+
...strikes ? { strikesStance: true } : {},
|
|
1534
1571
|
quote: void 0
|
|
1535
1572
|
});
|
|
1536
1573
|
}
|
|
@@ -1541,10 +1578,10 @@ function splitByMarkers(content, allowed) {
|
|
|
1541
1578
|
* quote 与发言原文做规范化子串校验(防线③),不匹配即丢弃。
|
|
1542
1579
|
* dimension 一律过 `resolveDimension`(落库值必须 ∈ allowed)。
|
|
1543
1580
|
*/
|
|
1544
|
-
async function splitUtterance(llm, config, expertKey, content, allowed) {
|
|
1581
|
+
async function splitUtterance(llm, config, expertKey, content, allowed, stance) {
|
|
1545
1582
|
const trimmed = content.replace(/\s+/g, " ").trim();
|
|
1546
1583
|
if (trimmed === "") return null;
|
|
1547
|
-
const raw = await callSplitLlm(llm, config, "你是 Crossfire「针锋」评审的缺陷拆分器。把下面一条红队专家发言拆成 1~3 条独立缺陷,每条缺陷只包含一个问题,不得发明原文没有的内容。dimension 必须**逐字取自给定清单 allowed 中的一项**,不得自创标签、不得输出编号;若该缺陷用「立场」中的某条理由打穿了用户的说法,把 strikes_stance 设为 true
|
|
1584
|
+
const raw = await callSplitLlm(llm, config, "你是 Crossfire「针锋」评审的缺陷拆分器。把下面一条红队专家发言拆成 1~3 条独立缺陷,每条缺陷只包含一个问题,不得发明原文没有的内容。dimension 必须**逐字取自给定清单 allowed 中的一项**,不得自创标签、不得输出编号;若该缺陷用「立场」中的某条理由打穿了用户的说法,把 strikes_stance 设为 true;**用户没有写立场时(下文明确标注「未填写立场」)一律不得把任何一条标成打穿**。对每条缺陷给出证据分级(C1):代码/bug 类缺陷的 evidence.kind=repro 并写可复现步骤(1. 2. 3.);设计类缺陷的 evidence.kind=argument 并写论证链——禁止为设计类缺陷编造\"运行某命令即可复现\"这类伪复现步骤。无法从原文提取证据时省略 evidence 字段。必须调用 split_crossfire_defect 工具返回结构化结果。", `本次评审采用的维度清单 allowed(dimension 只能取其中之一):${allowed.length > 0 ? allowed.join(" / ") : "(未限定,按通用维度判断)"}\n\n用户在本次评审里写下的「立场与理由」:\n${stance.trim() === "" ? "(未填写立场)用户这次**没有**写下立场与理由。因此不得把任何一条缺陷标成 strikes_stance=true(没有靶子就没有打穿)。" : stance.trim()}\n\n专家 ${expertKey} 的发言:\n${trimmed}`);
|
|
1548
1585
|
if (raw !== null) {
|
|
1549
1586
|
let parsed;
|
|
1550
1587
|
try {
|
|
@@ -1564,6 +1601,7 @@ async function splitUtterance(llm, config, expertKey, content, allowed) {
|
|
|
1564
1601
|
const candidate = obj.quote.replace(/\s+/g, " ").trim();
|
|
1565
1602
|
if (candidate !== "" && candidate.length <= 120 && trimmed.includes(candidate)) quote$1 = candidate;
|
|
1566
1603
|
}
|
|
1604
|
+
const strikes = obj?.strikes_stance === true || stanceMarkerStrikes(line) || stanceMarkerStrikes(quote$1);
|
|
1567
1605
|
const resolved = resolveDimension(obj?.dimension, allowed);
|
|
1568
1606
|
let evidence;
|
|
1569
1607
|
const ev = obj?.evidence;
|
|
@@ -1578,7 +1616,7 @@ async function splitUtterance(llm, config, expertKey, content, allowed) {
|
|
|
1578
1616
|
content: line,
|
|
1579
1617
|
quote: quote$1,
|
|
1580
1618
|
dimension: resolved.dimension,
|
|
1581
|
-
...
|
|
1619
|
+
...strikes ? { strikesStance: true } : {},
|
|
1582
1620
|
...resolved.fallback ? { dimensionFallback: true } : {},
|
|
1583
1621
|
evidence
|
|
1584
1622
|
});
|
|
@@ -1624,10 +1662,8 @@ function planExperts(record, maxExperts, roster = []) {
|
|
|
1624
1662
|
};
|
|
1625
1663
|
if (matched !== void 0) {
|
|
1626
1664
|
if (matched.role !== void 0 && matched.role.trim() !== "") expert.role = matched.role.trim();
|
|
1627
|
-
if (matched.provider !== void 0 && matched.
|
|
1628
|
-
|
|
1629
|
-
expert.model = matched.model;
|
|
1630
|
-
}
|
|
1665
|
+
if (matched.provider !== void 0 && matched.provider.trim() !== "") expert.provider = matched.provider.trim();
|
|
1666
|
+
if (matched.model !== void 0 && matched.model.trim() !== "") expert.model = matched.model.trim();
|
|
1631
1667
|
}
|
|
1632
1668
|
experts.push(expert);
|
|
1633
1669
|
}
|
|
@@ -1713,7 +1749,7 @@ async function collectDefects(input) {
|
|
|
1713
1749
|
};
|
|
1714
1750
|
try {
|
|
1715
1751
|
return {
|
|
1716
|
-
lines: await splitUtterance(activeLlm, input.splitConfig, item.utterance.expertKey, item.content, allowed),
|
|
1752
|
+
lines: await splitUtterance(activeLlm, input.splitConfig, item.utterance.expertKey, item.content, allowed, record.stance.text),
|
|
1717
1753
|
usedLlm: true
|
|
1718
1754
|
};
|
|
1719
1755
|
} catch {
|
|
@@ -1785,10 +1821,37 @@ function rosterFrom(input) {
|
|
|
1785
1821
|
}
|
|
1786
1822
|
/**
|
|
1787
1823
|
* 把收集结果并入记录(原地修改;调用方负责落盘)。
|
|
1824
|
+
*
|
|
1825
|
+
* ## 幂等(A2)
|
|
1826
|
+
*
|
|
1827
|
+
* 此前是纯拼接(`[...record.defects, ...added]`),于是**同一条发言被收集两次**
|
|
1828
|
+
* 就在裁决面上出现两条一模一样的缺陷:重试、双击、两个并发请求都能触发它。
|
|
1829
|
+
* 用户面对的是一个"必须点两次驳回、理由还得写两遍"的列表。
|
|
1830
|
+
*
|
|
1831
|
+
* 现在按 `defect.id` 去重(`id` 由 `collectDefects` 从 `utteranceId` + 序号生成,
|
|
1832
|
+
* 因此同一批输入两次得到同一批 id):**保留先到的那一条**,后来的同 id 项直接丢弃。
|
|
1833
|
+
* 保留先到的而不是新的,是因为"用户已经开始表态"的那一条不能被后来的副本顶掉
|
|
1834
|
+
* (缺陷的三态与驳回理由挂在 id 上,换掉实例等于丢掉已做出的判断)。
|
|
1835
|
+
*
|
|
1836
|
+
* `collectDefects` 里的 `utteranceId` 幂等键是第一道闸,这里的是第二道:前者防的是
|
|
1837
|
+
* "转录又被读了一遍",后者防的是"合并写回被施加了同一批结果" —— 两道都需要,
|
|
1838
|
+
* 因为并发请求可以在第一道闸之后、第二道闸之前交错。
|
|
1839
|
+
*
|
|
1840
|
+
* @returns **实际追加的条数**(被去重丢弃的不算)。调用方必须如实上报这个数字,
|
|
1841
|
+
* 而不是 `added.length` —— 那会把"请求成功了但什么都没新增"报成
|
|
1842
|
+
* "新增了 N 条",用户看到条数却对不上列表。
|
|
1788
1843
|
*/
|
|
1789
1844
|
function applyCollected(record, added) {
|
|
1790
|
-
|
|
1845
|
+
const known = new Set(record.defects.map((defect) => defect.id));
|
|
1846
|
+
const fresh = [];
|
|
1847
|
+
for (const defect of added) {
|
|
1848
|
+
if (known.has(defect.id)) continue;
|
|
1849
|
+
known.add(defect.id);
|
|
1850
|
+
fresh.push(defect);
|
|
1851
|
+
}
|
|
1852
|
+
record.defects = [...record.defects, ...fresh];
|
|
1791
1853
|
for (const expert of record.experts) expert.defectCount = record.defects.filter((defect) => defect.expertKey === expert.key).length;
|
|
1854
|
+
return fresh.length;
|
|
1792
1855
|
}
|
|
1793
1856
|
|
|
1794
1857
|
//#endregion
|
|
@@ -2042,6 +2105,64 @@ function expertWelcome(input) {
|
|
|
2042
2105
|
//#region src/members.ts
|
|
2043
2106
|
/** 专家子代理的 label 前缀(持久化为该子会话的创建标签,便于在宿主侧辨认)。 */
|
|
2044
2107
|
const EXPERT_LABEL_PREFIX = "crossfire-expert:";
|
|
2108
|
+
/** 取一个"真的写了东西"的字符串;空串与纯空白一律视为**缺席**。 */
|
|
2109
|
+
function nonEmpty(value) {
|
|
2110
|
+
if (value === void 0) return void 0;
|
|
2111
|
+
const trimmed = value.trim();
|
|
2112
|
+
return trimmed === "" ? void 0 : trimmed;
|
|
2113
|
+
}
|
|
2114
|
+
/**
|
|
2115
|
+
* 把"专家显式指定的路由"与"宿主默认路由"收敛成一份确定的路由决定(A5 + B3)。
|
|
2116
|
+
*
|
|
2117
|
+
* ## 一条铁律
|
|
2118
|
+
*
|
|
2119
|
+
* **写回记录的值 = 真正下发的值。** 记录与下发必须由**同一次决定**产生,否则界面
|
|
2120
|
+
* 会显示一条并非事实的路由(红队抓出的原缺陷:只填了 provider 的成员,记录里却被
|
|
2121
|
+
* 写成 `defaultProvider` + `defaultModel` —— 界面说跑 A,实际跑 B)。
|
|
2122
|
+
*
|
|
2123
|
+
* ## 四种组合的行为(全都是确定的,没有"看情况")
|
|
2124
|
+
*
|
|
2125
|
+
* | 专家显式指定 | agentOptions(下发) | 写回记录 |
|
|
2126
|
+
* |---|---|---|
|
|
2127
|
+
* | provider + model | `{provider, model}` | provider、model |
|
|
2128
|
+
* | 只有 provider | `{provider}` | provider(**不补 model**) |
|
|
2129
|
+
* | 只有 model | `{defaultProvider?, model}` | provider(补上的那个)、model |
|
|
2130
|
+
* | 都没有(继承) | `{}`(不干预) | defaultProvider、defaultModel |
|
|
2131
|
+
*
|
|
2132
|
+
* 逐条理由:
|
|
2133
|
+
* - **只有 provider 时刻意不补 model**:`provider` 自己会选它的默认模型,我们没有
|
|
2134
|
+
* 任何依据知道那会是哪一个;拿 `config.defaultModel` 去顶替就是编造(那正是原
|
|
2135
|
+
* 缺陷的写法)。记录的 `model` 缺席是诚实的"不知道"。
|
|
2136
|
+
* - **只有 model 时补 `defaultProvider`**:一个不给 provider 的 model id 在多数路由上
|
|
2137
|
+
* 无法解析,而"只写 model、不写 provider"在用户眼里就是"用默认 provider 跑这个
|
|
2138
|
+
* 模型"。补上的值同时写回记录 —— 于是界面显示的正是真正下发的。
|
|
2139
|
+
* - **都没有时 agentOptions 为空**:这一种情形下我们**不替宿主做决定**,真正的继承
|
|
2140
|
+
* 必须保持是继承。记录里那份 `defaultProvider`/`defaultModel` 是配置里声明的
|
|
2141
|
+
* "继承主持人时**实际生效**的模型路由"(见 index.ts / ToolsConfig 的同一段注释),
|
|
2142
|
+
* 写它只为把"继承"画成真实品牌块(否则头像永远是中性灰 `?`,那是用户报过的缺陷)。
|
|
2143
|
+
* 这是**唯一**一处记录值不等于 agentOptions 的字段 —— 而它们仍然同源:记录里出现的
|
|
2144
|
+
* 每一个字段,要么是我们显式下发的,要么是宿主默认路由本身。
|
|
2145
|
+
* - **取不到生效值的字段一律不写**(保持"缺失 = 继承"的诚实语义),也不写空串。
|
|
2146
|
+
*
|
|
2147
|
+
* 纯函数:没有 I/O、没有时间、不改入参,所以四种组合都能被单测直接钉住
|
|
2148
|
+
* (见 `test/members.test.mjs`)。
|
|
2149
|
+
*/
|
|
2150
|
+
function resolveExpertRoute(expert, config) {
|
|
2151
|
+
const explicitProvider = nonEmpty(expert.provider);
|
|
2152
|
+
const explicitModel = nonEmpty(expert.model);
|
|
2153
|
+
const fallbackProvider = nonEmpty(config.defaultProvider);
|
|
2154
|
+
const fallbackModel = nonEmpty(config.defaultModel);
|
|
2155
|
+
const agentOptions = {};
|
|
2156
|
+
if (explicitProvider !== void 0) agentOptions.provider = explicitProvider;
|
|
2157
|
+
else if (explicitModel !== void 0 && fallbackProvider !== void 0) agentOptions.provider = fallbackProvider;
|
|
2158
|
+
if (explicitModel !== void 0) agentOptions.model = explicitModel;
|
|
2159
|
+
const route = { agentOptions };
|
|
2160
|
+
if (agentOptions.provider !== void 0) route.provider = agentOptions.provider;
|
|
2161
|
+
else if (explicitProvider === void 0 && explicitModel === void 0 && fallbackProvider !== void 0) route.provider = fallbackProvider;
|
|
2162
|
+
if (agentOptions.model !== void 0) route.model = agentOptions.model;
|
|
2163
|
+
else if (explicitProvider === void 0 && explicitModel === void 0 && fallbackModel !== void 0) route.model = fallbackModel;
|
|
2164
|
+
return route;
|
|
2165
|
+
}
|
|
2045
2166
|
/**
|
|
2046
2167
|
* 拉起一位红队专家,返回它的子会话 id。
|
|
2047
2168
|
*
|
|
@@ -2064,12 +2185,14 @@ async function spawnExpert(ctx, config, record, expert, parent, signal, limits =
|
|
|
2064
2185
|
maxOpinions: limits.maxOpinions ?? 0,
|
|
2065
2186
|
...expert.role === void 0 ? {} : { role: expert.role }
|
|
2066
2187
|
};
|
|
2067
|
-
const
|
|
2068
|
-
|
|
2069
|
-
agentOptions.provider = expert.provider;
|
|
2070
|
-
agentOptions.model = expert.model;
|
|
2071
|
-
}
|
|
2188
|
+
const route = resolveExpertRoute(expert, config);
|
|
2189
|
+
const agentOptions = { ...route.agentOptions };
|
|
2072
2190
|
if (limits.maxTokens !== void 0 && limits.maxTokens > 0) agentOptions.maxTokens = limits.maxTokens;
|
|
2191
|
+
if (route.provider === void 0) delete expert.provider;
|
|
2192
|
+
else expert.provider = route.provider;
|
|
2193
|
+
if (route.model === void 0) delete expert.model;
|
|
2194
|
+
else expert.model = route.model;
|
|
2195
|
+
const persona = expertPersona(input);
|
|
2073
2196
|
const started = await ctx.subagents.startContinuable({
|
|
2074
2197
|
provider: config.provider,
|
|
2075
2198
|
label: `${EXPERT_LABEL_PREFIX}${record.reviewId}:${expert.key}`,
|
|
@@ -2079,13 +2202,14 @@ async function spawnExpert(ctx, config, record, expert, parent, signal, limits =
|
|
|
2079
2202
|
text: expertWelcome(input)
|
|
2080
2203
|
}],
|
|
2081
2204
|
parent,
|
|
2082
|
-
persona
|
|
2205
|
+
persona,
|
|
2083
2206
|
toolFilter: expertToolRestriction(),
|
|
2084
2207
|
...Object.keys(agentOptions).length > 0 ? { agentOptions } : {},
|
|
2085
2208
|
...config.maxDepth === void 0 ? {} : { maxDepth: config.maxDepth }
|
|
2086
2209
|
},
|
|
2087
2210
|
signal
|
|
2088
2211
|
});
|
|
2212
|
+
expert.persona = persona;
|
|
2089
2213
|
return String(started.childId);
|
|
2090
2214
|
}
|
|
2091
2215
|
|
|
@@ -2102,7 +2226,7 @@ async function spawnExpert(ctx, config, record, expert, parent, signal, limits =
|
|
|
2102
2226
|
* @module dsh-plugin-crossfire/version
|
|
2103
2227
|
*/
|
|
2104
2228
|
/** 当前插件版本(必须与 package.json 的 version 逐字一致)。 */
|
|
2105
|
-
const PLUGIN_VERSION = "0.1.
|
|
2229
|
+
const PLUGIN_VERSION = "0.1.1";
|
|
2106
2230
|
/** 导出物头部使用的插件标识行。 */
|
|
2107
2231
|
const PLUGIN_ID = `@y9931666/dsh-plugin-crossfire v${PLUGIN_VERSION}`;
|
|
2108
2232
|
/** DeepSeek Harness 版本区间(导出头部展示用)。
|
|
@@ -2594,6 +2718,35 @@ function registerRpc(ctx, runtime) {
|
|
|
2594
2718
|
status
|
|
2595
2719
|
});
|
|
2596
2720
|
}
|
|
2721
|
+
case "crossfire/expert.prompt": {
|
|
2722
|
+
const expertKey = typeof body.expertKey === "string" ? body.expertKey : "";
|
|
2723
|
+
if (expertKey === "") return fail("payload must be { reviewId, expertKey }");
|
|
2724
|
+
const located = await load(reviewId, session, preferredRoot);
|
|
2725
|
+
const expert = located.record.experts.find((item) => item.key === expertKey);
|
|
2726
|
+
if (expert === void 0) return fail(`no expert "${expertKey}" in this review`);
|
|
2727
|
+
if (expert.persona !== void 0 && expert.persona !== "") return ok({
|
|
2728
|
+
expertKey,
|
|
2729
|
+
dimension: expert.dimension,
|
|
2730
|
+
persona: expert.persona,
|
|
2731
|
+
rebuilt: false
|
|
2732
|
+
});
|
|
2733
|
+
const persona = expertPersona({
|
|
2734
|
+
reviewId: located.record.reviewId,
|
|
2735
|
+
dimension: expert.dimension,
|
|
2736
|
+
title: located.record.subject.title,
|
|
2737
|
+
subject: located.record.subject.content,
|
|
2738
|
+
stance: located.record.stance.text,
|
|
2739
|
+
dimensions: located.record.dimensions,
|
|
2740
|
+
maxOpinions: runtime.tools.getDefaults().expertMaxOpinions,
|
|
2741
|
+
...expert.role === void 0 ? {} : { role: expert.role }
|
|
2742
|
+
});
|
|
2743
|
+
return ok({
|
|
2744
|
+
expertKey,
|
|
2745
|
+
dimension: expert.dimension,
|
|
2746
|
+
persona,
|
|
2747
|
+
rebuilt: true
|
|
2748
|
+
});
|
|
2749
|
+
}
|
|
2597
2750
|
case "crossfire/expert.setRole": {
|
|
2598
2751
|
const expertKey = typeof body.expertKey === "string" ? body.expertKey : "";
|
|
2599
2752
|
if (expertKey === "") return fail("payload must be { reviewId, expertKey, role }");
|
|
@@ -2654,8 +2807,16 @@ function registerRpc(ctx, runtime) {
|
|
|
2654
2807
|
const markdown = renderReviewMarkdown(located.record, countDefects(located.record));
|
|
2655
2808
|
const path = exportFile(located.stateRoot, reviewId);
|
|
2656
2809
|
await writeTextAtomic(path, markdown);
|
|
2810
|
+
let textPath;
|
|
2811
|
+
try {
|
|
2812
|
+
textPath = exportTextFile(located.stateRoot, reviewId);
|
|
2813
|
+
await writeTextAtomicWithBom(textPath, markdown);
|
|
2814
|
+
} catch {
|
|
2815
|
+
textPath = "";
|
|
2816
|
+
}
|
|
2657
2817
|
return ok({
|
|
2658
2818
|
path,
|
|
2819
|
+
textPath,
|
|
2659
2820
|
bytes: Buffer.byteLength(markdown, "utf8"),
|
|
2660
2821
|
markdown
|
|
2661
2822
|
});
|
|
@@ -2685,13 +2846,14 @@ async function advance(ctx, runtime, located, step, body, session) {
|
|
|
2685
2846
|
const reviewId = located.record.reviewId;
|
|
2686
2847
|
const defaults = runtime.tools.getDefaults();
|
|
2687
2848
|
if (step === "attack") {
|
|
2688
|
-
if (await readRecord(stateRoot, reviewId) === void 0) return fail(`no review "${reviewId}"`);
|
|
2689
2849
|
const agent = agentOfSession(ctx, session);
|
|
2690
2850
|
if (agent === void 0) return fail("the session that owns this review is not live right now, so Crossfire cannot send in the red team from the card — ask in the conversation instead (crossfire_attack)", "session-offline");
|
|
2691
|
-
|
|
2851
|
+
return await withCrossfireLock(reviewLockKey(stateRoot, reviewId), async () => {
|
|
2692
2852
|
const current = await readRecord(stateRoot, reviewId);
|
|
2693
|
-
if (current === void 0)
|
|
2694
|
-
if (current.status !== "draft" && current.status !== "attacking")
|
|
2853
|
+
if (current === void 0) return fail(`no review "${reviewId}"`);
|
|
2854
|
+
if (current.status !== "draft" && current.status !== "attacking") return fail(`review "${reviewId}" is "${current.status}" — cannot send in the red team now (call crossfire_recheck to open a new pass on a settled review)`);
|
|
2855
|
+
const running = current.experts.filter((expert) => expert.status === "running");
|
|
2856
|
+
if (running.length > 0) return fail(`experts are already running for this review (${running.map((expert) => expert.key).join(", ")}) — wait for them to report, or collect what they have already submitted`);
|
|
2695
2857
|
const prefs = runtime.prefs.get();
|
|
2696
2858
|
const roster = rosterFrom({
|
|
2697
2859
|
squads: prefs.squads.map((squad) => ({
|
|
@@ -2715,99 +2877,101 @@ async function advance(ctx, runtime, located, step, body, session) {
|
|
|
2715
2877
|
current.experts = experts;
|
|
2716
2878
|
current.status = "attacking";
|
|
2717
2879
|
await writeRecord(stateRoot, current);
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
expert.note = error instanceof Error ? error.message : String(error);
|
|
2743
|
-
failures.push(`${expert.key}: ${expert.note}`);
|
|
2880
|
+
const failures = [];
|
|
2881
|
+
for (const expert of experts) {
|
|
2882
|
+
const controller = new AbortController();
|
|
2883
|
+
try {
|
|
2884
|
+
const childId = await spawnExpert(ctx, {
|
|
2885
|
+
provider: runtime.tools.memberProvider,
|
|
2886
|
+
...runtime.tools.memberMaxDepth === void 0 ? {} : { maxDepth: runtime.tools.memberMaxDepth },
|
|
2887
|
+
...runtime.tools.defaultProvider === void 0 ? {} : { defaultProvider: runtime.tools.defaultProvider },
|
|
2888
|
+
...runtime.tools.defaultModel === void 0 ? {} : { defaultModel: runtime.tools.defaultModel }
|
|
2889
|
+
}, current, expert, agent, controller.signal, {
|
|
2890
|
+
maxTokens: defaults.expertMaxTokens,
|
|
2891
|
+
maxOpinions: defaults.expertMaxOpinions
|
|
2892
|
+
});
|
|
2893
|
+
expert.childId = childId;
|
|
2894
|
+
expert.status = "running";
|
|
2895
|
+
registerExpertChild(childId, {
|
|
2896
|
+
stateRoot,
|
|
2897
|
+
reviewId
|
|
2898
|
+
});
|
|
2899
|
+
} catch (error) {
|
|
2900
|
+
expert.status = "failed";
|
|
2901
|
+
expert.note = error instanceof Error ? error.message : String(error);
|
|
2902
|
+
failures.push(`${expert.key}: ${expert.note}`);
|
|
2903
|
+
}
|
|
2744
2904
|
}
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
failures
|
|
2905
|
+
current.experts = experts;
|
|
2906
|
+
await writeRecord(stateRoot, current);
|
|
2907
|
+
return ok({
|
|
2908
|
+
spawned: experts.filter((expert) => expert.status === "running").length,
|
|
2909
|
+
failed: failures.length,
|
|
2910
|
+
failures
|
|
2911
|
+
});
|
|
2753
2912
|
});
|
|
2754
2913
|
}
|
|
2755
2914
|
if (step === "collect") {
|
|
2756
|
-
const record$1 = await readRecord(stateRoot, reviewId);
|
|
2757
|
-
if (record$1 === void 0) return fail(`no review "${reviewId}"`);
|
|
2758
|
-
if (record$1.status !== "attacking" && record$1.status !== "judging") return fail(`review "${reviewId}" is "${record$1.status}" — cannot collect now`);
|
|
2759
|
-
const transcript = await readTranscriptReport(stateRoot, reviewId);
|
|
2760
2915
|
const llm = ctx.get("llm");
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2916
|
+
return await withCrossfireLock(reviewLockKey(stateRoot, reviewId), async () => {
|
|
2917
|
+
const current = await readRecord(stateRoot, reviewId);
|
|
2918
|
+
if (current === void 0) return fail(`no review "${reviewId}"`);
|
|
2919
|
+
if (current.status !== "attacking" && current.status !== "judging") return fail(`review "${reviewId}" is "${current.status}" — cannot collect now`);
|
|
2920
|
+
const transcript = await readTranscriptReport(stateRoot, reviewId);
|
|
2921
|
+
const outcome = await collectDefects({
|
|
2922
|
+
record: current,
|
|
2923
|
+
transcript: transcript.items,
|
|
2924
|
+
malformed: transcript.malformed,
|
|
2925
|
+
llm,
|
|
2926
|
+
splitConfig: runtime.tools.reviewSplit
|
|
2927
|
+
});
|
|
2928
|
+
const appended = applyCollected(current, outcome.added);
|
|
2770
2929
|
current.status = "judging";
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2930
|
+
await writeRecord(stateRoot, current);
|
|
2931
|
+
return ok({
|
|
2932
|
+
collected: appended,
|
|
2933
|
+
droppedDuplicates: outcome.droppedDuplicates,
|
|
2934
|
+
skippedNoise: outcome.skippedNoise,
|
|
2935
|
+
splitAttempts: outcome.splitAttempts,
|
|
2936
|
+
recheckRound: current.reviewPass > 1 && current.history.length > 0
|
|
2937
|
+
});
|
|
2778
2938
|
});
|
|
2779
2939
|
}
|
|
2780
2940
|
if (step === "finish") {
|
|
2781
2941
|
const summary = typeof body.summary === "string" ? body.summary.trim() : "";
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2942
|
+
return await withCrossfireLock(reviewLockKey(stateRoot, reviewId), async () => {
|
|
2943
|
+
const current = await readRecord(stateRoot, reviewId);
|
|
2944
|
+
if (current === void 0) return fail(`no review "${reviewId}"`);
|
|
2945
|
+
if (current.status !== "judging" && current.status !== "attacking" && current.status !== "settled") return fail(`review "${reviewId}" is "${current.status}" — cannot settle now`);
|
|
2946
|
+
const snapshot = {
|
|
2947
|
+
pass: current.reviewPass,
|
|
2948
|
+
subject: current.subject.content,
|
|
2949
|
+
stanceText: current.stance.text,
|
|
2950
|
+
dimensions: [...current.dimensions],
|
|
2951
|
+
intensity: current.intensity,
|
|
2952
|
+
defects: current.defects.map((defect) => ({ ...defect })),
|
|
2953
|
+
finishedAt: Date.now(),
|
|
2954
|
+
...summary === "" ? {} : { revisedSummary: summary }
|
|
2955
|
+
};
|
|
2956
|
+
const existing = current.history.findIndex((item) => item.pass === current.reviewPass);
|
|
2957
|
+
if (existing >= 0) current.history[existing] = snapshot;
|
|
2958
|
+
else current.history = [...current.history, snapshot];
|
|
2959
|
+
current.status = "settled";
|
|
2960
|
+
current.settledAt = Date.now();
|
|
2961
|
+
await writeRecord(stateRoot, current);
|
|
2962
|
+
return ok({
|
|
2963
|
+
settled: true,
|
|
2964
|
+
counts: countDefects(current)
|
|
2965
|
+
});
|
|
2802
2966
|
});
|
|
2803
2967
|
}
|
|
2804
|
-
const record = await readRecord(stateRoot, reviewId);
|
|
2805
|
-
if (record === void 0) return fail(`no review "${reviewId}"`);
|
|
2806
|
-
if (record.status !== "settled") return fail(`review "${reviewId}" is "${record.status}" — only a settled review can be re-checked`);
|
|
2807
2968
|
const approved = body.approved === true;
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2969
|
+
return await withCrossfireLock(reviewLockKey(stateRoot, reviewId), async () => {
|
|
2970
|
+
const current = await readRecord(stateRoot, reviewId);
|
|
2971
|
+
if (current === void 0) return fail(`no review "${reviewId}"`);
|
|
2972
|
+
if (current.status !== "settled") return fail(`review "${reviewId}" is "${current.status}" — only a settled review can be re-checked`);
|
|
2973
|
+
const nextPass = current.reviewPass + 1;
|
|
2974
|
+
if (nextPass > current.maxReviewPass && !approved) return fail(`re-review cap reached (pass ${nextPass} > max ${current.maxReviewPass}) — the user must approve continuing`, "cap-reached");
|
|
2811
2975
|
const items = buildRecheckItems(current);
|
|
2812
2976
|
current.reviewPass = beginPass(current);
|
|
2813
2977
|
if (approved) current.maxReviewPass = Math.max(current.maxReviewPass, current.reviewPass);
|
|
@@ -2815,14 +2979,11 @@ async function advance(ctx, runtime, located, step, body, session) {
|
|
|
2815
2979
|
current.experts = [];
|
|
2816
2980
|
current.status = "attacking";
|
|
2817
2981
|
current.settledAt = void 0;
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
return ok({
|
|
2824
|
-
reviewPass: result?.pass ?? nextPass,
|
|
2825
|
-
recheckItems: result?.items ?? 0
|
|
2982
|
+
await writeRecord(stateRoot, current);
|
|
2983
|
+
return ok({
|
|
2984
|
+
reviewPass: current.reviewPass,
|
|
2985
|
+
recheckItems: items.length
|
|
2986
|
+
});
|
|
2826
2987
|
});
|
|
2827
2988
|
}
|
|
2828
2989
|
/**
|
|
@@ -2918,7 +3079,9 @@ function cloneDefect(defect) {
|
|
|
2918
3079
|
function registerCrossfireTools(ctx, config) {
|
|
2919
3080
|
const memberConfig = {
|
|
2920
3081
|
provider: config.memberProvider,
|
|
2921
|
-
...config.memberMaxDepth === void 0 ? {} : { maxDepth: config.memberMaxDepth }
|
|
3082
|
+
...config.memberMaxDepth === void 0 ? {} : { maxDepth: config.memberMaxDepth },
|
|
3083
|
+
...config.defaultProvider === void 0 ? {} : { defaultProvider: config.defaultProvider },
|
|
3084
|
+
...config.defaultModel === void 0 ? {} : { defaultModel: config.defaultModel }
|
|
2922
3085
|
};
|
|
2923
3086
|
ctx.tools.register(defineTool({
|
|
2924
3087
|
name: "crossfire_open",
|
|
@@ -3097,6 +3260,8 @@ function registerCrossfireTools(ctx, config) {
|
|
|
3097
3260
|
requireOwner(record, caller.id);
|
|
3098
3261
|
assertBudget(record);
|
|
3099
3262
|
requireStatus(record, ["draft", "attacking"]);
|
|
3263
|
+
const running = record.experts.filter((expert) => expert.status === "running");
|
|
3264
|
+
if (running.length > 0) throw new Error(`experts are already running for review "${reviewId}" (${running.map((expert) => expert.key).join(", ")}) — wait for them to report, or collect what they have already submitted`);
|
|
3100
3265
|
const spec = INTENSITY_SPECS[record.intensity];
|
|
3101
3266
|
const experts = [];
|
|
3102
3267
|
for (const dimension of record.dimensions) {
|
|
@@ -3113,40 +3278,40 @@ function registerCrossfireTools(ctx, config) {
|
|
|
3113
3278
|
record.experts = trimmed;
|
|
3114
3279
|
record.status = "attacking";
|
|
3115
3280
|
await writeRecord(stateRoot, record);
|
|
3281
|
+
const failures = [];
|
|
3282
|
+
for (const expert of trimmed) {
|
|
3283
|
+
const controller = new AbortController();
|
|
3284
|
+
try {
|
|
3285
|
+
const childId = await spawnExpert(ctx, memberConfig, record, expert, caller, controller.signal, {
|
|
3286
|
+
maxTokens: defaults.expertMaxTokens,
|
|
3287
|
+
maxOpinions: defaults.expertMaxOpinions
|
|
3288
|
+
});
|
|
3289
|
+
expert.childId = childId;
|
|
3290
|
+
expert.status = "running";
|
|
3291
|
+
registerExpertChild(childId, {
|
|
3292
|
+
stateRoot,
|
|
3293
|
+
reviewId
|
|
3294
|
+
});
|
|
3295
|
+
} catch (error) {
|
|
3296
|
+
expert.status = "failed";
|
|
3297
|
+
expert.note = error instanceof Error ? error.message : String(error);
|
|
3298
|
+
failures.push(`${expert.key}: ${expert.note}`);
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3301
|
+
record.experts = trimmed;
|
|
3302
|
+
await writeRecord(stateRoot, record);
|
|
3116
3303
|
return {
|
|
3117
3304
|
record,
|
|
3118
|
-
experts: trimmed
|
|
3305
|
+
experts: trimmed,
|
|
3306
|
+
failures
|
|
3119
3307
|
};
|
|
3120
3308
|
});
|
|
3121
|
-
const failures = [];
|
|
3122
|
-
for (const expert of planned.experts) {
|
|
3123
|
-
const controller = new AbortController();
|
|
3124
|
-
try {
|
|
3125
|
-
const childId = await spawnExpert(ctx, memberConfig, planned.record, expert, caller, controller.signal, {
|
|
3126
|
-
maxTokens: defaults.expertMaxTokens,
|
|
3127
|
-
maxOpinions: defaults.expertMaxOpinions
|
|
3128
|
-
});
|
|
3129
|
-
expert.childId = childId;
|
|
3130
|
-
expert.status = "running";
|
|
3131
|
-
registerExpertChild(childId, {
|
|
3132
|
-
stateRoot,
|
|
3133
|
-
reviewId
|
|
3134
|
-
});
|
|
3135
|
-
} catch (error) {
|
|
3136
|
-
expert.status = "failed";
|
|
3137
|
-
expert.note = error instanceof Error ? error.message : String(error);
|
|
3138
|
-
failures.push(`${expert.key}: ${expert.note}`);
|
|
3139
|
-
}
|
|
3140
|
-
}
|
|
3141
|
-
await mutateRecord(stateRoot, reviewId, (record) => {
|
|
3142
|
-
record.experts = planned.experts;
|
|
3143
|
-
});
|
|
3144
3309
|
return {
|
|
3145
3310
|
spawned: planned.experts.filter((expert) => expert.status === "running").length,
|
|
3146
|
-
failed: failures.length,
|
|
3311
|
+
failed: planned.failures.length,
|
|
3147
3312
|
dimensions: planned.record.dimensions,
|
|
3148
3313
|
review_pass: planned.record.reviewPass,
|
|
3149
|
-
failures
|
|
3314
|
+
failures: planned.failures
|
|
3150
3315
|
};
|
|
3151
3316
|
}
|
|
3152
3317
|
}));
|
|
@@ -3207,36 +3372,33 @@ function registerCrossfireTools(ctx, config) {
|
|
|
3207
3372
|
const stateRoot = stateRootOf(workspaceOf(caller), config.stateDir);
|
|
3208
3373
|
const reviewId = text(args.review_id);
|
|
3209
3374
|
if (reviewId === "") throw new Error("review_id must not be empty");
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
applyCollected(
|
|
3224
|
-
|
|
3375
|
+
return await withCrossfireLock(reviewLockKey(stateRoot, reviewId), async () => {
|
|
3376
|
+
const record = await requireRecord(stateRoot, reviewId);
|
|
3377
|
+
requireOwner(record, caller.id);
|
|
3378
|
+
requireStatus(record, ["attacking", "judging"]);
|
|
3379
|
+
const transcript = await readTranscriptReport(stateRoot, reviewId);
|
|
3380
|
+
const llm = ctx.get("llm");
|
|
3381
|
+
const outcome = await collectDefects({
|
|
3382
|
+
record,
|
|
3383
|
+
transcript: transcript.items,
|
|
3384
|
+
malformed: transcript.malformed,
|
|
3385
|
+
llm,
|
|
3386
|
+
splitConfig: config.reviewSplit
|
|
3387
|
+
});
|
|
3388
|
+
const appended = applyCollected(record, outcome.added);
|
|
3389
|
+
record.status = "judging";
|
|
3390
|
+
await writeRecord(stateRoot, record);
|
|
3225
3391
|
return {
|
|
3226
|
-
|
|
3227
|
-
|
|
3392
|
+
collected: appended,
|
|
3393
|
+
dropped_duplicates: outcome.droppedDuplicates,
|
|
3394
|
+
skipped_noise: outcome.skippedNoise,
|
|
3395
|
+
split_attempts: outcome.splitAttempts,
|
|
3396
|
+
malformed_lines: outcome.malformedLines,
|
|
3397
|
+
total_defects: record.defects.length,
|
|
3398
|
+
status: record.status,
|
|
3399
|
+
recheck_round: record.reviewPass > 1 && record.history.length > 0
|
|
3228
3400
|
};
|
|
3229
3401
|
});
|
|
3230
|
-
return {
|
|
3231
|
-
collected: outcome.added.length,
|
|
3232
|
-
dropped_duplicates: outcome.droppedDuplicates,
|
|
3233
|
-
skipped_noise: outcome.skippedNoise,
|
|
3234
|
-
split_attempts: outcome.splitAttempts,
|
|
3235
|
-
malformed_lines: outcome.malformedLines,
|
|
3236
|
-
total_defects: result?.total ?? record.defects.length,
|
|
3237
|
-
status: result?.status ?? "judging",
|
|
3238
|
-
recheck_round: record.reviewPass > 1 && record.history.length > 0
|
|
3239
|
-
};
|
|
3240
3402
|
}
|
|
3241
3403
|
}));
|
|
3242
3404
|
ctx.tools.register(defineTool({
|
|
@@ -3458,10 +3620,16 @@ function registerCrossfireTools(ctx, config) {
|
|
|
3458
3620
|
assertBudget(record, { allowMuted: true });
|
|
3459
3621
|
const markdown = renderReviewMarkdown(record, countDefects(record));
|
|
3460
3622
|
const path = exportFile(stateRoot, reviewId);
|
|
3461
|
-
const { writeTextAtomic: writeTextAtomic$1 } = await import("./state-
|
|
3623
|
+
const { writeTextAtomic: writeTextAtomic$1, writeTextAtomicWithBom: writeTextAtomicWithBom$1 } = await import("./state-B2anbn4l.js");
|
|
3462
3624
|
await writeTextAtomic$1(path, markdown);
|
|
3625
|
+
let textPath = "";
|
|
3626
|
+
try {
|
|
3627
|
+
textPath = exportTextFile(stateRoot, reviewId);
|
|
3628
|
+
await writeTextAtomicWithBom$1(textPath, markdown);
|
|
3629
|
+
} catch {}
|
|
3463
3630
|
return {
|
|
3464
3631
|
path,
|
|
3632
|
+
textPath,
|
|
3465
3633
|
bytes: Buffer.byteLength(markdown, "utf8"),
|
|
3466
3634
|
markdown
|
|
3467
3635
|
};
|
|
@@ -3724,6 +3892,8 @@ const Config = z.object({
|
|
|
3724
3892
|
"deep"
|
|
3725
3893
|
]).default("standard"),
|
|
3726
3894
|
memberMaxDepth: z.natural().default(1),
|
|
3895
|
+
defaultProvider: z.string().default("deepseek-official"),
|
|
3896
|
+
defaultModel: z.string().default("deepseek-flash"),
|
|
3727
3897
|
promptSectionOrder: z.natural().default(117)
|
|
3728
3898
|
});
|
|
3729
3899
|
function apply(ctx, config) {
|
|
@@ -3732,7 +3902,9 @@ function apply(ctx, config) {
|
|
|
3732
3902
|
memberProvider: config.memberProvider ?? "spawn",
|
|
3733
3903
|
maxExperts: config.maxExperts ?? 7,
|
|
3734
3904
|
defaultIntensity: config.defaultIntensity ?? "standard",
|
|
3735
|
-
memberMaxDepth: config.memberMaxDepth ?? 1
|
|
3905
|
+
memberMaxDepth: config.memberMaxDepth ?? 1,
|
|
3906
|
+
defaultProvider: config.defaultProvider ?? "deepseek-official",
|
|
3907
|
+
defaultModel: config.defaultModel ?? "deepseek-flash"
|
|
3736
3908
|
};
|
|
3737
3909
|
ctx.systemPrompt.section({
|
|
3738
3910
|
name: "crossfire:usage",
|
|
@@ -3752,6 +3924,8 @@ function apply(ctx, config) {
|
|
|
3752
3924
|
stateDir: resolved.stateDir,
|
|
3753
3925
|
memberProvider: resolved.memberProvider,
|
|
3754
3926
|
maxExperts: resolved.maxExperts,
|
|
3927
|
+
defaultProvider: resolved.defaultProvider,
|
|
3928
|
+
defaultModel: resolved.defaultModel,
|
|
3755
3929
|
...resolved.memberMaxDepth === void 0 ? {} : { memberMaxDepth: resolved.memberMaxDepth },
|
|
3756
3930
|
reviewSplit: {
|
|
3757
3931
|
provider: "deepseek-official",
|