@zhushanwen/pi-llm-shared 0.8.0 → 0.9.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/package.json +14 -4
- package/src/__tests__/ui-locale.test.ts +88 -0
- package/src/config.ts +1 -1
- package/src/index.ts +2 -0
- package/src/ui-locale.ts +67 -0
package/package.json
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-llm-shared",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Shared LLM invocation library for Pi extensions — model resolution (ref exact only), LLM calling (completeSimple), and config read/write with mtime+size caching. Shared library, not a Pi extension.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./src/index.ts",
|
|
10
|
+
"import": "./src/index.ts"
|
|
11
|
+
},
|
|
12
|
+
"./ui-locale": {
|
|
13
|
+
"types": "./src/ui-locale.ts",
|
|
14
|
+
"import": "./src/ui-locale.ts"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
7
17
|
"keywords": [
|
|
8
18
|
"pi-package",
|
|
9
19
|
"pi",
|
|
@@ -17,9 +27,9 @@
|
|
|
17
27
|
"src/"
|
|
18
28
|
],
|
|
19
29
|
"dependencies": {
|
|
20
|
-
"@zhushanwen/pi-extension-logger": "0.6.
|
|
21
|
-
"@zhushanwen/pi-ext-guards": "0.4.
|
|
22
|
-
"@zhushanwen/pi-file-lock": "0.4.
|
|
30
|
+
"@zhushanwen/pi-extension-logger": "0.6.1",
|
|
31
|
+
"@zhushanwen/pi-ext-guards": "0.4.1",
|
|
32
|
+
"@zhushanwen/pi-file-lock": "0.4.1"
|
|
23
33
|
},
|
|
24
34
|
"peerDependencies": {
|
|
25
35
|
"@earendil-works/pi-ai": "^0.84.4",
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync, utimesSync, writeFileSync } from "node:fs";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
|
|
7
|
+
import { DEFAULT_UI_LOCALE, readUiLocale } from "../ui-locale.ts";
|
|
8
|
+
|
|
9
|
+
// node:fs 的 ESM namespace 不可配置,vi.spyOn 对具名导出失效(vitest 限制,
|
|
10
|
+
// 同 config.test.ts):mock 包装 readFileSync/statSync(默认透传 actual),
|
|
11
|
+
// 供缓存命中断言 override / 计数;其余 fs 操作原样透传。
|
|
12
|
+
vi.mock("node:fs", async (importOriginal) => {
|
|
13
|
+
const actual = await importOriginal() as typeof import("node:fs");
|
|
14
|
+
return {
|
|
15
|
+
...actual,
|
|
16
|
+
readFileSync: vi.fn(actual.readFileSync),
|
|
17
|
+
statSync: vi.fn(actual.statSync),
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
let dir: string;
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
dir = mkdtempSync(join(tmpdir(), "llm-shared-ui-locale-"));
|
|
25
|
+
vi.stubEnv("TAIJI_AGENT_DATA_DIR", dir);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
vi.mocked(fs.readFileSync).mockClear();
|
|
30
|
+
vi.mocked(fs.statSync).mockClear();
|
|
31
|
+
rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
32
|
+
vi.unstubAllEnvs();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
/** 在 <dir>/ui-preferences.json 写入 locale 并钉住 mtime(防快速连写 mtime 粒度截断)。 */
|
|
36
|
+
function writePreferences(locale: unknown, mtimeSec: number): void {
|
|
37
|
+
const filePath = join(dir, "ui-preferences.json");
|
|
38
|
+
writeFileSync(filePath, JSON.stringify({ v: 1, locale, updatedAt: 0 }));
|
|
39
|
+
utimesSync(filePath, mtimeSec, mtimeSec);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe("readUiLocale(<dataDir>/ui-preferences.json 读单点)", () => {
|
|
43
|
+
it("zh-CN / en-US 命中;文件缺失 / env 缺失 → 回落 en-US", () => {
|
|
44
|
+
expect(readUiLocale()).toBe(DEFAULT_UI_LOCALE); // 文件缺失
|
|
45
|
+
|
|
46
|
+
writePreferences("zh-CN", 1_000);
|
|
47
|
+
expect(readUiLocale()).toBe("zh-CN");
|
|
48
|
+
|
|
49
|
+
writePreferences("en-US", 2_000);
|
|
50
|
+
expect(readUiLocale()).toBe("en-US");
|
|
51
|
+
|
|
52
|
+
vi.stubEnv("TAIJI_AGENT_DATA_DIR", undefined);
|
|
53
|
+
expect(readUiLocale()).toBe(DEFAULT_UI_LOCALE); // env 缺失
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("locale 值域外 / 顶层非对象 / JSON 损坏 → 回落 en-US", () => {
|
|
57
|
+
// 值域外:换新目录绕开 (filePath, mtime, size) 模块缓存(同 plan i18n.test.ts 手法)
|
|
58
|
+
const dir2 = mkdtempSync(join(tmpdir(), "llm-shared-ui-locale-oor-"));
|
|
59
|
+
vi.stubEnv("TAIJI_AGENT_DATA_DIR", dir2);
|
|
60
|
+
writePreferences("fr-FR", 1_000);
|
|
61
|
+
expect(readUiLocale()).toBe(DEFAULT_UI_LOCALE);
|
|
62
|
+
rmSync(dir2, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
63
|
+
|
|
64
|
+
vi.stubEnv("TAIJI_AGENT_DATA_DIR", dir);
|
|
65
|
+
writeFileSync(join(dir, "ui-preferences.json"), "not-json{{");
|
|
66
|
+
utimesSync(join(dir, "ui-preferences.json"), 3_000, 3_000);
|
|
67
|
+
expect(readUiLocale()).toBe(DEFAULT_UI_LOCALE); // JSON 损坏(缓存已驱逐,重读失败同路径)
|
|
68
|
+
|
|
69
|
+
writeFileSync(join(dir, "ui-preferences.json"), JSON.stringify([1, 2]));
|
|
70
|
+
utimesSync(join(dir, "ui-preferences.json"), 4_000, 4_000);
|
|
71
|
+
expect(readUiLocale()).toBe(DEFAULT_UI_LOCALE); // 顶层非对象
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("mtime+size 缓存:文件未变不重读;mtime 变化后重读生效", () => {
|
|
75
|
+
writePreferences("zh-CN", 1_000);
|
|
76
|
+
expect(readUiLocale()).toBe("zh-CN");
|
|
77
|
+
const readsAfterFirst = vi.mocked(fs.readFileSync).mock.calls.length;
|
|
78
|
+
|
|
79
|
+
// 未变文件:再读走缓存,readFileSync 调用数不增
|
|
80
|
+
expect(readUiLocale()).toBe("zh-CN");
|
|
81
|
+
expect(vi.mocked(fs.readFileSync).mock.calls.length).toBe(readsAfterFirst);
|
|
82
|
+
|
|
83
|
+
// runtime 写盘语义:同路径内容翻转(zh-CN→en-US 等长,size 双键失效)+ mtime 前进
|
|
84
|
+
writePreferences("en-US", 2_000);
|
|
85
|
+
expect(readUiLocale()).toBe("en-US");
|
|
86
|
+
expect(vi.mocked(fs.readFileSync).mock.calls.length).toBe(readsAfterFirst + 1);
|
|
87
|
+
});
|
|
88
|
+
});
|
package/src/config.ts
CHANGED
|
@@ -143,7 +143,7 @@ function uniqueTmpPath(configPath: string): string {
|
|
|
143
143
|
* @returns 成功 {success:true};失败 {success:false, error}
|
|
144
144
|
*
|
|
145
145
|
* 🔒 跨进程锁(D1e/W4,integrity-hardening.md §3.1,登记表 §6 rename-session 行):
|
|
146
|
-
* ext-config 家族被
|
|
146
|
+
* ext-config 家族被 taiji runtime(如 setRenameModel 写 model 字段,W1b 已持锁)与
|
|
147
147
|
* pi 子进程内扩展(本函数)双写。互斥只依赖同一 lockfile(<config>.lock),本侧
|
|
148
148
|
* withFileLockSync 协议与 runtime 侧 settings.json 写锁逐字对齐(realpath:false +
|
|
149
149
|
* stale 30s + busy-wait 1s 预算 fail-fast)。锁获取失败不降级无锁写——对端
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// resolve: 模型解析(仅 ref 精确指定)+ selector 归一化 + thinking 级别校验
|
|
3
3
|
// call: LLM 调用(completeSimple + 凭证 + 文本提取)
|
|
4
4
|
// config: 泛型配置读写(mtime+size 双 key 缓存 + 原子写)
|
|
5
|
+
// ui-locale: UI 语言读取单点(<dataDir>/ui-preferences.json,mtime+size 读缓存 + 降级)
|
|
5
6
|
export {
|
|
6
7
|
resolveModel,
|
|
7
8
|
parseModelRef,
|
|
@@ -12,4 +13,5 @@ export {
|
|
|
12
13
|
} from "./resolve.ts";
|
|
13
14
|
export { callLLM, joinTextBlocks, extractText, type CallLLMOptions, type CallLLMResult } from "./call.ts";
|
|
14
15
|
export { getConfigPath, loadConfig, saveConfig, clearConfigCache } from "./config.ts";
|
|
16
|
+
export { readUiLocale, DEFAULT_UI_LOCALE, type UiLocale } from "./ui-locale.ts";
|
|
15
17
|
export { migrateLegacyConfig } from "./migrate.ts";
|
package/src/ui-locale.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI 语言(u-locale-channel)读取单点:`<dataDir>/ui-preferences.json` 的 extension 侧
|
|
3
|
+
* 唯一消费实现(需求方 = plan / scheduler 等 extension 的 i18n 层,经本模块 import)。
|
|
4
|
+
*
|
|
5
|
+
* 语言归属原则:跨边界只走数据——renderer 是语言权威,进程只读派生态。文件唯一写方 =
|
|
6
|
+
* runtime `config.setUiLocale` handler → `writeUiPreferences`(tmp + rename 原子写)。
|
|
7
|
+
*
|
|
8
|
+
* @data-owner #39 `ui-preferences.json`(本模块是读缓存,非第二写方;登记行见
|
|
9
|
+
* `docs/architecture/data-source-registry.md`)。runtime 以
|
|
10
|
+
* `TAIJI_AGENT_DATA_DIR = getConfigDir()` 注入 pi 子进程;文件形如
|
|
11
|
+
* `{ v: 1, locale: 'zh-CN' | 'en-US', updatedAt }`。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readFileSync, statSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
|
|
17
|
+
/** UI 语言两值(与 ui-preferences.json 读写两端守卫测试对齐的字面量词表)。 */
|
|
18
|
+
export type UiLocale = "zh-CN" | "en-US";
|
|
19
|
+
|
|
20
|
+
export const DEFAULT_UI_LOCALE: UiLocale = "en-US";
|
|
21
|
+
|
|
22
|
+
const UI_PREFERENCES_FILENAME = "ui-preferences.json";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 模块级 (filePath, mtimeMs, size) 三键读缓存(先例 = system-prompt 的 mtime+size
|
|
26
|
+
* 双键读缓存;mtime+size 防 APFS 等文件系统 mtime 精度截断):每次读仍 stat 判变
|
|
27
|
+
* (runtime 写盘后下一轮即生效),文件未变时跳过 readFileSync/JSON.parse。
|
|
28
|
+
*/
|
|
29
|
+
let localeCache: { filePath: string; mtimeMs: number; size: number; locale: UiLocale } | null = null;
|
|
30
|
+
|
|
31
|
+
/** 读界面语言(数据目录动态推导,禁硬编码路径);任何失败回落 en-US 且不出声。 */
|
|
32
|
+
export function readUiLocale(): UiLocale {
|
|
33
|
+
const dataDir = process.env.TAIJI_AGENT_DATA_DIR;
|
|
34
|
+
if (!dataDir) return DEFAULT_UI_LOCALE;
|
|
35
|
+
|
|
36
|
+
const filePath = join(dataDir, UI_PREFERENCES_FILENAME);
|
|
37
|
+
try {
|
|
38
|
+
const stat = statSync(filePath);
|
|
39
|
+
const cached = localeCache;
|
|
40
|
+
if (
|
|
41
|
+
cached !== null &&
|
|
42
|
+
cached.filePath === filePath &&
|
|
43
|
+
cached.mtimeMs === stat.mtimeMs &&
|
|
44
|
+
cached.size === stat.size
|
|
45
|
+
) {
|
|
46
|
+
return cached.locale;
|
|
47
|
+
}
|
|
48
|
+
const parsed: unknown = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
49
|
+
const locale = parseUiLocale(parsed);
|
|
50
|
+
localeCache = { filePath, mtimeMs: stat.mtimeMs, size: stat.size, locale };
|
|
51
|
+
return locale;
|
|
52
|
+
} catch {
|
|
53
|
+
// 文件缺失 / 不可读 / JSON 损坏均归此路径(设计口径:损坏 = 驱逐缓存 + 回落默认 + 无出声面)
|
|
54
|
+
localeCache = null;
|
|
55
|
+
return DEFAULT_UI_LOCALE;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
60
|
+
return typeof value === "object" && value !== null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseUiLocale(raw: unknown): UiLocale {
|
|
64
|
+
if (!isRecord(raw)) return DEFAULT_UI_LOCALE;
|
|
65
|
+
const locale = raw.locale;
|
|
66
|
+
return locale === "zh-CN" || locale === "en-US" ? locale : DEFAULT_UI_LOCALE;
|
|
67
|
+
}
|