@wingsbutterfly/pi-zh 0.3.0
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 +107 -0
- package/extensions/index.ts +195 -0
- package/lib/coexist.ts +86 -0
- package/lib/config.ts +71 -0
- package/lib/dict.ts +303 -0
- package/lib/patch.ts +126 -0
- package/lib/sdk.ts +44 -0
- package/lib/targets.ts +96 -0
- package/lib/telegram-commands.ts +185 -0
- package/package.json +45 -0
- package/scripts/zh-dict.mjs +154 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 wings1848
|
|
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,107 @@
|
|
|
1
|
+
# pi-zh — Pi 汉化插件
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@wingsbutterfly/pi-zh)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](https://github.com/wings1848/pi-zh/actions/workflows/ci.yml)
|
|
6
|
+
|
|
7
|
+
统一汉化 Pi 及其生态插件的界面文案。**默认不启用**,通过 `/pi-zh on` 优雅开启,`/pi-zh off` 一键还原。零侵入:不改动任何上游 API 行为,仅做**可逆幂等补丁**与 Telegram API 同步。
|
|
8
|
+
|
|
9
|
+
## 安装
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# 方式一:npm 包(推荐)
|
|
13
|
+
pi install npm:@wingsbutterfly/pi-zh
|
|
14
|
+
|
|
15
|
+
# 方式二:GitHub 仓库
|
|
16
|
+
pi install git:github.com/wings1848/pi-zh@v0.3.0
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
安装后重启 pi,执行 `/pi-zh on` 启用汉化。
|
|
20
|
+
|
|
21
|
+
## 使用
|
|
22
|
+
|
|
23
|
+
```text
|
|
24
|
+
/pi-zh # 查看状态
|
|
25
|
+
/pi-zh on # 启用汉化(立即打补丁 + 安装 i18n SDK + 同步 Telegram 命令)
|
|
26
|
+
/pi-zh off # 关闭并还原全部文件到英文
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
配置文件:`~/.pi/agent/pi-zh.json`(`{ "enabled": true, "targets": [...] }`,缺省 = 全部已安装)
|
|
30
|
+
|
|
31
|
+
## 覆盖范围(存在才处理,缺省自动跳过)
|
|
32
|
+
|
|
33
|
+
| target | 内容 | 方式 |
|
|
34
|
+
| --- | --- | --- |
|
|
35
|
+
| `pi` | 内置 slash 命令补全(23 条)、/hotkeys 面板(3 节 + 30+ 动作)、/session 面板(15+ 字段)、/settings 面板(35 设置项 + thinking 级别) | 补丁 `dist/.../slash-commands.js` + bundle chunk |
|
|
36
|
+
| `telegram` | /start 菜单卡片、状态卡、子菜单标题 | 补丁 `@llblab/pi-telegram/lib/*.ts` |
|
|
37
|
+
| `cache-optimizer` | `/cache-optimizer` 命令与提示 | 补丁 `pi-cache-optimizer/index.ts` |
|
|
38
|
+
| i18n SDK | rpiv-todo / rpiv-ask-user-question 等自带 `locales/zh.json` | 自动安装 `@juicesharp/rpiv-i18n`,`/languages` 切换 |
|
|
39
|
+
|
|
40
|
+
> 工具 schema(如 codegraph / mcp-adapter 的 description)**不汉化**——那是给 AI 看的功能说明,汉化反而降低准确率。
|
|
41
|
+
|
|
42
|
+
## 架构
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
~/.pi/agent/pi-zh/
|
|
46
|
+
├── package.json
|
|
47
|
+
├── extensions/index.ts # 入口:开关编排 + /pi-zh 命令
|
|
48
|
+
├── lib/
|
|
49
|
+
│ ├── config.ts # 开关配置(默认关)
|
|
50
|
+
│ ├── dict.ts # ★ 词典总表(唯一数据源,按 target 分组)
|
|
51
|
+
│ ├── patch.ts # 双向补丁引擎(patch/restore 幂等可逆)
|
|
52
|
+
│ ├── targets.ts # 插件探测(存在才处理)
|
|
53
|
+
│ ├── sdk.ts # rpiv-i18n SDK 管理
|
|
54
|
+
│ └── telegram-commands.ts # Telegram 命令同步(30s 防回滚)
|
|
55
|
+
└── scripts/zh-dict.mjs # outbound 词典脚本(telegram.json 引用)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## 设计要点
|
|
59
|
+
|
|
60
|
+
- **默认不启用**:无配置文件或 `enabled !== true` 一律关闭,启动零开销
|
|
61
|
+
- **可逆还原**:restore 按词典反向替换(zh→en),与 patch 共用同一数据源,无备份文件依赖
|
|
62
|
+
- **安全降级**:任一 target 探测/补丁失败不影响其他 target 与 pi 本身;匹配不到原文时静默跳过
|
|
63
|
+
- **更新安全**:pi/插件更新覆盖补丁后,下次启动或 `/pi-zh on` 自动重打;SDK 缺失时插件回退英文,**不产生异常**
|
|
64
|
+
- **新增汉化**:编辑 `lib/dict.ts` 加一行 `{ en, zh, targetFiles?, note? }`;新增插件则加一个 target 定义(`targets.ts`)并挂上词典
|
|
65
|
+
|
|
66
|
+
## 与 pi-di18n 共存(自动分工,无冲突)
|
|
67
|
+
|
|
68
|
+
每个字符串只有一个 owner:
|
|
69
|
+
|
|
70
|
+
| pi-di18n 状态 | pi 本体负责 | pi-zh 负责 |
|
|
71
|
+
| --- | --- | --- |
|
|
72
|
+
| 激活(locale 非 en,含 LANG=zh_CN 自动触发) | pi-di18n(运行时 300+ 条) | telegram / cache-optimizer |
|
|
73
|
+
| 未激活 | pi-zh(磁盘补丁) | telegram / cache-optimizer |
|
|
74
|
+
|
|
75
|
+
- 判定依据:pi-di18n 配置 locale / 环境变量(LANG、PI_LOCALE)是否非 en,且 `coreHacksEnabled !== false`
|
|
76
|
+
- 让位原因:pi-di18n 的 slash 命令翻译以运行时数组的 "original" 为基准,若 pi-zh 先磁盘补丁成中文会造成基准错位与措辞覆盖(重复汉化)→ 让位根治
|
|
77
|
+
- 已安装 pi-di18n 但未激活时,pi-zh 仍全量接管;`/pi-zh status` 会显示当前 owner
|
|
78
|
+
|
|
79
|
+
## 新增 target 步骤
|
|
80
|
+
|
|
81
|
+
1. `lib/targets.ts` 加探测定义(包根候选 + marker + probeFiles)
|
|
82
|
+
2. `lib/dict.ts` 加 `TARGET_DICTS[名称] = [...]`
|
|
83
|
+
3. `lib/patch.ts` 无需改动(通用)
|
|
84
|
+
4. `extensions/index.ts` 如有默认文件路径需求,加 `defaults` 映射
|
|
85
|
+
|
|
86
|
+
## 验证
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
# 启用后重启 pi 的日志:
|
|
90
|
+
[pi-zh] patched pi:dist/core/slash-commands.js (23)
|
|
91
|
+
[pi-zh] patched telegram:lib/commands.ts (6)
|
|
92
|
+
[pi-zh] telegram commands synced (1/1)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## 开发
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
bun install # 安装依赖
|
|
99
|
+
bun run typecheck # 类型检查
|
|
100
|
+
bun test # 单元测试(bun:test)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
新增汉化/新 target 指南见 [CONTRIBUTING.md](CONTRIBUTING.md),版本历史见 [CHANGELOG.md](CHANGELOG.md)。
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
[MIT](LICENSE) © 2026 wings1848
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-zh — Pi 汉化插件(统一入口 v0.3)
|
|
3
|
+
*
|
|
4
|
+
* 功能(按配置开关,默认关闭):
|
|
5
|
+
* 1. Pi 内置 slash 命令补全 + TUI 文案汉化(target: pi)
|
|
6
|
+
* 2. pi-telegram 菜单卡片/状态卡汉化(target: telegram)
|
|
7
|
+
* 3. pi-cache-optimizer 命令与提示汉化(target: cache-optimizer)
|
|
8
|
+
* 4. rpiv-i18n SDK 激活(内置 locales 插件的中文支持,如 rpiv-todo)
|
|
9
|
+
* 5. Telegram bot 命令菜单中文同步(30s 防回滚轮询)
|
|
10
|
+
*
|
|
11
|
+
* 架构:
|
|
12
|
+
* - lib/config.ts 开关配置(~/.pi/agent/pi-zh.json,默认关)
|
|
13
|
+
* - lib/dict.ts 词典总表(唯一数据源,按 target 分组)
|
|
14
|
+
* - lib/targets.ts 插件探测(存在才处理)
|
|
15
|
+
* - lib/patch.ts 双向补丁引擎(patch/restore 幂等可逆)
|
|
16
|
+
* - lib/sdk.ts rpiv-i18n SDK 管理
|
|
17
|
+
*
|
|
18
|
+
* 启用/关闭:TUI 中执行 /pi-zh on | off | status
|
|
19
|
+
* 安全:任何 target 探测失败/补丁失败都不影响其他 target 与 pi 本身。
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
23
|
+
import { homedir } from "node:os";
|
|
24
|
+
import { join } from "node:path";
|
|
25
|
+
import { getPiTargetOwner, isPiDi18nActive } from "../lib/coexist.ts";
|
|
26
|
+
import { disableConfig, enableConfig, readConfig, getConfigPath } from "../lib/config.ts";
|
|
27
|
+
import { TARGET_DICTS } from "../lib/dict.ts";
|
|
28
|
+
import { applyTargets, type PatchTarget } from "../lib/patch.ts";
|
|
29
|
+
import { ensureI18nSdk, isI18nSdkInstalled } from "../lib/sdk.ts";
|
|
30
|
+
import { getInstalledTargets, getTargetByName, resolveTargetRoot, type TargetDefinition } from "../lib/targets.ts";
|
|
31
|
+
import { ensureZhCommands, readBotTokens, syncAllTokens } from "../lib/telegram-commands.ts";
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// 补丁编排
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
/** 生成某 target 的补丁目标文件列表(按词典 targetFiles 集合 + 包根) */
|
|
38
|
+
function buildTargetFiles(target: TargetDefinition): PatchTarget[] {
|
|
39
|
+
const root = resolveTargetRoot(target);
|
|
40
|
+
if (!root) return [];
|
|
41
|
+
const entries = TARGET_DICTS[target.name] ?? [];
|
|
42
|
+
const files = new Set<string>();
|
|
43
|
+
for (const entry of entries) {
|
|
44
|
+
for (const file of entry.targetFiles ?? []) {
|
|
45
|
+
files.add(file);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// 无 targetFiles 限定的条目(如 pi 的 slash-commands 需特殊处理)在 files 为空时补默认文件
|
|
49
|
+
const defaults: Record<string, string[]> = {
|
|
50
|
+
pi: [
|
|
51
|
+
"dist/core/slash-commands.js",
|
|
52
|
+
"dist/bundle/chunks/chunk-OMWWHBTG.js",
|
|
53
|
+
],
|
|
54
|
+
telegram: ["lib/commands.ts", "lib/status.ts", "lib/menu-status.ts", "lib/menu-model.ts", "lib/menu-thinking.ts", "lib/menu-queue.ts", "lib/menu-settings.ts"],
|
|
55
|
+
"cache-optimizer": ["index.ts"],
|
|
56
|
+
};
|
|
57
|
+
const targetFiles = files.size > 0 ? files : new Set(defaults[target.name] ?? []);
|
|
58
|
+
return [...targetFiles].map((f) => ({
|
|
59
|
+
filePath: join(root, f),
|
|
60
|
+
label: `${target.name}:${f}`,
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** 对某 target 执行 patch 或 restore,返回修改数 */
|
|
65
|
+
function applyTarget(
|
|
66
|
+
target: TargetDefinition,
|
|
67
|
+
mode: "patch" | "restore",
|
|
68
|
+
): { changed: boolean; reports: Array<{ label: string; applied: number }> } {
|
|
69
|
+
const entries = TARGET_DICTS[target.name] ?? [];
|
|
70
|
+
const files = buildTargetFiles(target);
|
|
71
|
+
return applyTargets(files, entries, mode);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 对全部已安装 target 执行操作(on=patch,off=restore),自动协调 pi-di18n 分工 */
|
|
75
|
+
function applyAll(mode: "patch" | "restore"): Array<{ target: string; applied: number }> {
|
|
76
|
+
const results: Array<{ target: string; applied: number }> = [];
|
|
77
|
+
const piOwner = getPiTargetOwner();
|
|
78
|
+
for (const target of getInstalledTargets()) {
|
|
79
|
+
// 协调:pi-di18n 激活时,pi target 让它负责(避免 slash 命令双 owner、基准错位)
|
|
80
|
+
if (target.name === "pi" && piOwner === "pi-di18n" && mode === "patch") {
|
|
81
|
+
console.log("[pi-zh] pi target delegated to pi-di18n (active), skipping");
|
|
82
|
+
results.push({ target: target.name, applied: 0 });
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
// restore 时仍需处理 pi:若之前 pi-zh 打过补丁,还原之(幂等:未打过则无操作)
|
|
86
|
+
const outcome = applyTarget(target, mode);
|
|
87
|
+
const applied = outcome.reports.reduce((sum, r) => sum + r.applied, 0);
|
|
88
|
+
for (const report of outcome.reports) {
|
|
89
|
+
console.log(`[pi-zh] ${mode === "patch" ? "patched" : "restored"} ${report.label} (${report.applied})`);
|
|
90
|
+
}
|
|
91
|
+
results.push({ target: target.name, applied });
|
|
92
|
+
}
|
|
93
|
+
return results;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// 开关动作(供 /pi-zh 命令与启动逻辑共用)
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
/** 启用:写配置 + 立即打补丁 + 确保 i18n SDK + 同步 Telegram 命令 */
|
|
101
|
+
async function turnOn(targetList?: string[]): Promise<string> {
|
|
102
|
+
enableConfig(targetList);
|
|
103
|
+
const results = applyAll("patch");
|
|
104
|
+
const sdkOk = ensureI18nSdk();
|
|
105
|
+
const counts = results.map((r) => `${r.target}:${r.applied}`).join(" ");
|
|
106
|
+
return `✅ pi-zh 已启用${targetList ? `(${targetList.join(", ")})` : ""}(${counts})${sdkOk ? ",i18n SDK 就绪" : ",i18n SDK 安装失败"}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** 关闭:还原所有已汉化文件 + 写配置 */
|
|
110
|
+
function turnOff(): string {
|
|
111
|
+
const results = applyAll("restore");
|
|
112
|
+
disableConfig();
|
|
113
|
+
const counts = results.map((r) => `${r.target}:${r.applied}`).join(" ");
|
|
114
|
+
return `✅ pi-zh 已关闭,已还原(${counts})`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** 状态:显示配置、已安装 target、SDK 状态、pi-di18n 分工 */
|
|
118
|
+
function status(): string {
|
|
119
|
+
const config = readConfig();
|
|
120
|
+
const installed = getInstalledTargets().map((t) => t.name).join(", ") || "无";
|
|
121
|
+
const enabledTargets = config.targets?.join(", ") ?? "全部";
|
|
122
|
+
const piOwner = getPiTargetOwner();
|
|
123
|
+
return [
|
|
124
|
+
`📋 pi-zh 状态:${config.enabled ? "启用中" : "已关闭(默认)"}`,
|
|
125
|
+
` 配置:${getConfigPath()}`,
|
|
126
|
+
` 已安装 target:${installed}`,
|
|
127
|
+
` 启用范围:${enabledTargets}`,
|
|
128
|
+
` pi 本体负责:${piOwner === "pi-di18n" ? "pi-di18n(已激活,避免重复)" : "pi-zh"}`,
|
|
129
|
+
` i18n SDK:${isI18nSdkInstalled() ? "已安装" : "未安装"}`,
|
|
130
|
+
` 用法:/pi-zh on | off | status`,
|
|
131
|
+
].join("\n");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// 扩展入口
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
const POLL_INTERVAL_MS = 30_000;
|
|
139
|
+
const TG_TOKENS = readBotTokens();
|
|
140
|
+
|
|
141
|
+
export default async function (pi: ExtensionAPI) {
|
|
142
|
+
// ---- 启动时按配置执行(默认关)----
|
|
143
|
+
const config = readConfig();
|
|
144
|
+
if (config.enabled) {
|
|
145
|
+
applyAll("patch");
|
|
146
|
+
if (["pi", "telegram", "cache-optimizer"].every((t) => config.targets?.includes(t) || !config.targets)) {
|
|
147
|
+
ensureI18nSdk();
|
|
148
|
+
}
|
|
149
|
+
if (TG_TOKENS.length > 0) {
|
|
150
|
+
const ok = await syncAllTokens();
|
|
151
|
+
console.log(`[pi-zh] telegram commands synced (${ok}/${TG_TOKENS.length})`);
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
console.log("[pi-zh] disabled (default) — enable with /pi-zh on");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ---- /pi-zh 命令:优雅开关 ----
|
|
158
|
+
pi.registerCommand("pi-zh", {
|
|
159
|
+
description: "管理 pi-zh 汉化(on/off/status)",
|
|
160
|
+
handler: async (args, ctx) => {
|
|
161
|
+
const cmd = (args ?? "").trim().toLowerCase();
|
|
162
|
+
let message: string;
|
|
163
|
+
if (cmd === "on") {
|
|
164
|
+
message = await turnOn();
|
|
165
|
+
} else if (cmd === "off") {
|
|
166
|
+
message = turnOff();
|
|
167
|
+
} else if (cmd === "status" || cmd === "") {
|
|
168
|
+
message = status();
|
|
169
|
+
} else {
|
|
170
|
+
message = `用法:/pi-zh on | off | status`;
|
|
171
|
+
}
|
|
172
|
+
await ctx.ui.notify(message, "info");
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// ---- Telegram 命令防回滚轮询(无论开关,若此前启用了命令菜单中文,保底修复)----
|
|
177
|
+
// 仅在已启用时轮询,避免无谓 API 调用
|
|
178
|
+
if (config.enabled && TG_TOKENS.length > 0) {
|
|
179
|
+
let pollTimer: NodeJS.Timeout | undefined;
|
|
180
|
+
pi.on("session_start", async () => {
|
|
181
|
+
if (pollTimer) return;
|
|
182
|
+
pollTimer = setInterval(() => {
|
|
183
|
+
for (const token of TG_TOKENS) {
|
|
184
|
+
ensureZhCommands(token).catch(() => {});
|
|
185
|
+
}
|
|
186
|
+
}, POLL_INTERVAL_MS);
|
|
187
|
+
});
|
|
188
|
+
pi.on("session_shutdown", () => {
|
|
189
|
+
if (pollTimer) {
|
|
190
|
+
clearInterval(pollTimer);
|
|
191
|
+
pollTimer = undefined;
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
package/lib/coexist.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-zh 与 pi-di18n 的协调层
|
|
3
|
+
*
|
|
4
|
+
* 分工原则(每个字符串只有一个 owner,避免冲突/重复汉化):
|
|
5
|
+
* - pi-di18n 激活(locale 非 en)→ pi 本体 runtime 文案(slash 命令描述、TUI 面板、
|
|
6
|
+
* 通知/错误提示等 300+ 条)归 pi-di18n;pi-zh 自动让位 pi target,只保留
|
|
7
|
+
* telegram / cache-optimizer(pi-di18n 不覆盖的生态)。
|
|
8
|
+
* - pi-di18n 未激活 → pi-zh 全管线接管(含 pi target)。
|
|
9
|
+
*
|
|
10
|
+
* 为什么让位:
|
|
11
|
+
* pi-di18n 的 patchCoreBuiltinSlashCommandDescriptions 会 import dist 的
|
|
12
|
+
* BUILTIN_SLASH_COMMANDS 数组并记录 "original" 作为翻译基准。若 pi-zh
|
|
13
|
+
* 先磁盘补丁成中文,pi-di18n 会把中文误当基准(基准错位),再按它的
|
|
14
|
+
* bundle 覆盖(措辞/繁简差异 = 重复汉化)。让位即可根治。
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
18
|
+
import { homedir } from "node:os";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
|
|
21
|
+
/** pi-di18n 包是否已安装(在 pi 扩展包目录) */
|
|
22
|
+
function isPiDi18nInstalled(): boolean {
|
|
23
|
+
const pkg = join(
|
|
24
|
+
homedir(),
|
|
25
|
+
".pi/agent/npm/node_modules/pi-di18n/package.json",
|
|
26
|
+
);
|
|
27
|
+
return existsSync(pkg);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** pi-di18n 的状态目录(与它源码 config.ts 保持一致) */
|
|
31
|
+
function getPiDi18nConfigPath(): string {
|
|
32
|
+
return join(homedir(), ".pi/agent/state/pi-di18n/config.json");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 读 pi-di18n 配置文件中的 locale(仿照其 loadI18nConfig 优先级) */
|
|
36
|
+
function readPiDi18nLocale(): string | undefined {
|
|
37
|
+
try {
|
|
38
|
+
const path = getPiDi18nConfigPath();
|
|
39
|
+
if (!existsSync(path)) return undefined;
|
|
40
|
+
const cfg = JSON.parse(readFileSync(path, "utf8")) as {
|
|
41
|
+
locale?: string;
|
|
42
|
+
fallbackLocale?: string;
|
|
43
|
+
coreHacksEnabled?: boolean;
|
|
44
|
+
};
|
|
45
|
+
// coreHacks 被显式禁用 → pi-di18n 不碰内核 → pi-zh 可接管
|
|
46
|
+
if (cfg.coreHacksEnabled === false) return undefined;
|
|
47
|
+
return cfg.locale ?? cfg.fallbackLocale;
|
|
48
|
+
} catch {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** pi-di18n 运行时 locale(模拟它的 detectLocaleFromEnv) */
|
|
54
|
+
export function readPiDi18nEnvLocale(): string | undefined {
|
|
55
|
+
const candidates = [
|
|
56
|
+
process.env.PI_LOCALE,
|
|
57
|
+
process.env.LC_ALL,
|
|
58
|
+
process.env.LANG,
|
|
59
|
+
].filter(Boolean) as string[];
|
|
60
|
+
for (const raw of candidates) {
|
|
61
|
+
const s = raw.trim();
|
|
62
|
+
if (!s) continue;
|
|
63
|
+
const base = (s.split(".")[0] ?? s).replace(/_/g, "-");
|
|
64
|
+
if (base) return base;
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** locale 是否为英文/中性环境(en、en-US、C、POSIX 等 → 未激活) */
|
|
70
|
+
export function isEnglishLocale(locale: string | undefined): boolean {
|
|
71
|
+
if (!locale || locale.trim() === "") return true;
|
|
72
|
+
const lang = locale.toLowerCase().split("-")[0]!;
|
|
73
|
+
return lang === "en" || lang === "c" || lang === "posix";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** pi-di18n 是否激活(已安装 + locale 非 en + coreHacks 未禁用) */
|
|
77
|
+
export function isPiDi18nActive(): boolean {
|
|
78
|
+
if (!isPiDi18nInstalled()) return false;
|
|
79
|
+
const locale = readPiDi18nLocale() ?? readPiDi18nEnvLocale();
|
|
80
|
+
return !isEnglishLocale(locale);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** 协调结果:当前 pi target 由谁负责 */
|
|
84
|
+
export function getPiTargetOwner(): "pi-zh" | "pi-di18n" {
|
|
85
|
+
return isPiDi18nActive() ? "pi-di18n" : "pi-zh";
|
|
86
|
+
}
|
package/lib/config.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-zh 开关配置模块
|
|
3
|
+
*
|
|
4
|
+
* 配置文件:~/.pi/agent/pi-zh.json
|
|
5
|
+
* 默认不启用(文件缺失或 enabled !== true 均视为关闭)。
|
|
6
|
+
*
|
|
7
|
+
* 格式:
|
|
8
|
+
* {
|
|
9
|
+
* "enabled": true,
|
|
10
|
+
* "targets": ["pi", "telegram"] // 可选,缺省为全部可用 target
|
|
11
|
+
* }
|
|
12
|
+
*
|
|
13
|
+
* 优雅启用:在 TUI 中执行 /pi-zh on(写入配置并立即生效),
|
|
14
|
+
* 关闭执行 /pi-zh off(还原所有已汉化文件)。
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
18
|
+
import { homedir } from "node:os";
|
|
19
|
+
import { dirname, join } from "node:path";
|
|
20
|
+
|
|
21
|
+
export interface PiZhConfig {
|
|
22
|
+
/** 是否启用汉化(默认 false) */
|
|
23
|
+
enabled: boolean;
|
|
24
|
+
/** 启用的 target 列表(缺省 = 全部可用) */
|
|
25
|
+
targets?: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const CONFIG_PATH = join(homedir(), ".pi/agent/pi-zh.json");
|
|
29
|
+
|
|
30
|
+
/** 读取配置;文件不存在或无效返回默认关闭配置 */
|
|
31
|
+
export function readConfig(path: string = CONFIG_PATH): PiZhConfig {
|
|
32
|
+
try {
|
|
33
|
+
if (!existsSync(path)) return { enabled: false };
|
|
34
|
+
const raw = JSON.parse(readFileSync(path, "utf8")) as Partial<PiZhConfig>;
|
|
35
|
+
return {
|
|
36
|
+
enabled: raw.enabled === true,
|
|
37
|
+
...(Array.isArray(raw.targets) ? { targets: raw.targets } : {}),
|
|
38
|
+
};
|
|
39
|
+
} catch {
|
|
40
|
+
return { enabled: false };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 写入配置(原子写:先写临时文件再 rename) */
|
|
45
|
+
export function writeConfig(config: PiZhConfig, path: string = CONFIG_PATH): void {
|
|
46
|
+
const parent = dirname(path);
|
|
47
|
+
if (!existsSync(parent)) return;
|
|
48
|
+
const tmp = `${path}.tmp`;
|
|
49
|
+
writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
50
|
+
// 同步替换避免读到半截文件
|
|
51
|
+
renameSync(tmp, path);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 简易启用:写入 enabled 配置 */
|
|
55
|
+
export function enableConfig(targets?: string[], path: string = CONFIG_PATH): PiZhConfig {
|
|
56
|
+
const config: PiZhConfig = { enabled: true, ...(targets ? { targets } : {}) };
|
|
57
|
+
writeConfig(config, path);
|
|
58
|
+
return config;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 简易关闭:写入 enabled:false(并清空 targets 以便下次重新探测) */
|
|
62
|
+
export function disableConfig(path: string = CONFIG_PATH): PiZhConfig {
|
|
63
|
+
const config: PiZhConfig = { enabled: false };
|
|
64
|
+
writeConfig(config, path);
|
|
65
|
+
return config;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 配置路径(供状态显示/调试) */
|
|
69
|
+
export function getConfigPath(): string {
|
|
70
|
+
return CONFIG_PATH;
|
|
71
|
+
}
|