ck-pi-zen-session 0.1.3 → 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`
@@ -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
  - 🌐 **独立供应商隔离命名空间**:注册为独立供应商 `opencode-zen-free`,彻底避开并兼容用户自带的官方登录 `opencode` / `opencode-zen` 套餐,互不干扰、平稳共存。
27
- - 🧠 **精准上下文与思维链适配**:严格对齐 9 款免费模型的 `contextWindow`、`maxTokens` 与 `thinkingLevelMap`(涵盖小米 MiMo、英伟达 Nemotron、Meta Muse Spark、Ling、Jev 等)。
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.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
  // 忽略重复注册
@@ -258,12 +261,13 @@ export async function handleZenCommand(
258
261
  notify(ctx, "正在生成全新 OpenCode Zen 会话 ID 并同步...", "info");
259
262
  const result = await syncZenConfiguration({ forceSession: true });
260
263
  if (pi) {
261
- registerZenProviderToPi(pi, result.apiKey, result.sessionId);
264
+ registerZenProviderToPi(pi, result.apiKey, result.sessionId, result.resolvedModels);
262
265
  }
263
266
  const successMsg = [
264
267
  "✓ OpenCode Zen Session 已成功刷新!",
265
268
  `• 全新 Session: ${result.sessionId}`,
266
- `• 请求头伪装: User-Agent, x-opencode-session 已注入`,
269
+ `• 免费模型: 已对齐 ${result.modelsCount} 款 0 额度消耗模型`,
270
+ `• 请求头伪装: 7 维官方客户端签名已注入`,
267
271
  `• 同步状态: models.json ✓ | CC-Switch ${result.ccSwitchUpdated ? "✓" : "-(未安装或无此条目)"}`,
268
272
  ].join("\n");
269
273
  notify(ctx, successMsg, "info");
@@ -276,16 +280,17 @@ export async function handleZenCommand(
276
280
  // 4. 输入了具体的 API Key: /zen oc_sk_... 或其它 key
277
281
  if (rawArg.length > 0 && !["status", "refresh", "list", "models"].includes(command)) {
278
282
  try {
279
- notify(ctx, "正在验证 OpenCode Zen API Key 并探测免费模型...", "info");
283
+ notify(ctx, "正在验证 OpenCode Zen API Key 并探测免费与零额度模型...", "info");
280
284
  const result = await syncZenConfiguration({ apiKey: rawArg, forceSession: true });
281
285
  if (pi) {
282
- registerZenProviderToPi(pi, result.apiKey, result.sessionId);
286
+ registerZenProviderToPi(pi, result.apiKey, result.sessionId, result.resolvedModels);
283
287
  }
284
288
  const msg = [
285
- "🎉 OpenCode Zen 配置成功!免费套餐与请求头已全部就绪:",
289
+ "🎉 OpenCode Zen 配置成功!免费套餐与零额度模型已自动识别并全量对接:",
286
290
  `• API Key: ${maskKey(result.apiKey)} (已验证并保存)`,
287
291
  `• 会话 Session: ${result.sessionId} (官方降序时间戳逆向算法)`,
288
- `• 发现免费模型: ${result.modelsCount} 个已自动配置 (含思维链 thinkingLevelMap 适配)`,
292
+ `• 自动对接免费模型 (${result.modelsCount} 款,全部标记 0 额度消耗):`,
293
+ ...result.models.map((id) => ` • ${id}`),
289
294
  `• 同步路径: ${result.modelsPath}`,
290
295
  result.ccSwitchUpdated ? "• CC-Switch: 本地数据库已同步更新" : "",
291
296
  ].filter(Boolean).join("\n");
@@ -304,13 +309,13 @@ export async function handleZenCommand(
304
309
  notify(ctx, "正在刷新 OpenCode Zen 会话与请求头...", "info");
305
310
  const result = await syncZenConfiguration({ forceSession: true });
306
311
  if (pi) {
307
- registerZenProviderToPi(pi, result.apiKey, result.sessionId);
312
+ registerZenProviderToPi(pi, result.apiKey, result.sessionId, result.resolvedModels);
308
313
  }
309
314
  const msg = [
310
315
  "✨ [OpenCode Zen 已自动刷新并就绪]",
311
316
  `• API Key: ${maskKey(result.apiKey)}`,
312
317
  `• 全新 Session: ${result.sessionId} (有效且已持久化)`,
313
- `• 免费模型: 已同步 ${result.modelsCount} 个可用免费模型`,
318
+ `• 自动对接免费模型: 已同步 ${result.modelsCount} 个 0 额度消耗模型`,
314
319
  `• 请求头保护: 30分钟自动轮换 + 7维官方签名 + 6大核心工具全注入`,
315
320
  "",
316
321
  "常用操作:",
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
  }