ck-pi-zen-session 0.1.1 → 0.1.3
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 +10 -4
- package/package.json +1 -1
- package/src/index.ts +63 -47
- package/src/models-registry.ts +140 -0
- package/src/session.ts +10 -1
- package/src/sync.ts +14 -0
package/README.md
CHANGED
|
@@ -8,15 +8,21 @@ 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
|
-
- 🛡️ **全套官方级伪装请求头**:自动注入全部
|
|
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
19
|
- ⏱️ **30 分钟无感前置平滑轮换**:官方后端限制免费会话时长约为 1 小时(超时直接 403 `FreeTierError`)。本插件在底层请求拦截器(`before_provider_headers`)和 Agent 循环守卫(`before_agent_start`)中设置 30 分钟主动换新阈值,兼顾会话上下文平滑与长期高频调用稳定性,确保永不触碰 1 小时硬限制。
|
|
19
|
-
-
|
|
20
|
+
- 🚑 **网关异常自愈守卫 (`after_provider_response`)**:新增底层响应拦截,一旦检测到远端网关返回 401 或 403 `FreeTierError`,立即当场触发 Session 换新与后台持久化,并提示用户,彻底终结会话失效死锁。
|
|
21
|
+
- 🔑 **多源凭据自适应感知**:支持优先读取 `OPENCODE_API_KEY`、`OPENCODE_ZEN_API_KEY` 环境变量,亦可直接使用 `/zen <key>` 配置,无缝兼容各类容器与终端环境。
|
|
22
|
+
- 🧰 **官方全套 6 大核心工具链守卫与字母序规约**:OpenCode Zen 免费端点对工具集有硬性要求(无工具直接拒绝)。插件在 `before_provider_request` 钩子中实时监控请求体:
|
|
23
|
+
- 若用户处于纯文本或无工具模式,自动补齐官方完整的 6 大核心工具(`bash`, `edit`, `glob`, `grep`, `read`, `write`)与 `tool_choice: "auto"`,彻底根除 403 拦截;
|
|
24
|
+
- 若请求已有工具,严格按照官方客户端规约(`localeCompare`)按函数名升序重排;
|
|
25
|
+
- 自动对齐流式用量元数据 `stream_options: { include_usage: true }`。
|
|
20
26
|
- 🌐 **独立供应商隔离命名空间**:注册为独立供应商 `opencode-zen-free`,彻底避开并兼容用户自带的官方登录 `opencode` / `opencode-zen` 套餐,互不干扰、平稳共存。
|
|
21
27
|
- 🧠 **精准上下文与思维链适配**:严格对齐 9 款免费模型的 `contextWindow`、`maxTokens` 与 `thinkingLevelMap`(涵盖小米 MiMo、英伟达 Nemotron、Meta Muse Spark、Ling、Jev 等)。
|
|
22
28
|
- 🔌 **全自动多端持久化同步**:自动双写 Pi 本地配置(`~/.pi/agent/models.json` 和 `~/.pi/agent/auth.json`),若检测到 CC-Switch 亦无缝同步其 SQLite 数据库。
|
|
@@ -46,7 +52,7 @@ pi install git:github.com/1837620622/ck-pi-extension
|
|
|
46
52
|
| 指令 | 说明 |
|
|
47
53
|
| --- | --- |
|
|
48
54
|
| `/zen <oc_sk_xxx>` | 设置/更新 API Key,自动在线验证并全量同步模型与全套请求头 |
|
|
49
|
-
| `/zen` |
|
|
55
|
+
| `/zen` | 立即一键强制换新 Session 并全面刷新所有请求头;若未配置 Key 则弹出交互输入框 |
|
|
50
56
|
| `/zen refresh` | 强制生成全新的合法降序 Session ID 并更新所有请求头与落盘配置 |
|
|
51
57
|
| `/zen status` | 查看当前 API Key 掩码、活跃会话精确存活时间及模型就绪状态 |
|
|
52
58
|
| `/zen list`(或 `/zen models`) | 查看当前已激活的所有免费模型列表 |
|
package/package.json
CHANGED
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 =
|
|
@@ -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.
|
|
109
|
+
// 3. 核心请求体守卫钩子:深度对齐 OpenCode 官方请求体结构与工具集
|
|
105
110
|
pi.on("before_provider_request", (event, ctx) => {
|
|
106
111
|
const model = ctx.model;
|
|
107
112
|
const isZenModel =
|
|
@@ -112,46 +117,56 @@ 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
|
-
|
|
120
|
+
let modified = false;
|
|
121
|
+
const transformed = { ...payload };
|
|
122
|
+
|
|
123
|
+
// A. 工具规范对齐:无工具时补齐官方 6 大核心工具;已有工具时按字母升序严格重排 (localeCompare)
|
|
116
124
|
if (!Array.isArray(tools) || tools.length === 0) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// 4. 网关响应守卫:遇 401/403 会话受限或过期时,立即自愈换新 Session ID
|
|
150
|
+
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)) {
|
|
158
|
+
// 自动异步刷新 Session
|
|
159
|
+
syncZenConfiguration({ forceSession: true }).catch(() => {});
|
|
160
|
+
if (ctx.hasUI) {
|
|
161
|
+
ctx.ui.notify(
|
|
162
|
+
`检测到 OpenCode Zen 会话受限 (HTTP ${event.status}),已自动轮换新 Session ID。`,
|
|
163
|
+
"warning",
|
|
164
|
+
);
|
|
150
165
|
}
|
|
151
166
|
}
|
|
152
167
|
});
|
|
153
168
|
|
|
154
|
-
//
|
|
169
|
+
// 5. 常驻生命周期守卫:在每次 Agent 循环执行前,自动检测 Session 有效性并无感续期
|
|
155
170
|
pi.on("before_agent_start", async () => {
|
|
156
171
|
try {
|
|
157
172
|
const currentSession = getStoredZenSessionId();
|
|
@@ -167,7 +182,7 @@ export default function piZenSession(pi: ExtensionAPI): void {
|
|
|
167
182
|
}
|
|
168
183
|
});
|
|
169
184
|
|
|
170
|
-
//
|
|
185
|
+
// 6. 注册 /zen 指令
|
|
171
186
|
pi.registerCommand("zen", {
|
|
172
187
|
description: "OpenCode Zen 免费模型与 Session 会话自动维护 (/zen [key|refresh|status|list])",
|
|
173
188
|
getArgumentCompletions: (prefix: string) => {
|
|
@@ -284,23 +299,24 @@ export async function handleZenCommand(
|
|
|
284
299
|
// 5. 无参调用: /zen
|
|
285
300
|
const status = getZenStatusInfo();
|
|
286
301
|
if (status.hasKey) {
|
|
287
|
-
// 已有 Key
|
|
302
|
+
// 已有 Key 时,自动强制刷新 Session 并重新注册热载
|
|
288
303
|
try {
|
|
289
|
-
notify(ctx, "
|
|
290
|
-
const result = await syncZenConfiguration({ forceSession:
|
|
304
|
+
notify(ctx, "正在刷新 OpenCode Zen 会话与请求头...", "info");
|
|
305
|
+
const result = await syncZenConfiguration({ forceSession: true });
|
|
291
306
|
if (pi) {
|
|
292
307
|
registerZenProviderToPi(pi, result.apiKey, result.sessionId);
|
|
293
308
|
}
|
|
294
309
|
const msg = [
|
|
295
|
-
"✨ [OpenCode Zen
|
|
310
|
+
"✨ [OpenCode Zen 已自动刷新并就绪]",
|
|
296
311
|
`• API Key: ${maskKey(result.apiKey)}`,
|
|
297
|
-
`•
|
|
298
|
-
`• 免费模型: ${result.modelsCount}
|
|
312
|
+
`• 全新 Session: ${result.sessionId} (有效且已持久化)`,
|
|
313
|
+
`• 免费模型: 已同步 ${result.modelsCount} 个可用免费模型`,
|
|
314
|
+
`• 请求头保护: 30分钟自动轮换 + 7维官方签名 + 6大核心工具全注入`,
|
|
299
315
|
"",
|
|
300
316
|
"常用操作:",
|
|
301
317
|
" /zen <key> - 更换 API Key",
|
|
302
|
-
" /zen refresh - 换新会话 ID",
|
|
303
318
|
" /zen status - 详情状态",
|
|
319
|
+
" /zen list - 查看所有可用免费模型",
|
|
304
320
|
].join("\n");
|
|
305
321
|
notify(ctx, msg, "info");
|
|
306
322
|
} catch (error) {
|
|
@@ -317,7 +333,7 @@ export async function handleZenCommand(
|
|
|
317
333
|
"oc_sk_",
|
|
318
334
|
);
|
|
319
335
|
if (enteredKey && enteredKey.trim()) {
|
|
320
|
-
await handleZenCommand(enteredKey.trim(), ctx);
|
|
336
|
+
await handleZenCommand(enteredKey.trim(), ctx, pi);
|
|
321
337
|
return;
|
|
322
338
|
}
|
|
323
339
|
notify(ctx, "已取消输入。如需配置请使用 /zen <oc_sk_xxx>", "info");
|
package/src/models-registry.ts
CHANGED
|
@@ -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()
|
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,
|
|
@@ -75,6 +76,15 @@ export async function fetchZenFreeModels(apiKey: string): Promise<string[]> {
|
|
|
75
76
|
* 读取当前系统中已配置的 OpenCode Zen API Key
|
|
76
77
|
*/
|
|
77
78
|
export function getStoredZenApiKey(authPath = defaultAuthPath(), modelsPath = defaultModelsPath()): string | null {
|
|
79
|
+
// 优先读取环境变量,支持免配置直接启动
|
|
80
|
+
const envKey =
|
|
81
|
+
process.env.OPENCODE_API_KEY ||
|
|
82
|
+
process.env.OPENCODE_ZEN_API_KEY ||
|
|
83
|
+
process.env.ZEN_API_KEY;
|
|
84
|
+
if (envKey && envKey.trim()) {
|
|
85
|
+
return envKey.trim();
|
|
86
|
+
}
|
|
87
|
+
|
|
78
88
|
try {
|
|
79
89
|
if (existsSync(authPath)) {
|
|
80
90
|
const auth = JSON.parse(readFileSync(authPath, "utf8")) as Record<string, { key?: string }>;
|
|
@@ -225,6 +235,7 @@ export async function syncZenConfiguration(options: ZenSyncOptions = {}): Promis
|
|
|
225
235
|
"User-Agent": ZEN_USER_AGENT,
|
|
226
236
|
"x-opencode-client": "cli",
|
|
227
237
|
"x-opencode-session": targetSession,
|
|
238
|
+
"x-opencode-project": generateZenProjectId(),
|
|
228
239
|
"x-session-affinity": targetSession,
|
|
229
240
|
"X-Session-Id": targetSession,
|
|
230
241
|
},
|
|
@@ -233,6 +244,7 @@ export async function syncZenConfiguration(options: ZenSyncOptions = {}): Promis
|
|
|
233
244
|
requiresReasoningContentOnAssistantMessages: true,
|
|
234
245
|
supportsDeveloperRole: false,
|
|
235
246
|
supportsStore: false,
|
|
247
|
+
supportsUsageInStreaming: true,
|
|
236
248
|
},
|
|
237
249
|
models: resolvedModels,
|
|
238
250
|
};
|
|
@@ -306,6 +318,7 @@ export function updateCcSwitchDb(
|
|
|
306
318
|
headers["User-Agent"] = ZEN_USER_AGENT;
|
|
307
319
|
headers["x-opencode-client"] = "cli";
|
|
308
320
|
headers["x-opencode-session"] = sessionId;
|
|
321
|
+
headers["x-opencode-project"] = generateZenProjectId();
|
|
309
322
|
headers["x-session-affinity"] = sessionId;
|
|
310
323
|
headers["X-Session-Id"] = sessionId;
|
|
311
324
|
cfg.headers = headers;
|
|
@@ -317,6 +330,7 @@ export function updateCcSwitchDb(
|
|
|
317
330
|
headers["User-Agent"] = ZEN_USER_AGENT;
|
|
318
331
|
headers["x-opencode-client"] = "cli";
|
|
319
332
|
headers["x-opencode-session"] = sessionId;
|
|
333
|
+
headers["x-opencode-project"] = generateZenProjectId();
|
|
320
334
|
headers["x-session-affinity"] = sessionId;
|
|
321
335
|
headers["X-Session-Id"] = sessionId;
|
|
322
336
|
opts.headers = headers;
|