forked from boommilk1996-milk/codex-api-switch
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathCodex_API_切换.app.js
More file actions
354 lines (322 loc) · 12.4 KB
/
Copy pathCodex_API_切换.app.js
File metadata and controls
354 lines (322 loc) · 12.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
// Codex API 切换 — JXA Applet (v2: history sync)
// Calls codex-api-switch Python script, checks Codex is closed before
// switching so conversation history labels can be synced automatically.
const app = Application.currentApplication();
app.includeStandardAdditions = true;
// ── helpers ────────────────────────────────────────────────────────────────
function logLine(msg) {
// Append to ~/Library/Logs/codex-api-switch-app.log for diagnostics.
try {
const home = app
.doShellScript('printf "%s" "$HOME"')
.trim();
const stamp = new Date().toISOString();
const safe = String(msg).replace(/'/g, "'\\''");
app.doShellScript(
`printf '%s\\n' '[${stamp}] ${safe}' >> '${home}/Library/Logs/codex-api-switch-app.log' 2>/dev/null || true`
);
} catch (_) {
// logging must never break the app
}
}
function sh(cmd) {
// Locate the switcher: $CODEX_API_SWITCH override -> PATH -> common spots.
// doShellScript in an applet runs with a minimal PATH, so we probe
// absolute locations explicitly before falling back to a bare name.
const override = app
.doShellScript('printf "%s" "$CODEX_API_SWITCH"')
.trim();
const home = app.doShellScript('printf "%s" "$HOME"').trim();
const candidates = override
? [override]
: [
"/usr/local/bin/codex-api-switch",
"/opt/homebrew/bin/codex-api-switch",
`${home}/.local/bin/codex-api-switch`,
"codex-api-switch",
];
let found = "";
for (const candidate of candidates) {
const q = "'" + String(candidate).replace(/'/g, "'\\''") + "'";
const ok = app
.doShellScript(
`if command -v ${q} >/dev/null 2>&1; then echo yes; else echo no; fi`
)
.trim();
if (ok === "yes") {
found = candidate;
break;
}
}
const q = "'" + found.replace(/'/g, "'\\''") + "'";
// Run switcher command, capturing stdout + stderr.
// Redirect stderr to stdout so errors are visible in the result.
return app.doShellScript(`${q} ${cmd} 2>&1`);
}
function shTry(cmd) {
try {
return { ok: true, out: sh(cmd) };
} catch (e) {
return { ok: false, error: String(e.message || e) };
}
}
function shJSON(cmd) {
const r = shTry(cmd);
if (!r.ok) {
logLine("sh failed for " + cmd + ": " + r.error);
return null;
}
try {
return JSON.parse(r.out);
} catch (e) {
logLine("JSON parse failed for " + cmd + ": " + e.message +
" | raw=" + r.out.slice(0, 400));
return null;
}
}
function status() { return shJSON("status --json"); }
function isRunning() {
try {
return sh("is-running").trim() === "running";
} catch (_) {
return false;
}
}
function emojiProvider(s) {
if (!s || s === "openai") return "⚡ OpenAI";
return "🧠 DeepSeek";
}
function maskKey(key) {
if (!key || key.length <= 10) return key || "(未配置)";
return key.slice(0, 5) + "***" + key.slice(-4);
}
// ── dialogs ────────────────────────────────────────────────────────────────
function showError(title, msg) {
app.displayAlert(title, {
message: String(msg).slice(0, 500),
as: "critical",
buttons: ["好"],
defaultButton: "好",
});
}
function showInfo(title, msg) {
app.displayDialog(String(msg).slice(0, 1600), {
withTitle: title,
buttons: ["好"],
defaultButton: "好",
});
}
// Ask the user to quit Codex first when it is still running.
// Returns true when it is safe to continue switching.
function confirmCodexQuit() {
if (!isRunning()) return true;
let choice;
try {
choice = app.displayDialog(
"Codex 正在运行,此时切换不会同步历史对话。\n\n" +
"请先完全退出 Codex(Cmd+Q),然后重新双击本应用点击切换,将自动把全部历史对话搬到新服务商。\n\n" +
"如果选择「仅切换配置」,历史对话保持原标签,之后可手动运行 codex-api-switch sync 同步。",
{
withTitle: "请先退出 Codex",
buttons: ["我先退出 Codex", "仅切换配置", "取消"],
defaultButton: "我先退出 Codex",
cancelButton: "取消",
}
).buttonReturned;
} catch (_) {
return false; // cancelled
}
if (choice === "我先退出 Codex") {
showInfo("已了解", "退出 Codex 后,重新双击本应用并点击切换即可,历史对话会自动全部显示。");
return false;
}
return choice === "仅切换配置";
}
// Ask which DeepSeek model to use (V4-Pro-0813 / V4-Flash-0731).
// currentModel: "deepseek-v4-pro", "deepseek-v4-flash", or null (fresh switch).
// Returns "pro", "flash", or null when cancelled.
function chooseDeepseekModel(currentModel) {
const isPro = currentModel !== "deepseek-v4-flash";
const proButton = "V4 Pro (0813)";
const flashButton = "V4 Flash (0731)";
let picked;
try {
picked = app.displayDialog(
"选择要使用的 DeepSeek 模型:\n\n" +
"V4 Pro (0813) —— 更强,适合复杂任务(默认)\n" +
"V4 Flash (0731) —— 更快更省,适合日常(可选)",
{
withTitle: "选择 DeepSeek 模型",
buttons: [proButton, flashButton, "取消"],
defaultButton: isPro ? proButton : flashButton,
cancelButton: "取消",
}
).buttonReturned;
} catch (_) {
return null; // cancelled
}
if (picked === proButton) return "pro";
if (picked === flashButton) return "flash";
return null;
}
// ── main ───────────────────────────────────────────────────────────────────
function main() {
// 1. Get current state
logLine("app launched");
let st;
st = status();
if (!st) {
const detail = shTry("status --json");
logLine("status returned null; raw call ok=" + detail.ok);
showError(
"获取状态失败",
"无法解析 Codex 配置状态。\n\n" +
"详细日志已写入 ~/Library/Logs/codex-api-switch-app.log\n" +
"请把该日志内容发给西西,或直接运行:\n" +
"codex-api-switch status --json\n" +
"看看是否输出正常 JSON。"
);
return;
}
// 2. Build status summary
const providerIcon = emojiProvider(st.provider);
const providerLabel = st.is_deepseek ? "DeepSeek" : "OpenAI";
const modelName = st.model || "(未设置)";
const deepseekInfo = st.deepseek_configured
? `DeepSeek: 已配置 (${st.deepseek_base_url || ""})`
: "DeepSeek: 未配置";
const keyInfo = st.deepseek_key_available
? "DeepSeek Key: 已保存(切换时无需再填)"
: "DeepSeek Key: 未保存(首次切换时填写一次即可)";
const restoreInfo = st.restore_available ? "恢复点: 可用" : "恢复点: 无";
const runningInfo = st.codex_running ? "Codex: 运行中(切换前请先退出)" : "Codex: 未运行(可安全切换)";
const statusLines = [
`当前: ${providerLabel} · ${modelName}`,
deepseekInfo,
keyInfo,
restoreInfo,
runningInfo,
"",
"切换后需重启 Codex 桌面端或新开 CLI 会话。",
].join("\n");
// 3. Choose action — buttons depend on current state
let buttons;
let defaultButton;
if (st.is_deepseek) {
buttons = ["切回 OpenAI", "切换 Pro/Flash", "取消"];
defaultButton = "切换 Pro/Flash";
} else {
buttons = ["切到 DeepSeek", "查看详情", "取消"];
defaultButton = "切到 DeepSeek";
}
let choice;
try {
choice = app.displayDialog(statusLines, {
withTitle: "Codex API 切换",
buttons: buttons,
defaultButton: defaultButton,
cancelButton: "取消",
}).buttonReturned;
} catch (_) {
// User cancelled
return;
}
// 4. Details (OpenAI state only)
if (choice === "查看详情") {
let detail;
try {
detail = sh("status");
} catch (e) {
showError("获取详情失败", e.message || e);
return;
}
showInfo("Codex API 详情", detail);
return;
}
// 5. Switching — require Codex to be quit so history sync can run
if (choice === "切换 Pro/Flash") {
const model = chooseDeepseekModel(st.model);
if (!model) return;
let result;
try {
result = sh(`deepseek --model ${model}`);
} catch (e) {
showError("切换失败", `切换 DeepSeek 模型时出错:\n\n${e.message || e}`);
return;
}
showInfo("已切换 DeepSeek 模型",
`${result}\n\n模型: ${model === "pro" ? "V4 Pro (0813)" : "V4 Flash (0731)"}\n请重启 Codex 后使用。`);
return;
}
const switchingToDeepseek = choice === "切到 DeepSeek";
if (!confirmCodexQuit()) return;
if (switchingToDeepseek) {
// If not configured, ask for API key first
let apiKeyArg = "";
if (!st.deepseek_key_available) {
let keyInput;
try {
keyInput = app.displayDialog(
"首次切换需要 DeepSeek API Key。\nKey 只会保存一次,之后切换不再需要填写。\n\n请输入你的 DeepSeek API Key(以 sk- 开头):",
{
withTitle: "配置 DeepSeek API Key",
defaultAnswer: "",
buttons: ["确认切换", "取消"],
defaultButton: "确认切换",
cancelButton: "取消",
hiddenAnswer: true,
}
);
} catch (_) {
return; // user cancelled
}
const key = (keyInput.textReturned || "").trim();
if (!key) {
showError("未输入 API Key", "需要提供 DeepSeek API Key 才能切换。");
return;
}
if (!key.startsWith("sk-")) {
showError("Key 格式错误", "DeepSeek API Key 应以 sk- 开头。未做任何更改。");
return;
}
apiKeyArg = ` --api-key '${key.replace(/'/g, "'\\''")}'`;
}
const model = chooseDeepseekModel(null);
if (!model) return;
let result;
try {
result = sh(`deepseek --model ${model}${apiKeyArg}`);
} catch (e) {
showError("切换失败", `切到 DeepSeek 时出错:\n\n${e.message || e}`);
return;
}
// Check if it actually switched by re-reading status
try { st = status(); } catch (_) { st = null; }
const newModel = st ? st.model : "deepseek-v4-flash";
showInfo("已切到 DeepSeek",
`${result}\n\n当前模型: ${newModel}\n(V4 Pro 0813 / V4 Flash 0731)\n请重启 Codex 后使用。`);
return;
}
if (choice === "切回 OpenAI") {
let result;
try {
result = sh("openai");
} catch (e) {
showError("切换失败", `切回 OpenAI 时出错:\n\n${e.message || e}`);
return;
}
try { st = status(); } catch (_) { st = null; }
const newModel = st ? st.model : "gpt-5.5";
showInfo("已切回 OpenAI",
`${result}\n\n当前模型: ${newModel}\n请重启 Codex 后使用。`);
return;
}
}
// ── entry ──────────────────────────────────────────────────────────────────
try {
main();
logLine("app finished");
} catch (e) {
logLine("top-level error: " + String(e.message || e));
showError("Codex API 切换出错", e.message || String(e));
}