@oldsuns/pi-switch 0.3.2 → 0.3.5
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/README.md +26 -4
- package/bin/pi-switch.js +12 -1
- package/package.json +10 -4
- package/pi-switch-native.darwin-arm64.node +0 -0
- package/pi-switch-native.darwin-x64.node +0 -0
- package/pi-switch-native.linux-x64-gnu.node +0 -0
- package/pi-switch-native.linux-x64-musl.node +0 -0
- package/pi-switch-native.win32-x64-msvc.node +0 -0
- package/web/native-client.js +51 -0
- package/web/native-worker.js +17 -0
- package/web/public/app.js +990 -0
- package/web/public/appearance.js +24 -0
- package/web/public/dialogs.js +191 -0
- package/web/public/favicon.svg +1 -0
- package/web/public/index.html +23 -0
- package/web/public/overview.js +83 -0
- package/web/public/profile-order.js +47 -0
- package/web/public/profiles.js +134 -0
- package/web/public/session-tree.js +120 -0
- package/web/public/sessions.js +164 -0
- package/web/public/settings.js +48 -0
- package/web/public/shell.js +48 -0
- package/web/public/styles.css +764 -0
- package/web/public/ui.js +135 -0
- package/web/server.js +137 -0
- package/web/start.js +63 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pi-switch
|
|
2
2
|
|
|
3
|
-
维护本地 provider 库并按需同步 [Pi](https://github.com/earendil-works/pi) 配置的终端 TUI
|
|
3
|
+
维护本地 provider 库并按需同步 [Pi](https://github.com/earendil-works/pi) 配置的终端 TUI 与本地 Web 界面:管理 provider / 模型 / 默认模型,以及 Pi session 的列表、预览与删除。
|
|
4
4
|
|
|
5
5
|
CLI:`pi-switch` · npm 包:`@oldsuns/pi-switch` · Node 薄壳 + Rust/napi 原生核心
|
|
6
6
|
|
|
@@ -35,6 +35,8 @@ CLI:
|
|
|
35
35
|
```bash
|
|
36
36
|
pi-switch # 打开 TUI(等同 tui)
|
|
37
37
|
pi-switch tui
|
|
38
|
+
pi-switch --web # 启动本地 Web 界面并打开浏览器
|
|
39
|
+
pi-switch --web --port 5210 --no-open # 指定端口,只打印访问地址
|
|
38
40
|
pi-switch doctor # 校验配置与默认模型
|
|
39
41
|
pi-switch --version # / -v
|
|
40
42
|
pi-switch --help # / -h / help
|
|
@@ -42,6 +44,19 @@ pi-switch --help # / -h / help
|
|
|
42
44
|
|
|
43
45
|
Windows 上无需 Rust:预构建原生模块随 npm 包分发,`pi-switch` 开箱即用。本地开发见下文「开发者」。
|
|
44
46
|
|
|
47
|
+
## Web 界面
|
|
48
|
+
|
|
49
|
+
`pi-switch --web` 启动仅监听 `127.0.0.1` 的本地服务,自动选择空闲端口并打开浏览器;终端会打印访问地址,按 `Ctrl+C` 停止服务。可使用 `--port` 指定端口、`--no-open` 禁用自动打开。
|
|
50
|
+
|
|
51
|
+
- **主页**:本地 provider / 模型计数、Pi 可用模型、直接切换默认模型、最近会话与常用操作。
|
|
52
|
+
- **配置**:provider / 模型主从分栏、搜索、创建、编辑、复制、删除、同步与默认模型;在线多选导入、元数据来源选择。
|
|
53
|
+
- **会话**:按工作目录浏览,支持命名筛选、用户消息筛选、分支折叠、树状与 Markdown 阅读、复制和删除。
|
|
54
|
+
- **设置**:中英文、模型元数据与缺省参数、配置检查、备份恢复、OpenCode 导入、手动检查和安装更新。自动检查更新开关与 TUI 共享,控制 TUI 启动时的检查。
|
|
55
|
+
|
|
56
|
+
Web 在「设置 → 外观」中提供 Catppuccin Mocha 暗色与 Latte 亮色主题,切换立即生效,并在浏览器中保存选择。沿用 TUI 的四页导航和状态语义,支持窄屏布局、键盘操作与减少动画偏好。按 `1`–`4` 切换页面,`/` 搜索,`?` 查看完整快捷键;文本输入期间不触发快捷键。
|
|
57
|
+
|
|
58
|
+
Web 与 TUI 共用 Rust 文档层,使用相同的文件锁、备份、校验和未知字段保留规则。取消默认 provider 的同步、删除和恢复备份均会明确确认。浏览器不会将 API key 保存到 localStorage;会话 Markdown 中的原始 HTML 以文本展示,图片以替代文字展示,不自动加载远程图片。
|
|
59
|
+
|
|
45
60
|
## 开发者
|
|
46
61
|
|
|
47
62
|
从源码构建:
|
|
@@ -50,10 +65,14 @@ Windows 上无需 Rust:预构建原生模块随 npm 包分发,`pi-switch`
|
|
|
50
65
|
npm install
|
|
51
66
|
npm run build:native:debug
|
|
52
67
|
node ./bin/pi-switch.js
|
|
68
|
+
# 启动本地 Web 界面
|
|
69
|
+
npm run dev:web
|
|
53
70
|
```
|
|
54
71
|
|
|
55
72
|
贡献流程见 [CONTRIBUTING.md](./CONTRIBUTING.md)。
|
|
56
73
|
|
|
74
|
+
Web 实现与使用说明见 [web/README.md](./web/README.md)。
|
|
75
|
+
|
|
57
76
|
## 界面与快捷键
|
|
58
77
|
|
|
59
78
|
全局导航:`j/k` 或方向键移动;菜单中 `Enter` / `l` 进入内容,各内容页按自己的方向键切换焦点;`?` 帮助;`q` 退出(多数界面 `Ctrl+C` 也退出,会话预览除外)。
|
|
@@ -128,7 +147,8 @@ Session 根目录优先级:非空 `PI_CODING_AGENT_SESSION_DIR` → `<Pi agent
|
|
|
128
147
|
- 已同步 provider 的编辑与 model 变更会同步两份文件;不同步项只更新本地库。
|
|
129
148
|
- 在线导入 model **不会**隐式同步到 Pi。
|
|
130
149
|
- 启动或手动重载时,以 `models.json` 中同 ID provider 为准回灌本地库;外部从 Pi 删除的 provider 仍作为不同步项保留。
|
|
131
|
-
- 从 Pi 移除当前默认 provider 会先确认并清除默认模型;`d` 永久删除本地副本,必要时同时从 Pi
|
|
150
|
+
- 从 Pi 移除当前默认 provider 会先确认并清除默认模型;`d` 永久删除本地副本,必要时同时从 Pi 删除;若该 provider 在 `auth.json` 有 `api_key` 凭据,删除时会询问是否一并删除(OAuth 等凭据不会被删除)。
|
|
151
|
+
- API 密钥保存位置可在 Settings 里切换:默认写入 Pi 的 `auth.json`(provider 的 ID 映射到 `{"type":"api_key","key":...}`,`models.json` 与 `providers.json` 不保存密钥);可切换回旧的 `models.json` 行为。provider 重命名时凭据随之迁移(重命名并输入新密钥时源 OAuth 凭据保留原位),目标 ID 已有凭据时先询问。切到 `models.json` 后保存密钥会移除同名 ID 上的 auth.json 条目(否则它会继续优先于 models.json);该条目含 `env` 等配置时先询问。回到 `auth.json` 后保存会清掉 provider 文档里遗留的同名明文密钥。
|
|
132
152
|
|
|
133
153
|
## 模型导入与价格
|
|
134
154
|
|
|
@@ -140,8 +160,8 @@ Session 根目录优先级:非空 `PI_CODING_AGENT_SESSION_DIR` → `<Pi agent
|
|
|
140
160
|
- `GET /api/pricing`
|
|
141
161
|
成功则按 NewAPI 换算覆盖模型 `cost`(`1 USD = 500_000 quota`,每 1M tokens 成本 ≈ `ratio × 2` USD);失败静默忽略。
|
|
142
162
|
3. 若开启 models.dev 元数据:请求 `https://models.dev/api.json`,补全 `contextWindow`、`maxTokens`、`cost`、reasoning 等。
|
|
143
|
-
- **在线导入**遇到同 model ID
|
|
144
|
-
-
|
|
163
|
+
- **在线导入**遇到同 model ID 多源歧义时:需要选择元数据来源;缺少匹配元数据或 models.dev 不可达时,选中的模型使用 Settings 中的默认参数导入,并明确提示。
|
|
164
|
+
- 先选择服务商返回的模型 ID,再为选中项获取元数据和价格;TUI 与 Web 使用相同流程。
|
|
145
165
|
- 网关价格在 catalog 元数据之上叠加。
|
|
146
166
|
4. 关闭实时元数据:使用 Settings 中的默认参数;空字段回落 Pi 官方默认(context window `128000`、max tokens `16384`、cost `0`)。
|
|
147
167
|
|
|
@@ -174,6 +194,7 @@ Pi `settings.json` 中由 pi-switch 管理的字段:
|
|
|
174
194
|
|------|------|
|
|
175
195
|
| `language` | `en` \| `zh-CN` |
|
|
176
196
|
| `fetchModelMetadata` | 是否拉 models.dev(默认 `true`) |
|
|
197
|
+
| `keyStorage` | API 密钥保存位置:`auth.json`(默认)或 `models.json` |
|
|
177
198
|
| `checkForUpdates` | 是否启动时检查 npm 新版本(默认 `true`) |
|
|
178
199
|
| `modelDefaults` | 关闭实时元数据时的导入缺省(context / maxTokens / cost) |
|
|
179
200
|
|
|
@@ -184,6 +205,7 @@ Pi `settings.json` 中由 pi-switch 管理的字段:
|
|
|
184
205
|
- 写前备份 `providers.json`、Pi `models.json` / `settings.json` 和 pi-switch `settings.json` 到 `~/.pi-switch/backups/`(version 3);最多保留最近 10 份。现有 version 2 备份仍可恢复并自动拆分设置,version 1 备份不支持恢复。
|
|
185
206
|
- 写入使用 `write.lock` 互斥;异常残留锁时 `doctor` 会提示。
|
|
186
207
|
- `providers.json` 损坏时归档为 `corrupt-providers-*.json`,再从当前 Pi 配置重建,启动时显示归档路径。
|
|
208
|
+
- `auth.json` 不参与备份;写入前以 `<auth.json>.lock` 目录与 Pi 的 proper-lockfile 互斥,并额外持有 `<auth.json>.lock.guard` 这个 OS 级锁(进程退出时内核自动释放),因此崩溃遗留的锁目录会在 30 秒后被安全接管,不会永久阻塞写入。新文件权限 `0600`,只修改目标 provider 的条目(OAuth 等其他凭据原样保留)。要替换目标 ID 上不属于该 provider 的凭据(重命名落到已有凭据的 ID、或给已有 OAuth 登录的 ID 写 API 密钥)时会先询问,未确认不写入。
|
|
187
209
|
- 原子写入,只 patch 目标字段,保留未知 JSON;格式错误时停止写入并显示错误。
|
|
188
210
|
- 支持 Pi 的 `$ENV` / `${ENV}` 插值与 `$$` / `$!` 转义;`!command` 原样保存,在线拉取**不会**执行它。
|
|
189
211
|
- Session 删除只作用于选中的 JSONL,并校验路径必须位于 session 根目录内;优先调用系统 `trash`,失败后再永久删除。
|
package/bin/pi-switch.js
CHANGED
|
@@ -8,6 +8,9 @@ if (command === "--help" || command === "-h" || command === "help") {
|
|
|
8
8
|
|
|
9
9
|
Usage:
|
|
10
10
|
pi-switch open the terminal UI
|
|
11
|
+
pi-switch --web open the local Web interface
|
|
12
|
+
pi-switch --web --port 5210 --no-open
|
|
13
|
+
choose a port without opening a browser
|
|
11
14
|
pi-switch doctor validate Pi documents and defaults
|
|
12
15
|
pi-switch --version print the native module version
|
|
13
16
|
`);
|
|
@@ -21,7 +24,15 @@ Usage:
|
|
|
21
24
|
if (checks.some((check) => !check.ok)) process.exitCode = 1;
|
|
22
25
|
} else if (!command || command === "tui") {
|
|
23
26
|
runTui();
|
|
27
|
+
} else if (command === "--web" || command === "web") {
|
|
28
|
+
try {
|
|
29
|
+
const { parseWebOptions, startWeb } = await import("../web/start.js");
|
|
30
|
+
await startWeb(parseWebOptions(process.argv.slice(3)));
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error(error.message);
|
|
33
|
+
process.exitCode = 1;
|
|
34
|
+
}
|
|
24
35
|
} else {
|
|
25
|
-
console.error("Usage: pi-switch [tui|doctor|--version]");
|
|
36
|
+
console.error("Usage: pi-switch [tui|--web|doctor|--version|--help]");
|
|
26
37
|
process.exitCode = 2;
|
|
27
38
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oldsuns/pi-switch",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "A
|
|
3
|
+
"version": "0.3.5",
|
|
4
|
+
"description": "A local terminal and Web UI for Pi provider and model configuration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"pi-switch": "./bin/pi-switch.js"
|
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
"types": "./index.d.ts",
|
|
11
11
|
"files": [
|
|
12
12
|
"bin",
|
|
13
|
+
"web/public",
|
|
14
|
+
"web/server.js",
|
|
15
|
+
"web/start.js",
|
|
16
|
+
"web/native-client.js",
|
|
17
|
+
"web/native-worker.js",
|
|
13
18
|
"index.js",
|
|
14
19
|
"index.d.ts",
|
|
15
20
|
"pi-switch-native.cjs",
|
|
@@ -26,8 +31,9 @@
|
|
|
26
31
|
]
|
|
27
32
|
},
|
|
28
33
|
"scripts": {
|
|
29
|
-
"
|
|
30
|
-
"build:native
|
|
34
|
+
"dev:web": "node ./bin/pi-switch.js --web",
|
|
35
|
+
"build:native": "napi build --platform --no-js --release --dts native.d.ts",
|
|
36
|
+
"build:native:debug": "napi build --platform --no-js --dts native.d.ts",
|
|
31
37
|
"test": "cargo test --locked --lib",
|
|
32
38
|
"check": "cargo fmt --check && cargo clippy --locked --all-targets -- -D warnings",
|
|
33
39
|
"pack:check": "npm pack --dry-run"
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Worker } from "node:worker_threads";
|
|
2
|
+
|
|
3
|
+
export async function createNativeClient() {
|
|
4
|
+
const worker = new Worker(new URL("./native-worker.js", import.meta.url));
|
|
5
|
+
const pending = new Map();
|
|
6
|
+
let nextId = 0;
|
|
7
|
+
let failure;
|
|
8
|
+
let readyResolve;
|
|
9
|
+
let readyReject;
|
|
10
|
+
const ready = new Promise((resolve, reject) => {
|
|
11
|
+
readyResolve = resolve;
|
|
12
|
+
readyReject = reject;
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
function fail(error) {
|
|
16
|
+
failure = error;
|
|
17
|
+
readyReject(error);
|
|
18
|
+
for (const entry of pending.values()) entry.reject(error);
|
|
19
|
+
pending.clear();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
worker.on("error", fail);
|
|
23
|
+
worker.on("exit", (code) => fail(new Error("The native Web worker stopped (exit " + code + ").")));
|
|
24
|
+
worker.on("message", (message) => {
|
|
25
|
+
if (message.ready) {
|
|
26
|
+
readyResolve();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const entry = pending.get(message.id);
|
|
30
|
+
if (!entry) return;
|
|
31
|
+
pending.delete(message.id);
|
|
32
|
+
if (message.error) entry.reject(new Error(message.error));
|
|
33
|
+
else entry.resolve(message.result);
|
|
34
|
+
});
|
|
35
|
+
await ready;
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
request(payload) {
|
|
39
|
+
if (failure) return Promise.reject(failure);
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
const id = ++nextId;
|
|
42
|
+
pending.set(id, { resolve, reject });
|
|
43
|
+
worker.postMessage({ id, payload });
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
async close() {
|
|
47
|
+
fail(new Error("The Web server is shutting down."));
|
|
48
|
+
await worker.terminate();
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { parentPort } from "node:worker_threads";
|
|
2
|
+
import binding from "../pi-switch-native.cjs";
|
|
3
|
+
|
|
4
|
+
if (typeof binding.WebSession !== "function") {
|
|
5
|
+
throw new Error("The native module has no Web interface. Run npm run build:native:debug first.");
|
|
6
|
+
}
|
|
7
|
+
const session = new binding.WebSession();
|
|
8
|
+
|
|
9
|
+
parentPort.on("message", ({ id, payload }) => {
|
|
10
|
+
try {
|
|
11
|
+
const result = JSON.parse(session.request(JSON.stringify(payload)));
|
|
12
|
+
parentPort.postMessage({ id, result });
|
|
13
|
+
} catch (error) {
|
|
14
|
+
parentPort.postMessage({ id, error: error.message });
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
parentPort.postMessage({ ready: true });
|