@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,139 @@
|
|
|
1
|
+
// 顶层菜单组件与菜单改动落盘。
|
|
2
|
+
// 顶层用 SettingsList 内嵌在 Container 里(上下边框),分组页与取值选择器见 panels.ts。
|
|
3
|
+
|
|
4
|
+
import { DynamicBorder } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { Container } from "@earendil-works/pi-tui";
|
|
7
|
+
import type { LanguageSetting } from "../i18n.ts";
|
|
8
|
+
import type { ModuleConfigScalar } from "../module.ts";
|
|
9
|
+
import type { Toolkit } from "../toolkit.ts";
|
|
10
|
+
import {
|
|
11
|
+
buildTopLevelItems,
|
|
12
|
+
fieldValueFromLabel,
|
|
13
|
+
ID_LANGUAGE,
|
|
14
|
+
languageFromLabel,
|
|
15
|
+
type MenuBuildContext,
|
|
16
|
+
type MenuState,
|
|
17
|
+
} from "./items.ts";
|
|
18
|
+
import { I18nSettingsList } from "./settings-list.ts";
|
|
19
|
+
import type { MenuTheme } from "./theme.ts";
|
|
20
|
+
|
|
21
|
+
const TOP_LEVEL_MAX_VISIBLE = 8;
|
|
22
|
+
|
|
23
|
+
export function readMenuState(toolkit: Toolkit): MenuState {
|
|
24
|
+
return {
|
|
25
|
+
config: toolkit.getConfig(),
|
|
26
|
+
modules: toolkit.modules,
|
|
27
|
+
problems: toolkit.problems,
|
|
28
|
+
services: toolkit.services,
|
|
29
|
+
language: toolkit.getLanguage(),
|
|
30
|
+
resolvedLanguage: toolkit.getResolvedLanguage(),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ToolkitMenuOptions {
|
|
35
|
+
readonly toolkit: Toolkit;
|
|
36
|
+
readonly theme: MenuTheme;
|
|
37
|
+
/** 打开菜单时的扩展上下文(模块自绘子菜单用) */
|
|
38
|
+
readonly context: ExtensionContext;
|
|
39
|
+
/** pi 的配置目录(传给模块菜单,供模块自写盘) */
|
|
40
|
+
readonly agentDir: string;
|
|
41
|
+
/** 异步动作结束后请宿主重绘 */
|
|
42
|
+
readonly requestRender: () => void;
|
|
43
|
+
/** 菜单里每次取值变化都先到这里(id + 显示文案) */
|
|
44
|
+
readonly onChange: (id: string, value: string) => void;
|
|
45
|
+
/** 顶层 Esc:关闭菜单 */
|
|
46
|
+
readonly onClose: () => void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class ToolkitMenu extends Container {
|
|
50
|
+
private readonly options: ToolkitMenuOptions;
|
|
51
|
+
private readonly build: MenuBuildContext;
|
|
52
|
+
private list: I18nSettingsList | undefined;
|
|
53
|
+
|
|
54
|
+
constructor(options: ToolkitMenuOptions) {
|
|
55
|
+
super();
|
|
56
|
+
this.options = options;
|
|
57
|
+
this.build = {
|
|
58
|
+
// 译者始终读当前语言,重建时 label 自然换成新语言
|
|
59
|
+
t: options.toolkit.getTranslator(),
|
|
60
|
+
theme: options.theme,
|
|
61
|
+
context: options.context,
|
|
62
|
+
agentDir: options.agentDir,
|
|
63
|
+
getState: () => readMenuState(options.toolkit),
|
|
64
|
+
getModuleConfig: (moduleId) => options.toolkit.getModuleConfig(moduleId),
|
|
65
|
+
onChange: options.onChange,
|
|
66
|
+
requestRender: () => options.requestRender(),
|
|
67
|
+
refresh: (selectId) => this.rebuild(selectId),
|
|
68
|
+
};
|
|
69
|
+
this.rebuild();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 语言切换后重建整棵菜单,让全部文案立刻生效 */
|
|
73
|
+
refresh(selectId?: string): void {
|
|
74
|
+
this.rebuild(selectId);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
handleInput(data: string): void {
|
|
78
|
+
this.list?.handleInput(data);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private rebuild(selectId?: string): void {
|
|
82
|
+
this.clear();
|
|
83
|
+
const items = buildTopLevelItems(this.build);
|
|
84
|
+
this.addChild(new DynamicBorder(this.options.theme.border));
|
|
85
|
+
const list = new I18nSettingsList({
|
|
86
|
+
items,
|
|
87
|
+
maxVisible: Math.min(Math.max(items.length, 1), TOP_LEVEL_MAX_VISIBLE),
|
|
88
|
+
theme: this.options.theme.settings,
|
|
89
|
+
t: this.build.t,
|
|
90
|
+
onChange: this.options.onChange,
|
|
91
|
+
onCancel: () => this.options.onClose(),
|
|
92
|
+
enableSearch: true,
|
|
93
|
+
});
|
|
94
|
+
this.list = list;
|
|
95
|
+
this.addChild(list);
|
|
96
|
+
this.addChild(new DynamicBorder(this.options.theme.border));
|
|
97
|
+
if (selectId) list.selectItem(selectId);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export type MenuChangeResult =
|
|
102
|
+
| { readonly kind: "language"; readonly language: LanguageSetting }
|
|
103
|
+
| {
|
|
104
|
+
readonly kind: "module-field";
|
|
105
|
+
readonly moduleId: string;
|
|
106
|
+
readonly field: string;
|
|
107
|
+
readonly value: ModuleConfigScalar;
|
|
108
|
+
}
|
|
109
|
+
| { readonly kind: "ignored" };
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 把菜单行 id 与显示文案翻回配置改动并持久化。
|
|
113
|
+
* 显示文案 → 规范化取值的映射见 items.ts(同一份 i18n 选项表)。
|
|
114
|
+
*/
|
|
115
|
+
export async function applyMenuChange(
|
|
116
|
+
toolkit: Toolkit,
|
|
117
|
+
id: string,
|
|
118
|
+
displayValue: string,
|
|
119
|
+
): Promise<MenuChangeResult> {
|
|
120
|
+
const t = toolkit.getTranslator();
|
|
121
|
+
|
|
122
|
+
if (id === ID_LANGUAGE) {
|
|
123
|
+
const language = languageFromLabel(t, displayValue);
|
|
124
|
+
if (!language) return { kind: "ignored" };
|
|
125
|
+
return { kind: "language", language: await toolkit.setLanguage(language) };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const separator = id.indexOf(".");
|
|
129
|
+
if (separator <= 0) return { kind: "ignored" };
|
|
130
|
+
const moduleId = id.slice(0, separator);
|
|
131
|
+
const field = id.slice(separator + 1);
|
|
132
|
+
const definition = toolkit.getModuleDefinition(moduleId);
|
|
133
|
+
const schemaField = definition?.configSchema[field];
|
|
134
|
+
if (!definition || !schemaField) return { kind: "ignored" };
|
|
135
|
+
|
|
136
|
+
const value = fieldValueFromLabel(t, schemaField, displayValue);
|
|
137
|
+
if (value === undefined) return { kind: "ignored" };
|
|
138
|
+
return { kind: "module-field", moduleId, field, value: await toolkit.setModuleField(moduleId, field, value) };
|
|
139
|
+
}
|
package/module.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// 模块契约:每个源插件迁入 pi-toolkit 后实现为一个 ModuleDefinition。
|
|
2
|
+
// 装配器只负责按 enabled 开关决定是否调用 register;模块自己决定注册哪些命令/工具/钩子。
|
|
3
|
+
|
|
4
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import type { SettingItem } from "@earendil-works/pi-tui";
|
|
6
|
+
import type { MessageKey, Translator } from "./i18n.ts";
|
|
7
|
+
import type { MenuTheme } from "./menu/theme.ts";
|
|
8
|
+
import type { ServiceRegistry } from "./services.ts";
|
|
9
|
+
|
|
10
|
+
/** 配置节里的标量取值;schema 字段(菜单控件)只接受标量 */
|
|
11
|
+
export type ModuleConfigScalar = string | number | boolean | null;
|
|
12
|
+
/**
|
|
13
|
+
* 配置节里能存的值:标量、数组与嵌套对象。
|
|
14
|
+
* 嵌套结构不属于 schema 字段(菜单不渲染),模块在自己的配置解析里读取校验,
|
|
15
|
+
* 例如视觉模块的 `backend.route.allowedModels` / `fixedModel`。
|
|
16
|
+
*/
|
|
17
|
+
export type ModuleConfigValue =
|
|
18
|
+
| ModuleConfigScalar
|
|
19
|
+
| ModuleConfigValue[]
|
|
20
|
+
| { [key: string]: ModuleConfigValue };
|
|
21
|
+
export type ModuleConfigRecord = Record<string, ModuleConfigValue>;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 一个配置字段。
|
|
25
|
+
* - default 为 boolean:开/关字段
|
|
26
|
+
* - default 为 string 且带 values:枚举字段(配置里存 values 中的规范化代码)
|
|
27
|
+
* - 枚举值的显示文案从 valueLabelKeys 取;某个值没配键时直接显示该值原文
|
|
28
|
+
*
|
|
29
|
+
* 数值/自由文本字段等后续工单需要时再扩展控件类型,本工单不预置无用分支。
|
|
30
|
+
*/
|
|
31
|
+
export interface ConfigField {
|
|
32
|
+
default: ModuleConfigScalar;
|
|
33
|
+
values?: readonly string[];
|
|
34
|
+
labelKey: MessageKey;
|
|
35
|
+
descriptionKey?: MessageKey;
|
|
36
|
+
/** 枚举值 → 显示文案键,必须覆盖 values 里的每一项 */
|
|
37
|
+
valueLabelKeys?: Readonly<Record<string, MessageKey>>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type ModuleConfigSchema = Record<string, ConfigField>;
|
|
41
|
+
|
|
42
|
+
/** 菜单分组:顶层菜单的两组 */
|
|
43
|
+
export type ModuleGroup = "general" | "subagents";
|
|
44
|
+
export const MODULE_GROUPS: readonly ModuleGroup[] = ["general", "subagents"];
|
|
45
|
+
|
|
46
|
+
/** 模块总开关字段名;每个模块的 schema 必须声明它 */
|
|
47
|
+
export const ENABLED_FIELD = "enabled";
|
|
48
|
+
|
|
49
|
+
export function enabledField(labelKey: MessageKey, descriptionKey?: MessageKey): ConfigField {
|
|
50
|
+
return { default: true, labelKey, descriptionKey };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function isBooleanField(field: ConfigField): boolean {
|
|
54
|
+
return typeof field.default === "boolean";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** 把外部输入(菜单回传的显示文案、配置文件里的值)规范化成 schema 接受的值;不接受则返回 undefined */
|
|
58
|
+
export function normalizeConfigValue(field: ConfigField, value: unknown): ModuleConfigScalar | undefined {
|
|
59
|
+
if (typeof field.default === "boolean") {
|
|
60
|
+
if (typeof value === "boolean") return value;
|
|
61
|
+
if (value === "true") return true;
|
|
62
|
+
if (value === "false") return false;
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
if (typeof field.default === "number") {
|
|
66
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : undefined;
|
|
67
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
68
|
+
const parsed = Number(value);
|
|
69
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
70
|
+
}
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
if (typeof value !== "string") return undefined;
|
|
74
|
+
if (field.values && !field.values.includes(value)) return undefined;
|
|
75
|
+
return value;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface ModuleContext {
|
|
79
|
+
readonly moduleId: string;
|
|
80
|
+
/** 收到 pi API 后自行注册命令、工具与钩子 */
|
|
81
|
+
readonly pi: ExtensionAPI;
|
|
82
|
+
readonly services: ServiceRegistry;
|
|
83
|
+
readonly t: Translator;
|
|
84
|
+
/** 实时读取本模块配置(已合并 schema 默认值) */
|
|
85
|
+
getConfig(): ModuleConfigRecord;
|
|
86
|
+
/**
|
|
87
|
+
* 重新从磁盘读取整份配置文件并刷新状态中枢的内存副本。
|
|
88
|
+
* 模块用自己的配置层写到盘上(如视觉路由)之后,靠它让 getConfig 跟上磁盘。
|
|
89
|
+
*/
|
|
90
|
+
reloadConfig(): Promise<void>;
|
|
91
|
+
/**
|
|
92
|
+
* 写入并持久化单个配置字段,返回规范化后的值。
|
|
93
|
+
* 只接受 schema 声明的字段;模块私有的结构化配置(非 schema 键)由模块自己的配置层写入。
|
|
94
|
+
*/
|
|
95
|
+
setConfig(key: string, value: ModuleConfigScalar): Promise<ModuleConfigScalar>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface ModuleMenuContext {
|
|
99
|
+
readonly t: Translator;
|
|
100
|
+
readonly services: ServiceRegistry;
|
|
101
|
+
getConfig(): ModuleConfigRecord;
|
|
102
|
+
/** 打开菜单时的扩展上下文:需要与宿主交互的菜单项(如视觉探测)从这里取 modelRegistry / model / signal */
|
|
103
|
+
readonly context: ExtensionContext;
|
|
104
|
+
/** pi 的配置目录(<agentDir>/pi-toolkit.json 就在它下面):模块自写盘时用 */
|
|
105
|
+
readonly agentDir: string;
|
|
106
|
+
/** 子菜单自绘面板上色用的主题 */
|
|
107
|
+
readonly theme: MenuTheme;
|
|
108
|
+
/** 异步动作结束后请宿主重绘(pi 的 TUI 是按需渲染) */
|
|
109
|
+
requestRender(): void;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface ModuleDefinition {
|
|
113
|
+
/** 模块 id:同时是配置节名与菜单项 id 前缀 */
|
|
114
|
+
readonly id: string;
|
|
115
|
+
readonly labelKey: MessageKey;
|
|
116
|
+
readonly descriptionKey: MessageKey;
|
|
117
|
+
readonly group: ModuleGroup;
|
|
118
|
+
/** 配置 schema,必须包含 enabled 字段 */
|
|
119
|
+
readonly configSchema: ModuleConfigSchema;
|
|
120
|
+
/** 仅在模块启用时调用 */
|
|
121
|
+
readonly register: (context: ModuleContext) => void;
|
|
122
|
+
/** 可选:schema 自动生成的行之外,本模块专属的菜单行;同样只在模块启用时渲染 */
|
|
123
|
+
readonly menuItems?: (context: ModuleMenuContext) => readonly SettingItem[];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const MODULE_ID_PATTERN = /^[a-z0-9][a-z0-9._-]*$/;
|
|
127
|
+
|
|
128
|
+
/** 装配期校验:schema 写错就当场抛出,不拖到菜单渲染时才炸 */
|
|
129
|
+
export function assertModuleDefinition(definition: ModuleDefinition): void {
|
|
130
|
+
if (!MODULE_ID_PATTERN.test(definition.id)) {
|
|
131
|
+
throw new Error(`模块 id 不合法:${JSON.stringify(definition.id)}`);
|
|
132
|
+
}
|
|
133
|
+
if (!MODULE_GROUPS.includes(definition.group)) {
|
|
134
|
+
throw new Error(`模块 ${definition.id} 的菜单分组不合法:${String(definition.group)}`);
|
|
135
|
+
}
|
|
136
|
+
const enabled = definition.configSchema[ENABLED_FIELD];
|
|
137
|
+
if (!enabled || !isBooleanField(enabled)) {
|
|
138
|
+
throw new Error(`模块 ${definition.id} 的 schema 必须包含 enabled 开/关字段`);
|
|
139
|
+
}
|
|
140
|
+
for (const [key, field] of Object.entries(definition.configSchema)) {
|
|
141
|
+
if (isBooleanField(field)) continue;
|
|
142
|
+
if (!field.values || field.values.length === 0) {
|
|
143
|
+
throw new Error(`模块 ${definition.id} 的字段 ${key} 需要 values 才能生成菜单控件`);
|
|
144
|
+
}
|
|
145
|
+
if (field.values.length !== new Set(field.values).size) {
|
|
146
|
+
throw new Error(`模块 ${definition.id} 的字段 ${key} 含重复 values`);
|
|
147
|
+
}
|
|
148
|
+
if (field.valueLabelKeys) {
|
|
149
|
+
for (const value of field.values) {
|
|
150
|
+
if (!field.valueLabelKeys[value]) {
|
|
151
|
+
throw new Error(`模块 ${definition.id} 的字段 ${key} 缺少取值 ${value} 的显示文案键`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// 共享视觉链:Pi 视觉模型选择、熔断与 deadline。
|
|
2
|
+
// 视觉模块持有一个实例,context 注入与 vision_query 的模型调用都经它提问。
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
callPiVisionModel,
|
|
6
|
+
piVisionModelKey,
|
|
7
|
+
selectAutomaticPiVisionModel,
|
|
8
|
+
type PiVisionModelRegistry,
|
|
9
|
+
type PiVisionModelSelection,
|
|
10
|
+
} from "./pi-model-backend.ts";
|
|
11
|
+
import { VisionCircuit, TurnMemory, classifyFailure, buildFailureJson, type FailureKind } from "./resilience.ts";
|
|
12
|
+
import type { VisionImage } from "./vision-types.ts";
|
|
13
|
+
|
|
14
|
+
export interface VisionAnswerOk {
|
|
15
|
+
ok: true;
|
|
16
|
+
text: string;
|
|
17
|
+
backend: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface VisionAnswerFail {
|
|
21
|
+
ok: false;
|
|
22
|
+
json: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type VisionAnswer = VisionAnswerOk | VisionAnswerFail;
|
|
26
|
+
|
|
27
|
+
export interface VisionRoutingConfig {
|
|
28
|
+
route: {
|
|
29
|
+
mode: "automatic" | "fixed";
|
|
30
|
+
allowedModels: PiVisionModelSelection[] | null;
|
|
31
|
+
fixedModel?: PiVisionModelSelection;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const DEFAULT_ROUTING: VisionRoutingConfig = {
|
|
36
|
+
route: { mode: "automatic", allowedModels: null },
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export class VisionChain {
|
|
40
|
+
readonly circuit = new VisionCircuit();
|
|
41
|
+
readonly turnMemory = new TurnMemory();
|
|
42
|
+
private turnCount = 0;
|
|
43
|
+
private routing: VisionRoutingConfig = DEFAULT_ROUTING;
|
|
44
|
+
private stickyAutomaticModel?: PiVisionModelSelection;
|
|
45
|
+
private readonly failedAutomaticModels = new Set<string>();
|
|
46
|
+
|
|
47
|
+
setRouting(config: VisionRoutingConfig): void {
|
|
48
|
+
if (config.route.mode === "fixed" && !config.route.fixedModel) {
|
|
49
|
+
throw new Error("fixed 路由必须指定 fixedModel");
|
|
50
|
+
}
|
|
51
|
+
this.routing = {
|
|
52
|
+
route: {
|
|
53
|
+
mode: config.route.mode,
|
|
54
|
+
allowedModels: config.route.allowedModels?.map((model) => ({ ...model })) ?? null,
|
|
55
|
+
...(config.route.fixedModel ? { fixedModel: { ...config.route.fixedModel } } : {}),
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
this.stickyAutomaticModel = undefined;
|
|
59
|
+
this.failedAutomaticModels.clear();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** 一次 agent run 记一轮:清上一轮的失败短路与无效请求熔断。 */
|
|
63
|
+
beginTurn(): void {
|
|
64
|
+
this.turnCount += 1;
|
|
65
|
+
this.turnMemory.newTurn(this.turnCount);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
selectTarget(
|
|
69
|
+
registry: PiVisionModelRegistry,
|
|
70
|
+
currentModel?: PiVisionModelSelection,
|
|
71
|
+
): PiVisionModelSelection | undefined {
|
|
72
|
+
return this.selectPiTarget(registry, currentModel);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private selectPiTarget(
|
|
76
|
+
registry: PiVisionModelRegistry,
|
|
77
|
+
currentModel?: PiVisionModelSelection,
|
|
78
|
+
): PiVisionModelSelection | undefined {
|
|
79
|
+
if (this.routing.route.mode === "fixed") return this.routing.route.fixedModel;
|
|
80
|
+
|
|
81
|
+
if (this.stickyAutomaticModel) {
|
|
82
|
+
const availableSticky = selectAutomaticPiVisionModel(registry, {
|
|
83
|
+
allowedModels: [this.stickyAutomaticModel],
|
|
84
|
+
excludedModels: this.failedAutomaticModels,
|
|
85
|
+
});
|
|
86
|
+
if (availableSticky) return availableSticky;
|
|
87
|
+
this.stickyAutomaticModel = undefined;
|
|
88
|
+
}
|
|
89
|
+
const selected = selectAutomaticPiVisionModel(registry, {
|
|
90
|
+
currentModel,
|
|
91
|
+
allowedModels: this.routing.route.allowedModels,
|
|
92
|
+
excludedModels: this.failedAutomaticModels,
|
|
93
|
+
});
|
|
94
|
+
this.stickyAutomaticModel = selected;
|
|
95
|
+
return selected;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 向视觉链提一次问。deadlineAt 是整个工具任务(可能含多次 ask)的共享截止,
|
|
100
|
+
* 单后端再叠加 per-call 上限;本轮已全失败时直接短路。
|
|
101
|
+
*/
|
|
102
|
+
async ask(
|
|
103
|
+
registry: PiVisionModelRegistry,
|
|
104
|
+
images: VisionImage[],
|
|
105
|
+
prompt: string,
|
|
106
|
+
options: {
|
|
107
|
+
signal?: AbortSignal;
|
|
108
|
+
deadlineAt: number;
|
|
109
|
+
perCallMs?: number;
|
|
110
|
+
currentModel?: PiVisionModelSelection;
|
|
111
|
+
},
|
|
112
|
+
): Promise<VisionAnswer> {
|
|
113
|
+
options.signal?.throwIfAborted();
|
|
114
|
+
if (this.turnMemory.allFailed) {
|
|
115
|
+
return { ok: false, json: buildFailureJson(["OTHER"], this.turnMemory.attempts) };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const piSelection = this.selectPiTarget(registry, options.currentModel);
|
|
119
|
+
if (!piSelection) {
|
|
120
|
+
const attempted = ["pi: no available vision model"];
|
|
121
|
+
this.turnMemory.recordAttempt(attempted[0]);
|
|
122
|
+
this.turnMemory.markAllFailed();
|
|
123
|
+
return { ok: false, json: buildFailureJson(["OTHER"], attempted) };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const targetId = `${piSelection.provider}/${piSelection.modelId}`;
|
|
127
|
+
const attempted: string[] = [];
|
|
128
|
+
const failureKinds: FailureKind[] = [];
|
|
129
|
+
const remaining = options.deadlineAt - Date.now();
|
|
130
|
+
if (remaining <= 0) {
|
|
131
|
+
failureKinds.push("TIMEOUT");
|
|
132
|
+
attempted.push(`${targetId}: skipped (task deadline exhausted)`);
|
|
133
|
+
} else {
|
|
134
|
+
const gate = this.circuit.inspect(targetId, this.turnCount);
|
|
135
|
+
if (gate.blocked) {
|
|
136
|
+
attempted.push(`${targetId}: skipped (circuit open: ${gate.reason})`);
|
|
137
|
+
} else {
|
|
138
|
+
options.signal?.throwIfAborted();
|
|
139
|
+
const perCall = Math.min(options.perCallMs ?? 60_000, remaining);
|
|
140
|
+
const callSignal = options.signal
|
|
141
|
+
? AbortSignal.any([options.signal, AbortSignal.timeout(perCall)])
|
|
142
|
+
: AbortSignal.timeout(perCall);
|
|
143
|
+
try {
|
|
144
|
+
const text = await callPiVisionModel(registry, piSelection, prompt, images, {
|
|
145
|
+
signal: callSignal,
|
|
146
|
+
maxTokens: 4096,
|
|
147
|
+
});
|
|
148
|
+
this.circuit.clear(targetId);
|
|
149
|
+
return { ok: true, text, backend: targetId };
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if (options.signal?.aborted) options.signal.throwIfAborted();
|
|
152
|
+
const failure = classifyFailure({
|
|
153
|
+
message: error instanceof Error ? error.message : String(error),
|
|
154
|
+
});
|
|
155
|
+
this.circuit.record(targetId, failure, this.turnCount);
|
|
156
|
+
if (this.routing.route.mode === "automatic" && failure.kind !== "INVALID_REQUEST") {
|
|
157
|
+
this.failedAutomaticModels.add(piVisionModelKey(piSelection.provider, piSelection.modelId));
|
|
158
|
+
this.stickyAutomaticModel = undefined;
|
|
159
|
+
}
|
|
160
|
+
this.turnMemory.recordAttempt(`${targetId}: ${failure.kind}`);
|
|
161
|
+
attempted.push(`${targetId}: ${failure.kind}`);
|
|
162
|
+
failureKinds.push(failure.kind);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// 自动模式每个请求只试一个 Pi 模型;该候选失败后,下次调用可换下一个。
|
|
167
|
+
const automaticCanTryAnotherModel =
|
|
168
|
+
this.routing.route.mode === "automatic" && piSelection !== undefined;
|
|
169
|
+
// 400/413/422 往往是本次参数或负载问题,允许调用方修正后重试。
|
|
170
|
+
if (!failureKinds.includes("INVALID_REQUEST") && !automaticCanTryAnotherModel) this.turnMemory.markAllFailed();
|
|
171
|
+
return { ok: false, json: buildFailureJson(failureKinds.length > 0 ? failureKinds : ["OTHER"], attempted) };
|
|
172
|
+
}
|
|
173
|
+
}
|