@buyi1net/pi-toolkit 0.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/LICENSE +21 -0
- package/README.md +45 -0
- package/assembler.ts +127 -0
- package/config.ts +201 -0
- package/i18n.ts +693 -0
- package/index.ts +99 -0
- package/menu/items.ts +264 -0
- package/menu/panels.ts +91 -0
- package/menu/settings-list.ts +84 -0
- package/menu/theme.ts +31 -0
- package/menu/toolkit-menu.ts +139 -0
- package/module.ts +156 -0
- package/modules/eyes/chain.ts +173 -0
- package/modules/eyes/config.ts +319 -0
- package/modules/eyes/index.ts +161 -0
- package/modules/eyes/menu.ts +828 -0
- package/modules/eyes/pi-model-backend.ts +245 -0
- package/modules/eyes/resilience.ts +161 -0
- package/modules/eyes/vision-bridge.ts +232 -0
- package/modules/eyes/vision-cache.ts +86 -0
- package/modules/eyes/vision-json.ts +19 -0
- package/modules/eyes/vision-preprocess.ts +192 -0
- package/modules/eyes/vision-probe.ts +21 -0
- package/modules/eyes/vision-prompt.ts +90 -0
- package/modules/eyes/vision-tool.ts +59 -0
- package/modules/eyes/vision-types.ts +33 -0
- package/modules/index.ts +10 -0
- package/modules/subagents/agents/researcher.md +51 -0
- package/modules/subagents/agents/scout.md +40 -0
- package/modules/subagents/agents/worker.md +79 -0
- package/modules/subagents/config.json.example +6 -0
- package/modules/subagents/config.ts +144 -0
- package/modules/subagents/index.ts +91 -0
- package/modules/subagents/menu.ts +388 -0
- package/modules/subagents/src/activity.ts +511 -0
- package/modules/subagents/src/agents.ts +126 -0
- package/modules/subagents/src/command.ts +37 -0
- package/modules/subagents/src/dependencies.ts +246 -0
- package/modules/subagents/src/diagnostics.ts +13 -0
- package/modules/subagents/src/display.ts +94 -0
- package/modules/subagents/src/headless.ts +342 -0
- package/modules/subagents/src/herdr.ts +203 -0
- package/modules/subagents/src/index.ts +2798 -0
- package/modules/subagents/src/inspect-tool.ts +839 -0
- package/modules/subagents/src/launch-config.ts +196 -0
- package/modules/subagents/src/layout-budget.ts +26 -0
- package/modules/subagents/src/list-tool.ts +292 -0
- package/modules/subagents/src/message-tool.ts +808 -0
- package/modules/subagents/src/names.ts +17 -0
- package/modules/subagents/src/pane-layout.ts +49 -0
- package/modules/subagents/src/params.ts +145 -0
- package/modules/subagents/src/renderers.ts +181 -0
- package/modules/subagents/src/result.ts +43 -0
- package/modules/subagents/src/retention.ts +114 -0
- package/modules/subagents/src/route-error.ts +212 -0
- package/modules/subagents/src/routing.ts +235 -0
- package/modules/subagents/src/runtime-registry.ts +159 -0
- package/modules/subagents/src/session.ts +801 -0
- package/modules/subagents/src/status.ts +513 -0
- package/modules/subagents/src/stop-tool.ts +235 -0
- package/modules/subagents/src/subagent-done.ts +590 -0
- package/modules/subagents/src/subagent-tool.ts +1155 -0
- package/modules/subagents/src/surface.ts +355 -0
- package/modules/subagents/src/team-dispatch-tool.ts +219 -0
- package/modules/subagents/src/team.ts +232 -0
- package/modules/subagents/src/tmux.ts +210 -0
- package/modules/subagents/src/tools/safe-bash.ts +72 -0
- package/modules/subagents/src/types.ts +131 -0
- package/modules/tui/adapter/provider-usage.ts +143 -0
- package/modules/tui/config.ts +50 -0
- package/modules/tui/index.ts +120 -0
- package/modules/tui/kernel/pkg/shared/grok-subscription.ts +169 -0
- package/modules/tui/kernel/pkg/shared/official-subscription.ts +237 -0
- package/modules/tui/kernel/pkg/shared/provider-catalog.ts +367 -0
- package/modules/tui/kernel/pkg/shared/provider-contracts.ts +150 -0
- package/modules/tui/kernel/pkg/shared/provider-display.ts +55 -0
- package/modules/tui/kernel/pkg/shared/provider-parsers.ts +171 -0
- package/modules/tui/kernel/pkg/shared/volcengine.ts +191 -0
- package/modules/tui/kernel/pkg/shared/zhipu.ts +149 -0
- package/modules/tui/kernel/pkg/usage-core/index.ts +335 -0
- package/modules/tui/kernel/pkg/usage-core/provider-routes.ts +187 -0
- package/modules/tui/kernel/pkg/usage-node/index.ts +635 -0
- package/modules/tui/kernel/pkg/usage-node/provider-usage.ts +388 -0
- package/modules/tui/kernel/usage-core.ts +2 -0
- package/modules/tui/kernel/usage-node.ts +2 -0
- package/modules/tui/menu.ts +418 -0
- package/modules/tui/plugin/editor.ts +396 -0
- package/modules/tui/plugin/footer.ts +161 -0
- package/modules/tui/plugin/index.ts +30 -0
- package/modules/tui/plugin/lifecycle.ts +637 -0
- package/modules/tui/plugin/package-order.ts +169 -0
- package/modules/tui/plugin/screen-transition.ts +203 -0
- package/modules/tui/plugin/settings-config.ts +297 -0
- package/modules/tui/plugin/status-sources.ts +49 -0
- package/modules/tui/plugin/transition-gate.ts +261 -0
- package/modules/tui/renderer/custom-header.ts +157 -0
- package/modules/tui/renderer/editor.ts +70 -0
- package/modules/tui/renderer/header.ts +72 -0
- package/modules/tui/renderer/icons.ts +149 -0
- package/modules/tui/renderer/pi-installer-logo.ts +194 -0
- package/modules/tui/status/auto-compaction.ts +67 -0
- package/modules/tui/status/project-status.ts +655 -0
- package/modules/tui/status/provider-status.ts +120 -0
- package/modules/tui/status/runtime-status.ts +307 -0
- package/modules/tui/status/session-status.ts +325 -0
- package/modules/tui/status/status-config.ts +95 -0
- package/modules/tui/status/status-segments.ts +263 -0
- package/modules/tui/status/turn-telemetry.ts +466 -0
- package/modules/tui/themes/LICENSE.pi-themes-bundle +21 -0
- package/modules/tui/themes/UPSTREAM.md +7 -0
- package/modules/tui/themes/catppuccin-latte.json +80 -0
- package/modules/tui/themes/catppuccin-mocha.json +79 -0
- package/modules/tui/themes/crimson-noir.json +85 -0
- package/modules/tui/themes/dracula.json +79 -0
- package/modules/tui/themes/everforest-dark.json +85 -0
- package/modules/tui/themes/gruvbox-dark.json +85 -0
- package/modules/tui/themes/gruvbox-light.json +85 -0
- package/modules/tui/themes/matrix.json +85 -0
- package/modules/tui/themes/nord.json +85 -0
- package/modules/tui/themes/one-dark.json +85 -0
- package/modules/tui/themes/rose-pine-dawn.json +85 -0
- package/modules/tui/themes/rose-pine.json +85 -0
- package/modules/tui/themes/solarized-dark.json +85 -0
- package/modules/tui/themes/solarized-light.json +85 -0
- package/modules/tui/themes/tokyo-night-storm.json +79 -0
- package/modules/tui/themes/tokyo-night.json +79 -0
- package/package.json +28 -0
- package/services.ts +38 -0
- package/toolkit.ts +147 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
// 视觉模块的配置解析与保存:读写 pi-toolkit.json 里 `modules.eyes` 节。
|
|
2
|
+
//
|
|
3
|
+
// 结构照搬 pi-eyes(`backend.route.{mode,allowedModels,fixedModel}`),语言不再重复存——
|
|
4
|
+
// 界面语言用骨架的顶层 `language` 键,本模块只保留视觉路由。
|
|
5
|
+
// 按规格决策 8,不做旧配置迁移:只认 automatic / fixed 两种模式。
|
|
6
|
+
// 项目层覆盖沿用 pi-eyes 语义:仅当项目受信任时读取 `<cwd>/.pi/pi-toolkit.json` 的同一节,
|
|
7
|
+
// 按字段(mode / allowedModels / fixedModel)逐项覆盖全局;非法层整体忽略并给出告警。
|
|
8
|
+
|
|
9
|
+
import { CONFIG_DIR_NAME, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { getToolkitConfigPath, loadToolkitConfig, saveToolkitConfig, TOOLKIT_CONFIG_FILENAME } from "../../config.ts";
|
|
12
|
+
import type { MessageKey, Translator } from "../../i18n.ts";
|
|
13
|
+
import type { ModuleConfigRecord, ModuleConfigValue } from "../../module.ts";
|
|
14
|
+
import type { VisionRoutingConfig } from "./chain.ts";
|
|
15
|
+
|
|
16
|
+
/** 模块 id:同时是配置节名与菜单项 id 前缀 */
|
|
17
|
+
export const EYES_MODULE_ID = "eyes";
|
|
18
|
+
|
|
19
|
+
const SECTION_FIELD = `modules.${EYES_MODULE_ID}`;
|
|
20
|
+
const ROUTE_FIELD = `${SECTION_FIELD}.backend.route`;
|
|
21
|
+
|
|
22
|
+
export type VisionRouteMode = "automatic" | "fixed";
|
|
23
|
+
|
|
24
|
+
export interface VisionModelSelection {
|
|
25
|
+
provider: string;
|
|
26
|
+
model: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface EyesRouteConfig {
|
|
30
|
+
mode: VisionRouteMode;
|
|
31
|
+
/** null 表示不限制候选;空数组表示关闭视觉辅助(pi-eyes 的 off 语义) */
|
|
32
|
+
allowedModels: VisionModelSelection[] | null;
|
|
33
|
+
fixedModel?: VisionModelSelection;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface EyesConfig {
|
|
37
|
+
route: EyesRouteConfig;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const DEFAULT_EYES_CONFIG: EyesConfig = {
|
|
41
|
+
route: { mode: "automatic", allowedModels: null },
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** 配置告警码:文案在 i18n 键表,配置层不带语言 */
|
|
45
|
+
export type EyesConfigProblemCode =
|
|
46
|
+
| "notObject"
|
|
47
|
+
| "notNonEmptyString"
|
|
48
|
+
| "badMode"
|
|
49
|
+
| "allowedModelsNotArray"
|
|
50
|
+
| "fixedRequired";
|
|
51
|
+
|
|
52
|
+
export interface EyesConfigProblem {
|
|
53
|
+
readonly code: EyesConfigProblemCode;
|
|
54
|
+
/** 出问题的字段路径,如 modules.eyes.backend.route.mode */
|
|
55
|
+
readonly field: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type EyesConfigWarning =
|
|
59
|
+
| { readonly kind: "problem"; readonly source: string; readonly problem: EyesConfigProblem }
|
|
60
|
+
| { readonly kind: "detail"; readonly detail: string };
|
|
61
|
+
|
|
62
|
+
const PROBLEM_KEYS: Record<EyesConfigProblemCode, MessageKey> = {
|
|
63
|
+
notObject: "module.eyes.configError.notObject",
|
|
64
|
+
notNonEmptyString: "module.eyes.configError.notNonEmptyString",
|
|
65
|
+
badMode: "module.eyes.configError.badMode",
|
|
66
|
+
allowedModelsNotArray: "module.eyes.configError.allowedModelsNotArray",
|
|
67
|
+
fixedRequired: "module.eyes.configError.fixedRequired",
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export function formatEyesProblem(problem: EyesConfigProblem, t: Translator): string {
|
|
71
|
+
return t(PROBLEM_KEYS[problem.code], { field: problem.field });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function formatEyesConfigWarning(warning: EyesConfigWarning, t: Translator): string {
|
|
75
|
+
if (warning.kind === "detail") return warning.detail;
|
|
76
|
+
return t("problem.summary", {
|
|
77
|
+
label: t("problem.config"),
|
|
78
|
+
source: warning.source,
|
|
79
|
+
detail: formatEyesProblem(warning.problem, t),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** 一层路由配置(低优先级在前,高优先级在后) */
|
|
84
|
+
export interface EyesRouteLayer {
|
|
85
|
+
readonly source: string;
|
|
86
|
+
readonly route: Partial<EyesRouteConfig>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
90
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function cloneModels(models: VisionModelSelection[] | null): VisionModelSelection[] | null {
|
|
94
|
+
return models === null ? null : models.map((model) => ({ ...model }));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function cloneRoute(route: EyesRouteConfig): EyesRouteConfig {
|
|
98
|
+
return {
|
|
99
|
+
mode: route.mode,
|
|
100
|
+
allowedModels: cloneModels(route.allowedModels),
|
|
101
|
+
...(route.fixedModel ? { fixedModel: { ...route.fixedModel } } : {}),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function cloneConfig(config: EyesConfig): EyesConfig {
|
|
106
|
+
return { route: cloneRoute(config.route) };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function parseModelSelection(value: unknown, field: string, problems: EyesConfigProblem[]): VisionModelSelection | undefined {
|
|
110
|
+
if (!isRecord(value)) {
|
|
111
|
+
problems.push({ code: "notObject", field });
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
if (typeof value.provider !== "string" || value.provider.trim() === "") {
|
|
115
|
+
problems.push({ code: "notNonEmptyString", field: `${field}.provider` });
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
if (typeof value.model !== "string" || value.model.trim() === "") {
|
|
119
|
+
problems.push({ code: "notNonEmptyString", field: `${field}.model` });
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
return { provider: value.provider, model: value.model };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** 从 pi-toolkit.json 的一个模块节里取出路由层;只保留通过校验的字段 */
|
|
126
|
+
function routeFromSection(
|
|
127
|
+
section: ModuleConfigRecord | undefined,
|
|
128
|
+
source: string,
|
|
129
|
+
): { layer?: EyesRouteLayer; warnings: EyesConfigWarning[] } {
|
|
130
|
+
const warnings: EyesConfigWarning[] = [];
|
|
131
|
+
if (section === undefined) return { warnings };
|
|
132
|
+
|
|
133
|
+
const backend = section.backend;
|
|
134
|
+
if (backend === undefined) return { warnings };
|
|
135
|
+
if (!isRecord(backend)) {
|
|
136
|
+
warnings.push({ kind: "problem", source, problem: { code: "notObject", field: `${SECTION_FIELD}.backend` } });
|
|
137
|
+
return { warnings };
|
|
138
|
+
}
|
|
139
|
+
if (backend.route === undefined) return { warnings };
|
|
140
|
+
if (!isRecord(backend.route)) {
|
|
141
|
+
warnings.push({ kind: "problem", source, problem: { code: "notObject", field: ROUTE_FIELD } });
|
|
142
|
+
return { warnings };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const raw = backend.route;
|
|
146
|
+
const problems: EyesConfigProblem[] = [];
|
|
147
|
+
const layer: {
|
|
148
|
+
mode?: VisionRouteMode;
|
|
149
|
+
allowedModels?: VisionModelSelection[] | null;
|
|
150
|
+
fixedModel?: VisionModelSelection;
|
|
151
|
+
} = {};
|
|
152
|
+
|
|
153
|
+
if (raw.mode !== undefined) {
|
|
154
|
+
if (raw.mode === "automatic" || raw.mode === "fixed") layer.mode = raw.mode;
|
|
155
|
+
else problems.push({ code: "badMode", field: `${ROUTE_FIELD}.mode` });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (raw.allowedModels !== undefined) {
|
|
159
|
+
if (raw.allowedModels === null) {
|
|
160
|
+
layer.allowedModels = null;
|
|
161
|
+
} else if (Array.isArray(raw.allowedModels)) {
|
|
162
|
+
const parsed: VisionModelSelection[] = [];
|
|
163
|
+
raw.allowedModels.forEach((entry, index) => {
|
|
164
|
+
const selection = parseModelSelection(entry, `${ROUTE_FIELD}.allowedModels[${index}]`, problems);
|
|
165
|
+
if (selection) parsed.push(selection);
|
|
166
|
+
});
|
|
167
|
+
// 有非法项就整条白名单不采用,避免悄悄改变候选集合
|
|
168
|
+
if (parsed.length === raw.allowedModels.length) layer.allowedModels = parsed;
|
|
169
|
+
} else {
|
|
170
|
+
problems.push({ code: "allowedModelsNotArray", field: `${ROUTE_FIELD}.allowedModels` });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (raw.fixedModel !== undefined) {
|
|
175
|
+
const fixed = parseModelSelection(raw.fixedModel, `${ROUTE_FIELD}.fixedModel`, problems);
|
|
176
|
+
if (fixed) layer.fixedModel = fixed;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
for (const problem of problems) warnings.push({ kind: "problem", source, problem });
|
|
180
|
+
const used = layer.mode !== undefined || layer.allowedModels !== undefined || layer.fixedModel !== undefined;
|
|
181
|
+
return { layer: used ? { source, route: layer } : undefined, warnings };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** 逐字段合并各层路由:高优先级层没写的字段保留低优先级取值 */
|
|
185
|
+
export function resolveEyesLayers(
|
|
186
|
+
layers: readonly EyesRouteLayer[],
|
|
187
|
+
): { config: EyesConfig; warnings: EyesConfigWarning[] } {
|
|
188
|
+
const warnings: EyesConfigWarning[] = [];
|
|
189
|
+
let route = cloneRoute(DEFAULT_EYES_CONFIG.route);
|
|
190
|
+
|
|
191
|
+
for (const layer of layers) {
|
|
192
|
+
const next = cloneRoute(route);
|
|
193
|
+
if (layer.route.mode !== undefined) next.mode = layer.route.mode;
|
|
194
|
+
if (layer.route.allowedModels !== undefined) next.allowedModels = cloneModels(layer.route.allowedModels);
|
|
195
|
+
if (layer.route.fixedModel !== undefined) next.fixedModel = { ...layer.route.fixedModel };
|
|
196
|
+
if (next.mode === "fixed" && !next.fixedModel) {
|
|
197
|
+
// 该层把配置推到非法状态:整层忽略,保留上一层的可用取值
|
|
198
|
+
warnings.push({
|
|
199
|
+
kind: "problem",
|
|
200
|
+
source: layer.source,
|
|
201
|
+
problem: { code: "fixedRequired", field: `${ROUTE_FIELD}.fixedModel` },
|
|
202
|
+
});
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
route = next;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return { config: { route }, warnings };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export interface LoadEyesConfigOptions {
|
|
212
|
+
/** 骨架解析出的全局模块节(ModuleContext.getConfig()) */
|
|
213
|
+
readonly section: ModuleConfigRecord | undefined;
|
|
214
|
+
readonly cwd: string;
|
|
215
|
+
readonly projectTrusted: boolean;
|
|
216
|
+
/** 全局配置来源,用于告警里的出处;默认取模块节名 */
|
|
217
|
+
readonly source?: string;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export interface LoadedEyesConfig {
|
|
221
|
+
readonly config: EyesConfig;
|
|
222
|
+
readonly warnings: readonly EyesConfigWarning[];
|
|
223
|
+
/** 项目层是否实际提供了路由配置 */
|
|
224
|
+
readonly projectLayer: boolean;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** 全局节 + 受信任的项目层逐字段合并,得到本会话实际生效的视觉配置 */
|
|
228
|
+
export async function loadEyesConfig(options: LoadEyesConfigOptions): Promise<LoadedEyesConfig> {
|
|
229
|
+
const source = options.source ?? getToolkitConfigPath(getAgentDir());
|
|
230
|
+
const warnings: EyesConfigWarning[] = [];
|
|
231
|
+
const layers: EyesRouteLayer[] = [];
|
|
232
|
+
|
|
233
|
+
const global = routeFromSection(options.section, source);
|
|
234
|
+
warnings.push(...global.warnings);
|
|
235
|
+
if (global.layer) layers.push(global.layer);
|
|
236
|
+
|
|
237
|
+
let projectLayer = false;
|
|
238
|
+
if (options.projectTrusted) {
|
|
239
|
+
const projectPath = join(options.cwd, CONFIG_DIR_NAME, TOOLKIT_CONFIG_FILENAME);
|
|
240
|
+
const loadedProject = await loadToolkitConfig(projectPath);
|
|
241
|
+
for (const detail of loadedProject.problems) warnings.push({ kind: "detail", detail });
|
|
242
|
+
const project = routeFromSection(loadedProject.config.modules[EYES_MODULE_ID], projectPath);
|
|
243
|
+
warnings.push(...project.warnings);
|
|
244
|
+
if (project.layer) {
|
|
245
|
+
layers.push(project.layer);
|
|
246
|
+
projectLayer = true;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const resolved = resolveEyesLayers(layers);
|
|
251
|
+
warnings.push(...resolved.warnings);
|
|
252
|
+
return { config: resolved.config, warnings, projectLayer };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function toChainRouting(config: EyesConfig): VisionRoutingConfig {
|
|
256
|
+
return {
|
|
257
|
+
route: {
|
|
258
|
+
mode: config.route.mode,
|
|
259
|
+
allowedModels: config.route.allowedModels?.map((model) => ({
|
|
260
|
+
provider: model.provider,
|
|
261
|
+
modelId: model.model,
|
|
262
|
+
})) ?? null,
|
|
263
|
+
...(config.route.fixedModel
|
|
264
|
+
? { fixedModel: { provider: config.route.fixedModel.provider, modelId: config.route.fixedModel.model } }
|
|
265
|
+
: {}),
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** 只读展示当前路由(菜单行用) */
|
|
271
|
+
export function describeEyesRoute(config: EyesConfig, t: Translator): string {
|
|
272
|
+
const route = config.route;
|
|
273
|
+
if (route.mode === "fixed" && route.fixedModel) {
|
|
274
|
+
return t("module.eyes.route.fixed", { model: `${route.fixedModel.provider}/${route.fixedModel.model}` });
|
|
275
|
+
}
|
|
276
|
+
if (route.allowedModels !== null && route.allowedModels.length === 0) return t("module.eyes.route.off");
|
|
277
|
+
return t("module.eyes.route.auto");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** 菜单渲染时同步解析全局节(不做 IO,拿不到项目层;项目层在会话启动时应用) */
|
|
281
|
+
export function resolveEyesSection(section: ModuleConfigRecord | undefined): EyesConfig {
|
|
282
|
+
const parsed = routeFromSection(section, SECTION_FIELD);
|
|
283
|
+
return resolveEyesLayers(parsed.layer ? [parsed.layer] : []).config;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function routeForWrite(patch: Partial<EyesRouteConfig>): ModuleConfigValue {
|
|
287
|
+
return {
|
|
288
|
+
...(patch.mode !== undefined ? { mode: patch.mode } : {}),
|
|
289
|
+
...(patch.allowedModels !== undefined
|
|
290
|
+
? {
|
|
291
|
+
allowedModels: patch.allowedModels === null
|
|
292
|
+
? null
|
|
293
|
+
: patch.allowedModels.map((model) => ({ provider: model.provider, model: model.model })),
|
|
294
|
+
}
|
|
295
|
+
: {}),
|
|
296
|
+
...(patch.fixedModel !== undefined
|
|
297
|
+
? { fixedModel: { provider: patch.fixedModel.provider, model: patch.fixedModel.model } }
|
|
298
|
+
: {}),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export interface SaveEyesRouteOptions {
|
|
303
|
+
/** 配置目录;默认 pi 的 agentDir */
|
|
304
|
+
readonly agentDir?: string;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* 写视觉路由到 `<agentDir>/pi-toolkit.json`。
|
|
309
|
+
* 走骨架的原子写 + 剥敏感键,另有 `mergeRaw` 深合并:只覆盖 patch 里出现的字段。
|
|
310
|
+
*/
|
|
311
|
+
export async function saveEyesRoute(
|
|
312
|
+
patch: Partial<EyesRouteConfig>,
|
|
313
|
+
options: SaveEyesRouteOptions = {},
|
|
314
|
+
): Promise<void> {
|
|
315
|
+
const path = getToolkitConfigPath(options.agentDir ?? getAgentDir());
|
|
316
|
+
await saveToolkitConfig(path, {
|
|
317
|
+
modules: { [EYES_MODULE_ID]: { backend: { route: routeForWrite(patch) } } },
|
|
318
|
+
});
|
|
319
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// 视觉模块:把 pi-eyes 的核心能力装进 pi-toolkit。
|
|
2
|
+
// - `context` 钩子:纯文本模型会话注入结构化视觉笔记(原生多模态模型旁路)
|
|
3
|
+
// - `vision_query` 工具:主模型针对最近一张截图追问辅助视觉模型
|
|
4
|
+
// - 服务句柄 `eyes.query-latest`:供其它模块查询(本单只注册,不做联动)
|
|
5
|
+
// 旧 `/pi-eyes` 命令按合并决策不再注册;交互式视觉配置由 modules/eyes/menu.ts 的原生子菜单承担。
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
getAgentDir,
|
|
9
|
+
type ExtensionAPI,
|
|
10
|
+
type ExtensionContext,
|
|
11
|
+
} from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import { getToolkitConfigPath } from "../../config.ts";
|
|
13
|
+
import { enabledField, type ModuleContext, type ModuleDefinition } from "../../module.ts";
|
|
14
|
+
import { VisionChain } from "./chain.ts";
|
|
15
|
+
import {
|
|
16
|
+
DEFAULT_EYES_CONFIG,
|
|
17
|
+
EYES_MODULE_ID,
|
|
18
|
+
formatEyesConfigWarning,
|
|
19
|
+
loadEyesConfig,
|
|
20
|
+
resolveEyesSection,
|
|
21
|
+
toChainRouting,
|
|
22
|
+
type EyesConfig,
|
|
23
|
+
type EyesRouteConfig,
|
|
24
|
+
} from "./config.ts";
|
|
25
|
+
import { buildEyesMenuItems, type EyesMenuRuntime, type VisionDiagnostic } from "./menu.ts";
|
|
26
|
+
import { registerVisionContext, VisionBridge } from "./vision-bridge.ts";
|
|
27
|
+
import { registerVisionQuery } from "./vision-tool.ts";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 服务句柄名。注册表要求全小写(服务名模式不允许大写字母),
|
|
31
|
+
* 句柄上暴露的方法沿用 pi-eyes 的 API 名 queryLatest。
|
|
32
|
+
*/
|
|
33
|
+
export const EYES_SERVICE_NAME = "eyes.query-latest";
|
|
34
|
+
|
|
35
|
+
export interface EyesService {
|
|
36
|
+
readonly id: "eyes";
|
|
37
|
+
/** 针对当前会话最近一张截图向辅助视觉模型提问 */
|
|
38
|
+
queryLatest: VisionBridge["queryLatest"];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 模块私有运行态:菜单展示的路由镜像、目录版本号与诊断缓存 */
|
|
42
|
+
interface EyesRuntimeState {
|
|
43
|
+
globalRoute: EyesRouteConfig;
|
|
44
|
+
/** 由 register 装上:重载配置并刷新视觉链,返回刷新后的全局路由 */
|
|
45
|
+
reapply: ((ctx: ExtensionContext) => Promise<EyesRouteConfig>) | undefined;
|
|
46
|
+
generation: number;
|
|
47
|
+
diagnostic: VisionDiagnostic | undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function createMenuRuntime(state: EyesRuntimeState): EyesMenuRuntime {
|
|
51
|
+
return {
|
|
52
|
+
globalRoute: () => state.globalRoute,
|
|
53
|
+
reapply: (ctx) => {
|
|
54
|
+
if (!state.reapply) throw new Error("视觉模块尚未完成注册,无法刷新视觉路由");
|
|
55
|
+
return state.reapply(ctx);
|
|
56
|
+
},
|
|
57
|
+
catalogueGeneration: () => state.generation,
|
|
58
|
+
bumpCatalogueGeneration: () => {
|
|
59
|
+
state.generation += 1;
|
|
60
|
+
},
|
|
61
|
+
diagnostic: () => state.diagnostic,
|
|
62
|
+
setDiagnostic: (diagnostic) => {
|
|
63
|
+
state.diagnostic = diagnostic;
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function describeError(error: unknown): string {
|
|
69
|
+
return error instanceof Error ? error.message : String(error);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function registerEyes(pi: ExtensionAPI, context: ModuleContext, state: EyesRuntimeState): void {
|
|
73
|
+
const chain = new VisionChain();
|
|
74
|
+
const bridge = new VisionBridge(chain, context.t);
|
|
75
|
+
|
|
76
|
+
/** 全局节 + 受信任的项目层 → 生效配置;顺带把路由交给视觉链,并把全局路由镜像给菜单 */
|
|
77
|
+
const applyConfig = async (ctx: ExtensionContext, notifyWarnings: boolean): Promise<EyesConfig> => {
|
|
78
|
+
// 视觉路由由模块自己的配置层写盘,状态中枢的内存副本可能落后:读之前先重载
|
|
79
|
+
await context.reloadConfig();
|
|
80
|
+
const section = context.getConfig();
|
|
81
|
+
const loaded = await loadEyesConfig({
|
|
82
|
+
section,
|
|
83
|
+
cwd: ctx.cwd,
|
|
84
|
+
projectTrusted: ctx.isProjectTrusted(),
|
|
85
|
+
source: getToolkitConfigPath(getAgentDir()),
|
|
86
|
+
});
|
|
87
|
+
chain.setRouting(toChainRouting(loaded.config));
|
|
88
|
+
state.globalRoute = resolveEyesSection(section).route;
|
|
89
|
+
if (notifyWarnings && ctx.hasUI) {
|
|
90
|
+
for (const warning of loaded.warnings) {
|
|
91
|
+
ctx.ui.notify(formatEyesConfigWarning(warning, context.t), "warning");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return loaded.config;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
state.reapply = async (ctx: ExtensionContext): Promise<EyesRouteConfig> => {
|
|
98
|
+
await applyConfig(ctx, false);
|
|
99
|
+
return state.globalRoute;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
context.services.register(EYES_SERVICE_NAME, {
|
|
103
|
+
id: "eyes",
|
|
104
|
+
queryLatest: (question: string, ctx: ExtensionContext) => bridge.queryLatest(question, ctx),
|
|
105
|
+
} satisfies EyesService);
|
|
106
|
+
|
|
107
|
+
registerVisionQuery(pi, bridge);
|
|
108
|
+
registerVisionContext(pi, bridge);
|
|
109
|
+
|
|
110
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
111
|
+
try {
|
|
112
|
+
await applyConfig(ctx, true);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
// 配置读取意外失败不能拖垮会话:只提示,后续按默认路由工作
|
|
115
|
+
if (ctx.hasUI) {
|
|
116
|
+
ctx.ui.notify(
|
|
117
|
+
context.t("problem.summary", {
|
|
118
|
+
label: context.t("problem.config"),
|
|
119
|
+
source: getToolkitConfigPath(getAgentDir()),
|
|
120
|
+
detail: describeError(error),
|
|
121
|
+
}),
|
|
122
|
+
"warning",
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// 一次 agent run 记一轮:清掉上一轮的失败短路与熔断
|
|
129
|
+
pi.on("agent_start", async () => {
|
|
130
|
+
chain.beginTurn();
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function createEyesModule(): ModuleDefinition {
|
|
135
|
+
const state: EyesRuntimeState = {
|
|
136
|
+
globalRoute: DEFAULT_EYES_CONFIG.route,
|
|
137
|
+
reapply: undefined,
|
|
138
|
+
generation: 0,
|
|
139
|
+
diagnostic: undefined,
|
|
140
|
+
};
|
|
141
|
+
const runtime = createMenuRuntime(state);
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
id: EYES_MODULE_ID,
|
|
145
|
+
labelKey: "module.eyes.label",
|
|
146
|
+
descriptionKey: "module.eyes.description",
|
|
147
|
+
group: "general",
|
|
148
|
+
// schema 只放总开关;视觉路由是结构化配置(非 schema 键),由本模块的菜单与配置层读写
|
|
149
|
+
configSchema: {
|
|
150
|
+
enabled: enabledField("module.eyes.enabled.label", "module.eyes.enabled.description"),
|
|
151
|
+
},
|
|
152
|
+
register(context): void {
|
|
153
|
+
registerEyes(context.pi, context, state);
|
|
154
|
+
},
|
|
155
|
+
menuItems(context): ReturnType<typeof buildEyesMenuItems> {
|
|
156
|
+
return buildEyesMenuItems(context, runtime);
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export const eyesModule: ModuleDefinition = createEyesModule();
|