ck-pi-zen-session 0.1.0 → 0.1.2

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
@@ -8,15 +8,19 @@ OpenCode Zen 免费模型同步、请求头伪装与 Session 会话自动维护
8
8
 
9
9
  - ⚡ **/zen 指令一键激活**:输入一次 API Key (`/zen oc_sk_...`),全自动验证、探测 9 款可用免费模型并注入 Pi 与 CC-Switch。
10
10
  - 🔄 **官方算法深度逆向**:完整对齐 OpenCode 客户端 `Identifier.descending("ses")` 与 `Identifier.ascending("msg")` 算法,生成 30 位降序会话 ID 与升序请求 ID,支持毫秒级时间戳反解。
11
- - 🛡️ **全套官方级伪装请求头**:自动注入全部 6 个官方客户端对齐请求头:
11
+ - 🛡️ **全套官方级伪装请求头**:自动注入全部 7 个官方客户端对齐请求头:
12
12
  - `User-Agent`: `opencode/1.18.32 ai-sdk/provider-utils/4.0.23 runtime/bun/1.3.14`
13
13
  - `x-opencode-client`: `cli`
14
14
  - `x-opencode-session`: `ses_<12位反转时间戳十六进制><14位Base62>`
15
+ - `x-opencode-request`: `msg_<12位升序时间戳十六进制><14位Base62>`(每次 HTTP 请求唯一)
16
+ - `x-opencode-project`: `prj_<16位工作区哈希>`(与官方项目哈希行为对齐)
15
17
  - `x-session-affinity`: 与 session 严格同步
16
18
  - `X-Session-Id`: 与 session 严格同步
17
- - `x-opencode-request`: `msg_<12位升序时间戳十六进制><14位Base62>`(每次 HTTP 请求唯一)
18
- - ⏱️ **45 分钟无感前置平滑轮换**:官方后端限制免费会话时长约为 1 小时(超时直接 403 `FreeTierError`)。本插件在底层请求拦截器(`before_provider_headers`)和 Agent 循环守卫(`before_agent_start`)中设置 45 分钟主动换新阈值,确保永不触碰 1 小时硬限制。
19
- - 🧰 **Agent 工具链兼容性守卫**:OpenCode Zen 免费端点对工具集有硬性要求(无工具直接拒绝)。插件在 `before_provider_request` 钩子中实时监控请求体,若用户处于无工具或纯文本问答模式,自动补齐兼容性工具定义,彻底根除 403 拦截。
19
+ - ⏱️ **30 分钟无感前置平滑轮换**:官方后端限制免费会话时长约为 1 小时(超时直接 403 `FreeTierError`)。本插件在底层请求拦截器(`before_provider_headers`)和 Agent 循环守卫(`before_agent_start`)中设置 30 分钟主动换新阈值,兼顾会话上下文平滑与长期高频调用稳定性,确保永不触碰 1 小时硬限制。
20
+ - 🧰 **官方全套 6 大核心工具链守卫与字母序规约**:OpenCode Zen 免费端点对工具集有硬性要求(无工具直接拒绝)。插件在 `before_provider_request` 钩子中实时监控请求体:
21
+ - 若用户处于纯文本或无工具模式,自动补齐官方完整的 6 大核心工具(`bash`, `edit`, `glob`, `grep`, `read`, `write`)与 `tool_choice: "auto"`,彻底根除 403 拦截;
22
+ - 若请求已有工具,严格按照官方客户端规约(`localeCompare`)按函数名升序重排;
23
+ - 自动对齐流式用量元数据 `stream_options: { include_usage: true }`。
20
24
  - 🌐 **独立供应商隔离命名空间**:注册为独立供应商 `opencode-zen-free`,彻底避开并兼容用户自带的官方登录 `opencode` / `opencode-zen` 套餐,互不干扰、平稳共存。
21
25
  - 🧠 **精准上下文与思维链适配**:严格对齐 9 款免费模型的 `contextWindow`、`maxTokens` 与 `thinkingLevelMap`(涵盖小米 MiMo、英伟达 Nemotron、Meta Muse Spark、Ling、Jev 等)。
22
26
  - 🔌 **全自动多端持久化同步**:自动双写 Pi 本地配置(`~/.pi/agent/models.json` 和 `~/.pi/agent/auth.json`),若检测到 CC-Switch 亦无缝同步其 SQLite 数据库。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ck-pi-zen-session",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
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
@@ -12,6 +12,7 @@
12
12
  import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
13
13
  import {
14
14
  KNOWN_ZEN_FREE_MODELS,
15
+ OPENCODE_OFFICIAL_TOOLS,
15
16
  ZEN_BASE_URL,
16
17
  ZEN_PROVIDER_ID,
17
18
  ZEN_USER_AGENT,
@@ -24,6 +25,7 @@ import {
24
25
  } from "./sync.js";
25
26
  import {
26
27
  DEFAULT_SESSION_MAX_AGE_MS,
28
+ generateZenProjectId,
27
29
  generateZenRequestId,
28
30
  generateZenSessionId,
29
31
  isZenSessionExpired,
@@ -45,6 +47,7 @@ export function registerZenProviderToPi(
45
47
  "User-Agent": ZEN_USER_AGENT,
46
48
  "x-opencode-client": "cli",
47
49
  "x-opencode-session": sessionId,
50
+ "x-opencode-project": generateZenProjectId(),
48
51
  "x-session-affinity": sessionId,
49
52
  "X-Session-Id": sessionId,
50
53
  },
@@ -53,6 +56,7 @@ export function registerZenProviderToPi(
53
56
  requiresReasoningContentOnAssistantMessages: true,
54
57
  supportsDeveloperRole: false,
55
58
  supportsStore: false,
59
+ supportsUsageInStreaming: true,
56
60
  },
57
61
  models: Object.values(KNOWN_ZEN_FREE_MODELS),
58
62
  });
@@ -69,7 +73,7 @@ export default function piZenSession(pi: ExtensionAPI): void {
69
73
  registerZenProviderToPi(pi, existingKey);
70
74
  }
71
75
 
72
- // 2. 核心请求头拦截钩子:在每一次请求发往 Provider 前注入完整伪装请求头
76
+ // 2. 核心请求头拦截钩子:在每一次请求发往 Provider 前注入完整官方客户端对齐请求头
73
77
  pi.on("before_provider_headers", (event, ctx) => {
74
78
  const model = ctx.model;
75
79
  const isZenModel =
@@ -78,7 +82,7 @@ export default function piZenSession(pi: ExtensionAPI): void {
78
82
  model?.id === "big-pickle";
79
83
 
80
84
  if (isZenModel) {
81
- // 检查 x-opencode-session 是否过期 (>45分钟) 或缺失
85
+ // 检查 x-opencode-session 是否过期 (>30分钟) 或缺失
82
86
  let currentSession = event.headers["x-opencode-session"];
83
87
  if (
84
88
  typeof currentSession !== "string" ||
@@ -91,17 +95,18 @@ export default function piZenSession(pi: ExtensionAPI): void {
91
95
  syncZenConfiguration({ forceSession: true }).catch(() => {});
92
96
  }
93
97
 
94
- // 注入全部官方客户端对齐请求头
98
+ // 注入全部官方客户端 7 维对齐请求头
95
99
  event.headers["User-Agent"] = ZEN_USER_AGENT;
96
100
  event.headers["x-opencode-client"] = "cli";
97
101
  event.headers["x-opencode-session"] = currentSession;
102
+ event.headers["x-opencode-project"] = generateZenProjectId();
98
103
  event.headers["x-session-affinity"] = currentSession;
99
104
  event.headers["X-Session-Id"] = currentSession;
100
105
  event.headers["x-opencode-request"] = generateZenRequestId();
101
106
  }
102
107
  });
103
108
 
104
- // 3. 核心请求体守卫钩子:OpenCode Zen 免费端点对工具集有硬性要求,若缺失工具会报 403 FreeTierError
109
+ // 3. 核心请求体守卫钩子:深度对齐 OpenCode 官方请求体结构与工具集
105
110
  pi.on("before_provider_request", (event, ctx) => {
106
111
  const model = ctx.model;
107
112
  const isZenModel =
@@ -112,41 +117,31 @@ export default function piZenSession(pi: ExtensionAPI): void {
112
117
  if (isZenModel && event.payload && typeof event.payload === "object") {
113
118
  const payload = event.payload as Record<string, unknown>;
114
119
  const tools = payload.tools;
115
- // 若当前请求没有附带任何工具定义(如纯文本问答模式),自动补齐兼容性工具定义以规避 403 拦截
120
+ let modified = false;
121
+ const transformed = { ...payload };
122
+
123
+ // A. 工具规范对齐:无工具时补齐官方 6 大核心工具;已有工具时按字母升序严格重排 (localeCompare)
116
124
  if (!Array.isArray(tools) || tools.length === 0) {
117
- return {
118
- ...payload,
119
- tools: [
120
- {
121
- type: "function",
122
- function: {
123
- name: "bash",
124
- description: "Execute bash command in the terminal",
125
- parameters: {
126
- type: "object",
127
- properties: {
128
- command: { type: "string", description: "Command to execute" },
129
- },
130
- required: ["command"],
131
- },
132
- },
133
- },
134
- {
135
- type: "function",
136
- function: {
137
- name: "read",
138
- description: "Read contents of a file",
139
- parameters: {
140
- type: "object",
141
- properties: {
142
- path: { type: "string", description: "Path to file" },
143
- },
144
- required: ["path"],
145
- },
146
- },
147
- },
148
- ],
149
- };
125
+ transformed.tools = OPENCODE_OFFICIAL_TOOLS;
126
+ transformed.tool_choice = "auto";
127
+ modified = true;
128
+ } else {
129
+ transformed.tools = [...tools].sort((a: any, b: any) => {
130
+ const nameA = a.function?.name || a.name || "";
131
+ const nameB = b.function?.name || b.name || "";
132
+ return nameA.localeCompare(nameB);
133
+ });
134
+ modified = true;
135
+ }
136
+
137
+ // B. 流式元数据对齐:补充 stream_options: { include_usage: true }
138
+ if (transformed.stream === true && !transformed.stream_options) {
139
+ transformed.stream_options = { include_usage: true };
140
+ modified = true;
141
+ }
142
+
143
+ if (modified) {
144
+ return transformed;
150
145
  }
151
146
  }
152
147
  });
@@ -155,7 +150,7 @@ export default function piZenSession(pi: ExtensionAPI): void {
155
150
  pi.on("before_agent_start", async () => {
156
151
  try {
157
152
  const currentSession = getStoredZenSessionId();
158
- // 若当前会话不存在或已使用超过 45 分钟,后台自动续期
153
+ // 若当前会话不存在或已使用超过 30 分钟,后台自动续期
159
154
  if (!currentSession || isZenSessionExpired(currentSession, DEFAULT_SESSION_MAX_AGE_MS)) {
160
155
  const apiKey = getStoredZenApiKey();
161
156
  if (apiKey) {
@@ -168,3 +168,143 @@ export function resolveModelDefinitions(discoveredIds: string[]): ZenModelDefini
168
168
 
169
169
  return list;
170
170
  }
171
+
172
+ /**
173
+ * OpenCode 官方 6 大核心内置工具规范与 Schema 定义
174
+ * 严格对齐 OpenCode 客户端源码(按字母顺序排序:bash, edit, glob, grep, read, write)
175
+ */
176
+ export const OPENCODE_OFFICIAL_TOOLS = [
177
+ {
178
+ type: "function",
179
+ function: {
180
+ name: "bash",
181
+ description: "Execute bash commands in the workspace environment",
182
+ parameters: {
183
+ type: "object",
184
+ properties: {
185
+ command: { type: "string", description: "Shell command string to execute" },
186
+ workdir: {
187
+ type: "string",
188
+ description:
189
+ "Working directory. Defaults to the active Location; relative paths resolve within it.",
190
+ },
191
+ },
192
+ required: ["command"],
193
+ },
194
+ },
195
+ },
196
+ {
197
+ type: "function",
198
+ function: {
199
+ name: "edit",
200
+ description: "Edit a file by replacing text",
201
+ parameters: {
202
+ type: "object",
203
+ properties: {
204
+ path: {
205
+ type: "string",
206
+ description:
207
+ "File path to edit. Relative paths resolve within the active Location.",
208
+ },
209
+ oldString: { type: "string", description: "The string in the file to be replaced" },
210
+ newString: { type: "string", description: "The string to replace oldString with" },
211
+ replaceAll: {
212
+ type: "boolean",
213
+ description: "Replace all occurrences of oldString (default false)",
214
+ },
215
+ },
216
+ required: ["path", "oldString", "newString"],
217
+ },
218
+ },
219
+ },
220
+ {
221
+ type: "function",
222
+ function: {
223
+ name: "glob",
224
+ description: "Find files matching a glob pattern",
225
+ parameters: {
226
+ type: "object",
227
+ properties: {
228
+ pattern: {
229
+ type: "string",
230
+ description: 'File pattern to include in the search (e.g. "*.js", "*.{ts,tsx}")',
231
+ },
232
+ path: {
233
+ type: "string",
234
+ description: "Relative directory to search in. Defaults to the active Location.",
235
+ },
236
+ },
237
+ required: ["pattern"],
238
+ },
239
+ },
240
+ },
241
+ {
242
+ type: "function",
243
+ function: {
244
+ name: "grep",
245
+ description: "Search file contents using regular expressions",
246
+ parameters: {
247
+ type: "object",
248
+ properties: {
249
+ pattern: {
250
+ type: "string",
251
+ description: "Regex pattern to search for in file contents",
252
+ },
253
+ path: {
254
+ type: "string",
255
+ description: "Relative directory to search in. Defaults to the active Location.",
256
+ },
257
+ },
258
+ required: ["pattern"],
259
+ },
260
+ },
261
+ },
262
+ {
263
+ type: "function",
264
+ function: {
265
+ name: "read",
266
+ description: "Read file contents",
267
+ parameters: {
268
+ type: "object",
269
+ properties: {
270
+ path: {
271
+ type: "string",
272
+ description:
273
+ "File path to read. Relative paths resolve within the active Location.",
274
+ },
275
+ offset: {
276
+ type: "number",
277
+ description: "The 1-based directory entry or text line offset",
278
+ },
279
+ limit: {
280
+ type: "number",
281
+ description: "The maximum number of lines to read (defaults to 2000)",
282
+ },
283
+ },
284
+ required: ["path"],
285
+ },
286
+ },
287
+ },
288
+ {
289
+ type: "function",
290
+ function: {
291
+ name: "write",
292
+ description: "Write or overwrite file contents",
293
+ parameters: {
294
+ type: "object",
295
+ properties: {
296
+ path: {
297
+ type: "string",
298
+ description:
299
+ "File path to write. Relative paths resolve within the active Location.",
300
+ },
301
+ content: {
302
+ type: "string",
303
+ description: "Content to write to the file",
304
+ },
305
+ },
306
+ required: ["path", "content"],
307
+ },
308
+ },
309
+ },
310
+ ];
package/src/session.ts CHANGED
@@ -10,13 +10,22 @@
10
10
  * 3. 降序特性(descending):时间越晚,按字典序排序越靠前。
11
11
  */
12
12
 
13
- import { webcrypto } from "node:crypto";
13
+ import { createHash, webcrypto } from "node:crypto";
14
14
 
15
15
  const B62_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
16
16
 
17
17
  let lastTimestamp = 0;
18
18
  let sequenceCounter = 0;
19
19
 
20
+ /**
21
+ * 生成符合 OpenCode 官方规约的项目标识符 (前缀 prj_)
22
+ * 基于工作目录哈希,保持确定性与隔离性
23
+ */
24
+ export function generateZenProjectId(cwd: string = process.cwd()): string {
25
+ const hash = createHash("sha256").update(cwd).digest("hex").slice(0, 16);
26
+ return `prj_${hash}`;
27
+ }
28
+
20
29
  /**
21
30
  * 生成符合 OpenCode 官方规约的 Session ID (前缀 ses_)
22
31
  * @param timestampMs 毫秒时间戳,默认为 Date.now()
@@ -139,14 +148,14 @@ export function extractZenSessionTimestamp(
139
148
 
140
149
  /**
141
150
  * 官方 OpenCode Zen 后端对会话生命周期严格限制(超 1 小时即报 403 FreeTierError)
142
- * 默认设定为 45 分钟,确保在达到 1 小时硬限制前无感平滑轮换
151
+ * 设定为 30 分钟无感前置轮换,兼顾会话上下文平滑与长期高频调用稳定性
143
152
  */
144
- export const DEFAULT_SESSION_MAX_AGE_MS = 45 * 60 * 1000;
153
+ export const DEFAULT_SESSION_MAX_AGE_MS = 30 * 60 * 1000;
145
154
 
146
155
  /**
147
156
  * 判断指定 Session ID 是否已过期
148
157
  * @param sessionId 会话 ID
149
- * @param maxAgeMs 最大允许有效时长(默认 45 分钟)
158
+ * @param maxAgeMs 最大允许有效时长(默认 30 分钟)
150
159
  * @param nowMs 当前时间戳
151
160
  */
152
161
  export function isZenSessionExpired(
package/src/sync.ts CHANGED
@@ -24,6 +24,7 @@ import {
24
24
  import {
25
25
  DEFAULT_SESSION_MAX_AGE_MS,
26
26
  extractZenSessionTimestamp,
27
+ generateZenProjectId,
27
28
  generateZenSessionId,
28
29
  isZenSessionExpired,
29
30
  isValidZenSessionId,
@@ -225,6 +226,7 @@ export async function syncZenConfiguration(options: ZenSyncOptions = {}): Promis
225
226
  "User-Agent": ZEN_USER_AGENT,
226
227
  "x-opencode-client": "cli",
227
228
  "x-opencode-session": targetSession,
229
+ "x-opencode-project": generateZenProjectId(),
228
230
  "x-session-affinity": targetSession,
229
231
  "X-Session-Id": targetSession,
230
232
  },
@@ -233,6 +235,7 @@ export async function syncZenConfiguration(options: ZenSyncOptions = {}): Promis
233
235
  requiresReasoningContentOnAssistantMessages: true,
234
236
  supportsDeveloperRole: false,
235
237
  supportsStore: false,
238
+ supportsUsageInStreaming: true,
236
239
  },
237
240
  models: resolvedModels,
238
241
  };
@@ -306,6 +309,7 @@ export function updateCcSwitchDb(
306
309
  headers["User-Agent"] = ZEN_USER_AGENT;
307
310
  headers["x-opencode-client"] = "cli";
308
311
  headers["x-opencode-session"] = sessionId;
312
+ headers["x-opencode-project"] = generateZenProjectId();
309
313
  headers["x-session-affinity"] = sessionId;
310
314
  headers["X-Session-Id"] = sessionId;
311
315
  cfg.headers = headers;
@@ -317,6 +321,7 @@ export function updateCcSwitchDb(
317
321
  headers["User-Agent"] = ZEN_USER_AGENT;
318
322
  headers["x-opencode-client"] = "cli";
319
323
  headers["x-opencode-session"] = sessionId;
324
+ headers["x-opencode-project"] = generateZenProjectId();
320
325
  headers["x-session-affinity"] = sessionId;
321
326
  headers["X-Session-Id"] = sessionId;
322
327
  opts.headers = headers;