icebear0828 Claude Opus 4.6 commited on
Commit
ea06916
·
1 Parent(s): dfae1c7

fix: harden extraction pipeline + update Codex Desktop v26.309.31024

Browse files

- Fallback originator scanning: try/catch extractFromMainJs, scan all
.vite/build/*.js on failure instead of aborting
- Webview model discovery: scan webview/assets/*.js for additional
model IDs not found in main.js
- Fix bracket search in extractPrompts(): limit lastIndexOf("[") to
50-char window instead of unbounded backward search
- Prompt validation in savePrompt(): reject content <50 chars or
with >3 garbled lines to prevent corruption
- Fix corrupted title-generation.md (garbled lines 17-35)
- Update Codex Desktop to v26.309.31024 (build 962)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

CHANGELOG.md CHANGED
@@ -8,18 +8,34 @@
8
 
9
  ### Added
10
 
 
11
  - 更新弹窗 + 自动重启:点击"有可用更新"弹出 Modal 显示 changelog,一键更新后服务器自动重启、前端自动刷新,零人工干预(git 模式 spawn 新进程、Docker/Electron 显示对应操作指引)
12
  - Model-aware 多计划账号路由:不同 plan(free/plus/business)的账号自动路由到各自支持的模型,business 账号可继续使用 gpt-5.4 等高端模型 (#57)
13
  - Structured Outputs 支持:`/v1/chat/completions` 支持 `response_format`(`json_object` / `json_schema`),Gemini 端点支持 `responseMimeType` + `responseSchema`,自动翻译为 Codex Responses API 的 `text.format`;`/v1/responses` 直通 `text` 字段
14
 
15
  ### Changed
16
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  - 模型目录大幅更新:后端移除 free 账号的 `gpt-5.4`、`gpt-5.3-codex` 全系列(plus 及以上仍可用),新旗舰模型为 `gpt-5.2-codex`(`codex` 别名指向此模型)
18
  - 新增模型:`gpt-5.2`、`gpt-5.1-codex`、`gpt-5.1`、`gpt-5-codex`、`gpt-5`、`gpt-oss-120b`、`gpt-oss-20b`、`gpt-5-codex-mini`
19
  - 模型目录从 23 个静态模型精简为 11 个(匹配后端实际返回)
20
 
21
  ### Fixed
22
 
 
 
 
23
  - JSON Schema `additionalProperties` 递归注入:`injectAdditionalProperties()` 递归注入 `additionalProperties: false` 到 JSON Schema 所有 object 节点,覆盖 `properties`、`patternProperties`、`$defs`/`definitions`、`items`、`prefixItems`、组合器(`oneOf`/`anyOf`/`allOf`)、条件(`if`/`then`/`else`),含循环检测;三个端点(OpenAI/Gemini/Responses passthrough)统一调用 (#64)
24
  - CONNECT tunnel header 解析:循环跳过中间 header block(CONNECT 200、100 Continue),修复代理模式下 tunnel 的 `HTTP/1.1 200` 被当作真实状态码导致上游 4xx 错误被掩盖为 502 的问题 (#64)
25
  - 上游 HTTP 状态码透传:非流式 collect 路径从错误消息提取真实 HTTP 状态码,不再硬编码 502;提取 `toErrorStatus()` 辅助函数统一 4 处 StatusCode 转换 (#64)
 
8
 
9
  ### Added
10
 
11
+ - 双窗口配额显示:Dashboard 账号卡片同时展示主窗口(小时限制)和次窗口(周限制)的用量百分比、进度条和重置时间,后端 `secondary_window` 不再被忽略
12
  - 更新弹窗 + 自动重启:点击"有可用更新"弹出 Modal 显示 changelog,一键更新后服务器自动重启、前端自动刷新,零人工干预(git 模式 spawn 新进程、Docker/Electron 显示对应操作指引)
13
  - Model-aware 多计划账号路由:不同 plan(free/plus/business)的账号自动路由到各自支持的模型,business 账号可继续使用 gpt-5.4 等高端模型 (#57)
14
  - Structured Outputs 支持:`/v1/chat/completions` 支持 `response_format`(`json_object` / `json_schema`),Gemini 端点支持 `responseMimeType` + `responseSchema`,自动翻译为 Codex Responses API 的 `text.format`;`/v1/responses` 直通 `text` 字段
15
 
16
  ### Changed
17
 
18
+ - 提取管道强化:`extract-fingerprint.ts` 新增 fallback 扫描(`.vite/build/*.js` 全文件回退)和 webview 模型发现(`webview/assets/*.js`),pattern 失败不再中断整个流程
19
+ - 模型/别名自动添加降级为 semi-auto:后端已通过 `isCodexCompatibleId()` 自动合并新模型,`apply-update.ts` 不再自动写入 `models.yaml`(避免 `mutateYaml` 破坏 YAML 格式)
20
+ - Codex Desktop 版本更新至 v26.309.31024 (build 962)
21
+
22
+ ### Fixed (pipeline)
23
+
24
+ - Prompt 提取括号定位修复:`extractPrompts()` 的 `lastIndexOf("[")` 无限回溯导致匹配到无关 `[`,截取错误代码片段产出乱码;改为 50 字符窗口内搜索
25
+ - Prompt 覆写安全校验:`savePrompt()` 和 `applyAutoChanges()` 新增内容验证(最小长度 50 字符、乱码行数 ≤3),拒绝将损坏数据写入 `config/prompts/`
26
+ - `title-generation.md` 修复:还原因提取 bug 损坏的 title 生成 prompt(第 17-35 行乱码)
27
+
28
+ ### Changed (previous)
29
+
30
  - 模型目录大幅更新:后端移除 free 账号的 `gpt-5.4`、`gpt-5.3-codex` 全系列(plus 及以上仍可用),新旗舰模型为 `gpt-5.2-codex`(`codex` 别名指向此模型)
31
  - 新增模型:`gpt-5.2`、`gpt-5.1-codex`、`gpt-5.1`、`gpt-5-codex`、`gpt-5`、`gpt-oss-120b`、`gpt-oss-20b`、`gpt-5-codex-mini`
32
  - 模型目录从 23 个静态模型精简为 11 个(匹配后端实际返回)
33
 
34
  ### Fixed
35
 
36
+ - 429 真实冷却时间:从 429 错误响应体解析 `resets_in_seconds` / `resets_at`,账号按后端实际冷却期(如 free 计划 5.5 天)标记限速,不再使用硬编码 60s 默认值 (#65)
37
+ - 429 自动降级:收到 429 后自动尝试下一个可用账号,所有账号耗尽后才返回 429 给客户端 (#65)
38
+ - 调度优先级优化:`least_used` 策略新增 `window_reset_at` 二级排序,配额窗口更早重置的账号优先使用 (#65)
39
  - JSON Schema `additionalProperties` 递归注入:`injectAdditionalProperties()` 递归注入 `additionalProperties: false` 到 JSON Schema 所有 object 节点,覆盖 `properties`、`patternProperties`、`$defs`/`definitions`、`items`、`prefixItems`、组合器(`oneOf`/`anyOf`/`allOf`)、条件(`if`/`then`/`else`),含循环检测;三个端点(OpenAI/Gemini/Responses passthrough)统一调用 (#64)
40
  - CONNECT tunnel header 解析:循环跳过中间 header block(CONNECT 200、100 Continue),修复代理模式下 tunnel 的 `HTTP/1.1 200` 被当作真实状态码导致上游 4xx 错误被掩盖为 502 的问题 (#64)
41
  - 上游 HTTP 状态码透传:非流式 collect 路径从错误消息提取真实 HTTP 状态码,不再硬编码 502;提取 `toErrorStatus()` 辅助函数统一 4 处 StatusCode 转换 (#64)
config/default.yaml CHANGED
@@ -3,8 +3,8 @@ api:
3
  timeout_seconds: 60
4
  client:
5
  originator: Codex Desktop
6
- app_version: 26.305.950
7
- build_number: "863"
8
  platform: darwin
9
  arch: arm64
10
  chromium_version: "144"
@@ -34,5 +34,4 @@ tls:
34
  curl_binary: auto
35
  impersonate_profile: chrome144
36
  proxy_url: null
37
- # 当代理不支持 HTTP/2 时启用此选项(如遇到 SETTINGS frame 错误)
38
  force_http11: true
 
3
  timeout_seconds: 60
4
  client:
5
  originator: Codex Desktop
6
+ app_version: 26.309.31024
7
+ build_number: "962"
8
  platform: darwin
9
  arch: arm64
10
  chromium_version: "144"
 
34
  curl_binary: auto
35
  impersonate_profile: chrome144
36
  proxy_url: null
 
37
  force_http11: true
config/prompts/title-generation.md CHANGED
@@ -14,22 +14,14 @@ Generate a single-line title that captures the question or core change requested
14
  - Do not use punctuation at the end.
15
  - Output the title as plain text with no surrounding quotes or backticks.
16
  - Use precise, non-redundant language.
17
- s locale (e.g., "Fix bug" -> "Corrige el error" in Spanish-ES), but leave code terms in English unless a widely adopted translation exists.`,"- If the user provides a title explicitly, reuse it (translated if needed) and skip generation logic.",
18
- Fix
19
- Add
20
- Find
21
- Locate
22
- Count
23
- ,"- Do NOT respond to the user, answer questions, or attempt to solve the problem; just write a title that can represent the user
24
- ,
25
- ,
26
- ,'- User:
27
- -> Add dark-mode support','- User:
28
- (de-DE) -> Login-Fehler 500 beheben','- User:
29
- (fr-FR) -> Refactoriser composant sidebar','- User:
30
- -> Troubleshoot login bug','- User:
31
- -> Locate foo_bar',`- User:
32
- -> Calculate 2+2`,
33
- ,
34
- ,
35
- ,
 
14
  - Do not use punctuation at the end.
15
  - Output the title as plain text with no surrounding quotes or backticks.
16
  - Use precise, non-redundant language.
17
+ - Translate fixed phrases into the user's locale (e.g., "Fix bug" -> "Corrige el error" in Spanish-ES), but leave code terms in English unless a widely adopted translation exists.
18
+ - If the user provides a title explicitly, reuse it (translated if needed) and skip generation logic.
19
+ - Do NOT respond to the user, answer questions, or attempt to solve the problem; just write a title that can represent the user's query.
20
+
21
+ Examples:
22
+ - User: "Can we add dark-mode support to the settings page?" -> Add dark-mode support
23
+ - User: "Fehlerbehebung: Beim Anmelden erscheint 500." (de-DE) -> Login-Fehler 500 beheben
24
+ - User: "Refactoriser le composant sidebar pour réduire le code dupliqué." (fr-FR) -> Refactoriser composant sidebar
25
+ - User: "How do I fix our login bug?" -> Troubleshoot login bug
26
+ - User: "Where in the codebase is foo_bar created" -> Locate foo_bar
27
+ - User: "what is 2+2?" -> Calculate 2+2
 
 
 
 
 
 
 
 
scripts/extract-fingerprint.ts CHANGED
@@ -188,6 +188,17 @@ function extractFromMainJs(
188
  };
189
  }
190
 
 
 
 
 
 
 
 
 
 
 
 
191
  /**
192
  * Step B (continued): Extract system prompts from main.js
193
  */
@@ -227,8 +238,8 @@ function extractPrompts(content: string): {
227
  // Find the enclosing array end: ].join(
228
  const joinIdx = content.indexOf("].join(", titleStart);
229
  if (joinIdx !== -1) {
230
- // Extract the array content between [ and ]
231
- const bracketStart = content.lastIndexOf("[", titleStart);
232
  if (bracketStart !== -1) {
233
  const arrayContent = content.slice(bracketStart + 1, joinIdx);
234
  // Parse string literals from the array
@@ -244,7 +255,7 @@ function extractPrompts(content: string): {
244
  if (prStart !== -1) {
245
  const joinIdx = content.indexOf("].join(", prStart);
246
  if (joinIdx !== -1) {
247
- const bracketStart = content.lastIndexOf("[", prStart);
248
  if (bracketStart !== -1) {
249
  const arrayContent = content.slice(bracketStart + 1, joinIdx);
250
  prGeneration = parseStringArray(arrayContent);
@@ -318,6 +329,17 @@ function savePrompt(name: string, content: string | null): { hash: string | null
318
  const sanitized = sanitizePrompt(content);
319
  if (!sanitized) return { hash: null, path: null };
320
 
 
 
 
 
 
 
 
 
 
 
 
321
  mkdirSync(PROMPTS_DIR, { recursive: true });
322
  const filePath = join(PROMPTS_DIR, `${name}.md`);
323
  writeFileSync(filePath, sanitized);
@@ -433,7 +455,47 @@ async function main() {
433
  if (mainJs) {
434
  console.log(`[extract] main.js loaded (${mainJs.split("\n").length} lines)`);
435
 
436
- mainJsResults = extractFromMainJs(mainJs, patterns.main_js);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
437
  console.log(` API base URL: ${mainJsResults.apiBaseUrl}`);
438
  console.log(` originator: ${mainJsResults.originator}`);
439
  console.log(` models: ${mainJsResults.models.join(", ")}`);
@@ -448,6 +510,31 @@ async function main() {
448
  console.log(` automation-response: ${promptResults.automationResponse ? "found" : "NOT FOUND"}`);
449
  }
450
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
451
  // Save extracted prompts
452
  const dc = savePrompt("desktop-context", promptResults.desktopContext);
453
  const tg = savePrompt("title-generation", promptResults.titleGeneration);
 
188
  };
189
  }
190
 
191
+ /**
192
+ * Find the nearest `[` bracket within maxDistance chars before the given position.
193
+ * Prevents unbounded `lastIndexOf("[")` from matching a wrong bracket thousands of chars away.
194
+ */
195
+ function findNearbyBracket(content: string, position: number, maxDistance = 50): number {
196
+ const searchStart = Math.max(0, position - maxDistance);
197
+ const slice = content.slice(searchStart, position);
198
+ const idx = slice.lastIndexOf("[");
199
+ return idx !== -1 ? searchStart + idx : -1;
200
+ }
201
+
202
  /**
203
  * Step B (continued): Extract system prompts from main.js
204
  */
 
238
  // Find the enclosing array end: ].join(
239
  const joinIdx = content.indexOf("].join(", titleStart);
240
  if (joinIdx !== -1) {
241
+ // Find the opening [ within 50 chars before the marker (not unbounded lastIndexOf)
242
+ const bracketStart = findNearbyBracket(content, titleStart);
243
  if (bracketStart !== -1) {
244
  const arrayContent = content.slice(bracketStart + 1, joinIdx);
245
  // Parse string literals from the array
 
255
  if (prStart !== -1) {
256
  const joinIdx = content.indexOf("].join(", prStart);
257
  if (joinIdx !== -1) {
258
+ const bracketStart = findNearbyBracket(content, prStart);
259
  if (bracketStart !== -1) {
260
  const arrayContent = content.slice(bracketStart + 1, joinIdx);
261
  prGeneration = parseStringArray(arrayContent);
 
329
  const sanitized = sanitizePrompt(content);
330
  if (!sanitized) return { hash: null, path: null };
331
 
332
+ // Validate: reject suspiciously short or garbled content
333
+ if (sanitized.length < 50) {
334
+ console.warn(`[extract] Prompt "${name}" too short (${sanitized.length} chars), skipping save`);
335
+ return { hash: null, path: null };
336
+ }
337
+ const garbageLines = sanitized.split("\n").filter((l) => /^[,`'"]\s*$/.test(l.trim()));
338
+ if (garbageLines.length > 3) {
339
+ console.warn(`[extract] Prompt "${name}" has ${garbageLines.length} garbled lines, skipping save`);
340
+ return { hash: null, path: null };
341
+ }
342
+
343
  mkdirSync(PROMPTS_DIR, { recursive: true });
344
  const filePath = join(PROMPTS_DIR, `${name}.md`);
345
  writeFileSync(filePath, sanitized);
 
455
  if (mainJs) {
456
  console.log(`[extract] main.js loaded (${mainJs.split("\n").length} lines)`);
457
 
458
+ try {
459
+ mainJsResults = extractFromMainJs(mainJs, patterns.main_js);
460
+ } catch (err) {
461
+ console.warn(`[extract] Primary extraction failed: ${(err as Error).message}`);
462
+ console.log("[extract] Scanning all .vite/build/*.js for fallback...");
463
+
464
+ const buildDir = join(asarRoot, ".vite/build");
465
+ if (existsSync(buildDir)) {
466
+ const jsFiles = readdirSync(buildDir).filter((f) => f.endsWith(".js"));
467
+ for (const file of jsFiles) {
468
+ const content = readFileSync(join(buildDir, file), "utf-8");
469
+ const origPattern = patterns.main_js.originator;
470
+ if (origPattern?.pattern) {
471
+ const m = content.match(new RegExp(origPattern.pattern));
472
+ if (m) {
473
+ mainJsResults.originator = m[origPattern.group ?? 0] ?? m[0];
474
+ console.log(`[extract] Originator found in fallback file: ${file}`);
475
+ break;
476
+ }
477
+ }
478
+ }
479
+ }
480
+
481
+ // Re-extract non-critical fields from mainJs
482
+ const apiPattern = patterns.main_js.api_base_url;
483
+ if (apiPattern?.pattern) {
484
+ const m = mainJs.match(new RegExp(apiPattern.pattern));
485
+ if (m) mainJsResults.apiBaseUrl = m[0];
486
+ }
487
+ const modelPattern = patterns.main_js.models;
488
+ if (modelPattern?.pattern) {
489
+ const re = new RegExp(modelPattern.pattern, "g");
490
+ const groupIdx = modelPattern.group ?? 0;
491
+ const modelSet = new Set<string>();
492
+ for (const m of mainJs.matchAll(re)) {
493
+ modelSet.add(m[groupIdx] ?? m[0]);
494
+ }
495
+ mainJsResults.models = [...modelSet].sort();
496
+ }
497
+ }
498
+
499
  console.log(` API base URL: ${mainJsResults.apiBaseUrl}`);
500
  console.log(` originator: ${mainJsResults.originator}`);
501
  console.log(` models: ${mainJsResults.models.join(", ")}`);
 
510
  console.log(` automation-response: ${promptResults.automationResponse ? "found" : "NOT FOUND"}`);
511
  }
512
 
513
+ // Scan webview assets for additional model IDs
514
+ const webviewAssetsDir = join(asarRoot, "webview/assets");
515
+ if (existsSync(webviewAssetsDir)) {
516
+ console.log("[extract] Scanning webview assets for additional models...");
517
+ const modelPattern = patterns.main_js.models;
518
+ if (modelPattern?.pattern) {
519
+ const webviewFiles = readdirSync(webviewAssetsDir).filter((f) => f.endsWith(".js"));
520
+ const webviewModels = new Set<string>();
521
+ for (const file of webviewFiles) {
522
+ const content = readFileSync(join(webviewAssetsDir, file), "utf-8");
523
+ const re = new RegExp(modelPattern.pattern, "g");
524
+ const groupIdx = modelPattern.group ?? 0;
525
+ for (const m of content.matchAll(re)) {
526
+ webviewModels.add(m[groupIdx] ?? m[0]);
527
+ }
528
+ }
529
+ const existingModels = new Set(mainJsResults.models);
530
+ const newFromWebview = [...webviewModels].filter((m) => !existingModels.has(m));
531
+ if (newFromWebview.length > 0) {
532
+ console.log(`[extract] Webview: ${newFromWebview.length} additional models: ${newFromWebview.join(", ")}`);
533
+ mainJsResults.models = [...mainJsResults.models, ...newFromWebview].sort();
534
+ }
535
+ }
536
+ }
537
+
538
  // Save extracted prompts
539
  const dc = savePrompt("desktop-context", promptResults.desktopContext);
540
  const tg = savePrompt("title-generation", promptResults.titleGeneration);