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 CHANGED
@@ -6,7 +6,7 @@ OpenCode Zen 免费模型同步、请求头伪装与 Session 会话自动维护
6
6
 
7
7
  ## 🌟 核心特性
8
8
 
9
- - ⚡ **/zen 指令一键激活**:输入一次 API Key (`/zen oc_sk_...`),全自动验证、探测 9 款可用免费模型并注入 Pi 与 CC-Switch。
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
- - 🌐 **独立供应商隔离命名空间**:注册为独立供应商 `opencode-zen-free`,彻底避开并兼容用户自带的官方登录 `opencode` / `opencode-zen` 套餐,互不干扰、平稳共存。
27
- - 🧠 **精准上下文与思维链适配**:严格对齐 9 款免费模型的 `contextWindow`、`maxTokens` 与 `thinkingLevelMap`(涵盖小米 MiMo、英伟达 Nemotron、Meta Muse Spark、Ling、Jev 等)。
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ck-pi-zen-session",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "OpenCode Zen session auto-refresh and free-tier model catalog synchronization for the Pi coding agent.",
5
5
  "keywords": [
6
6
  "pi-package",
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
- const model = ctx.model;
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
- const model = ctx.model;
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
- const model = ctx.model;
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
- `• 请求头伪装: User-Agent, x-opencode-session 已注入`,
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 并探测免费模型...", "info");
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
- `• 发现免费模型: ${result.modelsCount} 个已自动配置 (含思维链 thinkingLevelMap 适配)`,
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
- `• 免费模型: 已同步 ${result.modelsCount} 个可用免费模型`,
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) => id.includes("free") || id === "big-pickle" || id.includes("pickle"));
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
  };
package/src/types.ts CHANGED
@@ -52,6 +52,7 @@ export interface ZenSyncResult {
52
52
  isNewSession: boolean;
53
53
  modelsCount: number;
54
54
  models: string[];
55
+ resolvedModels: ZenModelDefinition[];
55
56
  modelsPath: string;
56
57
  ccSwitchUpdated: boolean;
57
58
  }