@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,828 @@
|
|
|
1
|
+
// 视觉配置子菜单:常规组里的 `视觉配置` 行打开这一页(原生 SettingsList 形态)。
|
|
2
|
+
// 页面四行:
|
|
3
|
+
// 视觉模型 —— SelectList 选 auto / off / 具体模型;改模型前先跑真实图片探测,通过才写盘
|
|
4
|
+
// 刷新模型目录 —— 重读 Pi 的模型目录(不联网),成功后候选列表与目录版本号都刷新
|
|
5
|
+
// 运行自检 —— 对当前生效路由发一张探测图(原向导 T 键的等价物),结果进诊断缓存
|
|
6
|
+
// 路由状态 —— 只读展示当前路由
|
|
7
|
+
//
|
|
8
|
+
// 原 setup.ts 里必须保留的业务流程都落在下面这些可单测的函数上:
|
|
9
|
+
// discoverCandidates / routePatchFor / saveRouteSelection / runVisionSelfCheck / refreshVisionCatalogue
|
|
10
|
+
// Esc 取消长任务靠 startCancellable 的 AbortController;探测常量在 vision-probe.ts。
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
type ExtensionContext,
|
|
14
|
+
type ModelRegistry,
|
|
15
|
+
} from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import {
|
|
17
|
+
Container,
|
|
18
|
+
Key,
|
|
19
|
+
matchesKey,
|
|
20
|
+
SelectList,
|
|
21
|
+
type SelectItem,
|
|
22
|
+
type SettingItem,
|
|
23
|
+
Text,
|
|
24
|
+
} from "@earendil-works/pi-tui";
|
|
25
|
+
import type { Translator } from "../../i18n.ts";
|
|
26
|
+
import { I18nSettingsList } from "../../menu/settings-list.ts";
|
|
27
|
+
import type { MenuTheme } from "../../menu/theme.ts";
|
|
28
|
+
import type { ModuleMenuContext } from "../../module.ts";
|
|
29
|
+
import { describeEyesRoute, saveEyesRoute, type EyesRouteConfig } from "./config.ts";
|
|
30
|
+
import { selectAutomaticPiVisionModel, testPiVisionModel } from "./pi-model-backend.ts";
|
|
31
|
+
import { VISION_PROBE_EXPECTED, VISION_PROBE_IMAGE } from "./vision-probe.ts";
|
|
32
|
+
|
|
33
|
+
/** 分组页里的入口行 id */
|
|
34
|
+
export const EYES_MENU_ITEM_ID = "eyes.settings";
|
|
35
|
+
|
|
36
|
+
const ROW_MODEL = "eyes.settings.model";
|
|
37
|
+
const ROW_REFRESH = "eyes.settings.refresh";
|
|
38
|
+
const ROW_CHECK = "eyes.settings.check";
|
|
39
|
+
const ROW_ROUTE = "eyes.settings.route";
|
|
40
|
+
|
|
41
|
+
const VALUE_AUTO = "auto";
|
|
42
|
+
const VALUE_OFF = "off";
|
|
43
|
+
/** 模型取值编码:provider + NUL + modelId;provider / modelId 里不会出现 NUL */
|
|
44
|
+
const MODEL_VALUE_SEPARATOR = "\u0000";
|
|
45
|
+
|
|
46
|
+
/** 菜单需要的模型注册表能力(取 ModelRegistry 的子集,方便 headless 自测塞 stub) */
|
|
47
|
+
export type VisionMenuRegistry = Pick<
|
|
48
|
+
ModelRegistry,
|
|
49
|
+
| "getAll"
|
|
50
|
+
| "getAvailable"
|
|
51
|
+
| "find"
|
|
52
|
+
| "getProviderAuthStatus"
|
|
53
|
+
| "complete"
|
|
54
|
+
| "hasConfiguredAuth"
|
|
55
|
+
| "refresh"
|
|
56
|
+
>;
|
|
57
|
+
|
|
58
|
+
export interface VisionSelection {
|
|
59
|
+
readonly provider: string;
|
|
60
|
+
readonly modelId: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface VisionCandidate extends VisionSelection {
|
|
64
|
+
/** 该供应商已配置认证;仅用于排序与候选描述 */
|
|
65
|
+
readonly authenticated: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface VisionDiagnostic {
|
|
69
|
+
readonly key: string;
|
|
70
|
+
readonly passed: boolean;
|
|
71
|
+
readonly elapsedMs: number;
|
|
72
|
+
readonly detail: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 菜单运行态:路由镜像、目录版本号与诊断缓存都由模块持有,跨菜单开关保留 */
|
|
76
|
+
export interface EyesMenuRuntime {
|
|
77
|
+
/** 写盘目标(全局节)当前路由;会话启动与每次保存后刷新 */
|
|
78
|
+
globalRoute(): EyesRouteConfig;
|
|
79
|
+
/** 保存后重载配置并刷新视觉链,返回刷新后的全局路由 */
|
|
80
|
+
reapply(context: ExtensionContext): Promise<EyesRouteConfig>;
|
|
81
|
+
catalogueGeneration(): number;
|
|
82
|
+
bumpCatalogueGeneration(): void;
|
|
83
|
+
diagnostic(): VisionDiagnostic | undefined;
|
|
84
|
+
setDiagnostic(diagnostic: VisionDiagnostic): void;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function describeError(error: unknown): string {
|
|
88
|
+
return error instanceof Error ? error.message : String(error);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function singleLine(text: string): string {
|
|
92
|
+
return text.replace(/\s+/g, " ").trim();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** config.ts 的只读路由文案吃整份视觉配置(EyesConfig),这里把路由包回去 */
|
|
96
|
+
function describeRoute(route: EyesRouteConfig, t: Translator): string {
|
|
97
|
+
return describeEyesRoute({ route }, t);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ─────────────────────────────────────────────────────────────
|
|
101
|
+
// 纯逻辑:候选发现、取值编码、写盘补丁、诊断缓存键
|
|
102
|
+
// ─────────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
export function candidateKey(candidate: VisionSelection): string {
|
|
105
|
+
return `${candidate.provider}${MODEL_VALUE_SEPARATOR}${candidate.modelId}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function isModelValue(value: string): boolean {
|
|
109
|
+
return value.includes(MODEL_VALUE_SEPARATOR);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function parseModelValue(value: string): VisionSelection | undefined {
|
|
113
|
+
const index = value.indexOf(MODEL_VALUE_SEPARATOR);
|
|
114
|
+
if (index <= 0) return undefined;
|
|
115
|
+
const provider = value.slice(0, index);
|
|
116
|
+
const modelId = value.slice(index + 1);
|
|
117
|
+
return provider && modelId ? { provider, modelId } : undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** 候选 = 模型目录里声明支持 image 且当前可用的模型;已认证的排前面,其余按 provider/model 排序 */
|
|
121
|
+
export function discoverCandidates(registry: VisionMenuRegistry): VisionCandidate[] {
|
|
122
|
+
const candidates: VisionCandidate[] = [];
|
|
123
|
+
const seen = new Set<string>();
|
|
124
|
+
for (const model of registry.getAvailable()) {
|
|
125
|
+
if (!model.input.includes("image")) continue;
|
|
126
|
+
const selection: VisionSelection = { provider: model.provider, modelId: model.id };
|
|
127
|
+
const key = candidateKey(selection);
|
|
128
|
+
if (seen.has(key)) continue;
|
|
129
|
+
seen.add(key);
|
|
130
|
+
candidates.push({ ...selection, authenticated: registry.hasConfiguredAuth(model) });
|
|
131
|
+
}
|
|
132
|
+
candidates.sort((left, right) => {
|
|
133
|
+
if (left.authenticated !== right.authenticated) return left.authenticated ? -1 : 1;
|
|
134
|
+
return candidateKey(left).localeCompare(candidateKey(right));
|
|
135
|
+
});
|
|
136
|
+
return candidates;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function candidateKeys(candidates: readonly VisionCandidate[]): string {
|
|
140
|
+
return candidates.map((candidate) => candidateKey(candidate)).join("|");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** 保存前重扫目录用的比较:只比 provider/model 联合键,与 pi-eyes 原判定一致 */
|
|
144
|
+
export function sameCandidates(
|
|
145
|
+
left: readonly VisionCandidate[],
|
|
146
|
+
right: readonly VisionCandidate[],
|
|
147
|
+
): boolean {
|
|
148
|
+
return candidateKeys(left) === candidateKeys(right);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function routeSelectionValue(route: EyesRouteConfig): string {
|
|
152
|
+
if (route.allowedModels !== null && route.allowedModels.length === 0) return VALUE_OFF;
|
|
153
|
+
if (route.mode === "fixed" && route.fixedModel) {
|
|
154
|
+
return `${route.fixedModel.provider}${MODEL_VALUE_SEPARATOR}${route.fixedModel.model}`;
|
|
155
|
+
}
|
|
156
|
+
return VALUE_AUTO;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** 选择器取值 → 路由补丁。auto 写 null 白名单,off 写空白名单,固定模型写 fixedModel */
|
|
160
|
+
export function routePatchFor(value: string): Partial<EyesRouteConfig> | undefined {
|
|
161
|
+
if (value === VALUE_AUTO) return { mode: "automatic", allowedModels: null };
|
|
162
|
+
if (value === VALUE_OFF) return { mode: "automatic", allowedModels: [] };
|
|
163
|
+
const selection = parseModelValue(value);
|
|
164
|
+
return selection
|
|
165
|
+
? {
|
|
166
|
+
mode: "fixed",
|
|
167
|
+
allowedModels: null,
|
|
168
|
+
fixedModel: { provider: selection.provider, model: selection.modelId },
|
|
169
|
+
}
|
|
170
|
+
: undefined;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** 菜单行显示的当前视觉模型(不带 fixed 前缀,区别于只读路由行) */
|
|
174
|
+
export function modelValueLabel(route: EyesRouteConfig, t: Translator): string {
|
|
175
|
+
if (route.allowedModels !== null && route.allowedModels.length === 0) {
|
|
176
|
+
return t("module.eyes.model.off");
|
|
177
|
+
}
|
|
178
|
+
if (route.mode === "fixed" && route.fixedModel) {
|
|
179
|
+
return `${route.fixedModel.provider}/${route.fixedModel.model}`;
|
|
180
|
+
}
|
|
181
|
+
return t("module.eyes.model.auto");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** 当前路由实际会调用的模型;off 返回 undefined,auto 走 Pi 的自动选择规则 */
|
|
185
|
+
export function resolveRouteCandidate(
|
|
186
|
+
route: EyesRouteConfig,
|
|
187
|
+
registry: VisionMenuRegistry,
|
|
188
|
+
currentModel?: { provider: string; id: string },
|
|
189
|
+
): VisionSelection | undefined {
|
|
190
|
+
if (route.allowedModels !== null && route.allowedModels.length === 0) return undefined;
|
|
191
|
+
if (route.mode === "fixed" && route.fixedModel) {
|
|
192
|
+
return { provider: route.fixedModel.provider, modelId: route.fixedModel.model };
|
|
193
|
+
}
|
|
194
|
+
const allowed = route.allowedModels?.map((model) => ({ provider: model.provider, modelId: model.model })) ?? null;
|
|
195
|
+
const selection = selectAutomaticPiVisionModel(registry, {
|
|
196
|
+
allowedModels: allowed,
|
|
197
|
+
...(currentModel ? { currentModel: { provider: currentModel.provider, modelId: currentModel.id } } : {}),
|
|
198
|
+
});
|
|
199
|
+
return selection ? { provider: selection.provider, modelId: selection.modelId } : undefined;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** 诊断缓存键:目录版本 + 当前选择 + 候选清单 + 会话当前模型(与 pi-eyes 原键一致) */
|
|
203
|
+
export function diagnosticCacheKey(options: {
|
|
204
|
+
readonly generation: number;
|
|
205
|
+
readonly route: EyesRouteConfig;
|
|
206
|
+
readonly candidates: readonly VisionCandidate[];
|
|
207
|
+
readonly currentModel?: { provider: string; id: string };
|
|
208
|
+
}): string {
|
|
209
|
+
return [
|
|
210
|
+
options.generation,
|
|
211
|
+
routeSelectionValue(options.route),
|
|
212
|
+
candidateKeys(options.candidates),
|
|
213
|
+
options.currentModel
|
|
214
|
+
? `${options.currentModel.provider}${MODEL_VALUE_SEPARATOR}${options.currentModel.id}`
|
|
215
|
+
: "no-current-model",
|
|
216
|
+
].join("\n");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export interface ProbeOutcome {
|
|
220
|
+
readonly passed: boolean;
|
|
221
|
+
readonly matched: number;
|
|
222
|
+
readonly total: number;
|
|
223
|
+
readonly text: string;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** 真实图片探测:发 6 格彩色格子图,6 格过 5 格判通过 */
|
|
227
|
+
export async function probeVisionModel(
|
|
228
|
+
registry: VisionMenuRegistry,
|
|
229
|
+
selection: VisionSelection,
|
|
230
|
+
signal?: AbortSignal,
|
|
231
|
+
): Promise<ProbeOutcome> {
|
|
232
|
+
const result = await testPiVisionModel(registry, selection, VISION_PROBE_IMAGE, VISION_PROBE_EXPECTED, {
|
|
233
|
+
...(signal ? { signal } : {}),
|
|
234
|
+
});
|
|
235
|
+
return { passed: result.passed, matched: result.matched, total: result.total, text: result.text };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export interface SaveRouteResult {
|
|
239
|
+
readonly kind: "saved" | "modelsChanged" | "failed";
|
|
240
|
+
readonly route?: EyesRouteConfig;
|
|
241
|
+
readonly reason?: string;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** 保存路由:先重扫目录,候选变了就中止;写盘走 02 的 saveEyesRoute,再让模块重载并刷新视觉链 */
|
|
245
|
+
export async function saveRouteSelection(options: {
|
|
246
|
+
readonly value: string;
|
|
247
|
+
readonly openedWith: readonly VisionCandidate[];
|
|
248
|
+
readonly registry: VisionMenuRegistry;
|
|
249
|
+
readonly runtime: EyesMenuRuntime;
|
|
250
|
+
readonly context: ExtensionContext;
|
|
251
|
+
readonly agentDir?: string;
|
|
252
|
+
}): Promise<SaveRouteResult> {
|
|
253
|
+
const patch = routePatchFor(options.value);
|
|
254
|
+
if (!patch) return { kind: "failed", reason: `unknown selection: ${options.value}` };
|
|
255
|
+
|
|
256
|
+
const latest = discoverCandidates(options.registry);
|
|
257
|
+
if (!sameCandidates(options.openedWith, latest)) return { kind: "modelsChanged" };
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
await saveEyesRoute(patch, options.agentDir === undefined ? {} : { agentDir: options.agentDir });
|
|
261
|
+
const route = await options.runtime.reapply(options.context);
|
|
262
|
+
return { kind: "saved", route };
|
|
263
|
+
} catch (error) {
|
|
264
|
+
return { kind: "failed", reason: describeError(error) };
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export interface SelfCheckResult {
|
|
269
|
+
readonly kind: "passed" | "failed" | "noRoute" | "noModel" | "cancelled";
|
|
270
|
+
readonly text: string;
|
|
271
|
+
readonly cached: boolean;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** 运行自检:对当前生效路由发探测图,命中诊断缓存时直接回放,不重复请求 */
|
|
275
|
+
export async function runVisionSelfCheck(options: {
|
|
276
|
+
readonly t: Translator;
|
|
277
|
+
readonly registry: VisionMenuRegistry;
|
|
278
|
+
readonly runtime: EyesMenuRuntime;
|
|
279
|
+
readonly context: ExtensionContext;
|
|
280
|
+
readonly route: EyesRouteConfig;
|
|
281
|
+
readonly candidates: readonly VisionCandidate[];
|
|
282
|
+
readonly signal?: AbortSignal;
|
|
283
|
+
}): Promise<SelfCheckResult> {
|
|
284
|
+
const { t, registry, runtime } = options;
|
|
285
|
+
if (options.route.allowedModels !== null && options.route.allowedModels.length === 0) {
|
|
286
|
+
return { kind: "noRoute", text: t("module.eyes.check.noRoute"), cached: false };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const currentModel = options.context.model
|
|
290
|
+
? { provider: options.context.model.provider, id: options.context.model.id }
|
|
291
|
+
: undefined;
|
|
292
|
+
const key = diagnosticCacheKey({
|
|
293
|
+
generation: runtime.catalogueGeneration(),
|
|
294
|
+
route: options.route,
|
|
295
|
+
candidates: options.candidates,
|
|
296
|
+
...(currentModel ? { currentModel } : {}),
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
const cached = runtime.diagnostic();
|
|
300
|
+
if (cached?.key === key) {
|
|
301
|
+
return cached.passed
|
|
302
|
+
? { kind: "passed", text: t("module.eyes.check.passed", { elapsed: cached.elapsedMs }), cached: true }
|
|
303
|
+
: { kind: "failed", text: t("module.eyes.check.failed", { reason: cached.detail }), cached: true };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const selection = resolveRouteCandidate(options.route, registry, currentModel);
|
|
307
|
+
if (!selection) {
|
|
308
|
+
runtime.setDiagnostic({ key, passed: false, elapsedMs: 0, detail: "" });
|
|
309
|
+
return { kind: "noModel", text: t("module.eyes.check.noModel"), cached: false };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const startedAt = Date.now();
|
|
313
|
+
try {
|
|
314
|
+
const probe = await probeVisionModel(registry, selection, options.signal);
|
|
315
|
+
if (options.signal?.aborted) {
|
|
316
|
+
return { kind: "cancelled", text: t("module.eyes.cancelled"), cached: false };
|
|
317
|
+
}
|
|
318
|
+
const elapsedMs = Date.now() - startedAt;
|
|
319
|
+
const detail = singleLine(probe.text || (probe.passed ? "OK" : "unknown error"));
|
|
320
|
+
runtime.setDiagnostic({ key, passed: probe.passed, elapsedMs, detail });
|
|
321
|
+
return probe.passed
|
|
322
|
+
? { kind: "passed", text: t("module.eyes.check.passed", { elapsed: elapsedMs }), cached: false }
|
|
323
|
+
: { kind: "failed", text: t("module.eyes.check.failed", { reason: detail }), cached: false };
|
|
324
|
+
} catch (error) {
|
|
325
|
+
if (options.signal?.aborted) {
|
|
326
|
+
return { kind: "cancelled", text: t("module.eyes.cancelled"), cached: false };
|
|
327
|
+
}
|
|
328
|
+
const elapsedMs = Date.now() - startedAt;
|
|
329
|
+
const detail = singleLine(describeError(error));
|
|
330
|
+
runtime.setDiagnostic({ key, passed: false, elapsedMs, detail });
|
|
331
|
+
return { kind: "failed", text: t("module.eyes.check.failed", { reason: detail }), cached: false };
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export interface RefreshResult {
|
|
336
|
+
readonly kind: "ok" | "cancelled" | "failed";
|
|
337
|
+
readonly reason?: string;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** 重读 Pi 的模型目录(不联网);provider 报错时把细节带出来 */
|
|
341
|
+
export async function refreshVisionCatalogue(
|
|
342
|
+
registry: VisionMenuRegistry,
|
|
343
|
+
signal?: AbortSignal,
|
|
344
|
+
): Promise<RefreshResult> {
|
|
345
|
+
try {
|
|
346
|
+
const result = await registry.refresh({ allowNetwork: false, ...(signal ? { signal } : {}) });
|
|
347
|
+
if (result.aborted || signal?.aborted) return { kind: "cancelled" };
|
|
348
|
+
if (result.errors.size > 0) {
|
|
349
|
+
const detail = [...result.errors.entries()]
|
|
350
|
+
.map(([provider, error]) => `${provider}: ${error.message}`)
|
|
351
|
+
.join("; ");
|
|
352
|
+
return { kind: "failed", reason: detail };
|
|
353
|
+
}
|
|
354
|
+
return { kind: "ok" };
|
|
355
|
+
} catch (error) {
|
|
356
|
+
if (signal?.aborted) return { kind: "cancelled" };
|
|
357
|
+
return { kind: "failed", reason: describeError(error) };
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ─────────────────────────────────────────────────────────────
|
|
362
|
+
// UI:取消控制器 + 状态面板 + 模型选择器 + 主面板
|
|
363
|
+
// ─────────────────────────────────────────────────────────────
|
|
364
|
+
|
|
365
|
+
type StatusColor = "dim" | "warning" | "success" | "error";
|
|
366
|
+
type StatusSetter = (text: string, color: StatusColor, running?: boolean) => void;
|
|
367
|
+
|
|
368
|
+
interface Cancellable {
|
|
369
|
+
readonly signal: AbortSignal;
|
|
370
|
+
abort(reason?: unknown): void;
|
|
371
|
+
dispose(): void;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** 自己的 AbortController,并把会话的 ctx.signal 一起接进来;Esc / 卸载时都从这里断路 */
|
|
375
|
+
function startCancellable(parent: AbortSignal | undefined): Cancellable {
|
|
376
|
+
const controller = new AbortController();
|
|
377
|
+
const forward = (): void => controller.abort(parent?.reason);
|
|
378
|
+
if (parent?.aborted) forward();
|
|
379
|
+
else parent?.addEventListener("abort", forward, { once: true });
|
|
380
|
+
return {
|
|
381
|
+
signal: controller.signal,
|
|
382
|
+
abort: (reason) => controller.abort(reason),
|
|
383
|
+
dispose: () => parent?.removeEventListener("abort", forward),
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
interface PanelEnv {
|
|
388
|
+
readonly t: Translator;
|
|
389
|
+
readonly theme: MenuTheme;
|
|
390
|
+
readonly context: ExtensionContext;
|
|
391
|
+
/** pi 配置目录:写盘目标 */
|
|
392
|
+
readonly agentDir: string;
|
|
393
|
+
readonly runtime: EyesMenuRuntime;
|
|
394
|
+
readonly requestRender: () => void;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export interface EyesStatusPanelOptions extends PanelEnv {
|
|
398
|
+
readonly title: string;
|
|
399
|
+
readonly hint: string;
|
|
400
|
+
readonly onClose: () => void;
|
|
401
|
+
readonly onResult?: (text: string) => void;
|
|
402
|
+
readonly run: (signal: AbortSignal, setStatus: StatusSetter) => Promise<void>;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** 一次异步动作的展示面板:运行中显示进度,结束后显示结果,Esc 取消或返回 */
|
|
406
|
+
export class EyesStatusPanel extends Container {
|
|
407
|
+
private readonly options: EyesStatusPanelOptions;
|
|
408
|
+
private readonly controller: Cancellable;
|
|
409
|
+
private readonly body: Text;
|
|
410
|
+
private token = 0;
|
|
411
|
+
private running = true;
|
|
412
|
+
|
|
413
|
+
constructor(options: EyesStatusPanelOptions) {
|
|
414
|
+
super();
|
|
415
|
+
this.options = options;
|
|
416
|
+
this.controller = startCancellable(options.context.signal);
|
|
417
|
+
this.addChild(new Text(options.theme.title(options.title), 1, 0));
|
|
418
|
+
this.body = new Text(options.theme.fg("warning", ` ${options.t("module.eyes.running")}`), 1, 0);
|
|
419
|
+
this.addChild(this.body);
|
|
420
|
+
this.addChild(new Text(options.theme.hint(options.hint), 1, 0));
|
|
421
|
+
void this.start();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
handleInput(data: string): void {
|
|
425
|
+
if (!matchesKey(data, Key.escape)) return;
|
|
426
|
+
if (this.running) {
|
|
427
|
+
this.cancel();
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
this.options.onClose();
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
dispose(): void {
|
|
434
|
+
this.token += 1;
|
|
435
|
+
this.controller.dispose();
|
|
436
|
+
this.controller.abort(new DOMException("Disposed", "AbortError"));
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
private async start(): Promise<void> {
|
|
440
|
+
const token = ++this.token;
|
|
441
|
+
const setStatus: StatusSetter = (text, color, running = false) => {
|
|
442
|
+
if (token !== this.token) return;
|
|
443
|
+
this.running = running;
|
|
444
|
+
this.body.setText(this.options.theme.fg(color, ` ${text}`));
|
|
445
|
+
if (!running) this.options.onResult?.(text);
|
|
446
|
+
this.options.requestRender();
|
|
447
|
+
};
|
|
448
|
+
try {
|
|
449
|
+
await this.options.run(this.controller.signal, setStatus);
|
|
450
|
+
} catch (error) {
|
|
451
|
+
setStatus(singleLine(describeError(error)), "error");
|
|
452
|
+
} finally {
|
|
453
|
+
if (token === this.token) {
|
|
454
|
+
this.running = false;
|
|
455
|
+
this.controller.dispose();
|
|
456
|
+
this.options.requestRender();
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
private cancel(): void {
|
|
462
|
+
this.token += 1;
|
|
463
|
+
this.running = false;
|
|
464
|
+
this.controller.abort(new DOMException("Cancelled", "AbortError"));
|
|
465
|
+
this.body.setText(this.options.theme.fg("dim", ` ${this.options.t("module.eyes.cancelled")}`));
|
|
466
|
+
this.options.requestRender();
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
export interface VisionModelPickerOptions extends PanelEnv {
|
|
471
|
+
readonly selection: EyesRouteConfig;
|
|
472
|
+
readonly candidates: readonly VisionCandidate[];
|
|
473
|
+
readonly onSaved: () => void;
|
|
474
|
+
readonly onDone: (value?: string) => void;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** 视觉模型选择器:SelectList 形态;选具体模型先探测,auto / off 直接写盘 */
|
|
478
|
+
export class VisionModelPicker extends Container {
|
|
479
|
+
private readonly options: VisionModelPickerOptions;
|
|
480
|
+
private readonly initialValue: string;
|
|
481
|
+
private readonly list: SelectList;
|
|
482
|
+
private readonly controller: Cancellable;
|
|
483
|
+
private token = 0;
|
|
484
|
+
/** picking:选择列表;busy:探测/保存中(Esc 取消);settled:结果显示中(Esc 返回) */
|
|
485
|
+
private phase: "picking" | "busy" | "settled" = "picking";
|
|
486
|
+
|
|
487
|
+
constructor(options: VisionModelPickerOptions) {
|
|
488
|
+
super();
|
|
489
|
+
this.options = options;
|
|
490
|
+
this.initialValue = routeSelectionValue(options.selection);
|
|
491
|
+
this.controller = startCancellable(options.context.signal);
|
|
492
|
+
|
|
493
|
+
this.addChild(new Text(options.theme.title(options.t("module.eyes.model.label")), 1, 0));
|
|
494
|
+
const items = this.buildItems();
|
|
495
|
+
const currentIndex = items.findIndex((item) => item.value === this.initialValue);
|
|
496
|
+
this.list = new SelectList(items, Math.min(items.length, 10), options.theme.select);
|
|
497
|
+
if (currentIndex > 0) this.list.setSelectedIndex(currentIndex);
|
|
498
|
+
this.list.onSelect = (item) => {
|
|
499
|
+
void this.choose(item.value);
|
|
500
|
+
};
|
|
501
|
+
this.list.onCancel = () => this.options.onDone();
|
|
502
|
+
this.addChild(this.list);
|
|
503
|
+
if (options.candidates.length === 0) {
|
|
504
|
+
this.addChild(new Text(options.theme.fg("warning", ` ${options.t("module.eyes.model.empty")}`), 1, 0));
|
|
505
|
+
}
|
|
506
|
+
this.addChild(new Text(options.theme.hint(options.t("hint.chooseOptions")), 1, 0));
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
handleInput(data: string): void {
|
|
510
|
+
if (this.phase === "picking") {
|
|
511
|
+
this.list.handleInput(data);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
if (!matchesKey(data, Key.escape)) return;
|
|
515
|
+
if (this.phase === "busy") {
|
|
516
|
+
this.token += 1;
|
|
517
|
+
this.controller.abort(new DOMException("Cancelled", "AbortError"));
|
|
518
|
+
this.showStatus(this.options.t("module.eyes.cancelled"), "dim", "settled");
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
this.options.onDone();
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
dispose(): void {
|
|
525
|
+
this.token += 1;
|
|
526
|
+
this.controller.dispose();
|
|
527
|
+
this.controller.abort(new DOMException("Disposed", "AbortError"));
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
private buildItems(): SelectItem[] {
|
|
531
|
+
const t = this.options.t;
|
|
532
|
+
const items: SelectItem[] = [
|
|
533
|
+
{
|
|
534
|
+
value: VALUE_AUTO,
|
|
535
|
+
label: t("module.eyes.model.auto"),
|
|
536
|
+
description: t("module.eyes.model.autoDescription"),
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
value: VALUE_OFF,
|
|
540
|
+
label: t("module.eyes.model.off"),
|
|
541
|
+
description: t("module.eyes.model.offDescription"),
|
|
542
|
+
},
|
|
543
|
+
];
|
|
544
|
+
const unavailable = isModelValue(this.initialValue)
|
|
545
|
+
&& !this.options.candidates.some((candidate) => candidateKey(candidate) === this.initialValue);
|
|
546
|
+
if (unavailable) {
|
|
547
|
+
const selection = parseModelValue(this.initialValue);
|
|
548
|
+
if (selection) {
|
|
549
|
+
items.push({
|
|
550
|
+
value: this.initialValue,
|
|
551
|
+
label: `${selection.provider}/${selection.modelId}`,
|
|
552
|
+
description: t("module.eyes.model.unavailable"),
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
for (const candidate of this.options.candidates) {
|
|
557
|
+
items.push({
|
|
558
|
+
value: candidateKey(candidate),
|
|
559
|
+
label: `${candidate.provider}/${candidate.modelId}`,
|
|
560
|
+
description: candidate.authenticated
|
|
561
|
+
? t("module.eyes.model.authenticated")
|
|
562
|
+
: t("module.eyes.model.noAuthRequired"),
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
return items;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
private async choose(value: string): Promise<void> {
|
|
569
|
+
if (value === this.initialValue) {
|
|
570
|
+
this.options.onDone();
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
const token = ++this.token;
|
|
574
|
+
const t = this.options.t;
|
|
575
|
+
|
|
576
|
+
if (isModelValue(value)) {
|
|
577
|
+
const selection = parseModelValue(value);
|
|
578
|
+
if (!selection) return;
|
|
579
|
+
const model = `${selection.provider}/${selection.modelId}`;
|
|
580
|
+
this.showStatus(t("module.eyes.probe.running", { model }), "warning", "busy");
|
|
581
|
+
let probe: ProbeOutcome;
|
|
582
|
+
try {
|
|
583
|
+
probe = await probeVisionModel(this.options.context.modelRegistry, selection, this.controller.signal);
|
|
584
|
+
} catch (error) {
|
|
585
|
+
if (this.token !== token) return;
|
|
586
|
+
if (this.controller.signal.aborted) {
|
|
587
|
+
this.showStatus(t("module.eyes.cancelled"), "dim", "settled");
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
this.showStatus(`${t("module.eyes.probe.failed", {
|
|
591
|
+
model,
|
|
592
|
+
matched: 0,
|
|
593
|
+
total: VISION_PROBE_EXPECTED.length,
|
|
594
|
+
reason: singleLine(describeError(error)),
|
|
595
|
+
})} ${t("module.eyes.probe.unchanged")}`, "error", "settled");
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
if (this.token !== token) return;
|
|
599
|
+
if (this.controller.signal.aborted) {
|
|
600
|
+
this.showStatus(t("module.eyes.cancelled"), "dim", "settled");
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
if (!probe.passed) {
|
|
604
|
+
this.showStatus(`${t("module.eyes.probe.failed", {
|
|
605
|
+
model,
|
|
606
|
+
matched: probe.matched,
|
|
607
|
+
total: probe.total,
|
|
608
|
+
reason: singleLine(probe.text),
|
|
609
|
+
})} ${t("module.eyes.probe.unchanged")}`, "error", "settled");
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
this.showStatus(t("module.eyes.probe.passed", {
|
|
613
|
+
model,
|
|
614
|
+
matched: probe.matched,
|
|
615
|
+
total: probe.total,
|
|
616
|
+
response: singleLine(probe.text),
|
|
617
|
+
}), "success", "busy");
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
await this.save(value, token);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
private async save(value: string, token: number): Promise<void> {
|
|
624
|
+
const t = this.options.t;
|
|
625
|
+
this.showStatus(t("module.eyes.save.running"), "warning", "busy");
|
|
626
|
+
const result = await saveRouteSelection({
|
|
627
|
+
value,
|
|
628
|
+
openedWith: this.options.candidates,
|
|
629
|
+
registry: this.options.context.modelRegistry,
|
|
630
|
+
runtime: this.options.runtime,
|
|
631
|
+
context: this.options.context,
|
|
632
|
+
agentDir: this.options.agentDir,
|
|
633
|
+
});
|
|
634
|
+
if (this.token !== token) return;
|
|
635
|
+
if (result.kind === "modelsChanged") {
|
|
636
|
+
this.showStatus(t("module.eyes.save.modelsChanged"), "error", "settled");
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
if (result.kind === "failed" || !result.route) {
|
|
640
|
+
this.showStatus(`${t("module.eyes.save.failed", {
|
|
641
|
+
reason: result.reason ?? "",
|
|
642
|
+
})} ${t("module.eyes.probe.unchanged")}`, "error", "settled");
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
this.options.onSaved();
|
|
646
|
+
this.options.onDone(modelValueLabel(result.route, t));
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
private showStatus(text: string, color: StatusColor, phase: "busy" | "settled"): void {
|
|
650
|
+
this.phase = phase;
|
|
651
|
+
this.clear();
|
|
652
|
+
this.addChild(new Text(this.options.theme.title(this.options.t("module.eyes.model.label")), 1, 0));
|
|
653
|
+
this.addChild(new Text(this.options.theme.fg(color, ` ${text}`), 1, 0));
|
|
654
|
+
this.addChild(new Text(
|
|
655
|
+
this.options.theme.hint(this.options.t(phase === "busy" ? "hint.cancel" : "hint.back")),
|
|
656
|
+
1,
|
|
657
|
+
0,
|
|
658
|
+
));
|
|
659
|
+
this.options.requestRender();
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
export interface EyesVisionPanelOptions extends PanelEnv {
|
|
664
|
+
readonly onDone: (value: string) => void;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/** 视觉配置页:四行 SettingsList,子菜单各自负责自己的异步动作 */
|
|
668
|
+
export class EyesVisionPanel extends Container {
|
|
669
|
+
private readonly options: EyesVisionPanelOptions;
|
|
670
|
+
private list: I18nSettingsList | undefined;
|
|
671
|
+
private candidates: VisionCandidate[];
|
|
672
|
+
private checkValue: string | undefined;
|
|
673
|
+
|
|
674
|
+
constructor(options: EyesVisionPanelOptions) {
|
|
675
|
+
super();
|
|
676
|
+
this.options = options;
|
|
677
|
+
this.candidates = discoverCandidates(options.context.modelRegistry);
|
|
678
|
+
this.addChild(new Text(options.theme.title(options.t("module.eyes.menu.label")), 1, 0));
|
|
679
|
+
this.list = new I18nSettingsList({
|
|
680
|
+
items: this.buildItems(),
|
|
681
|
+
maxVisible: 8,
|
|
682
|
+
theme: options.theme.settings,
|
|
683
|
+
t: options.t,
|
|
684
|
+
onChange: (id) => {
|
|
685
|
+
if (id === ROW_MODEL) this.refreshRows();
|
|
686
|
+
},
|
|
687
|
+
onCancel: () => this.close(),
|
|
688
|
+
});
|
|
689
|
+
this.addChild(this.list);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
handleInput(data: string): void {
|
|
693
|
+
this.list?.handleInput(data);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
private close(): void {
|
|
697
|
+
this.options.onDone(describeRoute(this.options.runtime.globalRoute(), this.options.t));
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
private refreshRows(): void {
|
|
701
|
+
const route = this.options.runtime.globalRoute();
|
|
702
|
+
this.list?.updateValue(ROW_MODEL, modelValueLabel(route, this.options.t));
|
|
703
|
+
this.list?.updateValue(ROW_ROUTE, describeRoute(route, this.options.t));
|
|
704
|
+
this.list?.updateValue(ROW_REFRESH, this.refreshValue());
|
|
705
|
+
if (this.checkValue) this.list?.updateValue(ROW_CHECK, this.checkValue);
|
|
706
|
+
this.options.requestRender();
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
private refreshValue(): string {
|
|
710
|
+
return this.options.t("module.eyes.refresh.value", { count: this.candidates.length });
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
private buildItems(): SettingItem[] {
|
|
714
|
+
const t = this.options.t;
|
|
715
|
+
const route = this.options.runtime.globalRoute();
|
|
716
|
+
return [
|
|
717
|
+
{
|
|
718
|
+
id: ROW_MODEL,
|
|
719
|
+
label: t("module.eyes.model.label"),
|
|
720
|
+
description: t("module.eyes.model.description"),
|
|
721
|
+
currentValue: modelValueLabel(route, t),
|
|
722
|
+
submenu: (_currentValue, done) =>
|
|
723
|
+
new VisionModelPicker({
|
|
724
|
+
...this.options,
|
|
725
|
+
selection: this.options.runtime.globalRoute(),
|
|
726
|
+
candidates: this.candidates,
|
|
727
|
+
onSaved: () => this.refreshRows(),
|
|
728
|
+
onDone: done,
|
|
729
|
+
}),
|
|
730
|
+
},
|
|
731
|
+
{
|
|
732
|
+
id: ROW_REFRESH,
|
|
733
|
+
label: t("module.eyes.refresh.label"),
|
|
734
|
+
description: t("module.eyes.refresh.description"),
|
|
735
|
+
currentValue: this.refreshValue(),
|
|
736
|
+
submenu: (_currentValue, done) =>
|
|
737
|
+
new EyesStatusPanel({
|
|
738
|
+
...this.options,
|
|
739
|
+
title: t("module.eyes.refresh.label"),
|
|
740
|
+
hint: t("hint.back"),
|
|
741
|
+
onClose: () => done(),
|
|
742
|
+
run: async (signal, setStatus) => {
|
|
743
|
+
setStatus(t("module.eyes.refresh.running"), "warning", true);
|
|
744
|
+
const result = await refreshVisionCatalogue(this.options.context.modelRegistry, signal);
|
|
745
|
+
if (signal.aborted) {
|
|
746
|
+
setStatus(t("module.eyes.cancelled"), "dim");
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
if (result.kind === "ok") {
|
|
750
|
+
this.options.runtime.bumpCatalogueGeneration();
|
|
751
|
+
this.candidates = discoverCandidates(this.options.context.modelRegistry);
|
|
752
|
+
setStatus(t("module.eyes.refresh.complete"), "success");
|
|
753
|
+
this.refreshRows();
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
if (result.kind === "cancelled") {
|
|
757
|
+
setStatus(t("module.eyes.cancelled"), "dim");
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
setStatus(t("module.eyes.refresh.failed", { reason: result.reason ?? "" }), "error");
|
|
761
|
+
},
|
|
762
|
+
}),
|
|
763
|
+
},
|
|
764
|
+
{
|
|
765
|
+
id: ROW_CHECK,
|
|
766
|
+
label: t("module.eyes.check.label"),
|
|
767
|
+
description: t("module.eyes.check.description"),
|
|
768
|
+
currentValue: this.checkValue ?? t("module.eyes.check.value"),
|
|
769
|
+
submenu: (_currentValue, done) =>
|
|
770
|
+
new EyesStatusPanel({
|
|
771
|
+
...this.options,
|
|
772
|
+
title: t("module.eyes.check.label"),
|
|
773
|
+
hint: t("hint.back"),
|
|
774
|
+
onClose: () => done(),
|
|
775
|
+
onResult: (text) => {
|
|
776
|
+
this.checkValue = text;
|
|
777
|
+
this.list?.updateValue(ROW_CHECK, text);
|
|
778
|
+
},
|
|
779
|
+
run: async (signal, setStatus) => {
|
|
780
|
+
setStatus(t("module.eyes.check.running"), "warning", true);
|
|
781
|
+
const result = await runVisionSelfCheck({
|
|
782
|
+
t,
|
|
783
|
+
registry: this.options.context.modelRegistry,
|
|
784
|
+
runtime: this.options.runtime,
|
|
785
|
+
context: this.options.context,
|
|
786
|
+
route: this.options.runtime.globalRoute(),
|
|
787
|
+
candidates: this.candidates,
|
|
788
|
+
signal,
|
|
789
|
+
});
|
|
790
|
+
setStatus(result.text, result.kind === "passed" ? "success" : result.kind === "cancelled" ? "dim" : "error");
|
|
791
|
+
},
|
|
792
|
+
}),
|
|
793
|
+
},
|
|
794
|
+
{
|
|
795
|
+
id: ROW_ROUTE,
|
|
796
|
+
label: t("module.eyes.route.label"),
|
|
797
|
+
description: t("module.eyes.route.description"),
|
|
798
|
+
currentValue: describeRoute(route, t),
|
|
799
|
+
},
|
|
800
|
+
];
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/** 模块的菜单行:一行入口,进去是四行视觉配置页 */
|
|
805
|
+
export function buildEyesMenuItems(
|
|
806
|
+
context: ModuleMenuContext,
|
|
807
|
+
runtime: EyesMenuRuntime,
|
|
808
|
+
): readonly SettingItem[] {
|
|
809
|
+
const t = context.t;
|
|
810
|
+
return [
|
|
811
|
+
{
|
|
812
|
+
id: EYES_MENU_ITEM_ID,
|
|
813
|
+
label: t("module.eyes.menu.label"),
|
|
814
|
+
description: t("module.eyes.menu.description"),
|
|
815
|
+
currentValue: describeRoute(runtime.globalRoute(), t),
|
|
816
|
+
submenu: (_currentValue, done) =>
|
|
817
|
+
new EyesVisionPanel({
|
|
818
|
+
t,
|
|
819
|
+
theme: context.theme,
|
|
820
|
+
context: context.context,
|
|
821
|
+
agentDir: context.agentDir,
|
|
822
|
+
runtime,
|
|
823
|
+
requestRender: () => context.requestRender(),
|
|
824
|
+
onDone: (value) => done(value),
|
|
825
|
+
}),
|
|
826
|
+
},
|
|
827
|
+
];
|
|
828
|
+
}
|