pi-firecode 0.8.1 → 1.0.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/README.md +58 -24
- package/README.zh-CN.md +74 -0
- package/config.example.jsonc +47 -57
- package/dist/config.example.jsonc +90 -0
- package/dist/index.js +11464 -0
- package/dist/master/prompts/master.en.md +22 -0
- package/dist/master/prompts/master.zh.md +22 -0
- package/dist/master/prompts/worker.en.md +1 -0
- package/dist/master/prompts/worker.zh.md +1 -0
- package/{review → dist/review}/prompts/review.en.md +7 -6
- package/{review → dist/review}/prompts/review.zh.md +7 -6
- package/dist/watcher/prompts/watch.en.md +43 -0
- package/{watcher → dist/watcher}/prompts/watch.zh.md +1 -1
- package/package.json +10 -27
- package/config.ts +0 -474
- package/deliver.ts +0 -32
- package/flame-frames.ts +0 -460
- package/format.ts +0 -80
- package/header.ts +0 -92
- package/herdr-client.ts +0 -60
- package/index.ts +0 -62
- package/jsonc.ts +0 -34
- package/master/event-card.ts +0 -106
- package/master/event-format.ts +0 -52
- package/master/index.ts +0 -1200
- package/master/prompt.ts +0 -26
- package/master/prompts/master.zh.md +0 -17
- package/master/prompts/worker.zh.md +0 -1
- package/master/role.ts +0 -13
- package/master/spawn.ts +0 -193
- package/master/state.ts +0 -201
- package/provider/claude-sub.ts +0 -129
- package/provider/openai-native/index.ts +0 -8
- package/provider/openai-native/src/compact-client.ts +0 -362
- package/provider/openai-native/src/config.ts +0 -297
- package/provider/openai-native/src/extension.ts +0 -93
- package/provider/openai-native/src/native-compaction.ts +0 -151
- package/provider/openai-native/src/native-details.ts +0 -157
- package/provider/openai-native/src/native-replay.ts +0 -253
- package/provider/openai-native/src/native-runtime.ts +0 -144
- package/provider/openai-native/src/options.ts +0 -85
- package/provider/openai-native/src/request-pipeline.ts +0 -21
- package/provider/openai-native/src/responses-input.ts +0 -433
- package/review/advisor.ts +0 -112
- package/review/card.ts +0 -385
- package/review/checkpoint.ts +0 -384
- package/review/evidence.ts +0 -232
- package/review/index.ts +0 -1246
- package/review/outcome.ts +0 -74
- package/review/progress.ts +0 -324
- package/review/prompt.ts +0 -210
- package/review/reviewer.ts +0 -356
- package/review/session.ts +0 -129
- package/review/state.ts +0 -905
- package/review/ui.ts +0 -528
- package/session/bark.ts +0 -157
- package/session/herdr-display.ts +0 -87
- package/session/presets.ts +0 -257
- package/session/rename.ts +0 -46
- package/session/stats.ts +0 -310
- package/session/working-flame.ts +0 -116
- package/statusbar/index.ts +0 -118
- package/statusbar/quota-cache.ts +0 -60
- package/statusbar/quota-parse.ts +0 -85
- package/statusbar/quota.ts +0 -203
- package/statusbar/render.ts +0 -181
- package/statusbar/tps.ts +0 -87
- package/theme.ts +0 -84
- package/tools/grouping.ts +0 -233
- package/tools/index.ts +0 -183
- package/tools/line.ts +0 -194
- package/tools/parts.ts +0 -143
- package/tools/timing.ts +0 -22
- package/watcher/card.ts +0 -85
- package/watcher/index.ts +0 -204
- package/watcher/observer.ts +0 -75
- package/watcher/transcript.ts +0 -64
- /package/{review → dist/review}/prompts/advisor.en.md +0 -0
- /package/{review → dist/review}/prompts/advisor.zh.md +0 -0
package/config.ts
DELETED
|
@@ -1,474 +0,0 @@
|
|
|
1
|
-
/** FireCode 配置:只读 Pi Agent 目录下的 `extensions/firecode/config.jsonc`。 */
|
|
2
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import { parseJsonc } from "./jsonc.js";
|
|
6
|
-
|
|
7
|
-
export type Language = "zh" | "en";
|
|
8
|
-
export type ThinkingLevelValue =
|
|
9
|
-
| "off"
|
|
10
|
-
| "minimal"
|
|
11
|
-
| "low"
|
|
12
|
-
| "medium"
|
|
13
|
-
| "high"
|
|
14
|
-
| "xhigh"
|
|
15
|
-
| "max";
|
|
16
|
-
|
|
17
|
-
export interface Preset {
|
|
18
|
-
provider?: string;
|
|
19
|
-
model?: string;
|
|
20
|
-
thinkingLevel?: ThinkingLevelValue;
|
|
21
|
-
tools?: string[];
|
|
22
|
-
instructions?: string;
|
|
23
|
-
/** 一键切换,如 alt+1;不填则无快捷键 */
|
|
24
|
-
key?: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface ReviewModel {
|
|
28
|
-
model: string;
|
|
29
|
-
thinking: ThinkingLevelValue;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** /fire-review 配置:审查者 / 顾问模型 + 循环限制。见 config.jsonc 的 review 节注释。 */
|
|
33
|
-
export interface ReviewConfig {
|
|
34
|
-
advisor: ReviewModel;
|
|
35
|
-
reviewers: ReviewModel[];
|
|
36
|
-
/** 审查轮数硬上限。 */
|
|
37
|
-
maxRounds: number;
|
|
38
|
-
/** 连续几轮失败触发顾问仲裁。 */
|
|
39
|
-
advisorAfterFailures: number;
|
|
40
|
-
/** 单个审查者 / 顾问会话超时(分钟)。 */
|
|
41
|
-
timeoutMinutes: number;
|
|
42
|
-
/** 审查者只读工具白名单。 */
|
|
43
|
-
tools: string[];
|
|
44
|
-
language: Language;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Master 角色表条目:原子模型已拆成运行时模型与思考档。 */
|
|
48
|
-
export interface MasterModelAtom {
|
|
49
|
-
model: string;
|
|
50
|
-
thinking: ThinkingLevelValue;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export const MASTER_ROLES = ["调研员", "工程师", "全栈", "架构师", "设计师", "哨兵"] as const;
|
|
54
|
-
export type MasterRoleName = (typeof MASTER_ROLES)[number];
|
|
55
|
-
|
|
56
|
-
export interface MasterRole extends MasterModelAtom {
|
|
57
|
-
role: MasterRoleName;
|
|
58
|
-
use: string;
|
|
59
|
-
fallback: MasterModelAtom[];
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export interface MasterConfig {
|
|
63
|
-
roles: MasterRole[];
|
|
64
|
-
workerExcludeExtensions: string[];
|
|
65
|
-
autoActivate: boolean;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** 观察员喂给观察会话的增量粒度:minimal 省略 reasoning 与 diff 正文。 */
|
|
69
|
-
export type WatcherContext = "minimal" | "full";
|
|
70
|
-
|
|
71
|
-
/** Watcher 观察员配置:模型与 thinking 必须显式配置,绝不回退默认模型。 */
|
|
72
|
-
export interface WatcherConfig {
|
|
73
|
-
enabled: boolean;
|
|
74
|
-
model: string;
|
|
75
|
-
thinking: ThinkingLevelValue;
|
|
76
|
-
context: WatcherContext;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export const FEATURES = [
|
|
80
|
-
"header",
|
|
81
|
-
"statusbar",
|
|
82
|
-
"tools",
|
|
83
|
-
"presets",
|
|
84
|
-
"rename",
|
|
85
|
-
"stats",
|
|
86
|
-
"claudeSub",
|
|
87
|
-
"openaiNative",
|
|
88
|
-
"workingFlame",
|
|
89
|
-
"bark",
|
|
90
|
-
"review",
|
|
91
|
-
"master",
|
|
92
|
-
"watcher",
|
|
93
|
-
] as const;
|
|
94
|
-
|
|
95
|
-
export type Feature = (typeof FEATURES)[number];
|
|
96
|
-
|
|
97
|
-
export const DEFAULT_KEYS = {
|
|
98
|
-
rename: "ctrl+r",
|
|
99
|
-
cyclePreset: "ctrl+shift+u",
|
|
100
|
-
fast: "ctrl+f",
|
|
101
|
-
} as const;
|
|
102
|
-
|
|
103
|
-
export type FireCodeKeys = {
|
|
104
|
-
rename: string;
|
|
105
|
-
cyclePreset: string;
|
|
106
|
-
fast: string;
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
export interface FireCodeConfig {
|
|
110
|
-
features: Partial<Record<Feature, boolean>>;
|
|
111
|
-
keys: FireCodeKeys;
|
|
112
|
-
presets: Record<string, Preset>;
|
|
113
|
-
review: ReviewConfig;
|
|
114
|
-
master: MasterConfig;
|
|
115
|
-
watcher: WatcherConfig;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export type LoadedConfig = {
|
|
119
|
-
config: FireCodeConfig;
|
|
120
|
-
problems: string[];
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
export const CONFIG_PATH = join(getAgentDir(), "extensions", "firecode", "config.jsonc");
|
|
124
|
-
|
|
125
|
-
function readFile(problems: string[]): Record<string, unknown> {
|
|
126
|
-
if (!existsSync(CONFIG_PATH)) {
|
|
127
|
-
problems.push("config.jsonc 不存在,已关闭可选功能");
|
|
128
|
-
return { features: Object.fromEntries(FEATURES.map((feature) => [feature, false])) };
|
|
129
|
-
}
|
|
130
|
-
try {
|
|
131
|
-
const parsed: unknown = parseJsonc(readFileSync(CONFIG_PATH, "utf8"));
|
|
132
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
133
|
-
problems.push("config.jsonc 顶层必须是对象");
|
|
134
|
-
return {};
|
|
135
|
-
}
|
|
136
|
-
return parsed as Record<string, unknown>;
|
|
137
|
-
} catch (error) {
|
|
138
|
-
// 统一前缀:文件级故障必须能被调用方识别并阻断功能,
|
|
139
|
-
// 不能因为消息文本不带节名就被当成无关问题过滤掉。
|
|
140
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
141
|
-
problems.push(`config.jsonc 解析失败:${message}`);
|
|
142
|
-
return {};
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
147
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function asRecord(value: unknown): Record<string, unknown> {
|
|
151
|
-
return value && typeof value === "object" && !Array.isArray(value)
|
|
152
|
-
? (value as Record<string, unknown>)
|
|
153
|
-
: {};
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function checkFeatures(features: Record<string, unknown>, problems: string[]): void {
|
|
157
|
-
for (const [key, value] of Object.entries(features)) {
|
|
158
|
-
if (!FEATURES.includes(key as Feature)) {
|
|
159
|
-
problems.push(`未知开关 features.${key},可用:${FEATURES.join(" / ")}`);
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
162
|
-
// 开关只能是布尔:写成字符串 "false" 时因为 `!== false` 仍会启用,
|
|
163
|
-
// 而启用 review 意味着真实的模型调用,不能静默放行。
|
|
164
|
-
if (typeof value !== "boolean")
|
|
165
|
-
problems.push(`features.${key} 必须是 true 或 false`);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function checkKeys(keys: FireCodeKeys, presets: Record<string, Preset>, problems: string[]): void {
|
|
170
|
-
const owners = new Map<string, string>([
|
|
171
|
-
[keys.rename, "keys.rename"],
|
|
172
|
-
[keys.cyclePreset, "keys.cyclePreset"],
|
|
173
|
-
[keys.fast, "keys.fast"],
|
|
174
|
-
]);
|
|
175
|
-
const declared = Object.entries(keys);
|
|
176
|
-
for (let index = 0; index < declared.length; index++) {
|
|
177
|
-
for (let other = index + 1; other < declared.length; other++) {
|
|
178
|
-
if (declared[index][1] === declared[other][1]) {
|
|
179
|
-
problems.push(
|
|
180
|
-
`快捷键 ${declared[index][1]} 被 keys.${declared[index][0]} 和 keys.${declared[other][0]} 重复占用`,
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
for (const [name, preset] of Object.entries(presets)) {
|
|
186
|
-
if (!preset?.key) continue;
|
|
187
|
-
const owner = owners.get(preset.key);
|
|
188
|
-
if (owner) problems.push(`快捷键 ${preset.key} 被 ${owner} 和预设 ${name} 重复占用`);
|
|
189
|
-
else owners.set(preset.key, `预设 ${name}`);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
let cached: LoadedConfig | undefined;
|
|
194
|
-
|
|
195
|
-
export function loadConfig(): LoadedConfig {
|
|
196
|
-
if (cached) return cached;
|
|
197
|
-
|
|
198
|
-
const problems: string[] = [];
|
|
199
|
-
const raw = readFile(problems);
|
|
200
|
-
// features 省略表示沿用默认全开;只要显式写了,就必须是对象。
|
|
201
|
-
// 非对象不能回退成 {},因为 {} 在入口语义里正是「全部启用」。
|
|
202
|
-
const invalidFeatures = raw.features !== undefined && !isPlainObject(raw.features);
|
|
203
|
-
if (invalidFeatures) problems.push("features 必须是对象");
|
|
204
|
-
const features: Partial<Record<Feature, boolean>> = invalidFeatures
|
|
205
|
-
? Object.fromEntries(FEATURES.map((feature) => [feature, false]))
|
|
206
|
-
: asRecord(raw.features);
|
|
207
|
-
const rawKeys = asRecord(raw.keys);
|
|
208
|
-
const presets = asRecord(raw.presets) as Record<string, Preset>;
|
|
209
|
-
const keys: FireCodeKeys = {
|
|
210
|
-
rename: typeof rawKeys.rename === "string" ? rawKeys.rename : DEFAULT_KEYS.rename,
|
|
211
|
-
cyclePreset:
|
|
212
|
-
typeof rawKeys.cyclePreset === "string" ? rawKeys.cyclePreset : DEFAULT_KEYS.cyclePreset,
|
|
213
|
-
fast: typeof rawKeys.fast === "string" ? rawKeys.fast : DEFAULT_KEYS.fast,
|
|
214
|
-
};
|
|
215
|
-
checkFeatures(features, problems);
|
|
216
|
-
checkKeys(keys, presets, problems);
|
|
217
|
-
// review 写成字符串/数组/null 或缺字段时不能静默补齐:会拿用户未选择的模型真实发起审查。
|
|
218
|
-
const reviewProblems: string[] = [];
|
|
219
|
-
if (raw.review !== undefined && !isPlainObject(raw.review))
|
|
220
|
-
reviewProblems.push("review 必须是对象");
|
|
221
|
-
const review = parseReviewConfig(asRecord(raw.review), reviewProblems);
|
|
222
|
-
if (raw.review !== undefined || features.review !== false) problems.push(...reviewProblems);
|
|
223
|
-
// master 同理:角色表错误会拿错模型真实发起 Worker,不能静默当空对象。
|
|
224
|
-
if (raw.master !== undefined && !isPlainObject(raw.master))
|
|
225
|
-
problems.push("master 必须是对象");
|
|
226
|
-
const master = parseMasterConfig(asRecord(raw.master), problems);
|
|
227
|
-
// watcher 同理:缺节或模型有误时功能拒绝启动,静默回退会拿用户没配的模型真实发起观察。
|
|
228
|
-
const watcherProblems: string[] = [];
|
|
229
|
-
if (raw.watcher !== undefined && !isPlainObject(raw.watcher))
|
|
230
|
-
watcherProblems.push("watcher 必须是对象");
|
|
231
|
-
const watcher = parseWatcherConfig(asRecord(raw.watcher), watcherProblems);
|
|
232
|
-
if (raw.watcher !== undefined || features.watcher !== false) problems.push(...watcherProblems);
|
|
233
|
-
|
|
234
|
-
cached = { config: { features, keys, presets, review, master, watcher }, problems };
|
|
235
|
-
return cached;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// ---- review 节 ----
|
|
239
|
-
|
|
240
|
-
const REVIEW_KEYS = new Set([
|
|
241
|
-
"advisor",
|
|
242
|
-
"reviewers",
|
|
243
|
-
"maxRounds",
|
|
244
|
-
"advisorAfterFailures",
|
|
245
|
-
"timeoutMinutes",
|
|
246
|
-
"tools",
|
|
247
|
-
"language",
|
|
248
|
-
]);
|
|
249
|
-
const DEFAULT_TOOLS = ["read", "grep", "find", "ls", "bash"];
|
|
250
|
-
const EMPTY_REVIEW_MODEL: ReviewModel = { model: "", thinking: "medium" };
|
|
251
|
-
const THINKING_LEVELS = new Set<ThinkingLevelValue>([
|
|
252
|
-
"off",
|
|
253
|
-
"minimal",
|
|
254
|
-
"low",
|
|
255
|
-
"medium",
|
|
256
|
-
"high",
|
|
257
|
-
"xhigh",
|
|
258
|
-
"max",
|
|
259
|
-
]);
|
|
260
|
-
const LANGUAGES = new Set<Language>(["zh", "en"]);
|
|
261
|
-
|
|
262
|
-
/** 导出供测试:严格拒绝未知字段(含嵌套),类型错误一律记录而非静默回退。 */
|
|
263
|
-
export function parseReviewConfig(raw: Record<string, unknown>, problems: string[]): ReviewConfig {
|
|
264
|
-
for (const key of Object.keys(raw)) {
|
|
265
|
-
if (REVIEW_KEYS.has(key)) continue;
|
|
266
|
-
problems.push(key === "background"
|
|
267
|
-
? "review.background 已随审查子进程层删除,请直接移除该键"
|
|
268
|
-
: `未知字段 review.${key}`);
|
|
269
|
-
}
|
|
270
|
-
for (const key of REVIEW_KEYS)
|
|
271
|
-
if (!(key in raw)) problems.push(`review.${key} 必须显式配置`);
|
|
272
|
-
const advisor = reviewModel(raw.advisor, "review.advisor", EMPTY_REVIEW_MODEL, problems);
|
|
273
|
-
const reviewers = reviewModels(raw.reviewers, problems);
|
|
274
|
-
return {
|
|
275
|
-
advisor,
|
|
276
|
-
reviewers,
|
|
277
|
-
maxRounds: reviewInt(raw.maxRounds, "review.maxRounds", 5, 1, 10, problems),
|
|
278
|
-
advisorAfterFailures: reviewInt(raw.advisorAfterFailures, "review.advisorAfterFailures", 2, 1, 5, problems),
|
|
279
|
-
timeoutMinutes: reviewInt(raw.timeoutMinutes, "review.timeoutMinutes", 20, 1, 60, problems),
|
|
280
|
-
tools: reviewTools(raw.tools, problems),
|
|
281
|
-
language: reviewLanguage(raw.language, problems),
|
|
282
|
-
};
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/** 嵌套对象也做键白名单:拼写错误必须报出来,不能静默回退默认值。 */
|
|
286
|
-
function rejectUnknownKeys(
|
|
287
|
-
record: Record<string, unknown>,
|
|
288
|
-
allowed: readonly string[],
|
|
289
|
-
field: string,
|
|
290
|
-
problems: string[],
|
|
291
|
-
) {
|
|
292
|
-
for (const key of Object.keys(record))
|
|
293
|
-
if (!allowed.includes(key)) problems.push(`未知字段 ${field}.${key}`);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
function reviewModel(
|
|
297
|
-
value: unknown,
|
|
298
|
-
field: string,
|
|
299
|
-
fallback: ReviewModel,
|
|
300
|
-
problems: string[],
|
|
301
|
-
): ReviewModel {
|
|
302
|
-
if (value === undefined) return fallback;
|
|
303
|
-
const record = asRecord(value);
|
|
304
|
-
rejectUnknownKeys(record, ["model", "thinking"], field, problems);
|
|
305
|
-
if (!record.model || typeof record.model !== "string") {
|
|
306
|
-
problems.push(`${field}.model 必须是非空字符串`);
|
|
307
|
-
return fallback;
|
|
308
|
-
}
|
|
309
|
-
const thinking =
|
|
310
|
-
typeof record.thinking === "string" && THINKING_LEVELS.has(record.thinking as ThinkingLevelValue)
|
|
311
|
-
? (record.thinking as ThinkingLevelValue)
|
|
312
|
-
: fallback.thinking;
|
|
313
|
-
if (thinking !== record.thinking) problems.push(`${field}.thinking 值无效`);
|
|
314
|
-
return { model: record.model, thinking };
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
function reviewModels(value: unknown, problems: string[]): ReviewModel[] {
|
|
318
|
-
if (!Array.isArray(value) || value.length === 0 || value.length > 5) {
|
|
319
|
-
if (value !== undefined) problems.push("review.reviewers 必须包含 1–5 个模型");
|
|
320
|
-
return [];
|
|
321
|
-
}
|
|
322
|
-
return value.map((item, index) =>
|
|
323
|
-
reviewModel(item, `review.reviewers[${index}]`, EMPTY_REVIEW_MODEL, problems),
|
|
324
|
-
);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
function reviewInt(
|
|
328
|
-
value: unknown,
|
|
329
|
-
field: string,
|
|
330
|
-
fallback: number,
|
|
331
|
-
min: number,
|
|
332
|
-
max: number,
|
|
333
|
-
problems: string[],
|
|
334
|
-
): number {
|
|
335
|
-
if (value === undefined) return fallback;
|
|
336
|
-
if (typeof value !== "number" || !Number.isInteger(value) || value < min || value > max) {
|
|
337
|
-
problems.push(`${field} 必须是 ${min}–${max} 的整数`);
|
|
338
|
-
return fallback;
|
|
339
|
-
}
|
|
340
|
-
return value;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
function reviewTools(value: unknown, problems: string[]): string[] {
|
|
344
|
-
if (value === undefined) return [...DEFAULT_TOOLS];
|
|
345
|
-
if (!Array.isArray(value)) {
|
|
346
|
-
problems.push("review.tools 必须是字符串数组");
|
|
347
|
-
return [...DEFAULT_TOOLS];
|
|
348
|
-
}
|
|
349
|
-
const tools = value.filter((item): item is string => typeof item === "string" && item.length > 0);
|
|
350
|
-
if (tools.length !== value.length || tools.length === 0)
|
|
351
|
-
problems.push("review.tools 必须是非空字符串数组");
|
|
352
|
-
return tools.length > 0 ? tools : [...DEFAULT_TOOLS];
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// ---- master 节 ----
|
|
356
|
-
|
|
357
|
-
/** 导出供测试:与 review 节同样严格拒绝未知字段,类型错误记录而非静默回退。 */
|
|
358
|
-
export function parseMasterConfig(raw: Record<string, unknown>, problems: string[]): MasterConfig {
|
|
359
|
-
for (const key of Object.keys(raw))
|
|
360
|
-
if (key !== "roles" && key !== "workerExcludeExtensions" && key !== "autoActivate")
|
|
361
|
-
problems.push(`未知字段 master.${key}`);
|
|
362
|
-
const exclusions = stringArray(raw.workerExcludeExtensions, "master.workerExcludeExtensions", problems);
|
|
363
|
-
const autoActivate = booleanValue(raw.autoActivate, "master.autoActivate", true, problems);
|
|
364
|
-
if (raw.roles === undefined)
|
|
365
|
-
return { roles: [], workerExcludeExtensions: exclusions, autoActivate };
|
|
366
|
-
if (!isPlainObject(raw.roles) || Object.keys(raw.roles).length === 0) {
|
|
367
|
-
problems.push("master.roles 必须是至少包含一个固定角色的对象");
|
|
368
|
-
return { roles: [], workerExcludeExtensions: exclusions, autoActivate };
|
|
369
|
-
}
|
|
370
|
-
const configured = raw.roles;
|
|
371
|
-
for (const role of Object.keys(configured))
|
|
372
|
-
if (!MASTER_ROLES.includes(role as MasterRoleName))
|
|
373
|
-
problems.push(`未知角色 master.roles.${role},可用:${MASTER_ROLES.join(" / ")}`);
|
|
374
|
-
const roles = MASTER_ROLES.flatMap((role) =>
|
|
375
|
-
Object.hasOwn(configured, role)
|
|
376
|
-
? [masterRole(configured[role], `master.roles.${role}`, role, problems)]
|
|
377
|
-
: []);
|
|
378
|
-
return { roles, workerExcludeExtensions: exclusions, autoActivate };
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
function booleanValue(value: unknown, field: string, fallback: boolean, problems: string[]): boolean {
|
|
382
|
-
if (value === undefined) return fallback;
|
|
383
|
-
if (typeof value === "boolean") return value;
|
|
384
|
-
problems.push(`${field} 必须是 true 或 false`);
|
|
385
|
-
return fallback;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
function stringArray(value: unknown, field: string, problems: string[]): string[] {
|
|
389
|
-
if (value === undefined) return [];
|
|
390
|
-
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || !item)) {
|
|
391
|
-
problems.push(`${field} 必须是非空字符串数组`);
|
|
392
|
-
return [];
|
|
393
|
-
}
|
|
394
|
-
return [...new Set(value)];
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
function masterRole(
|
|
398
|
-
value: unknown,
|
|
399
|
-
field: string,
|
|
400
|
-
role: MasterRoleName,
|
|
401
|
-
problems: string[],
|
|
402
|
-
): MasterRole {
|
|
403
|
-
const record = asRecord(value);
|
|
404
|
-
rejectUnknownKeys(record, ["model", "use", "fallback"], field, problems);
|
|
405
|
-
const atom = masterModelAtom(record.model, `${field}.model`, problems);
|
|
406
|
-
const use = typeof record.use === "string" && record.use ? record.use : "";
|
|
407
|
-
if (!use) problems.push(`${field}.use 必须是非空字符串`);
|
|
408
|
-
const fallback = masterFallback(record.fallback, `${field}.fallback`, problems);
|
|
409
|
-
return { role, ...atom, use, fallback };
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
function masterFallback(value: unknown, field: string, problems: string[]): MasterModelAtom[] {
|
|
413
|
-
if (value === undefined) return [];
|
|
414
|
-
if (!Array.isArray(value) || value.length > 2) {
|
|
415
|
-
problems.push(`${field} 必须是至多 2 项的数组`);
|
|
416
|
-
return [];
|
|
417
|
-
}
|
|
418
|
-
return value.map((item, index) => masterModelAtom(item, `${field}[${index}]`, problems));
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
function masterModelAtom(value: unknown, field: string, problems: string[]): MasterModelAtom {
|
|
422
|
-
if (typeof value !== "string" || !value) {
|
|
423
|
-
problems.push(`${field} 必须是“供应商/模型/思考档”字符串`);
|
|
424
|
-
return { model: "", thinking: "medium" };
|
|
425
|
-
}
|
|
426
|
-
const slash = value.lastIndexOf("/");
|
|
427
|
-
const model = slash > 0 ? value.slice(0, slash) : "";
|
|
428
|
-
const thinking = slash > 0 ? value.slice(slash + 1) : value;
|
|
429
|
-
if (!THINKING_LEVELS.has(thinking as ThinkingLevelValue))
|
|
430
|
-
problems.push(`${field} 思考档无效:${thinking}`);
|
|
431
|
-
const providerSlash = model.indexOf("/");
|
|
432
|
-
if (providerSlash <= 0 || providerSlash === model.length - 1)
|
|
433
|
-
problems.push(`${field} 模型无效:必须是 provider/model`);
|
|
434
|
-
return {
|
|
435
|
-
model,
|
|
436
|
-
thinking: THINKING_LEVELS.has(thinking as ThinkingLevelValue)
|
|
437
|
-
? thinking as ThinkingLevelValue
|
|
438
|
-
: "medium",
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
// ---- watcher 节 ----
|
|
443
|
-
|
|
444
|
-
const WATCHER_KEYS = ["enabled", "model", "thinking", "context"] as const;
|
|
445
|
-
const WATCHER_CONTEXTS = new Set<WatcherContext>(["minimal", "full"]);
|
|
446
|
-
|
|
447
|
-
/** 导出供测试:model/thinking 必填,enabled 默认 true、context 默认 minimal。 */
|
|
448
|
-
export function parseWatcherConfig(raw: Record<string, unknown>, problems: string[]): WatcherConfig {
|
|
449
|
-
rejectUnknownKeys(raw, WATCHER_KEYS, "watcher", problems);
|
|
450
|
-
const enabled = booleanValue(raw.enabled, "watcher.enabled", true, problems);
|
|
451
|
-
const model = typeof raw.model === "string" && raw.model ? raw.model : "";
|
|
452
|
-
if (!model) problems.push("watcher.model 必须显式配置为非空字符串");
|
|
453
|
-
let thinking: ThinkingLevelValue | undefined;
|
|
454
|
-
if (raw.thinking === undefined) problems.push("watcher.thinking 必须显式配置");
|
|
455
|
-
else if (typeof raw.thinking === "string" && THINKING_LEVELS.has(raw.thinking as ThinkingLevelValue))
|
|
456
|
-
thinking = raw.thinking as ThinkingLevelValue;
|
|
457
|
-
else problems.push("watcher.thinking 值无效");
|
|
458
|
-
let context: WatcherContext = "minimal";
|
|
459
|
-
if (raw.context !== undefined) {
|
|
460
|
-
if (typeof raw.context === "string" && WATCHER_CONTEXTS.has(raw.context as WatcherContext))
|
|
461
|
-
context = raw.context as WatcherContext;
|
|
462
|
-
else problems.push("watcher.context 必须是 minimal 或 full");
|
|
463
|
-
}
|
|
464
|
-
return { enabled, model, thinking: thinking ?? "low", context };
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
function reviewLanguage(value: unknown, problems: string[]): Language {
|
|
468
|
-
if (value === undefined) return "zh";
|
|
469
|
-
if (typeof value !== "string" || !LANGUAGES.has(value as Language)) {
|
|
470
|
-
problems.push("review.language 必须是 zh 或 en");
|
|
471
|
-
return "zh";
|
|
472
|
-
}
|
|
473
|
-
return value as Language;
|
|
474
|
-
}
|
package/deliver.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 统一投递入口(Master 事件与观察员发言共用):宿主流式中投自定义卡片、经
|
|
3
|
-
* steer 队列在句缝送达;会话歇透时改走 sendUserMessage 前门唤起——宿主的
|
|
4
|
-
* triggerTurn 唤醒会跳过 before_agent_start(上游缺陷,#33),前门唤醒自带
|
|
5
|
-
* 完整开跑仪式,系统提示注入不随回合抖动。
|
|
6
|
-
*
|
|
7
|
-
* 忙闲判断与发送必须在同一事件循环节拍内完成,两者之间禁止 await:会话落定
|
|
8
|
-
* 是下一节拍的事件,同节拍读到的忙闲不会骑墙;宿主在回合结束前清空 steer
|
|
9
|
-
* 队列,忙时入队的消息以同回合续跑送达,不会沦为唤醒者。
|
|
10
|
-
*/
|
|
11
|
-
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
12
|
-
|
|
13
|
-
export interface Envelope<T> {
|
|
14
|
-
customType: string;
|
|
15
|
-
content: string;
|
|
16
|
-
details?: T;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export async function deliver<T>(
|
|
20
|
-
pi: ExtensionAPI,
|
|
21
|
-
ctx: ExtensionContext,
|
|
22
|
-
envelope: Envelope<T>,
|
|
23
|
-
): Promise<void> {
|
|
24
|
-
if (ctx.isIdle?.() === true) {
|
|
25
|
-
await pi.sendUserMessage(envelope.content);
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
pi.sendMessage<T>(
|
|
29
|
-
{ customType: envelope.customType, content: envelope.content, display: true, details: envelope.details },
|
|
30
|
-
{ deliverAs: "steer" },
|
|
31
|
-
);
|
|
32
|
-
}
|