ck-pi-zen-session 0.1.2 → 0.1.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 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`
@@ -17,12 +17,15 @@ OpenCode Zen 免费模型同步、请求头伪装与 Session 会话自动维护
17
17
  - `x-session-affinity`: 与 session 严格同步
18
18
  - `X-Session-Id`: 与 session 严格同步
19
19
  - ⏱️ **30 分钟无感前置平滑轮换**:官方后端限制免费会话时长约为 1 小时(超时直接 403 `FreeTierError`)。本插件在底层请求拦截器(`before_provider_headers`)和 Agent 循环守卫(`before_agent_start`)中设置 30 分钟主动换新阈值,兼顾会话上下文平滑与长期高频调用稳定性,确保永不触碰 1 小时硬限制。
20
+ - 🚑 **网关异常自愈守卫 (`after_provider_response`)**:新增底层响应拦截,一旦检测到远端网关返回 401 或 403 `FreeTierError`,立即当场触发 Session 换新与后台持久化,并提示用户,彻底终结会话失效死锁。
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`)即可让物理连接从新节点发起,安全解除限速与额度。
20
23
  - 🧰 **官方全套 6 大核心工具链守卫与字母序规约**:OpenCode Zen 免费端点对工具集有硬性要求(无工具直接拒绝)。插件在 `before_provider_request` 钩子中实时监控请求体:
21
24
  - 若用户处于纯文本或无工具模式,自动补齐官方完整的 6 大核心工具(`bash`, `edit`, `glob`, `grep`, `read`, `write`)与 `tool_choice: "auto"`,彻底根除 403 拦截;
22
25
  - 若请求已有工具,严格按照官方客户端规约(`localeCompare`)按函数名升序重排;
23
26
  - 自动对齐流式用量元数据 `stream_options: { include_usage: true }`。
24
27
  - 🌐 **独立供应商隔离命名空间**:注册为独立供应商 `opencode-zen-free`,彻底避开并兼容用户自带的官方登录 `opencode` / `opencode-zen` 套餐,互不干扰、平稳共存。
25
- - 🧠 **精准上下文与思维链适配**:严格对齐 9 款免费模型的 `contextWindow`、`maxTokens` 与 `thinkingLevelMap`(涵盖小米 MiMo、英伟达 Nemotron、Meta Muse Spark、Ling、Jev 等)。
28
+ - 🧠 **精准上下文与思维链适配**:严格对齐免费模型的 `contextWindow`、`maxTokens` 与 `thinkingLevelMap`(涵盖小米 MiMo、英伟达 Nemotron、Meta Muse Spark、Ling、Jev 等)。
26
29
  - 🔌 **全自动多端持久化同步**:自动双写 Pi 本地配置(`~/.pi/agent/models.json` 和 `~/.pi/agent/auth.json`),若检测到 CC-Switch 亦无缝同步其 SQLite 数据库。
27
30
 
28
31
  ---
@@ -50,7 +53,7 @@ pi install git:github.com/1837620622/ck-pi-extension
50
53
  | 指令 | 说明 |
51
54
  | --- | --- |
52
55
  | `/zen <oc_sk_xxx>` | 设置/更新 API Key,自动在线验证并全量同步模型与全套请求头 |
53
- | `/zen` | 智能检查当前会话状态;若会话临近过期自动换新;若未配置 Key 则弹出交互输入框 |
56
+ | `/zen` | 立即一键强制换新 Session 并全面刷新所有请求头;若未配置 Key 则弹出交互输入框 |
54
57
  | `/zen refresh` | 强制生成全新的合法降序 Session ID 并更新所有请求头与落盘配置 |
55
58
  | `/zen status` | 查看当前 API Key 掩码、活跃会话精确存活时间及模型就绪状态 |
56
59
  | `/zen list`(或 `/zen models`) | 查看当前已激活的所有免费模型列表 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ck-pi-zen-session",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
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,7 +61,7 @@ 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
  // 忽略重复注册
@@ -146,7 +149,27 @@ export default function piZenSession(pi: ExtensionAPI): void {
146
149
  }
147
150
  });
148
151
 
149
- // 4. 常驻生命周期守卫:在每次 Agent 循环执行前,自动检测 Session 有效性并无感续期
152
+ // 4. 网关响应守卫:遇 401/403 会话受限或过期时,立即自愈换新 Session ID
153
+ pi.on("after_provider_response", (event, ctx) => {
154
+ const model = ctx.model;
155
+ const isZenModel =
156
+ model?.provider === ZEN_PROVIDER_ID ||
157
+ model?.id?.includes("-free") ||
158
+ model?.id === "big-pickle";
159
+
160
+ if (isZenModel && (event.status === 401 || event.status === 403)) {
161
+ // 自动异步刷新 Session
162
+ syncZenConfiguration({ forceSession: true }).catch(() => {});
163
+ if (ctx.hasUI) {
164
+ ctx.ui.notify(
165
+ `检测到 OpenCode Zen 会话受限 (HTTP ${event.status}),已自动轮换新 Session ID。`,
166
+ "warning",
167
+ );
168
+ }
169
+ }
170
+ });
171
+
172
+ // 5. 常驻生命周期守卫:在每次 Agent 循环执行前,自动检测 Session 有效性并无感续期
150
173
  pi.on("before_agent_start", async () => {
151
174
  try {
152
175
  const currentSession = getStoredZenSessionId();
@@ -162,7 +185,7 @@ export default function piZenSession(pi: ExtensionAPI): void {
162
185
  }
163
186
  });
164
187
 
165
- // 4. 注册 /zen 指令
188
+ // 6. 注册 /zen 指令
166
189
  pi.registerCommand("zen", {
167
190
  description: "OpenCode Zen 免费模型与 Session 会话自动维护 (/zen [key|refresh|status|list])",
168
191
  getArgumentCompletions: (prefix: string) => {
@@ -238,12 +261,13 @@ export async function handleZenCommand(
238
261
  notify(ctx, "正在生成全新 OpenCode Zen 会话 ID 并同步...", "info");
239
262
  const result = await syncZenConfiguration({ forceSession: true });
240
263
  if (pi) {
241
- registerZenProviderToPi(pi, result.apiKey, result.sessionId);
264
+ registerZenProviderToPi(pi, result.apiKey, result.sessionId, result.resolvedModels);
242
265
  }
243
266
  const successMsg = [
244
267
  "✓ OpenCode Zen Session 已成功刷新!",
245
268
  `• 全新 Session: ${result.sessionId}`,
246
- `• 请求头伪装: User-Agent, x-opencode-session 已注入`,
269
+ `• 免费模型: 已对齐 ${result.modelsCount} 款 0 额度消耗模型`,
270
+ `• 请求头伪装: 7 维官方客户端签名已注入`,
247
271
  `• 同步状态: models.json ✓ | CC-Switch ${result.ccSwitchUpdated ? "✓" : "-(未安装或无此条目)"}`,
248
272
  ].join("\n");
249
273
  notify(ctx, successMsg, "info");
@@ -256,16 +280,17 @@ export async function handleZenCommand(
256
280
  // 4. 输入了具体的 API Key: /zen oc_sk_... 或其它 key
257
281
  if (rawArg.length > 0 && !["status", "refresh", "list", "models"].includes(command)) {
258
282
  try {
259
- notify(ctx, "正在验证 OpenCode Zen API Key 并探测免费模型...", "info");
283
+ notify(ctx, "正在验证 OpenCode Zen API Key 并探测免费与零额度模型...", "info");
260
284
  const result = await syncZenConfiguration({ apiKey: rawArg, forceSession: true });
261
285
  if (pi) {
262
- registerZenProviderToPi(pi, result.apiKey, result.sessionId);
286
+ registerZenProviderToPi(pi, result.apiKey, result.sessionId, result.resolvedModels);
263
287
  }
264
288
  const msg = [
265
- "🎉 OpenCode Zen 配置成功!免费套餐与请求头已全部就绪:",
289
+ "🎉 OpenCode Zen 配置成功!免费套餐与零额度模型已自动识别并全量对接:",
266
290
  `• API Key: ${maskKey(result.apiKey)} (已验证并保存)`,
267
291
  `• 会话 Session: ${result.sessionId} (官方降序时间戳逆向算法)`,
268
- `• 发现免费模型: ${result.modelsCount} 个已自动配置 (含思维链 thinkingLevelMap 适配)`,
292
+ `• 自动对接免费模型 (${result.modelsCount} 款,全部标记 0 额度消耗):`,
293
+ ...result.models.map((id) => ` • ${id}`),
269
294
  `• 同步路径: ${result.modelsPath}`,
270
295
  result.ccSwitchUpdated ? "• CC-Switch: 本地数据库已同步更新" : "",
271
296
  ].filter(Boolean).join("\n");
@@ -279,23 +304,24 @@ export async function handleZenCommand(
279
304
  // 5. 无参调用: /zen
280
305
  const status = getZenStatusInfo();
281
306
  if (status.hasKey) {
282
- // 已有 Key 时,自动检测并刷新 Session
307
+ // 已有 Key 时,自动强制刷新 Session 并重新注册热载
283
308
  try {
284
- notify(ctx, "正在检测并刷新 OpenCode Zen 会话...", "info");
285
- const result = await syncZenConfiguration({ forceSession: status.sessionExpired });
309
+ notify(ctx, "正在刷新 OpenCode Zen 会话与请求头...", "info");
310
+ const result = await syncZenConfiguration({ forceSession: true });
286
311
  if (pi) {
287
- registerZenProviderToPi(pi, result.apiKey, result.sessionId);
312
+ registerZenProviderToPi(pi, result.apiKey, result.sessionId, result.resolvedModels);
288
313
  }
289
314
  const msg = [
290
- "✨ [OpenCode Zen 已连接就绪]",
315
+ "✨ [OpenCode Zen 已自动刷新并就绪]",
291
316
  `• API Key: ${maskKey(result.apiKey)}`,
292
- `• 活跃 Session: ${result.sessionId} (${result.isNewSession ? "已换新" : "仍有效"})`,
293
- `• 免费模型: ${result.modelsCount} 个可用`,
317
+ `• 全新 Session: ${result.sessionId} (有效且已持久化)`,
318
+ `• 自动对接免费模型: 已同步 ${result.modelsCount} 个 0 额度消耗模型`,
319
+ `• 请求头保护: 30分钟自动轮换 + 7维官方签名 + 6大核心工具全注入`,
294
320
  "",
295
321
  "常用操作:",
296
322
  " /zen <key> - 更换 API Key",
297
- " /zen refresh - 换新会话 ID",
298
323
  " /zen status - 详情状态",
324
+ " /zen list - 查看所有可用免费模型",
299
325
  ].join("\n");
300
326
  notify(ctx, msg, "info");
301
327
  } catch (error) {
@@ -312,7 +338,7 @@ export async function handleZenCommand(
312
338
  "oc_sk_",
313
339
  );
314
340
  if (enteredKey && enteredKey.trim()) {
315
- await handleZenCommand(enteredKey.trim(), ctx);
341
+ await handleZenCommand(enteredKey.trim(), ctx, pi);
316
342
  return;
317
343
  }
318
344
  notify(ctx, "已取消输入。如需配置请使用 /zen <oc_sk_xxx>", "info");
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,22 +60,60 @@ 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
  */
78
107
  export function getStoredZenApiKey(authPath = defaultAuthPath(), modelsPath = defaultModelsPath()): string | null {
108
+ // 优先读取环境变量,支持免配置直接启动
109
+ const envKey =
110
+ process.env.OPENCODE_API_KEY ||
111
+ process.env.OPENCODE_ZEN_API_KEY ||
112
+ process.env.ZEN_API_KEY;
113
+ if (envKey && envKey.trim()) {
114
+ return envKey.trim();
115
+ }
116
+
79
117
  try {
80
118
  if (existsSync(authPath)) {
81
119
  const auth = JSON.parse(readFileSync(authPath, "utf8")) as Record<string, { key?: string }>;
@@ -273,6 +311,7 @@ export async function syncZenConfiguration(options: ZenSyncOptions = {}): Promis
273
311
  isNewSession,
274
312
  modelsCount: resolvedModels.length,
275
313
  models: resolvedModels.map((m) => m.id),
314
+ resolvedModels,
276
315
  modelsPath,
277
316
  ccSwitchUpdated,
278
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
  }