pi-ccswitch-auto-switch 0.3.2 → 0.3.4
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 +40 -1
- package/README.zh-CN.md +40 -1
- package/health.ts +6 -0
- package/index.ts +64 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,6 +29,19 @@ The extension observes real Pi requests, records sanitized health signals, and
|
|
|
29
29
|
- CC Switch `3.20+` is recommended for its native Pi model configuration support.
|
|
30
30
|
- Node.js `22.19+` only for local development and tests. Pi supplies the runtime for the extension itself.
|
|
31
31
|
|
|
32
|
+
## Configuration
|
|
33
|
+
|
|
34
|
+
The extension requires **no user configuration**: it observes Pi's real requests and reads only the model metadata Pi already exposes. It never reads CC Switch databases, Pi auth files, API keys, cookies, or `.env` files, and it never writes Pi model settings or CC Switch data.
|
|
35
|
+
|
|
36
|
+
All knobs below are optional and have sensible defaults:
|
|
37
|
+
|
|
38
|
+
| Setting | Scope | Default | Notes |
|
|
39
|
+
| --- | --- | --- | --- |
|
|
40
|
+
| `PI_CODING_AGENT_DIR` | Extension state | `~/.pi/agent` | Where health state, logs, and failure reports are stored. Only relevant if you relocated your Pi agent directory; the extension follows Pi's own convention. |
|
|
41
|
+
| `PI_BIN` | Headless runner only | `pi` on `PATH` | Must point to the real `pi` executable, never to `pi-ccswitch-run`. |
|
|
42
|
+
| Model scope (`/model` etc.) | Failover candidates | Full registry | When Pi has an active model scope, failover only considers models inside that scope; otherwise the full registry is used. The status bar shows which source is active. |
|
|
43
|
+
| `baseUrl` metadata | Endpoint isolation | provider key | Endpoint-level platform isolation groups models by `baseUrl` (provided by CC Switch `3.20+`). Without it, isolation degrades to provider-level grouping, which still works. |
|
|
44
|
+
|
|
32
45
|
## Installation
|
|
33
46
|
|
|
34
47
|
### Pi package install (recommended)
|
|
@@ -49,7 +62,32 @@ CC Switch should be configured normally. This extension deliberately does not wr
|
|
|
49
62
|
|
|
50
63
|
### Headless / automation use
|
|
51
64
|
|
|
52
|
-
|
|
65
|
+
`pi-ccswitch-run`(`runner.mjs`)是独立于 Pi 扩展的 headless 调用入口:它启动一个 `pi --mode rpc` 会话,把一次请求自动切换到健康模型后只返回最终成功结果。它**不在** `pi install` 的扩展加载路径里自动暴露给下游项目,需要单独安装。
|
|
66
|
+
|
|
67
|
+
#### 在消费方项目内安装(推荐,保证 `require.resolve` 可解析)
|
|
68
|
+
|
|
69
|
+
`pi install git:...` 只把扩展装进 Pi 全局目录(`~/.pi/agent/...`),**不会**把它放进下游项目的 `node_modules` 解析路径。因此消费方代码里 `require.resolve('pi-ccswitch-auto-switch/runner.mjs')` 需要把本包安装进**自己的项目依赖**:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# 在消费方项目目录内执行
|
|
73
|
+
npm i github:JunyWuuuu91/pi-ccswitch-auto-switch
|
|
74
|
+
# 或从 npm registry 安装最新版
|
|
75
|
+
npm i pi-ccswitch-auto-switch@latest
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
安装后 `require.resolve('pi-ccswitch-auto-switch/runner.mjs')` 会命中项目自身 `node_modules`,bin `pi-ccswitch-run` 也可直接调用。
|
|
79
|
+
|
|
80
|
+
#### 版本锁定警告(0.x caret 陷阱)
|
|
81
|
+
|
|
82
|
+
npm 对 `^0.1.6` 的 caret 语义**只匹配 `0.1.x`**,不会自动升到含 `runner.mjs` 的 `0.3.x`。如果机器上 `~/.pi/agent/npm/node_modules/pi-ccswitch-auto-switch` 仍停留在 `0.1.6`(该版本无 `runner.mjs`、无 `pi-ccswitch-run` bin),重复执行 `pi install` 也不会升级。请手动升级 npm 侧版本:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
cd ~/.pi/agent/npm && npm install pi-ccswitch-auto-switch@latest
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
升级后确认 `~/.pi/agent/npm/node_modules/pi-ccswitch-auto-switch/runner.mjs` 存在。也可以在扩展内执行 `/ccswitch-doctor` 检查 `runner.mjs` 是否可解析。
|
|
89
|
+
|
|
90
|
+
#### 运行方式
|
|
53
91
|
|
|
54
92
|
```bash
|
|
55
93
|
pi-ccswitch-run --no-tools --no-context-files @prompt.md "Summarize the attached text"
|
|
@@ -70,6 +108,7 @@ Exit codes are `0` for success, `1` when candidates are exhausted, `2` for invoc
|
|
|
70
108
|
| `/ccswitch disable <provider/model>` | Manually exclude a model from failover. |
|
|
71
109
|
| `/ccswitch reset <provider/model\|all>` | Delete selected health history after confirmation; `all` also clears learned content-policy constraints. |
|
|
72
110
|
| `/ccswitch-test` | Inspect candidate discovery without changing models. |
|
|
111
|
+
| `/ccswitch-doctor` | Diagnose whether `runner.mjs` is present and resolvable. |
|
|
73
112
|
|
|
74
113
|
Examples:
|
|
75
114
|
|
package/README.zh-CN.md
CHANGED
|
@@ -27,6 +27,19 @@
|
|
|
27
27
|
- 建议使用 CC Switch `3.20+`,其已支持原生维护 Pi 模型配置。
|
|
28
28
|
- 本地开发和测试需要 Node.js `22.19+`;Pi 运行插件本身不需要额外安装 Node。
|
|
29
29
|
|
|
30
|
+
## 配置
|
|
31
|
+
|
|
32
|
+
插件**不需要任何用户配置**:它只观察 Pi 的真实请求,读取 Pi 已暴露的模型元数据;从不读取 CC Switch 数据库、Pi 认证文件、API Key、Cookie 或 `.env` 文件,也从不写入 Pi 模型设置或 CC Switch 数据。
|
|
33
|
+
|
|
34
|
+
以下开关全部可选,且有合理默认值:
|
|
35
|
+
|
|
36
|
+
| 设置项 | 作用域 | 默认值 | 说明 |
|
|
37
|
+
| --- | --- | --- | --- |
|
|
38
|
+
| `PI_CODING_AGENT_DIR` | 扩展状态 | `~/.pi/agent` | 健康状态、日志与失败报告的存放目录;仅在你迁移了 Pi agent 目录时需要,插件跟随 Pi 自身约定。 |
|
|
39
|
+
| `PI_BIN` | 仅 headless runner | `PATH` 中的 `pi` | 必须指向真实 `pi` 可执行文件,不能指向 `pi-ccswitch-run`。 |
|
|
40
|
+
| 模型 scope(`/model` 等) | 故障转移候选 | 全量注册表 | Pi 启用了 model scope 时,只在 scope 内模型间切换;否则使用全量注册表。状态栏会显示当前数据源。 |
|
|
41
|
+
| `baseUrl` 元数据 | 端点隔离 | provider key | 端点级平台隔离按 `baseUrl` 分组模型(由 CC Switch `3.20+` 提供);缺失时退化为 provider 级分组,仍然可用。 |
|
|
42
|
+
|
|
30
43
|
## 安装
|
|
31
44
|
|
|
32
45
|
### 通过 Pi Package 安装(推荐)
|
|
@@ -45,7 +58,32 @@ pi install npm:pi-ccswitch-auto-switch
|
|
|
45
58
|
|
|
46
59
|
### 无头自动化调用
|
|
47
60
|
|
|
48
|
-
|
|
61
|
+
`pi-ccswitch-run`(`runner.mjs`)是独立于 Pi 扩展的 headless 调用入口:它启动一个 `pi --mode rpc` 会话,把一次请求自动切换到健康模型后只返回最终成功结果。它**不在** `pi install` 的扩展加载路径里自动暴露给下游项目,需要单独安装。
|
|
62
|
+
|
|
63
|
+
#### 在消费方项目内安装(推荐,保证 `require.resolve` 可解析)
|
|
64
|
+
|
|
65
|
+
`pi install git:...` 只把扩展装进 Pi 全局目录(`~/.pi/agent/...`),**不会**把它放进下游项目的 `node_modules` 解析路径。因此消费方代码里 `require.resolve('pi-ccswitch-auto-switch/runner.mjs')` 需要把本包安装进**自己的项目依赖**:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
# 在消费方项目目录内执行
|
|
69
|
+
npm i github:JunyWuuuu91/pi-ccswitch-auto-switch
|
|
70
|
+
# 或从 npm registry 安装最新版
|
|
71
|
+
npm i pi-ccswitch-auto-switch@latest
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
安装后 `require.resolve('pi-ccswitch-auto-switch/runner.mjs')` 会命中项目自身 `node_modules`,bin `pi-ccswitch-run` 也可直接调用。
|
|
75
|
+
|
|
76
|
+
#### 版本锁定警告(0.x caret 陷阱)
|
|
77
|
+
|
|
78
|
+
npm 对 `^0.1.6` 的 caret 语义**只匹配 `0.1.x`**,不会自动升到含 `runner.mjs` 的 `0.3.x`。如果机器上 `~/.pi/agent/npm/node_modules/pi-ccswitch-auto-switch` 仍停留在 `0.1.6`(该版本无 `runner.mjs`、无 `pi-ccswitch-run` bin),重复执行 `pi install` 也不会升级。请手动升级 npm 侧版本:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
cd ~/.pi/agent/npm && npm install pi-ccswitch-auto-switch@latest
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
升级后确认 `~/.pi/agent/npm/node_modules/pi-ccswitch-auto-switch/runner.mjs` 存在。也可以在扩展内执行 `/ccswitch-doctor` 检查 `runner.mjs` 是否可解析。
|
|
85
|
+
|
|
86
|
+
#### 运行方式
|
|
49
87
|
|
|
50
88
|
```bash
|
|
51
89
|
pi-ccswitch-run --no-tools --no-context-files @prompt.md "请总结附件"
|
|
@@ -66,6 +104,7 @@ runner 内部只显式加载本扩展并启动一次 Pi RPC;`PI_BIN` 只能指
|
|
|
66
104
|
| `/ccswitch disable <provider/model>` | 手动排除模型。 |
|
|
67
105
|
| `/ccswitch reset <provider/model\|all>` | 确认后删除相应健康历史;`all` 同时清除已学习的审查约束。 |
|
|
68
106
|
| `/ccswitch-test` | 仅检查候选发现,不切换模型。 |
|
|
107
|
+
| `/ccswitch-doctor` | 诊断 `runner.mjs` 可解析性和安装情况。 |
|
|
69
108
|
|
|
70
109
|
## 状态栏
|
|
71
110
|
|
package/health.ts
CHANGED
|
@@ -105,6 +105,8 @@ export class HealthStore {
|
|
|
105
105
|
private dirty = false
|
|
106
106
|
private replaceOnFlush = false
|
|
107
107
|
private resetModels = new Set<string>()
|
|
108
|
+
/** 合并磁盘状态后仍需强制清空的审查约束(session 边界) */
|
|
109
|
+
private clearContentPolicyOnFlush = false
|
|
108
110
|
|
|
109
111
|
constructor(dir = agentDir()) { this.dir = dir }
|
|
110
112
|
get file(): string { return join(this.dir, STATE_FILE) }
|
|
@@ -242,6 +244,7 @@ export class HealthStore {
|
|
|
242
244
|
*/
|
|
243
245
|
clearContentPolicyConstraints(): void {
|
|
244
246
|
this.state.contentPolicyFamilies = {}
|
|
247
|
+
this.clearContentPolicyOnFlush = true
|
|
245
248
|
this.touch()
|
|
246
249
|
}
|
|
247
250
|
|
|
@@ -270,6 +273,8 @@ export class HealthStore {
|
|
|
270
273
|
await this.withLock(async () => {
|
|
271
274
|
if (!this.replaceOnFlush) this.state = mergeState(await this.readDisk(), this.state)
|
|
272
275
|
for (const key of this.resetModels) delete this.state.models[key]
|
|
276
|
+
// 必须在 merge 之后再清空:磁盘上残留的旧 session 约束不能因合并而重新出现
|
|
277
|
+
if (this.clearContentPolicyOnFlush) this.state.contentPolicyFamilies = {}
|
|
273
278
|
await this.commit()
|
|
274
279
|
}).catch(() => {})
|
|
275
280
|
}
|
|
@@ -308,6 +313,7 @@ export class HealthStore {
|
|
|
308
313
|
this.dirty = false
|
|
309
314
|
this.replaceOnFlush = false
|
|
310
315
|
this.resetModels.clear()
|
|
316
|
+
this.clearContentPolicyOnFlush = false
|
|
311
317
|
}
|
|
312
318
|
private bucket(scope: HealthScope): Record<string, HealthRecord> {
|
|
313
319
|
return scope === 'model' ? this.state.models : scope === 'provider' ? this.state.providers : this.state.endpoints
|
package/index.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { createRequire } from 'node:module'
|
|
3
|
+
import { homedir } from 'node:os'
|
|
4
|
+
import { dirname, join } from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
1
6
|
import type { ExtensionAPI, ExtensionContext, FailureObservation, ModelRef } from './types.ts'
|
|
2
7
|
import { classifyFailure, parseRetryAfter } from './classify.ts'
|
|
3
8
|
import { candidateSnapshot, effectiveCandidates, chooseCandidate, modelFamily, summarizeCandidateHealth } from './candidates.ts'
|
|
@@ -8,7 +13,7 @@ const STREAM_IDLE_TIMEOUT = 120_000
|
|
|
8
13
|
const MAX_ATTEMPTS = 5
|
|
9
14
|
const ROUND_LIMIT = 8 * 60_000
|
|
10
15
|
const RPC_PROTOCOL_VERSION = 1
|
|
11
|
-
const EXTENSION_VERSION = '0.3.
|
|
16
|
+
const EXTENSION_VERSION = '0.3.4'
|
|
12
17
|
// 同端点(BaseURL 相同)连续失败达到该次数即隔离该端点,避免同一个平台的多个模型逐个试错耗尽本轮切换
|
|
13
18
|
const ENDPOINT_FAIL_THRESHOLD = 3
|
|
14
19
|
|
|
@@ -108,6 +113,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
108
113
|
'/ccswitch disable <provider/model> — 手动禁用模型',
|
|
109
114
|
'/ccswitch reset <provider/model|all> — 清除健康历史;all 也会清除已学习的审查约束(需确认)',
|
|
110
115
|
'/ccswitch-test — 自检候选模型,不实际切换',
|
|
116
|
+
'/ccswitch-doctor — 诊断 runner.mjs 可解析性和安装情况',
|
|
111
117
|
].join('\n'), 'info')
|
|
112
118
|
}
|
|
113
119
|
const refresh = async (ctx: ExtensionContext) => {
|
|
@@ -381,4 +387,61 @@ export default function (pi: ExtensionAPI) {
|
|
|
381
387
|
const source = snapshot.source === 'scoped' ? `Pi scope ${snapshot.sourceEntries} 条` : `Pi 注册表 ${snapshot.sourceEntries} 条`
|
|
382
388
|
notify(ctx, `CCSwitch 自检:${source} · 唯一模型 ${counts.total} · 健康 ${counts.healthy} · 自动冷却 ${counts.cooling} · 手动禁用 ${counts.disabled} · 熔断记录 ${counts.breakerRecords} · 审查约束系列 ${policyFamilies.size} · 本session切换 ${sessionSwitches} · 累计切换 ${state.switches ?? 0}`, counts.total ? 'info' : 'warning')
|
|
383
389
|
}})
|
|
390
|
+
pi.registerCommand('ccswitch-doctor', { description: '诊断 runner.mjs 可解析性和安装情况', handler: async (_args, ctx) => {
|
|
391
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
392
|
+
const localRunner = join(__dirname, 'runner.mjs')
|
|
393
|
+
const hasLocal = existsSync(localRunner)
|
|
394
|
+
let resolvable = false
|
|
395
|
+
let resolvedPath = ''
|
|
396
|
+
try {
|
|
397
|
+
const cRequire = createRequire(import.meta.url)
|
|
398
|
+
resolvedPath = cRequire.resolve('pi-ccswitch-auto-switch/runner.mjs')
|
|
399
|
+
resolvable = true
|
|
400
|
+
} catch { /* not resolvable — expected when extension is in git checkout */ }
|
|
401
|
+
|
|
402
|
+
// 检查 npm 全局安装版本
|
|
403
|
+
let npmVersion = ''
|
|
404
|
+
const npmPkgDir = join(homedir(), '.pi/agent/npm/node_modules/pi-ccswitch-auto-switch')
|
|
405
|
+
const npmPkgJson = join(npmPkgDir, 'package.json')
|
|
406
|
+
if (existsSync(npmPkgJson)) {
|
|
407
|
+
try {
|
|
408
|
+
npmVersion = JSON.parse(readFileSync(npmPkgJson, 'utf-8')).version
|
|
409
|
+
} catch { /* ignore */ }
|
|
410
|
+
}
|
|
411
|
+
const hasNpmRunner = npmVersion && existsSync(join(npmPkgDir, 'runner.mjs'))
|
|
412
|
+
|
|
413
|
+
const lines = [
|
|
414
|
+
`CCSwitch 诊断 v${EXTENSION_VERSION}`,
|
|
415
|
+
'',
|
|
416
|
+
`扩展加载目录:${__dirname}`,
|
|
417
|
+
`本地 runner.mjs:${hasLocal ? '✅ 存在' : '❌ 缺失'}`,
|
|
418
|
+
`resolve('pi-ccswitch-auto-switch/runner.mjs'):${resolvable ? '✅ 可解析' : '❌ 不可解析'}`,
|
|
419
|
+
'',
|
|
420
|
+
]
|
|
421
|
+
if (npmVersion) {
|
|
422
|
+
lines.push(`npm 全局安装版本:${npmVersion}${hasNpmRunner ? ' ✅ 含 runner.mjs' : ' ❌ 不含 runner.mjs(版本过旧)'}`)
|
|
423
|
+
lines.push('')
|
|
424
|
+
}
|
|
425
|
+
lines.push('--- 下游消费方正确安装方式 ---',
|
|
426
|
+
'pi-ccswitch-auto-switch 是 Pi 扩展,`pi install` 只装到 Pi 全局目录,',
|
|
427
|
+
'下游项目 require.resolve() 在自己的 node_modules 路径上解析不到。',
|
|
428
|
+
'',
|
|
429
|
+
'正确做法:在消费方项目内安装本包',
|
|
430
|
+
' npm i github:JunyWuuuu91/pi-ccswitch-auto-switch',
|
|
431
|
+
' 或 npm i pi-ccswitch-auto-switch@latest',
|
|
432
|
+
'',
|
|
433
|
+
)
|
|
434
|
+
if (npmVersion && !hasNpmRunner) {
|
|
435
|
+
lines.push('--- 版本锁定修复 ---',
|
|
436
|
+
`npm 全局版本 ${npmVersion} 不含 runner.mjs,因为 ^0.1.x 的 caret 语义只匹配 0.1.x。`,
|
|
437
|
+
'需手动升级 npm 侧版本:',
|
|
438
|
+
' cd ~/.pi/agent/npm && npm install pi-ccswitch-auto-switch@latest',
|
|
439
|
+
'',
|
|
440
|
+
)
|
|
441
|
+
}
|
|
442
|
+
lines.push('也可通过 NODE_PATH 或全局路径引用:',
|
|
443
|
+
` ${__dirname}/runner.mjs`,
|
|
444
|
+
)
|
|
445
|
+
notify(ctx, lines.join('\n'), 'info')
|
|
446
|
+
}})
|
|
384
447
|
}
|