Spaces:
Paused
Paused
icebear0828 Claude Opus 4.6 commited on
Commit ·
8728276
1
Parent(s): 2def35e
chore: deduplicate model selector, auto-extract chromium version, harden update pipeline
Browse files- Remove independent model dropdown from Anthropic SDK Setup; use shared Default Model from API Configuration
- Auto-extract Chromium version from Electron via electron-to-chromium in extract-fingerprint
- Add 5-min timeout kill timer for full-update child process to prevent permanent lock
- Add try/finally in model-fetcher initial timer to prevent refresh loop interruption
- Bump Codex Desktop to v26.228.1430 (build 760)
- Update poll interval from 30min to 3 days
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- .gitignore +1 -0
- CHANGELOG.md +7 -0
- config/default.yaml +2 -2
- config/prompts/automation-response.md +1 -1
- config/prompts/desktop-context.md +2 -2
- package-lock.json +8 -0
- package.json +1 -0
- scripts/extract-fingerprint.ts +31 -26
- src/models/model-fetcher.ts +5 -2
- src/update-checker.ts +11 -1
- web/src/App.tsx +0 -1
- web/src/components/AnthropicSetup.tsx +4 -15
.gitignore
CHANGED
|
@@ -15,6 +15,7 @@ scripts/extract-fingerprint.ts
|
|
| 15 |
scripts/full-update.ts
|
| 16 |
scripts/apply-update.ts
|
| 17 |
scripts/check-update.ts
|
|
|
|
| 18 |
scripts/test-*.ts
|
| 19 |
scripts/cron-update.sh
|
| 20 |
config/extraction-patterns.yaml
|
|
|
|
| 15 |
scripts/full-update.ts
|
| 16 |
scripts/apply-update.ts
|
| 17 |
scripts/check-update.ts
|
| 18 |
+
scripts/types.ts
|
| 19 |
scripts/test-*.ts
|
| 20 |
scripts/cron-update.sh
|
| 21 |
config/extraction-patterns.yaml
|
CHANGELOG.md
CHANGED
|
@@ -8,6 +8,7 @@
|
|
| 8 |
|
| 9 |
### Added
|
| 10 |
|
|
|
|
| 11 |
- 动态模型列表:后台从 Codex 后端自动获取模型目录,与静态 YAML 合并(`src/models/model-store.ts`、`src/models/model-fetcher.ts`)
|
| 12 |
- `/debug/models` 诊断端点,展示模型来源(static/backend)与刷新状态
|
| 13 |
- 完整 Codex 模型目录:GPT-5.3/5.2/5.1 全系列 base/high/mid/low/max/mini 变体(23 个静态模型)
|
|
@@ -18,6 +19,7 @@
|
|
| 18 |
|
| 19 |
### Changed
|
| 20 |
|
|
|
|
| 21 |
- 模型管理从纯静态 YAML 迁移至静态+动态混合架构(后端优先,YAML 兜底)
|
| 22 |
- 默认模型改为 `gpt-5.2-codex`
|
| 23 |
- Dashboard "Claude Code Quick Setup" 重命名为 "Anthropic SDK Setup"
|
|
@@ -25,6 +27,11 @@
|
|
| 25 |
|
| 26 |
### Fixed
|
| 27 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
- 强化提示词注入防护:`SUPPRESS_PROMPT` 从弱 "ignore" 措辞改为声明式覆盖("NOT applicable"、"standard OpenAI API model"),解决 mini 模型仍泄露 Codex Desktop 身份的问题
|
| 29 |
- 非流式请求错误处理:`collectTranslator` 抛出 generic Error 时返回 502 JSON 而非 500 HTML(`proxy-handler.ts`)
|
| 30 |
- `desktop-context.md` 提取损坏修复:`extractPrompts()` 的 end marker 从 `` `; `` 改为 `` `[,;)] `` 正则,防止压缩 JS 代码注入 instructions 导致 tool_calls 失效(#13)
|
|
|
|
| 8 |
|
| 9 |
### Added
|
| 10 |
|
| 11 |
+
- 自动提取 Chromium 版本:`extract-fingerprint.ts` 从 `package.json` 读取 Electron 版本,通过 `electron-to-chromium` 映射为 Chromium 大版本,`apply-update.ts` 自动更新 `chromium_version` 和 TLS impersonate profile
|
| 12 |
- 动态模型列表:后台从 Codex 后端自动获取模型目录,与静态 YAML 合并(`src/models/model-store.ts`、`src/models/model-fetcher.ts`)
|
| 13 |
- `/debug/models` 诊断端点,展示模型来源(static/backend)与刷新状态
|
| 14 |
- 完整 Codex 模型目录:GPT-5.3/5.2/5.1 全系列 base/high/mid/low/max/mini 变体(23 个静态模型)
|
|
|
|
| 19 |
|
| 20 |
### Changed
|
| 21 |
|
| 22 |
+
- Dashboard 模型选择器去重:移除 Anthropic SDK Setup 的独立模型下拉框,统一使用 API Configuration 的 Default Model
|
| 23 |
- 模型管理从纯静态 YAML 迁移至静态+动态混合架构(后端优先,YAML 兜底)
|
| 24 |
- 默认模型改为 `gpt-5.2-codex`
|
| 25 |
- Dashboard "Claude Code Quick Setup" 重命名为 "Anthropic SDK Setup"
|
|
|
|
| 27 |
|
| 28 |
### Fixed
|
| 29 |
|
| 30 |
+
- `apply-update.ts` 模型比较不再误报删除:静态提取只含 2 个硬编码模型,与 YAML 的 24 个比较会产生 22 个假删除,现在只报新增
|
| 31 |
+
- `update-checker.ts` 子进程超时保护:`fork()` 添加 5 分钟 kill timer,防止挂起导致 `_updateInProgress` 永久锁定
|
| 32 |
+
- `model-fetcher.ts` 初始定时器添加 try/finally,防止异常中断刷新循环
|
| 33 |
+
- `apply-update.ts` 移除 `any` 类型(`mutateYaml` 回调参数)
|
| 34 |
+
- `ExtractedFingerprint` 接口统一:提取到 `scripts/types.ts` 共享,`extract-fingerprint.ts` 和 `apply-update.ts` 共用
|
| 35 |
- 强化提示词注入防护:`SUPPRESS_PROMPT` 从弱 "ignore" 措辞改为声明式覆盖("NOT applicable"、"standard OpenAI API model"),解决 mini 模型仍泄露 Codex Desktop 身份的问题
|
| 36 |
- 非流式请求错误处理:`collectTranslator` 抛出 generic Error 时返回 502 JSON 而非 500 HTML(`proxy-handler.ts`)
|
| 37 |
- `desktop-context.md` 提取损坏修复:`extractPrompts()` 的 end marker 从 `` `; `` 改为 `` `[,;)] `` 正则,防止压缩 JS 代码注入 instructions 导致 tool_calls 失效(#13)
|
config/default.yaml
CHANGED
|
@@ -3,8 +3,8 @@ api:
|
|
| 3 |
timeout_seconds: 60
|
| 4 |
client:
|
| 5 |
originator: Codex Desktop
|
| 6 |
-
app_version: 26.
|
| 7 |
-
build_number: "
|
| 8 |
platform: darwin
|
| 9 |
arch: arm64
|
| 10 |
chromium_version: "137"
|
|
|
|
| 3 |
timeout_seconds: 60
|
| 4 |
client:
|
| 5 |
originator: Codex Desktop
|
| 6 |
+
app_version: 26.228.1430
|
| 7 |
+
build_number: "760"
|
| 8 |
platform: darwin
|
| 9 |
arch: arm64
|
| 10 |
chromium_version: "137"
|
config/prompts/automation-response.md
CHANGED
|
@@ -48,4 +48,4 @@ Response MUST end with a remark-directive block.
|
|
| 48 |
- Waiting on user decision:
|
| 49 |
- \`::inbox-item{title="Choose API shape for filters" summary="Two options drafted; pick A vs B"}\`
|
| 50 |
- Status update with next step:
|
| 51 |
-
- \`::inbox-item{title="PR comments addressed" summary="Ready for re-review; focus on auth edge case"}\
|
|
|
|
| 48 |
- Waiting on user decision:
|
| 49 |
- \`::inbox-item{title="Choose API shape for filters" summary="Two options drafted; pick A vs B"}\`
|
| 50 |
- Status update with next step:
|
| 51 |
+
- \`::inbox-item{title="PR comments addressed" summary="Ready for re-review; focus on auth edge case"}\
|
config/prompts/desktop-context.md
CHANGED
|
@@ -34,7 +34,7 @@
|
|
| 34 |
- Keep automation prompts self-sufficient because the user may have limited availability to answer questions. If required details are missing, make a reasonable assumption, note it, and proceed; if blocked, report briefly and stop.
|
| 35 |
- When helpful, include clear output expectations (file path, format, sections) and gating rules (only if X, skip if exists) to reduce ambiguity.
|
| 36 |
- Automations should always open an inbox item.
|
| 37 |
-
- Archiving rule: only include `::archive-thread{}` when there is nothing actionable for the user.
|
| 38 |
- Safe to archive: "no findings" checks (bug scans that found nothing, clean lint runs, monitoring checks with no incidents).
|
| 39 |
- Do not archive: deliverables or follow-ups (briefs, reports, summaries, plans, recommendations).
|
| 40 |
- If you do archive, include the archive directive after the inbox item.
|
|
@@ -67,4 +67,4 @@
|
|
| 67 |
|
| 68 |
### Archiving
|
| 69 |
- If a user specifically asks you to end a thread/conversation, you can return the archive directive ::archive{...} to archive the thread/conversation.
|
| 70 |
-
- Example: ::archive{reason="User requested to end conversation"}
|
|
|
|
| 34 |
- Keep automation prompts self-sufficient because the user may have limited availability to answer questions. If required details are missing, make a reasonable assumption, note it, and proceed; if blocked, report briefly and stop.
|
| 35 |
- When helpful, include clear output expectations (file path, format, sections) and gating rules (only if X, skip if exists) to reduce ambiguity.
|
| 36 |
- Automations should always open an inbox item.
|
| 37 |
+
- Archiving rule: only include \`::archive-thread{}\` when there is nothing actionable for the user.
|
| 38 |
- Safe to archive: "no findings" checks (bug scans that found nothing, clean lint runs, monitoring checks with no incidents).
|
| 39 |
- Do not archive: deliverables or follow-ups (briefs, reports, summaries, plans, recommendations).
|
| 40 |
- If you do archive, include the archive directive after the inbox item.
|
|
|
|
| 67 |
|
| 68 |
### Archiving
|
| 69 |
- If a user specifically asks you to end a thread/conversation, you can return the archive directive ::archive{...} to archive the thread/conversation.
|
| 70 |
+
- Example: ::archive{reason="User requested to end conversation"}
|
package-lock.json
CHANGED
|
@@ -19,6 +19,7 @@
|
|
| 19 |
"@electron/asar": "^3.2.0",
|
| 20 |
"@types/js-yaml": "^4.0.0",
|
| 21 |
"@types/node": "^22.0.0",
|
|
|
|
| 22 |
"js-beautify": "^1.15.0",
|
| 23 |
"tsx": "^4.0.0",
|
| 24 |
"typescript": "^5.5.0",
|
|
@@ -1310,6 +1311,13 @@
|
|
| 1310 |
"url": "https://github.com/sponsors/isaacs"
|
| 1311 |
}
|
| 1312 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1313 |
"node_modules/emoji-regex": {
|
| 1314 |
"version": "9.2.2",
|
| 1315 |
"resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
|
|
|
| 19 |
"@electron/asar": "^3.2.0",
|
| 20 |
"@types/js-yaml": "^4.0.0",
|
| 21 |
"@types/node": "^22.0.0",
|
| 22 |
+
"electron-to-chromium": "^1.5.302",
|
| 23 |
"js-beautify": "^1.15.0",
|
| 24 |
"tsx": "^4.0.0",
|
| 25 |
"typescript": "^5.5.0",
|
|
|
|
| 1311 |
"url": "https://github.com/sponsors/isaacs"
|
| 1312 |
}
|
| 1313 |
},
|
| 1314 |
+
"node_modules/electron-to-chromium": {
|
| 1315 |
+
"version": "1.5.302",
|
| 1316 |
+
"resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz",
|
| 1317 |
+
"integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==",
|
| 1318 |
+
"dev": true,
|
| 1319 |
+
"license": "ISC"
|
| 1320 |
+
},
|
| 1321 |
"node_modules/emoji-regex": {
|
| 1322 |
"version": "9.2.2",
|
| 1323 |
"resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
package.json
CHANGED
|
@@ -34,6 +34,7 @@
|
|
| 34 |
"@electron/asar": "^3.2.0",
|
| 35 |
"@types/js-yaml": "^4.0.0",
|
| 36 |
"@types/node": "^22.0.0",
|
|
|
|
| 37 |
"js-beautify": "^1.15.0",
|
| 38 |
"tsx": "^4.0.0",
|
| 39 |
"typescript": "^5.5.0",
|
|
|
|
| 34 |
"@electron/asar": "^3.2.0",
|
| 35 |
"@types/js-yaml": "^4.0.0",
|
| 36 |
"@types/node": "^22.0.0",
|
| 37 |
+
"electron-to-chromium": "^1.5.302",
|
| 38 |
"js-beautify": "^1.15.0",
|
| 39 |
"tsx": "^4.0.0",
|
| 40 |
"typescript": "^5.5.0",
|
scripts/extract-fingerprint.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { resolve, join } from "path";
|
|
| 17 |
import { createHash } from "crypto";
|
| 18 |
import { execSync } from "child_process";
|
| 19 |
import yaml from "js-yaml";
|
|
|
|
| 20 |
|
| 21 |
const ROOT = resolve(import.meta.dirname, "..");
|
| 22 |
const OUTPUT_PATH = resolve(ROOT, "data/extracted-fingerprint.json");
|
|
@@ -36,29 +37,6 @@ interface ExtractionPatterns {
|
|
| 36 |
}>;
|
| 37 |
}
|
| 38 |
|
| 39 |
-
interface ExtractedFingerprint {
|
| 40 |
-
app_version: string;
|
| 41 |
-
build_number: string;
|
| 42 |
-
api_base_url: string | null;
|
| 43 |
-
originator: string | null;
|
| 44 |
-
models: string[];
|
| 45 |
-
wham_endpoints: string[];
|
| 46 |
-
user_agent_contains: string;
|
| 47 |
-
sparkle_feed_url: string | null;
|
| 48 |
-
prompts: {
|
| 49 |
-
desktop_context_hash: string | null;
|
| 50 |
-
desktop_context_path: string | null;
|
| 51 |
-
title_generation_hash: string | null;
|
| 52 |
-
title_generation_path: string | null;
|
| 53 |
-
pr_generation_hash: string | null;
|
| 54 |
-
pr_generation_path: string | null;
|
| 55 |
-
automation_response_hash: string | null;
|
| 56 |
-
automation_response_path: string | null;
|
| 57 |
-
};
|
| 58 |
-
extracted_at: string;
|
| 59 |
-
source_path: string;
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
function sha256(content: string): string {
|
| 63 |
return `sha256:${createHash("sha256").update(content, "utf-8").digest("hex").slice(0, 16)}`;
|
| 64 |
}
|
|
@@ -123,6 +101,7 @@ function extractFromPackageJson(root: string): {
|
|
| 123 |
version: string;
|
| 124 |
buildNumber: string;
|
| 125 |
sparkleFeedUrl: string | null;
|
|
|
|
| 126 |
} {
|
| 127 |
const pkgPath = join(root, "package.json");
|
| 128 |
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
@@ -131,6 +110,7 @@ function extractFromPackageJson(root: string): {
|
|
| 131 |
version: pkg.version ?? "unknown",
|
| 132 |
buildNumber: String(pkg.codexBuildNumber ?? "unknown"),
|
| 133 |
sparkleFeedUrl: pkg.codexSparkleFeedUrl ?? null,
|
|
|
|
| 134 |
};
|
| 135 |
}
|
| 136 |
|
|
@@ -373,9 +353,32 @@ async function main() {
|
|
| 373 |
|
| 374 |
// Step A: package.json
|
| 375 |
console.log("[extract] Reading package.json...");
|
| 376 |
-
const { version, buildNumber, sparkleFeedUrl } = extractFromPackageJson(asarRoot);
|
| 377 |
-
console.log(` version:
|
| 378 |
-
console.log(` build:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 379 |
|
| 380 |
// Step B: main.js (or main-XXXXX.js chunk)
|
| 381 |
console.log("[extract] Loading main.js...");
|
|
@@ -455,6 +458,8 @@ async function main() {
|
|
| 455 |
const fingerprint: ExtractedFingerprint = {
|
| 456 |
app_version: version,
|
| 457 |
build_number: buildNumber,
|
|
|
|
|
|
|
| 458 |
api_base_url: mainJsResults.apiBaseUrl,
|
| 459 |
originator: mainJsResults.originator,
|
| 460 |
models: mainJsResults.models,
|
|
|
|
| 17 |
import { createHash } from "crypto";
|
| 18 |
import { execSync } from "child_process";
|
| 19 |
import yaml from "js-yaml";
|
| 20 |
+
import type { ExtractedFingerprint } from "./types.js";
|
| 21 |
|
| 22 |
const ROOT = resolve(import.meta.dirname, "..");
|
| 23 |
const OUTPUT_PATH = resolve(ROOT, "data/extracted-fingerprint.json");
|
|
|
|
| 37 |
}>;
|
| 38 |
}
|
| 39 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
function sha256(content: string): string {
|
| 41 |
return `sha256:${createHash("sha256").update(content, "utf-8").digest("hex").slice(0, 16)}`;
|
| 42 |
}
|
|
|
|
| 101 |
version: string;
|
| 102 |
buildNumber: string;
|
| 103 |
sparkleFeedUrl: string | null;
|
| 104 |
+
electronVersion: string | null;
|
| 105 |
} {
|
| 106 |
const pkgPath = join(root, "package.json");
|
| 107 |
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
|
|
| 110 |
version: pkg.version ?? "unknown",
|
| 111 |
buildNumber: String(pkg.codexBuildNumber ?? "unknown"),
|
| 112 |
sparkleFeedUrl: pkg.codexSparkleFeedUrl ?? null,
|
| 113 |
+
electronVersion: pkg.devDependencies?.electron ?? null,
|
| 114 |
};
|
| 115 |
}
|
| 116 |
|
|
|
|
| 353 |
|
| 354 |
// Step A: package.json
|
| 355 |
console.log("[extract] Reading package.json...");
|
| 356 |
+
const { version, buildNumber, sparkleFeedUrl, electronVersion } = extractFromPackageJson(asarRoot);
|
| 357 |
+
console.log(` version: ${version}`);
|
| 358 |
+
console.log(` build: ${buildNumber}`);
|
| 359 |
+
console.log(` electron: ${electronVersion ?? "not found"}`);
|
| 360 |
+
|
| 361 |
+
// Resolve Chromium version from Electron version
|
| 362 |
+
let chromiumVersion: string | null = null;
|
| 363 |
+
if (electronVersion) {
|
| 364 |
+
const electronMajor = parseInt(electronVersion.replace(/^[^0-9]*/, ""), 10);
|
| 365 |
+
if (!isNaN(electronMajor)) {
|
| 366 |
+
try {
|
| 367 |
+
const { versions } = await import("electron-to-chromium");
|
| 368 |
+
const versionMap = versions as Record<string, string>;
|
| 369 |
+
// versions keys use "major.minor" format (e.g. "40.0"), try both
|
| 370 |
+
const chromium = versionMap[`${electronMajor}.0`] ?? versionMap[electronMajor.toString()];
|
| 371 |
+
if (chromium) {
|
| 372 |
+
chromiumVersion = chromium;
|
| 373 |
+
console.log(` chromium: ${chromiumVersion} (from electron ${electronMajor})`);
|
| 374 |
+
} else {
|
| 375 |
+
console.warn(`[extract] No Chromium mapping for Electron ${electronMajor}`);
|
| 376 |
+
}
|
| 377 |
+
} catch {
|
| 378 |
+
console.warn("[extract] electron-to-chromium not available, skipping chromium resolution");
|
| 379 |
+
}
|
| 380 |
+
}
|
| 381 |
+
}
|
| 382 |
|
| 383 |
// Step B: main.js (or main-XXXXX.js chunk)
|
| 384 |
console.log("[extract] Loading main.js...");
|
|
|
|
| 458 |
const fingerprint: ExtractedFingerprint = {
|
| 459 |
app_version: version,
|
| 460 |
build_number: buildNumber,
|
| 461 |
+
electron_version: electronVersion,
|
| 462 |
+
chromium_version: chromiumVersion,
|
| 463 |
api_base_url: mainJsResults.apiBaseUrl,
|
| 464 |
originator: mainJsResults.originator,
|
| 465 |
models: mainJsResults.models,
|
src/models/model-fetcher.ts
CHANGED
|
@@ -71,8 +71,11 @@ export function startModelRefresh(
|
|
| 71 |
|
| 72 |
// Initial fetch after short delay
|
| 73 |
_refreshTimer = setTimeout(async () => {
|
| 74 |
-
|
| 75 |
-
|
|
|
|
|
|
|
|
|
|
| 76 |
}, INITIAL_DELAY_MS);
|
| 77 |
|
| 78 |
console.log("[ModelFetcher] Scheduled initial model fetch in 5s");
|
|
|
|
| 71 |
|
| 72 |
// Initial fetch after short delay
|
| 73 |
_refreshTimer = setTimeout(async () => {
|
| 74 |
+
try {
|
| 75 |
+
await fetchModelsFromBackend(accountPool, cookieJar);
|
| 76 |
+
} finally {
|
| 77 |
+
scheduleNext(accountPool, cookieJar);
|
| 78 |
+
}
|
| 79 |
}, INITIAL_DELAY_MS);
|
| 80 |
|
| 81 |
console.log("[ModelFetcher] Scheduled initial model fetch in 5s");
|
src/update-checker.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { mutateYaml } from "./utils/yaml-mutate.js";
|
|
| 15 |
const CONFIG_PATH = resolve(process.cwd(), "config/default.yaml");
|
| 16 |
const STATE_PATH = resolve(process.cwd(), "data/update-state.json");
|
| 17 |
const APPCAST_URL = "https://persistent.oaistatic.com/codex-app-prod/appcast.xml";
|
| 18 |
-
const POLL_INTERVAL_MS =
|
| 19 |
|
| 20 |
export interface UpdateState {
|
| 21 |
last_check: string;
|
|
@@ -77,6 +77,8 @@ function applyVersionUpdate(version: string, build: string): void {
|
|
| 77 |
* Downloads new Codex.app, extracts fingerprint, and applies config updates.
|
| 78 |
* Protected by a lock to prevent concurrent runs.
|
| 79 |
*/
|
|
|
|
|
|
|
| 80 |
function triggerFullUpdate(): void {
|
| 81 |
if (_updateInProgress) {
|
| 82 |
console.log("[UpdateChecker] Full update already in progress, skipping");
|
|
@@ -95,6 +97,12 @@ function triggerFullUpdate(): void {
|
|
| 95 |
},
|
| 96 |
);
|
| 97 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
let output = "";
|
| 99 |
child.stdout?.on("data", (chunk: Buffer) => {
|
| 100 |
output += chunk.toString();
|
|
@@ -104,6 +112,7 @@ function triggerFullUpdate(): void {
|
|
| 104 |
});
|
| 105 |
|
| 106 |
child.on("exit", (code) => {
|
|
|
|
| 107 |
_updateInProgress = false;
|
| 108 |
if (code === 0) {
|
| 109 |
console.log("[UpdateChecker] Full update completed. Reloading config...");
|
|
@@ -125,6 +134,7 @@ function triggerFullUpdate(): void {
|
|
| 125 |
});
|
| 126 |
|
| 127 |
child.on("error", (err) => {
|
|
|
|
| 128 |
_updateInProgress = false;
|
| 129 |
console.error("[UpdateChecker] Failed to spawn full-update:", err.message);
|
| 130 |
});
|
|
|
|
| 15 |
const CONFIG_PATH = resolve(process.cwd(), "config/default.yaml");
|
| 16 |
const STATE_PATH = resolve(process.cwd(), "data/update-state.json");
|
| 17 |
const APPCAST_URL = "https://persistent.oaistatic.com/codex-app-prod/appcast.xml";
|
| 18 |
+
const POLL_INTERVAL_MS = 3 * 24 * 60 * 60 * 1000; // 3 days
|
| 19 |
|
| 20 |
export interface UpdateState {
|
| 21 |
last_check: string;
|
|
|
|
| 77 |
* Downloads new Codex.app, extracts fingerprint, and applies config updates.
|
| 78 |
* Protected by a lock to prevent concurrent runs.
|
| 79 |
*/
|
| 80 |
+
const UPDATE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
| 81 |
+
|
| 82 |
function triggerFullUpdate(): void {
|
| 83 |
if (_updateInProgress) {
|
| 84 |
console.log("[UpdateChecker] Full update already in progress, skipping");
|
|
|
|
| 97 |
},
|
| 98 |
);
|
| 99 |
|
| 100 |
+
// Kill the child if it hangs beyond the timeout
|
| 101 |
+
const killTimer = setTimeout(() => {
|
| 102 |
+
console.warn("[UpdateChecker] Full update timed out, killing child");
|
| 103 |
+
child.kill("SIGTERM");
|
| 104 |
+
}, UPDATE_TIMEOUT_MS);
|
| 105 |
+
|
| 106 |
let output = "";
|
| 107 |
child.stdout?.on("data", (chunk: Buffer) => {
|
| 108 |
output += chunk.toString();
|
|
|
|
| 112 |
});
|
| 113 |
|
| 114 |
child.on("exit", (code) => {
|
| 115 |
+
clearTimeout(killTimer);
|
| 116 |
_updateInProgress = false;
|
| 117 |
if (code === 0) {
|
| 118 |
console.log("[UpdateChecker] Full update completed. Reloading config...");
|
|
|
|
| 134 |
});
|
| 135 |
|
| 136 |
child.on("error", (err) => {
|
| 137 |
+
clearTimeout(killTimer);
|
| 138 |
_updateInProgress = false;
|
| 139 |
console.error("[UpdateChecker] Failed to spawn full-update:", err.message);
|
| 140 |
});
|
web/src/App.tsx
CHANGED
|
@@ -39,7 +39,6 @@ function Dashboard() {
|
|
| 39 |
/>
|
| 40 |
<AnthropicSetup
|
| 41 |
apiKey={status.apiKey}
|
| 42 |
-
models={status.models}
|
| 43 |
selectedModel={status.selectedModel}
|
| 44 |
/>
|
| 45 |
<CodeExamples
|
|
|
|
| 39 |
/>
|
| 40 |
<AnthropicSetup
|
| 41 |
apiKey={status.apiKey}
|
|
|
|
| 42 |
selectedModel={status.selectedModel}
|
| 43 |
/>
|
| 44 |
<CodeExamples
|
web/src/components/AnthropicSetup.tsx
CHANGED
|
@@ -1,24 +1,22 @@
|
|
| 1 |
-
import {
|
| 2 |
import { useT } from "../i18n/context";
|
| 3 |
import { CopyButton } from "./CopyButton";
|
| 4 |
|
| 5 |
interface AnthropicSetupProps {
|
| 6 |
apiKey: string;
|
| 7 |
-
models: string[];
|
| 8 |
selectedModel: string;
|
| 9 |
}
|
| 10 |
|
| 11 |
-
export function AnthropicSetup({ apiKey,
|
| 12 |
const t = useT();
|
| 13 |
-
const [model, setModel] = useState(selectedModel);
|
| 14 |
|
| 15 |
const origin = typeof window !== "undefined" ? window.location.origin : "http://localhost:8080";
|
| 16 |
|
| 17 |
const envLines = useMemo(() => ({
|
| 18 |
ANTHROPIC_BASE_URL: origin,
|
| 19 |
ANTHROPIC_API_KEY: apiKey,
|
| 20 |
-
ANTHROPIC_MODEL:
|
| 21 |
-
}), [origin, apiKey,
|
| 22 |
|
| 23 |
const allEnvText = useMemo(
|
| 24 |
() => Object.entries(envLines).map(([k, v]) => `${k}=${v}`).join("\n"),
|
|
@@ -39,15 +37,6 @@ export function AnthropicSetup({ apiKey, models, selectedModel }: AnthropicSetup
|
|
| 39 |
</svg>
|
| 40 |
<h2 class="text-[0.95rem] font-bold">{t("anthropicSetup")}</h2>
|
| 41 |
</div>
|
| 42 |
-
<select
|
| 43 |
-
class="px-3 py-1.5 text-xs font-mono rounded-lg bg-slate-100 dark:bg-bg-dark border border-gray-200 dark:border-border-dark text-slate-700 dark:text-text-main outline-none"
|
| 44 |
-
value={model}
|
| 45 |
-
onChange={(e) => setModel((e.target as HTMLSelectElement).value)}
|
| 46 |
-
>
|
| 47 |
-
{models.map((m) => (
|
| 48 |
-
<option key={m} value={m}>{m}</option>
|
| 49 |
-
))}
|
| 50 |
-
</select>
|
| 51 |
</div>
|
| 52 |
|
| 53 |
{/* Env vars */}
|
|
|
|
| 1 |
+
import { useMemo, useCallback } from "preact/hooks";
|
| 2 |
import { useT } from "../i18n/context";
|
| 3 |
import { CopyButton } from "./CopyButton";
|
| 4 |
|
| 5 |
interface AnthropicSetupProps {
|
| 6 |
apiKey: string;
|
|
|
|
| 7 |
selectedModel: string;
|
| 8 |
}
|
| 9 |
|
| 10 |
+
export function AnthropicSetup({ apiKey, selectedModel }: AnthropicSetupProps) {
|
| 11 |
const t = useT();
|
|
|
|
| 12 |
|
| 13 |
const origin = typeof window !== "undefined" ? window.location.origin : "http://localhost:8080";
|
| 14 |
|
| 15 |
const envLines = useMemo(() => ({
|
| 16 |
ANTHROPIC_BASE_URL: origin,
|
| 17 |
ANTHROPIC_API_KEY: apiKey,
|
| 18 |
+
ANTHROPIC_MODEL: selectedModel,
|
| 19 |
+
}), [origin, apiKey, selectedModel]);
|
| 20 |
|
| 21 |
const allEnvText = useMemo(
|
| 22 |
() => Object.entries(envLines).map(([k, v]) => `${k}=${v}`).join("\n"),
|
|
|
|
| 37 |
</svg>
|
| 38 |
<h2 class="text-[0.95rem] font-bold">{t("anthropicSetup")}</h2>
|
| 39 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
</div>
|
| 41 |
|
| 42 |
{/* Env vars */}
|