@wingsbutterfly/pi-zh 0.3.1 → 0.4.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/extensions/index.ts +10 -3
- package/lib/sdk.ts +149 -19
- package/package.json +1 -1
package/extensions/index.ts
CHANGED
|
@@ -101,9 +101,15 @@ function applyAll(mode: "patch" | "restore"): Array<{ target: string; applied: n
|
|
|
101
101
|
async function turnOn(targetList?: string[]): Promise<string> {
|
|
102
102
|
enableConfig(targetList);
|
|
103
103
|
const results = applyAll("patch");
|
|
104
|
-
const
|
|
104
|
+
const sdkState = ensureI18nSdk();
|
|
105
|
+
const sdkNote =
|
|
106
|
+
sdkState === "installed"
|
|
107
|
+
? ",i18n SDK 就绪"
|
|
108
|
+
: sdkState === "installing"
|
|
109
|
+
? ",i18n SDK 后台安装中(重启后生效)"
|
|
110
|
+
: ",i18n SDK 缺失(语言切换回退英文)";
|
|
105
111
|
const counts = results.map((r) => `${r.target}:${r.applied}`).join(" ");
|
|
106
|
-
return `✅ pi-zh 已启用${targetList ? `(${targetList.join(", ")})` : ""}(${counts})${
|
|
112
|
+
return `✅ pi-zh 已启用${targetList ? `(${targetList.join(", ")})` : ""}(${counts})${sdkNote}`;
|
|
107
113
|
}
|
|
108
114
|
|
|
109
115
|
/** 关闭:还原所有已汉化文件 + 写配置 */
|
|
@@ -144,7 +150,8 @@ export default async function (pi: ExtensionAPI) {
|
|
|
144
150
|
if (config.enabled) {
|
|
145
151
|
applyAll("patch");
|
|
146
152
|
if (["pi", "telegram", "cache-optimizer"].every((t) => config.targets?.includes(t) || !config.targets)) {
|
|
147
|
-
|
|
153
|
+
// 非阻塞:SDK 缺失时后台安装,不拖慢启动
|
|
154
|
+
void ensureI18nSdk();
|
|
148
155
|
}
|
|
149
156
|
if (TG_TOKENS.length > 0) {
|
|
150
157
|
const ok = await syncAllTokens();
|
package/lib/sdk.ts
CHANGED
|
@@ -6,39 +6,169 @@
|
|
|
6
6
|
* SDK 缺失时静默回退英文)。安装到 .pi/agent/npm 即全局生效,
|
|
7
7
|
* 用户可用插件提供的 /languages 命令实时切换。
|
|
8
8
|
*
|
|
9
|
-
* 本模块只在「启用时」确保 SDK
|
|
10
|
-
* 到 pi
|
|
9
|
+
* 本模块只在「启用时」确保 SDK 存在:存在则跳过,缺失则自动安装
|
|
10
|
+
* 到 pi 的扩展包目录。
|
|
11
|
+
*
|
|
12
|
+
* 性能设计(v0.4 起):
|
|
13
|
+
* - 同步检测(existsSync)毫秒级,永不阻塞启动;
|
|
14
|
+
* - 安装走异步 execFile 而非 execFileSync,避免 pi 启动被包安装卡死
|
|
15
|
+
* (旧版 npm install 与 pnpm 目录不兼容时曾阻塞 30~80s);
|
|
16
|
+
* - 自动探测包管理器:优先 pnpm,回退 npm/bun,且与目录特征匹配;
|
|
17
|
+
* - 后台安装并发去重(同一进程只触发一次),失败静默,下次再试。
|
|
11
18
|
*/
|
|
12
19
|
|
|
13
|
-
import {
|
|
14
|
-
import { existsSync } from "node:fs";
|
|
20
|
+
import { execFile } from "node:child_process";
|
|
21
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
15
22
|
import { homedir } from "node:os";
|
|
16
23
|
import { join } from "node:path";
|
|
17
24
|
|
|
18
25
|
const SDK_NAME = "@juicesharp/rpiv-i18n";
|
|
19
26
|
|
|
27
|
+
/** pi 扩展包统一目录(可注入,便于测试) */
|
|
28
|
+
export function getDefaultNpmDir(): string {
|
|
29
|
+
return join(homedir(), ".pi/agent/npm");
|
|
30
|
+
}
|
|
31
|
+
|
|
20
32
|
/** SDK 是否已安装(在 pi 扩展包目录) */
|
|
21
|
-
export function isI18nSdkInstalled(): boolean {
|
|
22
|
-
const sdkPath = join(
|
|
33
|
+
export function isI18nSdkInstalled(npmDir: string = getDefaultNpmDir()): boolean {
|
|
34
|
+
const sdkPath = join(npmDir, "node_modules", SDK_NAME, "package.json");
|
|
23
35
|
return existsSync(sdkPath);
|
|
24
36
|
}
|
|
25
37
|
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// 包管理器探测
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
export type PackageManager = "pnpm" | "npm" | "bun";
|
|
43
|
+
|
|
26
44
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
45
|
+
* 探测包管理器:
|
|
46
|
+
* 1. 优先匹配目录特征(pnpm 的 .pnpm / lockfile),因为 npm 在
|
|
47
|
+
* pnpm 目录里装包会触发 arborist 崩溃(Cannot read properties of null);
|
|
48
|
+
* 2. 无特征时读 pi settings.json 的 npmCommand 配置;
|
|
49
|
+
* 3. 仍无则按 PATH 存在性回退(pnpm > npm > bun)。
|
|
29
50
|
*/
|
|
30
|
-
export function
|
|
31
|
-
|
|
51
|
+
export function detectPackageManager(
|
|
52
|
+
npmDir: string = getDefaultNpmDir(),
|
|
53
|
+
): PackageManager {
|
|
54
|
+
// 1) 目录特征:pnpm 用锁文件/stores 结构标记
|
|
55
|
+
if (
|
|
56
|
+
existsSync(join(npmDir, "node_modules", ".pnpm")) ||
|
|
57
|
+
existsSync(join(npmDir, "pnpm-lock.yaml"))
|
|
58
|
+
) {
|
|
59
|
+
return "pnpm";
|
|
60
|
+
}
|
|
61
|
+
if (existsSync(join(npmDir, "package-lock.json"))) {
|
|
62
|
+
return "npm";
|
|
63
|
+
}
|
|
64
|
+
if (existsSync(join(npmDir, "bun.lock")) || existsSync(join(npmDir, "bun.lockb"))) {
|
|
65
|
+
return "bun";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 2) pi 配置 npmCommand(用户显式声明了包管理器)
|
|
32
69
|
try {
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
70
|
+
const settingsPath = join(homedir(), ".pi/agent/settings.json");
|
|
71
|
+
const raw = readFileSync(settingsPath, "utf8");
|
|
72
|
+
const settings = JSON.parse(raw) as { npmCommand?: unknown };
|
|
73
|
+
const cmd = Array.isArray(settings.npmCommand)
|
|
74
|
+
? String(settings.npmCommand[0]).toLowerCase()
|
|
75
|
+
: typeof settings.npmCommand === "string"
|
|
76
|
+
? settings.npmCommand.toLowerCase()
|
|
77
|
+
: "";
|
|
78
|
+
if (cmd === "pnpm" || cmd === "npm" || cmd === "bun") return cmd;
|
|
41
79
|
} catch {
|
|
42
|
-
|
|
80
|
+
// settings.json 缺失/损坏 → 走 PATH 探测
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 3) PATH 探测:pnpm 优先(与 pi 现代安装方式一致)
|
|
84
|
+
if (commandExists("pnpm")) return "pnpm";
|
|
85
|
+
if (commandExists("npm")) return "npm";
|
|
86
|
+
if (commandExists("bun")) return "bun";
|
|
87
|
+
return "npm"; // 兜底,install 会失败但静默
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** PATH 中是否存在可执行文件(无外部依赖,直接查 PATH 目录) */
|
|
91
|
+
function commandExists(cmd: string): boolean {
|
|
92
|
+
const pathEnv = process.env.PATH ?? "";
|
|
93
|
+
for (const dir of pathEnv.split(":")) {
|
|
94
|
+
if (!dir) continue;
|
|
95
|
+
if (existsSync(join(dir, cmd))) return true;
|
|
96
|
+
if (existsSync(join(dir, cmd + ".exe"))) return true; // Windows
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// 安装(异步、非阻塞)
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
/** 后台安装任务去重:同一进程只允许一个在途安装 */
|
|
106
|
+
let installInFlight: Promise<boolean> | null = null;
|
|
107
|
+
|
|
108
|
+
/** 按包管理器拼装安装参数(不写 package.json 的方式各异,统一处理) */
|
|
109
|
+
function buildInstallArgs(pm: PackageManager): { cmd: string; args: string[] } {
|
|
110
|
+
switch (pm) {
|
|
111
|
+
case "pnpm":
|
|
112
|
+
// pnpm add/install 不支持 --no-save;install <pkg> 会写入 dependencies,
|
|
113
|
+
// 这反而是期望行为:让 pi 后续 pnpm install 也能带上 SDK
|
|
114
|
+
return { cmd: "pnpm", args: ["install", SDK_NAME, "--prefer-offline"] };
|
|
115
|
+
case "bun":
|
|
116
|
+
return { cmd: "bun", args: ["add", SDK_NAME, "--no-save"] };
|
|
117
|
+
case "npm":
|
|
118
|
+
default:
|
|
119
|
+
return { cmd: "npm", args: ["install", SDK_NAME, "--no-save", "--omit=dev"] };
|
|
43
120
|
}
|
|
44
121
|
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 异步安装 SDK(不阻塞调用方)。返回安装是否成功。
|
|
125
|
+
* 安装失败(网络/包管理器问题)静默返回 false,绝不影响使用方。
|
|
126
|
+
*/
|
|
127
|
+
export function installI18nSdk(
|
|
128
|
+
npmDir: string = getDefaultNpmDir(),
|
|
129
|
+
): Promise<boolean> {
|
|
130
|
+
if (isI18nSdkInstalled(npmDir)) return Promise.resolve(true);
|
|
131
|
+
if (!existsSync(npmDir)) return Promise.resolve(false);
|
|
132
|
+
|
|
133
|
+
// 并发去重:复用同一在途任务
|
|
134
|
+
if (installInFlight) return installInFlight;
|
|
135
|
+
|
|
136
|
+
const { cmd, args } = buildInstallArgs(detectPackageManager(npmDir));
|
|
137
|
+
|
|
138
|
+
installInFlight = new Promise<boolean>((resolve) => {
|
|
139
|
+
execFile(
|
|
140
|
+
cmd,
|
|
141
|
+
args,
|
|
142
|
+
{ cwd: npmDir, timeout: 120_000, maxBuffer: 4 * 1024 * 1024 },
|
|
143
|
+
(_error, _stdout, _stderr) => {
|
|
144
|
+
const ok = isI18nSdkInstalled(npmDir);
|
|
145
|
+
resolve(ok);
|
|
146
|
+
},
|
|
147
|
+
);
|
|
148
|
+
}).finally(() => {
|
|
149
|
+
installInFlight = null;
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
return installInFlight;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 确保 SDK 安装——同步返回,毫秒级,永不在启动路径阻塞。
|
|
157
|
+
*
|
|
158
|
+
* 返回:
|
|
159
|
+
* - "installed" 已就绪(或刚触发后台安装完成前已存在)
|
|
160
|
+
* - "installing" 未安装,已触发后台安装(下次检查生效)
|
|
161
|
+
* - "missing" 扩展包目录不存在或无法安装(静默回退英文)
|
|
162
|
+
*
|
|
163
|
+
* 注意:安装本身是 fire-and-forget 的,调用方任何时候都不应 await 它
|
|
164
|
+
* 参与启动时序;需要确认结果时请等待 installI18nSdk() 返回的 Promise。
|
|
165
|
+
*/
|
|
166
|
+
export function ensureI18nSdk(
|
|
167
|
+
npmDir: string = getDefaultNpmDir(),
|
|
168
|
+
): "installed" | "installing" | "missing" {
|
|
169
|
+
if (isI18nSdkInstalled(npmDir)) return "installed";
|
|
170
|
+
if (!existsSync(npmDir)) return "missing";
|
|
171
|
+
// 触发后台安装,不等待
|
|
172
|
+
void installI18nSdk(npmDir);
|
|
173
|
+
return "installing";
|
|
174
|
+
}
|