ck-pi-zen-session 0.1.3 → 0.1.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 +4 -3
- package/package.json +1 -1
- package/src/index.ts +35 -31
- package/src/sync.ts +35 -5
- package/src/types.ts +1 -0
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ OpenCode Zen 免费模型同步、请求头伪装与 Session 会话自动维护
|
|
|
6
6
|
|
|
7
7
|
## 🌟 核心特性
|
|
8
8
|
|
|
9
|
-
- ⚡ **/zen
|
|
9
|
+
- ⚡ **/zen 指令一键激活与动态免费模型全量探测**:输入一次 API Key (`/zen oc_sk_...`),全自动在线探测所有带有 `free` / `zero` / `pickle` 标识的模型,全量标记 0 额度消耗 (`cost: { input: 0, output: 0 }`) 并动态热挂载进 Pi 与 CC-Switch。
|
|
10
10
|
- 🔄 **官方算法深度逆向**:完整对齐 OpenCode 客户端 `Identifier.descending("ses")` 与 `Identifier.ascending("msg")` 算法,生成 30 位降序会话 ID 与升序请求 ID,支持毫秒级时间戳反解。
|
|
11
11
|
- 🛡️ **全套官方级伪装请求头**:自动注入全部 7 个官方客户端对齐请求头:
|
|
12
12
|
- `User-Agent`: `opencode/1.18.32 ai-sdk/provider-utils/4.0.23 runtime/bun/1.3.14`
|
|
@@ -19,12 +19,13 @@ OpenCode Zen 免费模型同步、请求头伪装与 Session 会话自动维护
|
|
|
19
19
|
- ⏱️ **30 分钟无感前置平滑轮换**:官方后端限制免费会话时长约为 1 小时(超时直接 403 `FreeTierError`)。本插件在底层请求拦截器(`before_provider_headers`)和 Agent 循环守卫(`before_agent_start`)中设置 30 分钟主动换新阈值,兼顾会话上下文平滑与长期高频调用稳定性,确保永不触碰 1 小时硬限制。
|
|
20
20
|
- 🚑 **网关异常自愈守卫 (`after_provider_response`)**:新增底层响应拦截,一旦检测到远端网关返回 401 或 403 `FreeTierError`,立即当场触发 Session 换新与后台持久化,并提示用户,彻底终结会话失效死锁。
|
|
21
21
|
- 🔑 **多源凭据自适应感知**:支持优先读取 `OPENCODE_API_KEY`、`OPENCODE_ZEN_API_KEY` 环境变量,亦可直接使用 `/zen <key>` 配置,无缝兼容各类容器与终端环境。
|
|
22
|
+
- 🌐 **原生代理支持解除限速与额度**:由于 OpenCode 端点受 Cloudflare WAF 保护,在 HTTP 头部伪造假 IP 无法欺骗底层 TCP 握手并会招致 WAF 封禁;如需轮换出口解除 IP 限制,直接配置标准网络代理(如 `export HTTPS_PROXY=http://127.0.0.1:7890`)即可让物理连接从新节点发起,安全解除限速与额度。
|
|
22
23
|
- 🧰 **官方全套 6 大核心工具链守卫与字母序规约**:OpenCode Zen 免费端点对工具集有硬性要求(无工具直接拒绝)。插件在 `before_provider_request` 钩子中实时监控请求体:
|
|
23
24
|
- 若用户处于纯文本或无工具模式,自动补齐官方完整的 6 大核心工具(`bash`, `edit`, `glob`, `grep`, `read`, `write`)与 `tool_choice: "auto"`,彻底根除 403 拦截;
|
|
24
25
|
- 若请求已有工具,严格按照官方客户端规约(`localeCompare`)按函数名升序重排;
|
|
25
26
|
- 自动对齐流式用量元数据 `stream_options: { include_usage: true }`。
|
|
26
|
-
- 🌐
|
|
27
|
-
- 🧠
|
|
27
|
+
- 🌐 **独立供应商隔离与零干扰保障 (`isZenModelTarget`)**:严格将生命周期守卫、请求头与请求体拦截限制在 `opencode-zen-free` 命名空间内,对 `relayhub`、`deepseek`、`anthropic`、`openai`、`onerouter`、`apmix` 等其他任何模型及 Claude Code(`pi-cc-extensions`)插件 100% 保持完全静默放行,零副作用、零冲突。
|
|
28
|
+
- 🧠 **精准上下文与思维链适配**:严格对齐免费模型的 `contextWindow`、`maxTokens` 与 `thinkingLevelMap`(涵盖小米 MiMo、英伟达 Nemotron、Meta Muse Spark、Ling、Jev 等)。
|
|
28
29
|
- 🔌 **全自动多端持久化同步**:自动双写 Pi 本地配置(`~/.pi/agent/models.json` 和 `~/.pi/agent/auth.json`),若检测到 CC-Switch 亦无缝同步其 SQLite 数据库。
|
|
29
30
|
|
|
30
31
|
---
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
} from "./models-registry.js";
|
|
20
20
|
import {
|
|
21
21
|
getStoredZenApiKey,
|
|
22
|
+
getStoredZenModels,
|
|
22
23
|
getStoredZenSessionId,
|
|
23
24
|
getZenStatusInfo,
|
|
24
25
|
syncZenConfiguration,
|
|
@@ -31,11 +32,13 @@ import {
|
|
|
31
32
|
isZenSessionExpired,
|
|
32
33
|
isValidZenSessionId,
|
|
33
34
|
} from "./session.js";
|
|
35
|
+
import type { ZenModelDefinition } from "./types.js";
|
|
34
36
|
|
|
35
37
|
export function registerZenProviderToPi(
|
|
36
38
|
pi: ExtensionAPI,
|
|
37
39
|
apiKey: string,
|
|
38
40
|
sessionId: string = getStoredZenSessionId() || generateZenSessionId(),
|
|
41
|
+
models: ZenModelDefinition[] = getStoredZenModels(),
|
|
39
42
|
): void {
|
|
40
43
|
try {
|
|
41
44
|
pi.registerProvider(ZEN_PROVIDER_ID, {
|
|
@@ -58,13 +61,27 @@ export function registerZenProviderToPi(
|
|
|
58
61
|
supportsStore: false,
|
|
59
62
|
supportsUsageInStreaming: true,
|
|
60
63
|
},
|
|
61
|
-
models: Object.values(KNOWN_ZEN_FREE_MODELS),
|
|
64
|
+
models: models && models.length > 0 ? models : Object.values(KNOWN_ZEN_FREE_MODELS),
|
|
62
65
|
});
|
|
63
66
|
} catch {
|
|
64
67
|
// 忽略重复注册
|
|
65
68
|
}
|
|
66
69
|
}
|
|
67
70
|
|
|
71
|
+
export function isZenModelTarget(model?: { provider?: string; id?: string }): boolean {
|
|
72
|
+
if (!model) return false;
|
|
73
|
+
// 严格隔离:仅处理 opencode-zen-free 或明确属于 opencode 官方的免费模型
|
|
74
|
+
// 绝不干涉 relayhub、deepseek、anthropic、openai 等其他任何供应商或 CC 插件
|
|
75
|
+
if (model.provider === ZEN_PROVIDER_ID) return true;
|
|
76
|
+
if (
|
|
77
|
+
model.provider === "opencode" &&
|
|
78
|
+
(model.id?.includes("-free") || model.id === "big-pickle")
|
|
79
|
+
) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
68
85
|
export default function piZenSession(pi: ExtensionAPI): void {
|
|
69
86
|
// 1. 若本地已配置 Key,启动时即刻注册独立供应商 opencode-zen-free
|
|
70
87
|
// 独立命名空间,彻底避开并兼容用户自带的官方登录 opencode / opencode-zen 套餐
|
|
@@ -75,13 +92,7 @@ export default function piZenSession(pi: ExtensionAPI): void {
|
|
|
75
92
|
|
|
76
93
|
// 2. 核心请求头拦截钩子:在每一次请求发往 Provider 前注入完整官方客户端对齐请求头
|
|
77
94
|
pi.on("before_provider_headers", (event, ctx) => {
|
|
78
|
-
|
|
79
|
-
const isZenModel =
|
|
80
|
-
model?.provider === ZEN_PROVIDER_ID ||
|
|
81
|
-
model?.id?.includes("-free") ||
|
|
82
|
-
model?.id === "big-pickle";
|
|
83
|
-
|
|
84
|
-
if (isZenModel) {
|
|
95
|
+
if (isZenModelTarget(ctx.model)) {
|
|
85
96
|
// 检查 x-opencode-session 是否过期 (>30分钟) 或缺失
|
|
86
97
|
let currentSession = event.headers["x-opencode-session"];
|
|
87
98
|
if (
|
|
@@ -108,13 +119,7 @@ export default function piZenSession(pi: ExtensionAPI): void {
|
|
|
108
119
|
|
|
109
120
|
// 3. 核心请求体守卫钩子:深度对齐 OpenCode 官方请求体结构与工具集
|
|
110
121
|
pi.on("before_provider_request", (event, ctx) => {
|
|
111
|
-
|
|
112
|
-
const isZenModel =
|
|
113
|
-
model?.provider === ZEN_PROVIDER_ID ||
|
|
114
|
-
model?.id?.includes("-free") ||
|
|
115
|
-
model?.id === "big-pickle";
|
|
116
|
-
|
|
117
|
-
if (isZenModel && event.payload && typeof event.payload === "object") {
|
|
122
|
+
if (isZenModelTarget(ctx.model) && event.payload && typeof event.payload === "object") {
|
|
118
123
|
const payload = event.payload as Record<string, unknown>;
|
|
119
124
|
const tools = payload.tools;
|
|
120
125
|
let modified = false;
|
|
@@ -148,13 +153,7 @@ export default function piZenSession(pi: ExtensionAPI): void {
|
|
|
148
153
|
|
|
149
154
|
// 4. 网关响应守卫:遇 401/403 会话受限或过期时,立即自愈换新 Session ID
|
|
150
155
|
pi.on("after_provider_response", (event, ctx) => {
|
|
151
|
-
|
|
152
|
-
const isZenModel =
|
|
153
|
-
model?.provider === ZEN_PROVIDER_ID ||
|
|
154
|
-
model?.id?.includes("-free") ||
|
|
155
|
-
model?.id === "big-pickle";
|
|
156
|
-
|
|
157
|
-
if (isZenModel && (event.status === 401 || event.status === 403)) {
|
|
156
|
+
if (isZenModelTarget(ctx.model) && (event.status === 401 || event.status === 403)) {
|
|
158
157
|
// 自动异步刷新 Session
|
|
159
158
|
syncZenConfiguration({ forceSession: true }).catch(() => {});
|
|
160
159
|
if (ctx.hasUI) {
|
|
@@ -167,8 +166,11 @@ export default function piZenSession(pi: ExtensionAPI): void {
|
|
|
167
166
|
});
|
|
168
167
|
|
|
169
168
|
// 5. 常驻生命周期守卫:在每次 Agent 循环执行前,自动检测 Session 有效性并无感续期
|
|
170
|
-
pi.on("before_agent_start", async () => {
|
|
169
|
+
pi.on("before_agent_start", async (_event, ctx) => {
|
|
171
170
|
try {
|
|
171
|
+
// 严格隔离:仅在当前激活/选择的是 Zen 模型时才触发轮换检测,绝不干扰其他模型或 CC 插件
|
|
172
|
+
if (!isZenModelTarget(ctx.model)) return;
|
|
173
|
+
|
|
172
174
|
const currentSession = getStoredZenSessionId();
|
|
173
175
|
// 若当前会话不存在或已使用超过 30 分钟,后台自动续期
|
|
174
176
|
if (!currentSession || isZenSessionExpired(currentSession, DEFAULT_SESSION_MAX_AGE_MS)) {
|
|
@@ -258,12 +260,13 @@ export async function handleZenCommand(
|
|
|
258
260
|
notify(ctx, "正在生成全新 OpenCode Zen 会话 ID 并同步...", "info");
|
|
259
261
|
const result = await syncZenConfiguration({ forceSession: true });
|
|
260
262
|
if (pi) {
|
|
261
|
-
registerZenProviderToPi(pi, result.apiKey, result.sessionId);
|
|
263
|
+
registerZenProviderToPi(pi, result.apiKey, result.sessionId, result.resolvedModels);
|
|
262
264
|
}
|
|
263
265
|
const successMsg = [
|
|
264
266
|
"✓ OpenCode Zen Session 已成功刷新!",
|
|
265
267
|
`• 全新 Session: ${result.sessionId}`,
|
|
266
|
-
`•
|
|
268
|
+
`• 免费模型: 已对齐 ${result.modelsCount} 款 0 额度消耗模型`,
|
|
269
|
+
`• 请求头伪装: 7 维官方客户端签名已注入`,
|
|
267
270
|
`• 同步状态: models.json ✓ | CC-Switch ${result.ccSwitchUpdated ? "✓" : "-(未安装或无此条目)"}`,
|
|
268
271
|
].join("\n");
|
|
269
272
|
notify(ctx, successMsg, "info");
|
|
@@ -276,16 +279,17 @@ export async function handleZenCommand(
|
|
|
276
279
|
// 4. 输入了具体的 API Key: /zen oc_sk_... 或其它 key
|
|
277
280
|
if (rawArg.length > 0 && !["status", "refresh", "list", "models"].includes(command)) {
|
|
278
281
|
try {
|
|
279
|
-
notify(ctx, "正在验证 OpenCode Zen API Key
|
|
282
|
+
notify(ctx, "正在验证 OpenCode Zen API Key 并探测免费与零额度模型...", "info");
|
|
280
283
|
const result = await syncZenConfiguration({ apiKey: rawArg, forceSession: true });
|
|
281
284
|
if (pi) {
|
|
282
|
-
registerZenProviderToPi(pi, result.apiKey, result.sessionId);
|
|
285
|
+
registerZenProviderToPi(pi, result.apiKey, result.sessionId, result.resolvedModels);
|
|
283
286
|
}
|
|
284
287
|
const msg = [
|
|
285
|
-
"🎉 OpenCode Zen
|
|
288
|
+
"🎉 OpenCode Zen 配置成功!免费套餐与零额度模型已自动识别并全量对接:",
|
|
286
289
|
`• API Key: ${maskKey(result.apiKey)} (已验证并保存)`,
|
|
287
290
|
`• 会话 Session: ${result.sessionId} (官方降序时间戳逆向算法)`,
|
|
288
|
-
`•
|
|
291
|
+
`• 自动对接免费模型 (${result.modelsCount} 款,全部标记 0 额度消耗):`,
|
|
292
|
+
...result.models.map((id) => ` • ${id}`),
|
|
289
293
|
`• 同步路径: ${result.modelsPath}`,
|
|
290
294
|
result.ccSwitchUpdated ? "• CC-Switch: 本地数据库已同步更新" : "",
|
|
291
295
|
].filter(Boolean).join("\n");
|
|
@@ -304,13 +308,13 @@ export async function handleZenCommand(
|
|
|
304
308
|
notify(ctx, "正在刷新 OpenCode Zen 会话与请求头...", "info");
|
|
305
309
|
const result = await syncZenConfiguration({ forceSession: true });
|
|
306
310
|
if (pi) {
|
|
307
|
-
registerZenProviderToPi(pi, result.apiKey, result.sessionId);
|
|
311
|
+
registerZenProviderToPi(pi, result.apiKey, result.sessionId, result.resolvedModels);
|
|
308
312
|
}
|
|
309
313
|
const msg = [
|
|
310
314
|
"✨ [OpenCode Zen 已自动刷新并就绪]",
|
|
311
315
|
`• API Key: ${maskKey(result.apiKey)}`,
|
|
312
316
|
`• 全新 Session: ${result.sessionId} (有效且已持久化)`,
|
|
313
|
-
`•
|
|
317
|
+
`• 自动对接免费模型: 已同步 ${result.modelsCount} 个 0 额度消耗模型`,
|
|
314
318
|
`• 请求头保护: 30分钟自动轮换 + 7维官方签名 + 6大核心工具全注入`,
|
|
315
319
|
"",
|
|
316
320
|
"常用操作:",
|
package/src/sync.ts
CHANGED
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
isZenSessionExpired,
|
|
30
30
|
isValidZenSessionId,
|
|
31
31
|
} from "./session.js";
|
|
32
|
-
import type { ZenStatusInfo, ZenSyncOptions, ZenSyncResult } from "./types.js";
|
|
32
|
+
import type { ZenModelDefinition, ZenStatusInfo, ZenSyncOptions, ZenSyncResult } from "./types.js";
|
|
33
33
|
|
|
34
34
|
export function defaultModelsPath(): string {
|
|
35
35
|
return join(homedir(), ".pi", "agent", "models.json");
|
|
@@ -44,7 +44,7 @@ export function defaultCcSwitchDbPath(): string {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
|
-
* 校验 API Key 并拉取 OpenCode Zen
|
|
47
|
+
* 校验 API Key 并拉取 OpenCode Zen 在线模型清单,自动识别所有免费与零额度模型
|
|
48
48
|
*/
|
|
49
49
|
export async function fetchZenFreeModels(apiKey: string): Promise<string[]> {
|
|
50
50
|
const res = await fetch(`${ZEN_BASE_URL}/models`, {
|
|
@@ -60,18 +60,47 @@ export async function fetchZenFreeModels(apiKey: string): Promise<string[]> {
|
|
|
60
60
|
throw new Error(`OpenCode Zen API 验证失败 (HTTP ${res.status}): ${text || res.statusText}`);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
const data = (await res.json()) as { data?: Array<{ id: string }> };
|
|
63
|
+
const data = (await res.json()) as { data?: Array<{ id: string; [key: string]: unknown }> };
|
|
64
64
|
if (!data || !Array.isArray(data.data)) {
|
|
65
65
|
return Object.keys(KNOWN_ZEN_FREE_MODELS);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
// 自动识别所有带 free / zero / pickle 标识的零额度消耗免费模型
|
|
68
69
|
const freeIds = data.data
|
|
69
|
-
.map((item) => item.id)
|
|
70
|
-
.filter((id) =>
|
|
70
|
+
.map((item) => String(item.id))
|
|
71
|
+
.filter((id) => {
|
|
72
|
+
const lower = id.toLowerCase();
|
|
73
|
+
return (
|
|
74
|
+
lower.includes("free") ||
|
|
75
|
+
lower.includes("pickle") ||
|
|
76
|
+
lower.includes("zero") ||
|
|
77
|
+
lower.endsWith("-free")
|
|
78
|
+
);
|
|
79
|
+
});
|
|
71
80
|
|
|
72
81
|
return freeIds.length > 0 ? freeIds : Object.keys(KNOWN_ZEN_FREE_MODELS);
|
|
73
82
|
}
|
|
74
83
|
|
|
84
|
+
/**
|
|
85
|
+
* 读取当前系统已持久化配置的 OpenCode Zen 模型清单
|
|
86
|
+
*/
|
|
87
|
+
export function getStoredZenModels(modelsPath = defaultModelsPath()): ZenModelDefinition[] {
|
|
88
|
+
try {
|
|
89
|
+
if (existsSync(modelsPath)) {
|
|
90
|
+
const modelsDoc = JSON.parse(readFileSync(modelsPath, "utf8")) as {
|
|
91
|
+
providers?: Record<string, { models?: ZenModelDefinition[] }>;
|
|
92
|
+
};
|
|
93
|
+
const list = modelsDoc.providers?.[ZEN_PROVIDER_ID]?.models;
|
|
94
|
+
if (Array.isArray(list) && list.length > 0) {
|
|
95
|
+
return list;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
// 忽略读取错误
|
|
100
|
+
}
|
|
101
|
+
return Object.values(KNOWN_ZEN_FREE_MODELS);
|
|
102
|
+
}
|
|
103
|
+
|
|
75
104
|
/**
|
|
76
105
|
* 读取当前系统中已配置的 OpenCode Zen API Key
|
|
77
106
|
*/
|
|
@@ -282,6 +311,7 @@ export async function syncZenConfiguration(options: ZenSyncOptions = {}): Promis
|
|
|
282
311
|
isNewSession,
|
|
283
312
|
modelsCount: resolvedModels.length,
|
|
284
313
|
models: resolvedModels.map((m) => m.id),
|
|
314
|
+
resolvedModels,
|
|
285
315
|
modelsPath,
|
|
286
316
|
ccSwitchUpdated,
|
|
287
317
|
};
|