@y9931666/dsh-plugin-crossfire 0.1.1 → 0.1.3

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 (38) hide show
  1. package/README.md +2 -0
  2. package/lib/client.js +160 -19
  3. package/lib/index.js +1 -1
  4. package/package.json +1 -1
  5. package/release-notes/v0.1.2.md +44 -0
  6. package/release-notes/v0.1.3.md +21 -0
  7. package/lib/types/budget.d.ts +0 -86
  8. package/lib/types/client/ReviewCard.d.ts +0 -41
  9. package/lib/types/client/SettingsView.d.ts +0 -34
  10. package/lib/types/client/brand.d.ts +0 -55
  11. package/lib/types/client/defectTitle.d.ts +0 -23
  12. package/lib/types/client/index.d.ts +0 -21
  13. package/lib/types/client/locales.d.ts +0 -19
  14. package/lib/types/client/merge.d.ts +0 -27
  15. package/lib/types/client/promptView.d.ts +0 -63
  16. package/lib/types/client/session-cache.d.ts +0 -49
  17. package/lib/types/client/slot-boundary.d.ts +0 -46
  18. package/lib/types/client/wire.d.ts +0 -192
  19. package/lib/types/dimensions.d.ts +0 -47
  20. package/lib/types/engine.d.ts +0 -172
  21. package/lib/types/etag.d.ts +0 -40
  22. package/lib/types/expert-events.d.ts +0 -110
  23. package/lib/types/export-md.d.ts +0 -21
  24. package/lib/types/harness-compat.d.ts +0 -104
  25. package/lib/types/index.d.ts +0 -89
  26. package/lib/types/members.d.ts +0 -143
  27. package/lib/types/noise.d.ts +0 -29
  28. package/lib/types/preferences.d.ts +0 -134
  29. package/lib/types/prompt.d.ts +0 -16
  30. package/lib/types/redteam.d.ts +0 -115
  31. package/lib/types/review.d.ts +0 -152
  32. package/lib/types/rpc.d.ts +0 -176
  33. package/lib/types/split.d.ts +0 -156
  34. package/lib/types/state.d.ts +0 -84
  35. package/lib/types/tools.d.ts +0 -59
  36. package/lib/types/types.d.ts +0 -308
  37. package/lib/types/version.d.ts +0 -18
  38. package/lib/types/web-guard.d.ts +0 -55
@@ -1,172 +0,0 @@
1
- /**
2
- * 评审引擎:不依赖 `exec.agent` 的核心动作。
3
- *
4
- * ## 为什么需要它
5
- *
6
- * 工具(`tools.ts`)和浏览器 RPC(`rpc.ts`)必须能驱动**同一套**状态机 —— 这正是
7
- * O1 决策(卡片按钮直推 host,不等 AI 回合)的落点。工具拿到的是 `exec.agent`,
8
- * RPC 拿到的是 `sessionId`,两者唯一的公共面就是"一段纯逻辑 + 一个 ctx"。
9
- *
10
- * 所以这里放的是:**凡是两条入口都需要、且复制一份就会分叉的**逻辑。
11
- * - {@link planExperts}:按强度档与维度算阵容(决定谁去攻击);
12
- * - {@link collectDefects}:拆分 + 噪音过滤 + 维度归一化 + 历史去重(决定用户看到
13
- * 什么)—— 这一块是整个插件里最容易分叉的地方,所以它是单点实现。
14
- *
15
- * 不放在这里的是:状态机门禁、写盘时序、专家拉起(那些短且各自语境不同)。
16
- *
17
- * @module dsh-plugin-crossfire/engine
18
- */
19
- import { type ReviewSplitConfig, type SplitLlmLike } from './split.ts';
20
- import type { CrossfireRecord, Defect, ExpertUtterance, RedTeamExpert } from './types.ts';
21
- /**
22
- * 一位专家的"人选来源":把用户偏好里的角色 / 模型路由喂给阵容规划。
23
- *
24
- * ## 为什么需要它(这是红队评审抓出的真实缺陷)
25
- *
26
- * 设置页的角色预设与阵容预设此前是**只写不读**的:`preferences.ts` 存了它们,
27
- * `SettingsView` 能增删改,但 `planExperts` 从不消费 —— 于是"编辑专家提示词"这件事
28
- * 在数据上根本没有出口,`expert.provider`/`expert.model` 也永远缺席(快照里那个
29
- * 品牌块因此只能显示中性 `?`)。这里把它接上。
30
- */
31
- export interface ExpertRosterEntry {
32
- /**
33
- * 匹配键:与**维度名**比较。
34
- *
35
- * 用维度名而不是顺序匹配,是因为维度是用户直接输入的东西,而阵容成员的 `key`
36
- * 本来就是自由文本(设置页提示了"专家 key")—— 让 key 就是维度名是最省心的用法。
37
- * 匹配不到时退回**顺序**兜底(见 {@link planExperts}),两种写法都能用。
38
- */
39
- key: string;
40
- /** 用户补充的角色定位(进 persona 的「你的角色定位」段)。 */
41
- role?: string;
42
- provider?: string;
43
- model?: string;
44
- /**
45
- * 预设名(只有角色预设带它)。
46
- *
47
- * 角色预设的主标识是 `name`,阵容成员的主标识是 `key`;这一层刻意**两种都收**,
48
- * 于是调用方不必先做一次形状转换 —— 形状分歧是缺陷的温床(红队抓出的"预设只写
49
- * 不读"就属于这类缝隙)。
50
- */
51
- name?: string;
52
- }
53
- /**
54
- * 按强度档与维度算出这一轮要拉起谁。
55
- *
56
- * 纯函数(可单测):同样一份记录 + 同一份 roster 永远得到同一份阵容。规模上限按
57
- * `record.dimensions` 的顺序截断,所以**用户排在前面的维度优先** —— 这是可预期的,
58
- * 而不是随机丢人。
59
- *
60
- * 人选来源两条,按优先级:
61
- * 1. `roster` 里 `key` 等于维度名的那一条(精确指定,最符合直觉);
62
- * 2. 否则按**顺序**取 roster 的第 i 条(i = 维度序号)—— 用户不必把 key 改名成
63
- * 维度名也能用上一份现成的阵容;
64
- * 3. 都取不到时只写维度与状态(专家继承主持人路由、无补充定位),与从前完全一致。
65
- *
66
- * @param roster - 可选人选来源;缺席或为空时行为与从前逐字一致。
67
- */
68
- export declare function planExperts(record: CrossfireRecord, maxExperts: number, roster?: readonly ExpertRosterEntry[]): RedTeamExpert[];
69
- /** 一次收集的结果。 */
70
- export interface CollectOutcome {
71
- added: Defect[];
72
- /** 因为"用户上一轮驳回过"而被丢弃的条数。 */
73
- droppedDuplicates: number;
74
- /** 被噪音规则拦下的发言条数。 */
75
- skippedNoise: number;
76
- /** 真正调用过 LLM 拆分的次数(0 表示全部走了本地兜底)。 */
77
- splitAttempts: number;
78
- /** 已收集过、本次跳过的发言条数(幂等键命中)。 */
79
- alreadyCollected: number;
80
- /** 解析失败的转录行数(原样带回,不许静默吞掉)。 */
81
- malformedLines: number;
82
- }
83
- /** 收集所需的输入。 */
84
- export interface CollectInput {
85
- record: CrossfireRecord;
86
- /** 转录条目(已过滤掉非对象/空内容)。 */
87
- transcript: readonly ExpertUtterance[];
88
- /** 转录里被丢弃的坏行数。 */
89
- malformed: number;
90
- /** LLM 服务;缺席时全部走本地启发式兜底。 */
91
- llm?: SplitLlmLike;
92
- splitConfig: ReviewSplitConfig;
93
- /**
94
- * 拆分阶段向 LLM 发起的最大并发数。
95
- *
96
- * **通常不必传**:缺省时读 `splitConfig.llmConcurrency`(再缺省才用 4),于是两个
97
- * 调用方(工具 / RPC 直推)天然一致。显式传入只用于测试与排障(`1` = 回到串行)。
98
- */
99
- llmConcurrency?: number;
100
- }
101
- /**
102
- * 有界并发的 `map`(零依赖)。
103
- *
104
- * 为什么需要它:拆分一条发言要打一次 LLM(`split.ts` 每次 60s 超时),而此前是
105
- * **串行** `await` —— 一场 7 位专家、每人报告 3 次的评审 = 21 次串行往返,收集动作
106
- * 的墙钟时间 ≈ 21 × 单次往返(十秒级),期间界面只有一个禁用的"收集"按钮,既没有
107
- * 进度也没有取消(红队评审抓出的性能缺陷)。
108
- *
109
- * 并发上限刻意**不高**:这些请求打的是同一个 provider,并发拉满只会撞速率限制、
110
- * 反而更慢,而且要留出余量给界面自己的请求。
111
- *
112
- * 结果顺序与输入**逐位对应**(按下标回填,而不是按完成先后),所以拆出来的缺陷顺序
113
- * 与串行版本完全一致 —— 这是可测的,见 `test/engine.test.mjs`。
114
- */
115
- export declare function collectWithConcurrency<T, R>(items: readonly T[], limit: number, worker: (item: T, index: number) => Promise<R>): Promise<R[]>;
116
- /**
117
- * 把本轮专家发言收成逐条可裁决的缺陷。
118
- *
119
- * 五道处理,顺序有意义:
120
- * 1. **只收本轮**(`utterance.pass === record.reviewPass`);
121
- * 2. **幂等**(`utteranceId` 已收集过就跳过 —— 拆分后 defect.id 会变,不能用作去重键);
122
- * 3. **噪音过滤**(`auto-capture` 与进度汇报;真实数据里前者曾被当成缺陷收进裁决面);
123
- * 4. **拆分**(LLM 三道防线优先 → 本地标记切分兜底 → 整条兜底 `seq = 0`);
124
- * 5. **维度归一化 + 历史去重**(落库维度必须 ∈ 本次采用维度;上轮被驳回的观点不再弹)。
125
- *
126
- * 复审轮(`reviewPass > 1` 且已有历史)**不新增缺陷**:那一轮唯一的任务是核对旧账。
127
- */
128
- export declare function collectDefects(input: CollectInput): Promise<CollectOutcome>;
129
- /** 把用户偏好里的阵容与角色预设合成一份 roster。 */
130
- export declare function rosterFrom(input: {
131
- /** 阵容预设(每套是一份完整方案;由调用方按 id 挑出要用的那一套传入)。 */
132
- squads?: readonly {
133
- id: string;
134
- members: readonly ExpertRosterEntry[];
135
- }[];
136
- /** 角色预设(阵容缺席时的平铺来源)。 */
137
- rolePresets?: readonly ExpertRosterEntry[];
138
- /**
139
- * 选中的阵容 id(可选)。
140
- *
141
- * 只认显式指定的那一个:`squads` 是**多个互斥的整体方案**(设置页里每个都是一套
142
- * 完整成员),把几套混在一起会得到一个用户从没设计过的阵容。
143
- *
144
- * **缺席时自动采用第一套**(而不是全部不用):用户只需要维护一份阵容、不必再回
145
- * 设置页"选中"它 —— 少一步配置就少一处"我明明配了却不生效"的困惑。指定 id 不存在
146
- * 或该套成员为空时,同样退回第一套;再没有就退回角色预设。
147
- */
148
- squadId?: string;
149
- }): ExpertRosterEntry[];
150
- /**
151
- * 把收集结果并入记录(原地修改;调用方负责落盘)。
152
- *
153
- * ## 幂等(A2)
154
- *
155
- * 此前是纯拼接(`[...record.defects, ...added]`),于是**同一条发言被收集两次**
156
- * 就在裁决面上出现两条一模一样的缺陷:重试、双击、两个并发请求都能触发它。
157
- * 用户面对的是一个"必须点两次驳回、理由还得写两遍"的列表。
158
- *
159
- * 现在按 `defect.id` 去重(`id` 由 `collectDefects` 从 `utteranceId` + 序号生成,
160
- * 因此同一批输入两次得到同一批 id):**保留先到的那一条**,后来的同 id 项直接丢弃。
161
- * 保留先到的而不是新的,是因为"用户已经开始表态"的那一条不能被后来的副本顶掉
162
- * (缺陷的三态与驳回理由挂在 id 上,换掉实例等于丢掉已做出的判断)。
163
- *
164
- * `collectDefects` 里的 `utteranceId` 幂等键是第一道闸,这里的是第二道:前者防的是
165
- * "转录又被读了一遍",后者防的是"合并写回被施加了同一批结果" —— 两道都需要,
166
- * 因为并发请求可以在第一道闸之后、第二道闸之前交错。
167
- *
168
- * @returns **实际追加的条数**(被去重丢弃的不算)。调用方必须如实上报这个数字,
169
- * 而不是 `added.length` —— 那会把"请求成功了但什么都没新增"报成
170
- * "新增了 N 条",用户看到条数却对不上列表。
171
- */
172
- export declare function applyCollected(record: CrossfireRecord, added: readonly Defect[]): number;
@@ -1,40 +0,0 @@
1
- /**
2
- * 快照路由的条件请求支持(ETag / If-None-Match)。
3
- *
4
- * ## 要解决的问题
5
- *
6
- * 卡片每秒轮询快照,而一次快照的代价是"把工作区下所有评审的 `review.json` 读一遍并
7
- * **整包重发**"—— 其中绝大多数字节从未变化(红队评审抓出的性能缺陷)。
8
- *
9
- * 读盘那一半只能靠减少轮询次数缓解(已做:可见性感知 + 间隔放宽)。**传输**这一半
10
- * 可以用 ETag 解决:内容没变时返回 `304 Not Modified`,浏览器直接把已有响应交给
11
- * 调用方,省掉整个响应体的传输与解析。
12
- *
13
- * ## 为什么是弱校验
14
- *
15
- * 摘要只覆盖 `JSON.stringify(payload)` 的字节。同一份内容若换了键序就会算出不同的
16
- * ETag —— 那只会**少省一次**传输,不会给出错误数据(宁可多发,不可发错)。
17
- *
18
- * @module @y9931666/dsh-plugin-crossfire/etag
19
- */
20
- /** 计算一个内容摘要,用作 ETag 的取值(不带引号,由调用方按需包)。 */
21
- export declare function contentDigest(body: string): string;
22
- /** 最小化的响应面:只要能用任意状态码 + 响应头把 body 发出去。 */
23
- export interface ConditionalResponse {
24
- writeHead(status: number, headers: Record<string, string>): unknown;
25
- end(body?: string): unknown;
26
- }
27
- /**
28
- * 发送一个 JSON 响应,并在内容未变时改发 `304 Not Modified`。
29
- *
30
- * @param requestHeaders - 入站请求头(读 `if-none-match`)。
31
- * @returns `true` 表示请求已被处理(200 或 304 都已发出);调用方**不要**再写响应。
32
- *
33
- * 两条边界:
34
- * - **没有 `if-none-match` 时行为与从前逐字相同**(只是多一个 ETag 头),所以这是
35
- * 可增量上线的改动;客户端不需要同时改。
36
- * - `cache-control` 用 `no-cache` 而不是 `no-store`:前者允许浏览器存副本但**每次
37
- * 都必须回源校验**(我们因此能收到 `if-none-match`),语义上依然是"不直接用陈旧
38
- * 数据"。`no-store` 会让浏览器根本不存,ETag 也就永远不会被回传。
39
- */
40
- export declare function sendJsonConditional(requestHeaders: Record<string, string | string[] | undefined>, res: ConditionalResponse, payload: unknown, status?: number): boolean;
@@ -1,110 +0,0 @@
1
- /**
2
- * 专家子代理的生命周期接入。
3
- *
4
- * ## 为什么要这一层(来自圆桌的真实事故)
5
- *
6
- * 专家的产出**只在它主动提交的那一刻**才落盘。圆桌 v0-2-36 那场评审里,一位红队
7
- * 专家完成了大量取证,却在提交之前进程中断 —— 观点全丢,记录永久停在
8
- * `reviewing`、缺陷列表为空;9 条被拉起的路由里 4 条 0 产出,而插件只留下一个
9
- * `removed` 墓碑,没有任何失败原因,只能靠主持人用自然语言反复催办。
10
- *
11
- * 宿主其实把两件事递到手上:
12
- * - `subagent/end` 带 `lastAssistantMessage`,且可续聊子代理的**每一个 activation
13
- * epoch** 都会发这个事件 —— 于是产出能在主动提交之外被自动捕获;
14
- * - 同一事件带 `stopReason`(`error` / `max-tokens` / `aborted` …),基础设施级
15
- * 失败时 `lastAssistantMessage` 为空;而 `agent/request-error` 进一步带
16
- * `provider` 与 `failure: LlmFailure`(含 HTTP `status`),于是"为什么拉不起来"
17
- * (额度、鉴权、模型名写错)可以被写成一条可读的专家备注。
18
- *
19
- * ## 与圆桌的关键差异
20
- *
21
- * 自动捕获的发言 kind 是 `auto-capture`,而 **`auto-capture` 绝不进入缺陷收集**
22
- * (见 noise.ts):圆桌的真实数据里出现过 `[当前状态] 已收到主持人任务…`
23
- * 这类过程叙述被当成缺陷收进裁决列表,污染了用户的表态面。这里保留落盘(诊断
24
- * 价值真实存在),但收集阶段会把它过滤掉。
25
- *
26
- * 索引是**进程内**的:childId → (stateRoot, reviewId)。冷启动后的旧专家无法归属,
27
- * 这是刻意的 —— 宁可漏捕获,也不要把别的会话的文本写进评审记录。
28
- *
29
- * @module dsh-plugin-crossfire/expert-events
30
- */
31
- import type { Context } from '@deepseek-ai/cordis';
32
- import type { ContentBlock } from '@deepseek-ai/dsh-llm';
33
- /** 一位专家的评审在磁盘上的位置。 */
34
- export interface ExpertLocation {
35
- stateRoot: string;
36
- reviewId: string;
37
- }
38
- /** 记住一位刚被拉起的专家属于哪场评审。 */
39
- export declare function registerExpertChild(childId: string, location: ExpertLocation): void;
40
- /** 忘掉一位专家的索引条目(专家失败回滚,或评审已清理)。 */
41
- export declare function forgetExpertChild(childId: string): void;
42
- /** 按子会话 id 查一位专家的评审位置。 */
43
- export declare function locateExpertChild(childId: string): ExpertLocation | undefined;
44
- /**
45
- * 宽限查询:索引里没有就等一个极短窗口再查一次。
46
- *
47
- * ## 为什么需要它(红队在真实评审里抓到的竞态)
48
- *
49
- * 索引是在 `spawnExpert` **返回之后**才登记的,而宿主的 `startContinuable` 文档
50
- * 写明它"resolve at initial idle" —— 也就是说子代理可能在 `spawnExpert` 返回的
51
- * 那一瞬间就已经跑完第一轮并提交了发言。这几毫秒的窗口里:
52
- *
53
- * - `crossfire_report` 查不到归属 → 抛"你不在任何评审里",专家的产出直接丢掉;
54
- * - `subagent/end` 查不到归属 → 被当作"不属于任何评审"丢弃,专家永远停在 running。
55
- *
56
- * 概率极低(需要专家在几毫秒内完成整轮),但代价是**丢掉一整位专家的工作**,而且
57
- * 它无法复现、无法从外部诊断。所以这里不是掩盖问题,而是把这个窗口等过去。
58
- *
59
- * @param childId - 子会话 id。
60
- * @param graceMs - 等待窗口(默认 400ms,仅在一次查不到时才付出这点时间)。
61
- */
62
- export declare function locateExpertChildWithGrace(childId: string, graceMs?: number): Promise<ExpertLocation | undefined>;
63
- /** 仅供测试:清空进程内索引。 */
64
- export declare function __resetExpertIndexForTests(): void;
65
- /** 把一条助手输出里的文本块拼起来(没有则返回空串)。 */
66
- export declare function blocksToText(blocks: readonly ContentBlock[] | undefined): string;
67
- /** 一次子代理结束对它的评审意味着什么。 */
68
- export interface ExpertEndOutcome {
69
- /** 捕获到的输出;空串表示这一轮没有任何可用产出。 */
70
- text: string;
71
- /** 可读原因,在 `text` 为空时写进专家备注。 */
72
- note: string;
73
- }
74
- /**
75
- * 判定一次结束的子代理运行给评审带来了什么。
76
- *
77
- * 一个 `completed` 但没有任何文本的运行**不是**硬故障(专家可能只回了一句确认),
78
- * 所以它有自己的一句措辞 —— 要点是绝不让用户去猜"还在跑"和"已经死了"。
79
- */
80
- export declare function describeExpertEnd(info: {
81
- readonly stopReason?: string;
82
- readonly lastAssistantMessage?: readonly ContentBlock[];
83
- }): ExpertEndOutcome;
84
- /** 一句可读的 provider 请求失败说明。 */
85
- export declare function describeRequestFailure(provider: string, failure: {
86
- message?: string;
87
- code?: string;
88
- status?: number;
89
- } | undefined): string;
90
- /**
91
- * 订阅两个让专家产出与专家失败变得可见的宿主事件。
92
- *
93
- * 刻意非侵入:`subagent/end` 是普通 emit;`agent/request-error` 是 waterfall
94
- * 监听器,且**永远通过 `next()` 委托**,因此宿主自己的重试/恢复策略完全不受影响
95
- * —— 本插件只观察。
96
- */
97
- export declare function attachExpertEvents(ctx: Context): void;
98
- /** 专家活性(批 3 的卡片用它显示"还在跑 / 已就绪 / 已经没了")。 */
99
- export type ExpertLiveness = 'running' | 'idle' | 'ready' | 'missing';
100
- /**
101
- * 把专家状态与宿主真实的子代理树对账。
102
- *
103
- * `ctx.agents.get(id)` 只知道**活着的**驱动,所以进程已经没了的专家看起来和只是
104
- * 在两轮之间的一样 —— 都报 `ready`,用户无法区分"待唤醒"和"已经没了"。问一次
105
- * 持久目录就能分开:宿主还列着的 id 是"冷但真实"的子会话,不再认识的 id 才是
106
- * `missing`。
107
- */
108
- export declare function reconcileExpertLiveness(ctx: Context, parentSessionId: string, experts: readonly {
109
- childId?: string;
110
- }[]): Promise<Map<string, ExpertLiveness>>;
@@ -1,21 +0,0 @@
1
- /**
2
- * 把一场评审渲染成 Markdown 交付物。
3
- *
4
- * 与圆桌会议的 `export_review` 同一形态(带预填头的 Markdown,可直接贴进 issue
5
- * 或存档),但内容围绕 crossfire 的语义重组:**先给已认定缺陷**,因为那才是这
6
- * 场评审真正的产出;被驳回的观点另立一节,因为"哪些被否掉了"同样是结论,而且
7
- * 它是下一轮去重的依据。
8
- *
9
- * @module dsh-plugin-crossfire/export-md
10
- */
11
- import type { CrossfireRecord } from './types.ts';
12
- /** 渲染所需的统计(由 review.countDefects 提供)。 */
13
- export interface ExportCounts {
14
- total: number;
15
- pending: number;
16
- endorsed: number;
17
- rejected: number;
18
- strikes: number;
19
- }
20
- /** 渲染整场评审。 */
21
- export declare function renderReviewMarkdown(record: CrossfireRecord, counts: ExportCounts): string;
@@ -1,104 +0,0 @@
1
- /**
2
- * 宿主兼容与能力边界(Host compatibility boundary)——「可选能力的懒探测层,绝不成为加载门禁」。
3
- *
4
- * **所有与 DSH 版本相关的形状都集中在这个文件里。**
5
- * 理由与做法直接借鉴社区项目 dsh-agent-teams 的 `src/harness-compat.ts`:
6
- *
7
- * > Keep version-specific shapes here: **API presence alone is not a promise of
8
- * > support for future versions.**
9
- *
10
- * 我们的宿主是 **rc 线**,API 尚未冻结,破坏性变更属于常态。所以在写任何
11
- * `ctx.get(...)` / `ctx.on(...)` / 服务方法调用之前,先问一句:**这个形状在别的
12
- * rc 上还成立吗?** 如果答案需要翻源码,那它就应该写进本文件,而不是散进业务代码。
13
- *
14
- * 两条铁律:
15
- * 1. **探测绝不成为新的加载门禁**。这里没有 `inject`、没有静态导入宿主包,
16
- * 缺失的能力一律降级(返回 `undefined`),进程照常起来 ——「整个界面消失」
17
- * 这类事故的根因就是可选能力被当成必需能力。本文件只做**懒探测**:
18
- * 调用方用到哪条路由、哪份工作区列表,才在那一步探测一次。
19
- * 2. **降级由调用方留痕**。这里刻意**不打日志、不维护审计表**(探测点常落在
20
- * tick / 轮询路径上,见文末说明):本层只负责把"能不能用"如实回答成
21
- * `undefined` 或服务面,把"为什么没用上"交给拿到 `undefined` 的调用方
22
- * 记一条告警,让"某个功能为什么没生效"有一个能查的答案。
23
- *
24
- * @module @y9931666/dsh-plugin-crossfire/harness-compat
25
- */
26
- import type { Context } from '@deepseek-ai/cordis';
27
- /**
28
- * 读一个**可选**宿主服务。
29
- *
30
- * 统一走 `ctx.get`(cordis 核心、非严格:缺失返回 `undefined`)而不是属性访问
31
- * —— 后者在没有 `inject` 时会**直接抛错**:
32
- * `cannot get property "connection" without inject`。
33
- *
34
- * 三种失败一律折成 `undefined`,**本函数不产生任何副作用**:
35
- * - 宿主 `Context` 上连 `get` 都没有(形状变化);
36
- * - `ctx.get(name)` 自己抛错;
37
- * - 服务未挂载(返回 `null` / `undefined`)。
38
- * 调用方拿到 `undefined` 后自行决定降级行为与是否留下一条诊断。
39
- */
40
- export declare function optionalService<T>(ctx: Context, name: string): T | undefined;
41
- /** 可选服务键的结构式候选(宿主可能改名;按序取第一个可用的)。 */
42
- export declare const WEB_SERVER_KEYS: readonly ["webServer", "httpServer"];
43
- export declare const WORKSPACE_KEYS: readonly ["workspaceRegistry", "workspace"];
44
- /** webServer 服务面(只声明我们真正调用的部分)。
45
- *
46
- * `register` 的返回值刻意只声明为 `() => void`:宿主不同 rc 上返回过
47
- * disposer 与 `unknown`,而调用处一律以 `ctx.effect(() => webServer.register(…))`
48
- * 的形式**忽略返回值**——effect 自带卸载。声明成 `unknown` 会让
49
- * `ctx.effect` 的重载解析失败(它要求回调返回具体 Effect,不接受 unknown),
50
- * 所以这里保留"当它是清理函数"的写法。 */
51
- export interface WebRouteHost {
52
- register(route: {
53
- kind: 'exact';
54
- path: string;
55
- handler: (req: import('node:http').IncomingMessage, res: import('node:http').ServerResponse) => Promise<void> | void;
56
- }): () => void;
57
- }
58
- /** workspace 注册表面。 */
59
- export interface WorkspaceListHost {
60
- list(): {
61
- title: string;
62
- path: string;
63
- }[];
64
- }
65
- /**
66
- * 解析 webServer 服务面。
67
- *
68
- * 服务键是**结构式候选**而非固定名:宿主可能把 `webServer` 换成别的名字,
69
- * 所以按 `WEB_SERVER_KEYS` 顺序探测。headless profile 下取不到属预期,
70
- * 只降级不报错(返回 `undefined`,本函数不抛错也不打日志)。
71
- */
72
- export declare function webRouteHostOf(ctx: Context): WebRouteHost | undefined;
73
- /** 解析 workspace 注册表面(同上:结构式候选 + 静默降级为 `undefined`)。 */
74
- export declare function workspaceRegistryOf(ctx: Context): WorkspaceListHost | undefined;
75
- /**
76
- * 判定一个 `rpc.handle(...)` 返回值能不能当清理函数用。
77
- *
78
- * 宿主签名在 rc 线上变过:0.1.1 时代第三参 `{ authority }` 被移除,返回值在
79
- * 不同版本里出现过 `disposer` / `undefined` / Promise。这里只做**形状判定**,
80
- * 不 import 宿主类型:合法就交出去,不合法就交给调用方记一条降级。
81
- */
82
- export declare function asDisposer(value: unknown): (() => void) | undefined;
83
- /**
84
- * 在可选的清理函数存在时注册一个 effect。
85
- *
86
- * `connection.rpc.handle` 的返回值形状随版本变化,直接 `ctx.effect(() => () =>
87
- * release())` 会在返回值不是函数时静默留下一个永远不执行的清理器(通道泄漏)。
88
- *
89
- * @returns `true` 表示已注册 effect;`false` 表示返回值不是清理函数、**没有**注册
90
- * (调用方据此决定是否报告一次降级)。
91
- */
92
- export declare function effectWithOptionalDisposer(ctx: {
93
- effect(callback: () => () => void, label?: string): unknown;
94
- }, disposer: unknown, label: string): boolean;
95
- /**
96
- * 圆桌插件曾在本文件维护一张进程级能力审计表(`capabilityAudit` /
97
- * `noteCapability` / `formatCapabilityAudit`),让 `status` 与 `doctor` 能打印
98
- * "每个探测点第一次被看到时是什么状态"。
99
- *
100
- * crossfire 不搬这套:本文件现在的定位是**纯粹的可选能力懒探测层** ——
101
- * 只回答"这个服务此刻能不能用",不留全局状态、不写日志、不参与加载决策。
102
- * 诊断信息由**探测的调用方**在拿到 `undefined` 时按自己的上下文报告;
103
- * 这样探测层不持有跨调用状态,也不会在轮询路径上积累内存。
104
- */
@@ -1,89 +0,0 @@
1
- /**
2
- * Crossfire(针锋)—— host 插件入口。
3
- *
4
- * 它是一个**只做对抗式评审**的 host 插件:把一个已定稿的成果摆上台,让红队
5
- * 专家从指定维度分头攻击,把攻击拆成逐条可裁决的缺陷,由人逐条认定或驳回。
6
- *
7
- * 与圆桌会议(`@y9931666/dsh-plugin-roundtable`)的关系:**零代码耦合**。针锋是从
8
- * 圆桌里长出来的一件东西,但它不 import 圆桌的任何模块、不读圆桌的状态目录、
9
- * 不改圆桌的任何文件。两者共存的唯一代价是各自维护一份内核。
10
- *
11
- * 批 1 交付:插件骨架(Config + 偏好存储 + 一条 RPC web 路由 + 系统提示段)。
12
- * 批 2 加数据模型与 `crossfire_*` 工具;批 3 加轨迹内卡片;批 4 加立场区与
13
- * 设置页全量。
14
- *
15
- * @module dsh-plugin-crossfire
16
- */
17
- import type { Context } from '@deepseek-ai/cordis';
18
- import z from '@deepseek-ai/schemastery';
19
- /** 插件名(cordis entry id / 日志前缀 / bundle patch 里的 id)。 */
20
- export declare const name = "crossfire";
21
- /**
22
- * REQUIRED services only —— 这份清单是**加载门禁**,所以它必须短。
23
- *
24
- * cordis 没有"可选注入"这种形式(`Inject` 只有 `string[] | Record`,而 record
25
- * 形式是拦截配置),因此写在这里的每个名字都会让整个插件 fiber 保持 PENDING
26
- * 直到该服务存在:没有工具、没有 `/plugins/dsh-plugin-crossfire/*` 路由、没有
27
- * 卡片、也没有设置页。而缺一个**可选**能力却写在这里,在用户眼里就是"针锋整个
28
- * 不见了",且任何地方都不报错。
29
- *
30
- * 所以 `llm` / `webServer` / `workspaceRegistry` / `connection` 一律**不列**,
31
- * 全部经 `ctx.get(...)` 或 harness-compat 的探测层懒取:
32
- * - `llm` 缺席时,观点拆分退到本地启发式兜底(零 token、确定性);
33
- * - `webServer` 缺席时,只有设置页与卡片失联,host 侧与工具照常工作;
34
- * - `connection` 缺席时,RPC 只走插件自有路由。
35
- */
36
- export declare const inject: string[];
37
- /** 插件配置。 */
38
- export interface Config {
39
- /** 评审状态目录名(相对会话工作区,默认 `.crossfire`)。 */
40
- stateDir?: string;
41
- /** `ctx.subagents` 里用于拉起红队专家的 provider(默认 `spawn`)。 */
42
- memberProvider?: string;
43
- /** 一场评审的专家数硬上限(默认 7,与固定维度池同宽)。 */
44
- maxExperts?: number;
45
- /** 默认攻击强度档(默认 `standard`)。 */
46
- defaultIntensity?: 'quick' | 'standard' | 'deep';
47
- /** 专家子代理的委派深度上限(默认 1:专家不得再招人)。 */
48
- memberMaxDepth?: number;
49
- /** 用法段在全局系统提示里的排序位(默认 117,避开圆桌的 116)。 */
50
- promptSectionOrder?: number;
51
- /**
52
- * 专家**继承主持人**时实际生效的模型路由(provider / model)。
53
- *
54
- * 它只影响可观测性:开打时把这个生效值写回专家记录,卡片才能把"继承"画成真实
55
- * 品牌(DeepSeek 的蓝色 DS 块),而不是一个中性灰 `?`。取不到时不写,界面会退化成
56
- * `?` + `title="继承主持人"`,不会显示一个并非事实的路由。
57
- */
58
- defaultProvider?: string;
59
- defaultModel?: string;
60
- }
61
- /**
62
- * 配置 schema(schemastery)。
63
- *
64
- * 刻意**不**写 `: z<Config>` 注解:schemastery 的 `.default()` 会把字段推导成
65
- * `T | Volatile<T>`(例如 `string | Volatile<string>`),而手写接口写的是
66
- * `string | undefined`,二者不兼容会让 `tsc --emitDeclarationOnly` 以 TS2322
67
- * 失败 —— 注意 `--noEmit` **不报**这个错,所以它只在生成声明时暴露。
68
- * schema 自己就是配置形状的权威来源;下面的 `Config` 接口只用于 `apply` 的签名。
69
- */
70
- export declare const Config: z<Schemastery.ObjectS<{
71
- stateDir: z<string, string>;
72
- memberProvider: z<string, string>;
73
- maxExperts: z<number, number>;
74
- defaultIntensity: z<"quick" | "standard" | "deep", "quick" | "standard" | "deep">;
75
- memberMaxDepth: z<number, number>;
76
- defaultProvider: z<string, string>;
77
- defaultModel: z<string, string>;
78
- promptSectionOrder: z<number, number>;
79
- }>, Schemastery.ObjectT<{
80
- stateDir: z<string, string>;
81
- memberProvider: z<string, string>;
82
- maxExperts: z<number, number>;
83
- defaultIntensity: z<"quick" | "standard" | "deep", "quick" | "standard" | "deep">;
84
- memberMaxDepth: z<number, number>;
85
- defaultProvider: z<string, string>;
86
- defaultModel: z<string, string>;
87
- promptSectionOrder: z<number, number>;
88
- }>>;
89
- export declare function apply(ctx: Context, config: Config): void;