@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 buyi1net
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# pi-toolkit
|
|
2
|
+
|
|
3
|
+
pi-toolkit 把日常编码中会用到的工具聚合进一个 Pi 扩展包,当前包含视觉辅助、子代理编排和供应商状态与外观三个模块,后续会继续加入 MCP 连接器等新能力。所有配置通过一个控制菜单完成,菜单的观感和操作方式与 Pi 原生 `/settings` 完全一致,支持英语、简体中文、繁体中文。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pi install npm:@buyi1net/pi-toolkit
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
或从 GitHub 安装:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pi install git:github.com/buyi1net/pi-toolkit
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
安装后在 Pi TUI 输入 `/pi-toolkit` 打开控制菜单。
|
|
18
|
+
|
|
19
|
+
## 模块
|
|
20
|
+
|
|
21
|
+
三个模块都可以在菜单顶层独立开关,关闭的模块不注册任何工具和钩子。
|
|
22
|
+
|
|
23
|
+
**视觉辅助(来自 pi-eyes)**:为纯文本模型自动补充图片理解能力,原生多模态模型直接旁路。主模型可通过 `vision_query` 工具针对最近截图追问。切换视觉模型前会先跑一次探测自检,不通过则保持原选择。
|
|
24
|
+
|
|
25
|
+
**子代理编排(来自 pi-subagents)**:spawn、调度、回收子代理。自动任务默认 headless 后台进程,演示型任务用 herdr 或 tmux 分屏,结果自动回注主会话。支持 tier 模型路由(fast / balanced / deep)与团队协作。`ask_question` 让子代理在卡住时向主会话提问。
|
|
26
|
+
|
|
27
|
+
**供应商状态与外观(来自 pi-tui)**:编辑器状态边框、Header、项目状态 Footer、启动与 reload 转场、单轮遥测,附带 16 个主题。供应商余额与套餐查询需要凭据时读取 `~/.pi/agent/pi-tui.json`(该文件由你手工维护,插件只读)。
|
|
28
|
+
|
|
29
|
+
## 配置
|
|
30
|
+
|
|
31
|
+
菜单改动写入 `~/.pi/agent/pi-toolkit.json`。子代理的 tier 路由也可被环境变量 `PI_SUBAGENTS_CONFIG`、项目或全局的 `pi-subagents.json` 覆盖,优先级与 pi-subagents 一致。
|
|
32
|
+
|
|
33
|
+
视觉辅助的图像预处理依赖 sharp,属于可选依赖:只在视觉模块启用且首次处理图片时加载;缺失时会提示安装命令,不影响其它模块。
|
|
34
|
+
|
|
35
|
+
## 隐私与边界
|
|
36
|
+
|
|
37
|
+
视觉图片只发送给你配置的 Pi 视觉模型 provider。凭据文件 `pi-tui.json` 永远不被本插件写入。子代理进程与你共享同一台机器的权限,请只派发可信任务。
|
|
38
|
+
|
|
39
|
+
## 致谢
|
|
40
|
+
|
|
41
|
+
子代理编排模块源自 [amosblomqvist/pi-interactive-subagents](https://github.com/amosblomqvist/pi-interactive-subagents)(MIT),在原实现基础上完成了 herdr 表面适配与进程内裁剪。
|
|
42
|
+
|
|
43
|
+
## 开源协议
|
|
44
|
+
|
|
45
|
+
pi-toolkit 的自有代码以 MIT License 发布。
|
package/assembler.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// 装配器:把模块定义按配置落成实际注册。
|
|
2
|
+
// 关键约束:enabled=false 的模块在这里被完全跳过,register 与 menuItems 都不会被触碰。
|
|
3
|
+
|
|
4
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import type { ToolkitConfig } from "./config.ts";
|
|
6
|
+
import type { Translator } from "./i18n.ts";
|
|
7
|
+
import {
|
|
8
|
+
assertModuleDefinition,
|
|
9
|
+
ENABLED_FIELD,
|
|
10
|
+
normalizeConfigValue,
|
|
11
|
+
type ModuleConfigRecord,
|
|
12
|
+
type ModuleConfigScalar,
|
|
13
|
+
type ModuleContext,
|
|
14
|
+
type ModuleDefinition,
|
|
15
|
+
} from "./module.ts";
|
|
16
|
+
import type { ServiceRegistry } from "./services.ts";
|
|
17
|
+
|
|
18
|
+
/** 通过校验并成功注册的模块 */
|
|
19
|
+
export interface ActiveModule {
|
|
20
|
+
readonly definition: ModuleDefinition;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ModuleFailure {
|
|
24
|
+
readonly definition: ModuleDefinition;
|
|
25
|
+
readonly error: unknown;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ModuleContextOptions {
|
|
29
|
+
readonly pi: ExtensionAPI;
|
|
30
|
+
readonly services: ServiceRegistry;
|
|
31
|
+
readonly t: Translator;
|
|
32
|
+
readonly getModuleConfig: (moduleId: string) => ModuleConfigRecord;
|
|
33
|
+
readonly reloadConfig: () => Promise<void>;
|
|
34
|
+
readonly setModuleConfig: (moduleId: string, key: string, value: ModuleConfigScalar) => Promise<ModuleConfigScalar>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface AssembleOptions extends ModuleContextOptions {
|
|
38
|
+
readonly modules: readonly ModuleDefinition[];
|
|
39
|
+
readonly config: ToolkitConfig;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface AssemblyResult {
|
|
43
|
+
readonly loaded: readonly ActiveModule[];
|
|
44
|
+
readonly failures: readonly ModuleFailure[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function moduleDefaults(definition: ModuleDefinition): ModuleConfigRecord {
|
|
48
|
+
const defaults: ModuleConfigRecord = {};
|
|
49
|
+
for (const [key, field] of Object.entries(definition.configSchema)) {
|
|
50
|
+
defaults[key] = field.default;
|
|
51
|
+
}
|
|
52
|
+
return defaults;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* schema 默认值 + 磁盘取值。
|
|
57
|
+
* schema 声明的键做规范化(非法值丢弃并用默认值兜底);schema 之外的键原样透传,
|
|
58
|
+
* 供模块读取自己的结构化配置(如视觉路由),菜单不渲染这些键。
|
|
59
|
+
*/
|
|
60
|
+
export function resolveModuleConfig(
|
|
61
|
+
definition: ModuleDefinition,
|
|
62
|
+
stored: ModuleConfigRecord | undefined,
|
|
63
|
+
): ModuleConfigRecord {
|
|
64
|
+
const resolved = moduleDefaults(definition);
|
|
65
|
+
if (!stored) return resolved;
|
|
66
|
+
for (const [key, value] of Object.entries(stored)) {
|
|
67
|
+
const field = definition.configSchema[key];
|
|
68
|
+
if (!field) {
|
|
69
|
+
resolved[key] = value;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const normalized = normalizeConfigValue(field, value);
|
|
73
|
+
if (normalized !== undefined) resolved[key] = normalized;
|
|
74
|
+
}
|
|
75
|
+
return resolved;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function isModuleEnabled(config: ModuleConfigRecord): boolean {
|
|
79
|
+
return config[ENABLED_FIELD] !== false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 模块菜单项 id:`<moduleId>.<field>`,模块自定义行自行避让该前缀 */
|
|
83
|
+
export function moduleFieldId(moduleId: string, field: string): string {
|
|
84
|
+
return `${moduleId}.${field}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function createModuleContext(
|
|
88
|
+
definition: ModuleDefinition,
|
|
89
|
+
options: ModuleContextOptions,
|
|
90
|
+
): ModuleContext {
|
|
91
|
+
return {
|
|
92
|
+
moduleId: definition.id,
|
|
93
|
+
pi: options.pi,
|
|
94
|
+
services: options.services,
|
|
95
|
+
t: options.t,
|
|
96
|
+
getConfig: () => options.getModuleConfig(definition.id),
|
|
97
|
+
reloadConfig: () => options.reloadConfig(),
|
|
98
|
+
setConfig: (key, value) => options.setModuleConfig(definition.id, key, value),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function assembleModules(options: AssembleOptions): AssemblyResult {
|
|
103
|
+
const loaded: ActiveModule[] = [];
|
|
104
|
+
const failures: ModuleFailure[] = [];
|
|
105
|
+
const seen = new Set<string>();
|
|
106
|
+
|
|
107
|
+
for (const definition of options.modules) {
|
|
108
|
+
assertModuleDefinition(definition);
|
|
109
|
+
if (seen.has(definition.id)) {
|
|
110
|
+
throw new Error(`模块 id 重复:${definition.id}`);
|
|
111
|
+
}
|
|
112
|
+
seen.add(definition.id);
|
|
113
|
+
|
|
114
|
+
const config = resolveModuleConfig(definition, options.config.modules[definition.id]);
|
|
115
|
+
if (!isModuleEnabled(config)) continue;
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
definition.register(createModuleContext(definition, options));
|
|
119
|
+
} catch (error) {
|
|
120
|
+
failures.push({ definition, error });
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
loaded.push({ definition });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return { loaded, failures };
|
|
127
|
+
}
|
package/config.ts
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// pi-toolkit 统一配置文件:<agentDir>/pi-toolkit.json
|
|
2
|
+
// 结构:schemaVersion + language + 各模块配置节。
|
|
3
|
+
// 写盘沿用 pi-eyes 的安全行为:剥掉敏感键、0600 权限、临时文件 + rename 原子替换。
|
|
4
|
+
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
7
|
+
import { basename, dirname, join } from "node:path";
|
|
8
|
+
import { isLanguageSetting, type LanguageSetting } from "./i18n.ts";
|
|
9
|
+
import type { ModuleConfigRecord, ModuleConfigValue } from "./module.ts";
|
|
10
|
+
|
|
11
|
+
export const TOOLKIT_CONFIG_FILENAME = "pi-toolkit.json";
|
|
12
|
+
export const TOOLKIT_SCHEMA_VERSION = 1;
|
|
13
|
+
|
|
14
|
+
export interface ToolkitConfig {
|
|
15
|
+
readonly schemaVersion: number;
|
|
16
|
+
readonly language: LanguageSetting;
|
|
17
|
+
/** 模块 id → 该模块的配置节(未经 schema 校验的原始值) */
|
|
18
|
+
readonly modules: Readonly<Record<string, ModuleConfigRecord>>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ToolkitConfigUpdate {
|
|
22
|
+
readonly language?: LanguageSetting;
|
|
23
|
+
readonly modules?: Readonly<Record<string, ModuleConfigRecord>>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface LoadedToolkitConfig {
|
|
27
|
+
readonly config: ToolkitConfig;
|
|
28
|
+
/** 文件缺失不算问题;解析异常、取值非法逐条记录,交给菜单展示 */
|
|
29
|
+
readonly problems: readonly string[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const DEFAULT_TOOLKIT_CONFIG: ToolkitConfig = {
|
|
33
|
+
schemaVersion: TOOLKIT_SCHEMA_VERSION,
|
|
34
|
+
language: "auto",
|
|
35
|
+
modules: {},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export function getToolkitConfigPath(agentDir: string): string {
|
|
39
|
+
return join(agentDir, TOOLKIT_CONFIG_FILENAME);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
43
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 配置节可存的值:标量、标量数组,或嵌套的对象/数组(模块私有结构) */
|
|
47
|
+
function isConfigValue(value: unknown): value is ModuleConfigValue {
|
|
48
|
+
if (value === null) return true;
|
|
49
|
+
const type = typeof value;
|
|
50
|
+
if (type === "string" || type === "number" || type === "boolean") return true;
|
|
51
|
+
if (Array.isArray(value)) return value.every(isConfigValue);
|
|
52
|
+
if (isRecord(value)) return Object.values(value).every(isConfigValue);
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function describeError(error: unknown): string {
|
|
57
|
+
return error instanceof Error ? error.message : String(error);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function parseToolkitConfig(raw: Record<string, unknown>, source: string, problems: string[]): ToolkitConfig {
|
|
61
|
+
const version = typeof raw.schemaVersion === "number" ? raw.schemaVersion : TOOLKIT_SCHEMA_VERSION;
|
|
62
|
+
if (version > TOOLKIT_SCHEMA_VERSION) {
|
|
63
|
+
problems.push(`${source}: schemaVersion ${version} 高于当前支持的 ${TOOLKIT_SCHEMA_VERSION},未识别的内容会原样保留`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let language: LanguageSetting = "auto";
|
|
67
|
+
if (raw.language !== undefined) {
|
|
68
|
+
if (isLanguageSetting(raw.language)) {
|
|
69
|
+
language = raw.language;
|
|
70
|
+
} else {
|
|
71
|
+
problems.push(`${source}: language 取值无效,已回退为 auto`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const modules: Record<string, ModuleConfigRecord> = {};
|
|
76
|
+
if (raw.modules !== undefined && !isRecord(raw.modules)) {
|
|
77
|
+
problems.push(`${source}: modules 不是对象,已忽略`);
|
|
78
|
+
} else if (isRecord(raw.modules)) {
|
|
79
|
+
for (const [moduleId, section] of Object.entries(raw.modules)) {
|
|
80
|
+
if (!isRecord(section)) {
|
|
81
|
+
problems.push(`${source}: 模块 ${moduleId} 的配置节不是对象,已忽略`);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const record: ModuleConfigRecord = {};
|
|
85
|
+
for (const [key, value] of Object.entries(section)) {
|
|
86
|
+
if (isConfigValue(value)) {
|
|
87
|
+
record[key] = value;
|
|
88
|
+
} else {
|
|
89
|
+
problems.push(`${source}: 模块 ${moduleId} 的字段 ${key} 取值类型不受支持,已忽略`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
modules[moduleId] = record;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { schemaVersion: TOOLKIT_SCHEMA_VERSION, language, modules };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function loadToolkitConfig(path: string): Promise<LoadedToolkitConfig> {
|
|
100
|
+
const problems: string[] = [];
|
|
101
|
+
let text: string;
|
|
102
|
+
try {
|
|
103
|
+
text = await readFile(path, "utf8");
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
106
|
+
problems.push(`${path}: 读取失败(${describeError(error)}),本次使用默认配置`);
|
|
107
|
+
}
|
|
108
|
+
return { config: DEFAULT_TOOLKIT_CONFIG, problems };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const parsed: unknown = JSON.parse(text);
|
|
113
|
+
if (!isRecord(parsed)) {
|
|
114
|
+
problems.push(`${path}: 顶层不是对象,本次使用默认配置`);
|
|
115
|
+
return { config: DEFAULT_TOOLKIT_CONFIG, problems };
|
|
116
|
+
}
|
|
117
|
+
return { config: parseToolkitConfig(parsed, path, problems), problems };
|
|
118
|
+
} catch (error) {
|
|
119
|
+
problems.push(`${path}: 解析失败(${describeError(error)}),本次使用默认配置`);
|
|
120
|
+
return { config: DEFAULT_TOOLKIT_CONFIG, problems };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function sensitiveKey(key: string): boolean {
|
|
125
|
+
const normalized = key.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
126
|
+
return (
|
|
127
|
+
normalized === "auth" ||
|
|
128
|
+
normalized === "authorization" ||
|
|
129
|
+
normalized === "headers" ||
|
|
130
|
+
normalized.includes("apikey") ||
|
|
131
|
+
normalized.includes("token") ||
|
|
132
|
+
normalized.includes("secret") ||
|
|
133
|
+
normalized.includes("password") ||
|
|
134
|
+
normalized.includes("credential")
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function sanitizeForWrite(value: unknown): unknown {
|
|
139
|
+
if (Array.isArray(value)) return value.map(sanitizeForWrite);
|
|
140
|
+
if (!isRecord(value)) return value;
|
|
141
|
+
const result: Record<string, unknown> = {};
|
|
142
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
143
|
+
if (!sensitiveKey(key) && entry !== undefined) result[key] = sanitizeForWrite(entry);
|
|
144
|
+
}
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function mergeRaw(base: Record<string, unknown>, update: Record<string, unknown>): Record<string, unknown> {
|
|
149
|
+
const result = { ...base };
|
|
150
|
+
for (const [key, value] of Object.entries(update)) {
|
|
151
|
+
if (value === undefined) continue;
|
|
152
|
+
const current = result[key];
|
|
153
|
+
result[key] = isRecord(current) && isRecord(value) ? mergeRaw(current, value) : value;
|
|
154
|
+
}
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function atomicWrite(path: string, text: string): Promise<void> {
|
|
159
|
+
await mkdir(dirname(path), { recursive: true });
|
|
160
|
+
const temp = join(dirname(path), `.${basename(path)}.${process.pid}-${randomUUID()}.tmp`);
|
|
161
|
+
try {
|
|
162
|
+
await writeFile(temp, text, { encoding: "utf8", mode: 0o600 });
|
|
163
|
+
await rename(temp, path);
|
|
164
|
+
} finally {
|
|
165
|
+
await rm(temp, { force: true });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function readRaw(path: string): Promise<Record<string, unknown> | undefined> {
|
|
170
|
+
try {
|
|
171
|
+
const parsed: unknown = JSON.parse(await readFile(path, "utf8"));
|
|
172
|
+
return isRecord(parsed) ? parsed : undefined;
|
|
173
|
+
} catch {
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function toRawUpdate(update: ToolkitConfigUpdate): Record<string, unknown> {
|
|
179
|
+
return {
|
|
180
|
+
...(update.language !== undefined ? { language: update.language } : {}),
|
|
181
|
+
...(update.modules !== undefined ? { modules: update.modules } : {}),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 合并写入:保留磁盘上未识别的键(合并进现有内容),剥掉敏感键后原子替换。
|
|
187
|
+
* 覆盖前把上一版内容留一份 `<path>.bak`。
|
|
188
|
+
*/
|
|
189
|
+
export async function saveToolkitConfig(path: string, update: ToolkitConfigUpdate): Promise<ToolkitConfig> {
|
|
190
|
+
const existing = await readRaw(path);
|
|
191
|
+
const cleanExisting = existing ? (sanitizeForWrite(existing) as Record<string, unknown>) : {};
|
|
192
|
+
const merged = mergeRaw(cleanExisting, toRawUpdate(update));
|
|
193
|
+
merged.schemaVersion = TOOLKIT_SCHEMA_VERSION;
|
|
194
|
+
|
|
195
|
+
const config = parseToolkitConfig(merged, path, []);
|
|
196
|
+
if (existing) {
|
|
197
|
+
await atomicWrite(`${path}.bak`, `${JSON.stringify(cleanExisting, null, 2)}\n`);
|
|
198
|
+
}
|
|
199
|
+
await atomicWrite(path, `${JSON.stringify(merged, null, 2)}\n`);
|
|
200
|
+
return config;
|
|
201
|
+
}
|