@y9931666/dsh-plugin-crossfire 0.1.3 → 0.2.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.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +10 -7
  3. package/lib/index.js +675 -66
  4. package/lib/types/budget.d.ts +86 -0
  5. package/lib/types/client/ReviewCard.d.ts +41 -0
  6. package/lib/types/client/SettingsView.d.ts +34 -0
  7. package/lib/types/client/brand.d.ts +55 -0
  8. package/lib/types/client/defectTitle.d.ts +32 -0
  9. package/lib/types/client/icons.d.ts +23 -0
  10. package/lib/types/client/index.d.ts +21 -0
  11. package/lib/types/client/locales.d.ts +19 -0
  12. package/lib/types/client/merge.d.ts +27 -0
  13. package/lib/types/client/promptView.d.ts +63 -0
  14. package/lib/types/client/session-cache.d.ts +49 -0
  15. package/lib/types/client/slot-boundary.d.ts +46 -0
  16. package/lib/types/client/wire.d.ts +192 -0
  17. package/lib/types/dimensions.d.ts +47 -0
  18. package/lib/types/engine.d.ts +190 -0
  19. package/lib/types/etag.d.ts +40 -0
  20. package/lib/types/expert-events.d.ts +110 -0
  21. package/lib/types/export-md.d.ts +21 -0
  22. package/lib/types/harness-compat.d.ts +104 -0
  23. package/lib/types/index.d.ts +89 -0
  24. package/lib/types/members.d.ts +143 -0
  25. package/lib/types/noise.d.ts +29 -0
  26. package/lib/types/plan.d.ts +111 -0
  27. package/lib/types/preferences.d.ts +134 -0
  28. package/lib/types/prompt.d.ts +16 -0
  29. package/lib/types/redteam.d.ts +123 -0
  30. package/lib/types/review.d.ts +152 -0
  31. package/lib/types/rpc.d.ts +176 -0
  32. package/lib/types/split.d.ts +156 -0
  33. package/lib/types/state.d.ts +84 -0
  34. package/lib/types/tools.d.ts +60 -0
  35. package/lib/types/types.d.ts +336 -0
  36. package/lib/types/version.d.ts +18 -0
  37. package/lib/types/web-guard.d.ts +55 -0
  38. package/package.json +40 -37
  39. package/release-notes/v0.2.0.md +44 -0
@@ -0,0 +1,123 @@
1
+ /**
2
+ * 红队专家的 persona、第一条任务消息与工具白名单。
3
+ *
4
+ * 这是整个插件**产品语义最敏感**的文件:针锋之所以成立,全靠这里把"只挑错、
5
+ * 不动手、不迎合"钉成硬约束。任何一条被软化,红队就会退化成又一个"给建议的
6
+ * 助手",而"表态由人做"的整条边界也随之失效。
7
+ *
8
+ * @module dsh-plugin-crossfire/redteam
9
+ */
10
+ import type { Dimension } from './types.ts';
11
+ /**
12
+ * 红队专家允许使用的工具(全局工具白名单,只有列出的这些保持可见)。
13
+ *
14
+ * 只有 5 个:`read`、`read_image`、`glob`、`grep` 四个只读检索工具,
15
+ * 外加唯一一个写入口 `crossfire_report`(写的是评审自己的 transcript,
16
+ * 不是工作区)。
17
+ *
18
+ * **刻意不给** `write` / `edit` / `bash` / `pwsh` / `web_search` / `web_fetch` /
19
+ * `todo_write` 等。理由(这是本插件最本质的安全边界,不是配置偷懒):
20
+ * - **只挑错、不动工作区**:评审一旦能改文件,"提缺陷"就会滑向"顺手修好",
21
+ * 用户拿到的就不再是待裁决的缺陷清单,而是一份未经同意的改动。红队与
22
+ * "智能体团队"成员最本质的区别就在这里:团队成员有写权限、以交付为目标;
23
+ * 红队没有写权限、以推翻为目标。
24
+ * - **不联网**:外部检索会把评审从"针对眼前这份成果"漂移成"泛泛的行业意见",
25
+ * 也会引入不可复现的外部依据;针锋要的是针对具体位置的论证与复现。
26
+ * - **不写待办**:红队没有执行阶段,`todo_write` 只会让它开始规划"怎么修",
27
+ * 直接违反"不产出方案"。
28
+ *
29
+ * 想放开任何一项之前,先回答:放开的那个工具能让专家**证明**一个缺陷,还是
30
+ * 只能让它**动手**?只有前者可以进这张表。
31
+ */
32
+ export declare const EXPERT_TOOL_ALLOW: readonly string[];
33
+ /**
34
+ * 红队专家禁止使用的工具(黑名单)。
35
+ *
36
+ * 这里列的是全部**主持人专属**的 `crossfire_*` 工具:启动前确认、开评审、发起
37
+ * 攻击、收集缺陷、定稿、复审、导出、写入立场。这些都是主持人的编排权,专家一旦
38
+ * 能调 `crossfire_plan_review`/`crossfire_collect`/`crossfire_finish`,就等于自己
39
+ * 给自己结案,"表态由人做"当场失效。
40
+ *
41
+ * 为什么白名单之外还要再来一份黑名单:两者是**与**关系(必须同时在白名单里
42
+ * 且不在黑名单里才可见)。将来有人为了加一个只读工具而改动
43
+ * {@link EXPERT_TOOL_ALLOW} 时,不会顺带把编排权放开 —— 黑名单这一层与被修改
44
+ * 的那一行无关,能拦住"删一项白名单等于多一份权限"的误操作。
45
+ *
46
+ * **注意**:`crossfire_report` 是**专家自己的**发言工具,绝不能列进本表。
47
+ */
48
+ export declare const EXPERT_TOOL_DENY: readonly string[];
49
+ /** 专家的工具限制(宿主 subagent/tool 配置接受的形状)。 */
50
+ export interface ToolRestriction {
51
+ /** 保持可见的全局工具。 */
52
+ readonly allow?: readonly string[];
53
+ /** 强制不可见的全局工具(与 allow 是"与"关系)。 */
54
+ readonly deny?: readonly string[];
55
+ }
56
+ /**
57
+ * 专家的工具限制。
58
+ *
59
+ * 每次返回**新的数组实例**,且以 `readonly` 声明:宿主侧可能在拿到后排序或增删,
60
+ * 共用模块级数组会让一次改动污染后续所有专家。黑名单恒为全量主持人工具,
61
+ * 与白名单内容无关 —— 它是与被修改的那一行**无关**的第二道闸。
62
+ */
63
+ export declare function expertToolRestriction(): ToolRestriction;
64
+ /** 生成专家 persona 与 welcome 所需的全部输入。 */
65
+ export interface ExpertPersonaInput {
66
+ /** 评审 id。 */
67
+ reviewId: string;
68
+ /** 这位专家负责的维度。 */
69
+ dimension: Dimension;
70
+ /** 待验证对象的标题。 */
71
+ title: string;
72
+ /** 待验证对象的正文。 */
73
+ subject: string;
74
+ /** 用户立场与理由(可能为空串)。 */
75
+ stance: string;
76
+ /** 本次采用的全部维度(让专家知道别人负责什么,避免重复)。 */
77
+ dimensions: readonly Dimension[];
78
+ /** 每轮最多提几条缺陷(0 = 不限制)。 */
79
+ maxOpinions: number;
80
+ /**
81
+ * 用户为这位专家写的**补充角色定位**(来自角色预设 / 阵容成员;可空)。
82
+ *
83
+ * 它进 persona 的唯一位置是「你的角色定位」那一段,而该段落带着一段**不可覆盖**
84
+ * 的声明(见 {@link expertPersona})。这条边界不是形式主义:这一段文本完全由用户
85
+ * 书写,如果它能覆盖硬约束,那么"专家只读"这条安全边界就变成了一个可配置项 ——
86
+ * 用户写一句"你可以修改文件",白名单就不再是最后一道闸。
87
+ */
88
+ role?: string;
89
+ /**
90
+ * 用户在启动前确认卡里为这位专家写的**补充提示词**(可空)。
91
+ *
92
+ * 它和 {@link role} 进 persona 的同一类段落(「评审者补充提示词」),带着同一套
93
+ * 不可覆盖声明:只能收窄专长,不能放宽只读、不能让它产出方案、不能让它迎合用户。
94
+ * 空/纯空白时 persona 与从前**逐字**相同。
95
+ */
96
+ prompt?: string;
97
+ }
98
+ /**
99
+ * 报告信封的格式说明(persona 与 welcome 共用同一份文本)。
100
+ *
101
+ * 为什么格式必须固定到这个程度:缺陷要能被**逐条裁决**,而裁决面是按条渲染的。
102
+ * 本地兜底切分器(模型没按格式走时的最后一道网)认的就是 `观点 N(指向 …,
103
+ * 维度Y):` 这个行首标记。标记缺失时整段发言只能作为一条缺陷收进列表,用户
104
+ * 就得面对一团散文去点"认定/驳回" —— 针锋存在的意义当场归零。
105
+ *
106
+ * @param maxOpinions - 最多几条(0 = 不限制),写进语气会随上限收紧。
107
+ */
108
+ export declare function reportEnvelopeSpec(maxOpinions: number): string;
109
+ /**
110
+ * 生成红队专家的 persona(系统级人设)。
111
+ *
112
+ * 九条硬约束逐条落在文本里(身份 / 单维度 / 不产出方案 / 不要客套 / 一条一缺陷 /
113
+ * 证据分级 / 立场对攻 / 工具边界 / 不许迎合)。这些约束的共同目标只有一个:
114
+ * 让产出的东西**可以被判定**,而不是听起来很有道理。
115
+ */
116
+ export declare function expertPersona(input: ExpertPersonaInput): string;
117
+ /**
118
+ * 生成发给这位专家的第一条任务消息。
119
+ *
120
+ * persona 已经写清了全部约束(不要重复粘贴,那既费 token 又稀释重点),这条
121
+ * 消息只做三件事:**分配维度**、**交出材料与立场**、**钉死格式与结束方式**。
122
+ */
123
+ export declare function expertWelcome(input: ExpertPersonaInput): string;
@@ -0,0 +1,152 @@
1
+ /**
2
+ * 评审记录的读写、净化与派生查询。
3
+ *
4
+ * 目录布局(见 state.ts):`<stateRoot>/crossfire/<reviewId>/`
5
+ * - `review.json` — 主记录(本模块负责)
6
+ * - `transcript.jsonl` — 专家发言(append-only)
7
+ *
8
+ * ## 设计要点
9
+ *
10
+ * 1. **schemaVersion 缺失视为 1**(与圆桌同款判定)。crossfire 的目录是独立的,
11
+ * 不可能读到圆桌的记录,所以这里刻意不背它的 v1→v2 迁移包袱 —— 但保留"版本
12
+ * 常量 + 迁移函数"的写法,因为下一次改形状时它仍然是对的。
13
+ * 2. **读失败绝不抛给调用方**:坏 JSON / 缺字段 / 类型不符一律降级为默认值,
14
+ * 由 `normalizeRecord` 收敛出一份**永远可用**的记录。界面宁可由空缺陷列表
15
+ * 组成,也不能因为一条脏数据整张卡片消失。
16
+ * 3. **落库的 dimension 必须是本次采用维度之一**(见 dimensions.ts 的理由)。
17
+ * 4. 所有写操作都走**同一把进程内锁**(`reviewLockKey`),与 state.ts 的原子写
18
+ * 叠加,保证同一场评审不会出现两个写者交错。
19
+ *
20
+ * @module dsh-plugin-crossfire/review
21
+ */
22
+ import type { CrossfireRecord, Defect, DefectStatus, ExpertUtterance, Intensity, RecheckVerdict, Subject } from './types.ts';
23
+ /** 当前 schema 版本。 */
24
+ export declare const CURRENT_SCHEMA_VERSION: 1;
25
+ /** `<reviewDir>/review.json`。 */
26
+ export declare function reviewFile(stateRoot: string, reviewId: string): string;
27
+ /** `<reviewDir>/transcript.jsonl`。 */
28
+ export declare function transcriptFile(stateRoot: string, reviewId: string): string;
29
+ /** `<reviewDir>/user-actions.jsonl`。 */
30
+ export declare function userActionsFile(stateRoot: string, reviewId: string): string;
31
+ /** `<reviewDir>/export.md`。 */
32
+ export declare function exportFile(stateRoot: string, reviewId: string): string;
33
+ /**
34
+ * `<reviewDir>/export.txt` —— 同一份内容的**人读副本**(UTF-8 带 BOM)。
35
+ *
36
+ * 为什么导出两份:`.md` 交给 Markdown 渲染器,但 Windows 上双击 `.md` 常被别的程序
37
+ * 抢走,加上无 BOM 的 UTF-8 会被记事本误判成 ANSI —— 于是"导出物打不开"。`.txt` 带
38
+ * BOM 后双击直接进记事本,中文也不乱码(用户反馈的正是这一点)。
39
+ */
40
+ export declare function exportTextFile(stateRoot: string, reviewId: string): string;
41
+ /** `<stateRoot>/crossfire`(诊断用)。 */
42
+ export declare function namespaceRoot(stateRoot: string): string;
43
+ /**
44
+ * 把任意一份读出来的记录收敛成当前 schema 的**完整可用**记录。
45
+ *
46
+ * @param raw - 磁盘上读到的原始值(可能来自旧版本、可能被人手改坏)。
47
+ * @param reviewId - 目录名(权威 id:即使文件里写的 id 不一致也以目录为准)。
48
+ */
49
+ export declare function normalizeRecord(raw: unknown, reviewId: string): CrossfireRecord;
50
+ /** `crossfire_open` 的输入。 */
51
+ export interface OpenReviewInput {
52
+ reviewId: string;
53
+ title: string;
54
+ content: string;
55
+ source: Subject['source'];
56
+ sourceRef?: string;
57
+ /** 本次采用的维度(已由 dimensions.resolveDimensions 归一化)。 */
58
+ dimensions: string[];
59
+ intensity: Intensity;
60
+ /** 用户手写的立场与理由(可空)。 */
61
+ stanceText?: string;
62
+ maxRounds: number;
63
+ maxTokens: number;
64
+ /** 从圆桌会议移交时记录来源会议 id(v0.1.0 只写不读)。 */
65
+ sourceMeeting?: string;
66
+ /** 开启这场评审的会话 id(用于校验调用者,见 types.ts)。 */
67
+ ownerSessionId?: string;
68
+ now?: number;
69
+ }
70
+ /** 按输入构造一份全新的记录(status 恒为 `draft`)。 */
71
+ export declare function newRecord(input: OpenReviewInput): CrossfireRecord;
72
+ /** 读一场评审;缺失或损坏都返回 undefined。 */
73
+ export declare function readRecord(stateRoot: string, reviewId: string): Promise<CrossfireRecord | undefined>;
74
+ /** 写一场评审(自动刷新 updatedAt)。 */
75
+ export declare function writeRecord(stateRoot: string, record: CrossfireRecord): Promise<void>;
76
+ /** 读发言记录(坏行计数一并返回,不许静默吞掉)。 */
77
+ export declare function readTranscriptReport(stateRoot: string, reviewId: string): Promise<{
78
+ items: ExpertUtterance[];
79
+ malformed: number;
80
+ }>;
81
+ /** 读发言记录。 */
82
+ export declare function readTranscript(stateRoot: string, reviewId: string): Promise<ExpertUtterance[]>;
83
+ /** 追加一条专家发言。 */
84
+ export declare function appendTranscript(stateRoot: string, reviewId: string, utterance: ExpertUtterance): Promise<void>;
85
+ /**
86
+ * 在锁内读-改-写一场评审。
87
+ *
88
+ * @returns 变更函数的返回值;记录不存在时返回 undefined。
89
+ */
90
+ export declare function mutateRecord<T>(stateRoot: string, reviewId: string, mutate: (record: CrossfireRecord) => T | undefined): Promise<T | undefined>;
91
+ /**
92
+ * 设置一条缺陷的三态(可互切)。
93
+ *
94
+ * 驳回必须给理由 —— 这条约束同时是"用户在想什么"这一信息的唯一来源,
95
+ * 因此它在数据层就被强制,而不是只靠界面拦。
96
+ *
97
+ * @returns true = 确有变更;false = 记录/缺陷不存在,或状态本来就相同。
98
+ */
99
+ export declare function setDefectStatus(stateRoot: string, reviewId: string, defectId: string, status: DefectStatus, rejectReason?: string): Promise<boolean>;
100
+ /** 设置一条核对项的复审核对结论。 */
101
+ export declare function setRecheckVerdict(stateRoot: string, reviewId: string, defectId: string, verdict: RecheckVerdict, note?: string): Promise<boolean>;
102
+ /** 三态计数(卡片顶部影响条的数据源)。 */
103
+ export interface DefectCounts {
104
+ total: number;
105
+ pending: number;
106
+ endorsed: number;
107
+ rejected: number;
108
+ /** 打穿用户立场的条数。 */
109
+ strikes: number;
110
+ }
111
+ /** 统计三态。 */
112
+ export declare function countDefects(record: CrossfireRecord): DefectCounts;
113
+ /** 已认定缺陷清单(定稿时固化的 known-flaws)。 */
114
+ export declare function knownFlaws(record: CrossfireRecord): Defect[];
115
+ /**
116
+ * 两条缺陷是否应视为"同一条"。
117
+ *
118
+ * 两档判据(刻意不同,因为代价不对称):
119
+ * - **完全相同**(规范化后)→ 同一条,**哪怕它很短**。"缺日志"就是"缺日志",
120
+ * 用户驳回过它,就不该第二次弹出来。
121
+ * - **部分包含** → 只有较短一方 ≥ 8 个字符时才敢判。否则"缺日志"会把
122
+ * "缺日志且没有轮转机制" 这类同族但不同的缺陷一起吞掉 —— 漏报一条真缺陷,
123
+ * 比多弹一条重复项严重得多。
124
+ */
125
+ export declare function isSameDefect(a: string, b: string): boolean;
126
+ /** 历史上被用户**驳回**过的全部缺陷内容(用于"不再重复弹同一观点")。 */
127
+ export declare function rejectedContents(record: CrossfireRecord): string[];
128
+ /** 上一轮(history 的最后一轮)中已被用户**认定**的缺陷。 */
129
+ export declare function lastEndorsedDefects(record: CrossfireRecord): Defect[];
130
+ /**
131
+ * 复审开轮:把上一轮已认定的缺陷变成本轮的"修复核对项"。
132
+ *
133
+ * 语义(R7):复审**只核对旧账**,不许开新战线。所以本轮的缺陷集合不是清的,
134
+ * 而是被这些核对项替换。
135
+ *
136
+ * **刻意不预置 `verdict`**:开轮时机器不替人下结论。此前这里写死
137
+ * `verdict: 'unfixed'`("未被证明修复之前都算没修"),结果是复审轮一打开,
138
+ * 三态按钮里"未修复"就已经是高亮选中态,而备注框旁的保存按钮会把这个机器默认值
139
+ * 当作人的表态落库(红队评审抓出的真实缺陷)。现在缺席即"尚未判断",由人来选。
140
+ */
141
+ export declare function buildRecheckItems(record: CrossfireRecord): Defect[];
142
+ /**
143
+ * 过滤掉"上一轮已被驳回"的候选缺陷。
144
+ *
145
+ * 这是 D8 允许的唯一历史用法:提升信噪比。**绝不**把用户偏好告诉红队 ——
146
+ * 模型一旦知道用户历史上总驳回某类观点,就会干脆不提那类问题,系统性削弱
147
+ * 对抗强度,与这个插件存在的理由正好相反。
148
+ */
149
+ export declare function dropRejectedDuplicates(candidates: Defect[], pastRejected: readonly string[]): {
150
+ kept: Defect[];
151
+ dropped: number;
152
+ };
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Crossfire 的浏览器面:一个 `dispatch`(RPC)+ 一个快照收集器(GET)。
3
+ *
4
+ * ## 两条传输,同一份 dispatch
5
+ *
6
+ * 1. 插件自有的 web POST 路由 `/plugins/dsh-plugin-crossfire/rpc`(首选:任何带 web
7
+ * 服务的 profile 都可达);
8
+ * 2. 宿主通用 connection 通道(兜底,经 `ctx.get` 懒取)。
9
+ *
10
+ * ## O1:卡片按钮**直推** host 状态机
11
+ *
12
+ * 这是 crossfire 与圆桌会议最重要的编排差异。圆桌的 UI 只能写"用户行为队列",
13
+ * 等主持人下一轮执行;针锋的按钮直接调用这里的 `crossfire/review.advance`,
14
+ * 因此用户点"开审"不必等一整个模型回合。
15
+ *
16
+ * 代价是:**RPC 路径没有 `exec.agent`**。凡是需要父 Agent 的动作(拉起子代理)
17
+ * 就得先按会话 id 把 Agent 找回来(`ctx.agents.get`);找不到(会话已冷)时如实
18
+ * 失败并让用户改用对话触发,而不是假装成功。
19
+ *
20
+ * ## 一条不可越界的规则
21
+ *
22
+ * `crossfire/defect.setStatus` **只允许人从界面调用**。它没有对应的 agent 工具,
23
+ * 也永远不该有:表态权是这个插件存在的理由本身。
24
+ *
25
+ * @module dsh-plugin-crossfire/rpc
26
+ */
27
+ import type { Context } from '@deepseek-ai/cordis';
28
+ import { type PreferenceStore } from './preferences.ts';
29
+ import { exportFile } from './review.ts';
30
+ import type { ToolsConfig } from './tools.ts';
31
+ import { type CrossfireRecord, type Intensity } from './types.ts';
32
+ /** 插件自有 RPC 路由(浏览器半体优先走这条)。 */
33
+ export declare const RPC_ROUTE = "/plugins/dsh-plugin-crossfire/rpc";
34
+ /** 插件自有快照路由(卡片每秒轮询它)。 */
35
+ export declare const STATE_ROUTE = "/plugins/dsh-plugin-crossfire/state";
36
+ /** 运行时依赖:偏好存储 + 工具/引擎共用的配置(单一来源)。 */
37
+ export interface CrossfireRuntime {
38
+ prefs: PreferenceStore;
39
+ tools: ToolsConfig;
40
+ }
41
+ /** RPC 失败包封。 */
42
+ export interface RpcFailure {
43
+ ok: false;
44
+ error: {
45
+ code: string;
46
+ message: string;
47
+ };
48
+ }
49
+ /** RPC 结果包封(成功 / 失败二选一)。 */
50
+ export type RpcResult = {
51
+ ok: true;
52
+ value: unknown;
53
+ } | RpcFailure;
54
+ /** dispatch 的类型:端点名 + 载荷 → 结果包封。 */
55
+ export type RpcDispatch = (endpoint: string, payload: unknown) => Promise<RpcResult>;
56
+ /** 卡片所需的评审快照(刻意不含 workspace 绝对路径之外的任何隐私内容)。 */
57
+ export interface ReviewSnapshot {
58
+ reviewId: string;
59
+ status: CrossfireRecord['status'];
60
+ subjectTitle: string;
61
+ subjectContent: string;
62
+ subjectSource: string;
63
+ stanceText: string;
64
+ stanceOrigin: string;
65
+ dimensions: string[];
66
+ intensity: Intensity;
67
+ intensityLabel: string;
68
+ reviewPass: number;
69
+ maxReviewPass: number;
70
+ experts: {
71
+ key: string;
72
+ dimension: string;
73
+ status: string;
74
+ defectCount: number;
75
+ reportCount?: number;
76
+ /** 实际模型路由;未指定(继承主持人)时两个字段都不出现。 */
77
+ provider?: string;
78
+ model?: string;
79
+ /** 用户补充的角色定位(专家列表里可编辑)。 */
80
+ role?: string;
81
+ note?: string;
82
+ }[];
83
+ defects: {
84
+ id: string;
85
+ expertKey: string;
86
+ dimension: string;
87
+ dimensionFallback?: boolean;
88
+ content: string;
89
+ quote?: string;
90
+ evidence?: {
91
+ kind: 'repro' | 'argument';
92
+ text: string;
93
+ };
94
+ strikesStance?: boolean;
95
+ status: 'pending' | 'endorsed' | 'rejected';
96
+ rejectReason?: string;
97
+ /** 复审核对结论;`verdict` 缺席 = 尚未判断(**不是**"未修复")。 */
98
+ recheck?: {
99
+ pass: number;
100
+ verdict?: string;
101
+ note?: string;
102
+ };
103
+ }[];
104
+ counts: {
105
+ total: number;
106
+ pending: number;
107
+ endorsed: number;
108
+ rejected: number;
109
+ strikes: number;
110
+ };
111
+ budget: {
112
+ maxRounds: number;
113
+ maxTokens: number;
114
+ usedRounds: number;
115
+ usedTokens: number;
116
+ };
117
+ startedAt: number;
118
+ updatedAt: number;
119
+ /** 这份评审所在的状态根(客户端回传后可免去再次遍历工作区)。 */
120
+ stateRoot: string;
121
+ }
122
+ /** 把记录收敛成卡片快照。 */
123
+ export declare function snapshotOf(record: CrossfireRecord, stateRoot: string): ReviewSnapshot;
124
+ /** 身份判定的结果。 */
125
+ export type ActorVerdict = {
126
+ ok: true;
127
+ bound: boolean;
128
+ } | {
129
+ ok: false;
130
+ code: 'no-actor' | 'foreign-owner';
131
+ message: string;
132
+ };
133
+ /**
134
+ * 判定"谁在操作这一场评审"。
135
+ *
136
+ * ## 为什么必须是严格的(这里出过一个真实的安全缺陷)
137
+ *
138
+ * 会话 id 在这个插件里**同时是凭据和权限**:`load()` 用它做归属校验,
139
+ * `review.advance` 的 attack 分支又用它换回 live Agent 去拉起专家。原先的写法把
140
+ * 空会话当成"跳过校验",于是当客户端没把 session 送进来时:
141
+ *
142
+ * - 归属校验整条短路(任何会话都能操作任何记录);
143
+ * - 而 attack 又会退回用**记录里的 owner** 换 Agent,照样把专家拉起来。
144
+ *
145
+ * 结果是一条能真正干活的越权通道,而且它的成功响应与合法调用完全同形 —— 验收时
146
+ * 根本分不出来。红队在验收流程里抓到了这一点。
147
+ *
148
+ * 所以现在的规则是:**必须声称一个会话**。规则本身是纯函数,因此可以被测试直接
149
+ * 钉住(`test/rpc-identity.test.mjs`),而不是只靠人工点一次按钮。
150
+ *
151
+ * @param ownerSessionId - 记录里的 owner(早期数据可能没有)。
152
+ * @param actorSession - 调用方声称的会话 id(缺失时为 `''`)。
153
+ */
154
+ export declare function judgeActor(ownerSessionId: string | undefined, actorSession: string): ActorVerdict;
155
+ /** 收集全部评审快照(卡片轮询的数据源)。 */
156
+ export declare function collectSnapshots(ctx: Context, stateDir: string, session: string): Promise<{
157
+ reviews: ReviewSnapshot[];
158
+ note?: string;
159
+ }>;
160
+ /** 注册 RPC 端点,返回可复用的 `dispatch`。 */
161
+ export declare function registerRpc(ctx: Context, runtime: CrossfireRuntime): RpcDispatch;
162
+ /**
163
+ * 从界面直接开一场评审。
164
+ *
165
+ * 与 `crossfire_open` 工具同源,区别只是工作区来自会话 id 而不是 `exec.agent`。
166
+ */
167
+ export declare function openReviewFromRpc(ctx: Context, runtime: CrossfireRuntime, input: {
168
+ session: string;
169
+ title: string;
170
+ content: string;
171
+ dimensions?: unknown;
172
+ intensity?: unknown;
173
+ stance?: string;
174
+ }): Promise<RpcResult>;
175
+ /** 导出路径(批 4 的下载按钮用)。 */
176
+ export { exportFile };
@@ -0,0 +1,156 @@
1
+ /**
2
+ * 观点拆分:把红队专家的一条发言拆成 1~3 条独立缺陷,用户即可对单条缺陷
3
+ * 「认定/驳回」。
4
+ *
5
+ * 三道防线(沿用圆桌里被真实数据验证过的设计,**不要改**):
6
+ * ① JSON schema 严格约束输出(tools 参数 + temperature 0)
7
+ * ② 任何解析失败 → 返回 null,由调用方整条兜底(seq=0)
8
+ * ③ quote 必须是发言原文的子串(规范化后 indexOf),否则丢弃该 quote
9
+ *
10
+ * crossfire 相对圆桌的三处改动:
11
+ * - **维度归一化**:拆分结果里的 dimension 不再直接采信,一律过
12
+ * `resolveDimension`,保证落库的 dimension 一定 ∈ `allowed`;
13
+ * - **打穿标记**:每条缺陷可带 `strikesStance`(是否打穿了用户立场里的某条理由);
14
+ * - 工具名与 system 提示词按 crossfire 改写。
15
+ *
16
+ * @module dsh-plugin-crossfire/split
17
+ */
18
+ /** 一条拆分结果(= 一条待裁决的缺陷)。 */
19
+ export interface SplitLine {
20
+ content: string;
21
+ quote?: string;
22
+ /** 归一化后的维度,只要 `allowed` 非空就一定 ∈ allowed。 */
23
+ dimension: string;
24
+ /** true = 模型给的原始维度越界/缺失,落库时被归一化过。 */
25
+ dimensionFallback?: boolean;
26
+ /** 是否打穿了用户在「立场」里给出的某条理由。 */
27
+ strikesStance?: boolean;
28
+ /** 证据分级(C1):代码/bug 类 = repro(可复现步骤);设计类 = argument(论证链)。 */
29
+ evidence?: {
30
+ kind: 'repro' | 'argument';
31
+ text: string;
32
+ };
33
+ }
34
+ /** 拆分调用的模型路由配置(默认 deepseek-official/deepseek-v4-flash,实测可用)。 */
35
+ export interface ReviewSplitConfig {
36
+ provider: string;
37
+ model: string;
38
+ /** 每条发言最多拆几条缺陷(默认 3)。 */
39
+ maxOpinions: number;
40
+ /**
41
+ * 拆分阶段的 LLM 并发上限(默认 4;`1` = 串行)。
42
+ *
43
+ * 它是"收集"这一步的墙钟时间决定项:一条发言一次往返,此前是串行 `await`,一场
44
+ * 21 条发言的评审就是 21 次串行往返(红队评审抓出的性能缺陷)。上限刻意不高 ——
45
+ * 这些请求打同一个 provider,拉满只会撞速率限制反而更慢。
46
+ *
47
+ * 放在这里而不是插件 Config:两个调用点(工具与 RPC 直推)读的都是同一个
48
+ * `reviewSplit`,因此两边的并发行为天然一致,不会分叉。
49
+ */
50
+ llmConcurrency?: number;
51
+ }
52
+ /** 最小化 LLM 服务形状(与 ctx.llm.stream 结构兼容)。 */
53
+ export interface SplitLlmLike {
54
+ stream(options: {
55
+ provider: string;
56
+ model: string;
57
+ messages: {
58
+ id: string;
59
+ role: 'user';
60
+ content: {
61
+ type: 'text';
62
+ text: string;
63
+ }[];
64
+ source: {
65
+ kind: 'user';
66
+ };
67
+ }[];
68
+ system?: string;
69
+ tools?: {
70
+ name: string;
71
+ description: string;
72
+ parameters: Record<string, unknown>;
73
+ }[];
74
+ temperature?: number;
75
+ maxTokens?: number;
76
+ signal?: AbortSignal;
77
+ }): AsyncIterable<unknown>;
78
+ }
79
+ /** 拆分工具的 JSON schema(防线①)。导出以便测试断言 schema 形状。 */
80
+ export declare const SPLIT_TOOL: {
81
+ readonly name: "split_crossfire_defect";
82
+ readonly description: "把一条红队专家发言拆成 1~3 条独立缺陷(每条缺陷只含一个问题)。";
83
+ readonly parameters: {
84
+ readonly type: "object";
85
+ readonly additionalProperties: false;
86
+ readonly properties: {
87
+ readonly defects: {
88
+ readonly type: "array";
89
+ readonly minItems: 1;
90
+ readonly maxItems: 3;
91
+ readonly items: {
92
+ readonly type: "object";
93
+ readonly additionalProperties: false;
94
+ readonly properties: {
95
+ readonly content: {
96
+ readonly type: "string";
97
+ readonly description: "单条缺陷的完整表述。";
98
+ };
99
+ readonly quote: {
100
+ readonly type: "string";
101
+ readonly description: "发言原文中与该缺陷对应的子串(可选,必须逐字来自发言原文)。";
102
+ };
103
+ readonly dimension: {
104
+ readonly type: "string";
105
+ readonly description: "缺陷所属维度。必须逐字取自给定清单 allowed 中的一项,不得自创。";
106
+ };
107
+ readonly strikes_stance: {
108
+ readonly type: "boolean";
109
+ readonly description: "该缺陷是否打穿了用户在「立场」里给出的某条理由。";
110
+ };
111
+ readonly evidence: {
112
+ readonly type: "object";
113
+ readonly description: "证据分级(C1):代码/bug 类缺陷填 repro(附可复现步骤 1. 2. 3.);设计类缺陷填 argument(附论证链,不编造伪复现步骤)。无法给出证据时省略该字段。";
114
+ readonly additionalProperties: false;
115
+ readonly properties: {
116
+ readonly kind: {
117
+ readonly type: "string";
118
+ readonly enum: readonly ["repro", "argument"];
119
+ readonly description: "repro=可复现步骤(代码/bug 类);argument=论证链(设计类)。";
120
+ };
121
+ readonly text: {
122
+ readonly type: "string";
123
+ readonly description: "证据正文:复现步骤(编号 1. 2. 3.)或论证链,≤600 字符。";
124
+ };
125
+ };
126
+ readonly required: readonly ["kind", "text"];
127
+ };
128
+ };
129
+ readonly required: readonly ["content", "dimension"];
130
+ };
131
+ };
132
+ };
133
+ readonly required: readonly ["defects"];
134
+ };
135
+ };
136
+ /**
137
+ * 判定这段文字里专家自己写的「针对立场」结论是不是**打穿**。
138
+ *
139
+ * ## 为什么不能只写 `includes('打穿')`(这是真实存在的坑)
140
+ *
141
+ * `未打穿` 的后两个字就是 `打穿`,纯子串匹配会把"没打穿"读成"打穿了",方向整个反过来。
142
+ * 所以这里匹配有界的标记(`针对立场…:打穿|未打穿`)并**显式取反**:只有 `未` 缺席时
143
+ * 才算打穿;标记压根不存在(含 `undefined`)也一律算没打穿。
144
+ *
145
+ * 之所以要有这个本地判定:专家的信封格式是**固定**的,报告正文里本来就写着结论。
146
+ * 模型拆分失败退到本地启发式({@link splitByMarkers})时不该把这个信号丢掉 ——
147
+ * 那正是"打穿立场"标签永远显示不出来的原因之一。
148
+ */
149
+ export declare function stanceMarkerStrikes(text: string | undefined): boolean;
150
+ export declare function splitByMarkers(content: string, allowed: readonly string[]): SplitLine[] | null;
151
+ /**
152
+ * 拆分一条发言为若干独立缺陷;任何失败返回 null(调用方整条兜底)。
153
+ * quote 与发言原文做规范化子串校验(防线③),不匹配即丢弃。
154
+ * dimension 一律过 `resolveDimension`(落库值必须 ∈ allowed)。
155
+ */
156
+ export declare function splitUtterance(llm: SplitLlmLike, config: ReviewSplitConfig, expertKey: string, content: string, allowed: readonly string[], stance: string): Promise<SplitLine[] | null>;