@kkmila/cpc 1.0.4 → 1.0.5
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 +34 -0
- package/package.json +1 -1
- package/registry.yaml +1 -0
- package/src/commands/provider.mjs +370 -88
- package/src/lib/agents.mjs +94 -1
- package/src/lib/baseline-models.mjs +55 -3
- package/src/lib/codex-toml.mjs +175 -0
- package/src/lib/env.mjs +115 -0
package/README.md
CHANGED
|
@@ -58,6 +58,29 @@ cpc add
|
|
|
58
58
|
cpc install openai --agent pi
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
### 安装到 Codex Agent(全局默认 provider)
|
|
62
|
+
|
|
63
|
+
Codex 一次只能使用一个模型(写入 `~/.codex/config.toml` 顶层的 `model` + `model_provider`)。
|
|
64
|
+
`cpc install` 会把 API key 跨平台落地到一个环境变量,并在 `[model_providers.<name>]` 段用 `env_key` 引用它,
|
|
65
|
+
装完直接 `codex` 即走该 provider,无需 `--profile`。
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
# 非交互:直接指定单模型 + wire_api
|
|
69
|
+
cpc install tele_home --agent codex --model qwen3.6-plus --wire-api responses
|
|
70
|
+
|
|
71
|
+
# 交互:多模型时会弹单选让你挑一个作为默认
|
|
72
|
+
# 并询问 wire_api(responses = Codex Responses 原生协议;chat_completions = 标准 OpenAI 兼容)
|
|
73
|
+
cpc install tele_home --agent codex
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
可选 `--reasoning-effort minimal|low|medium|high` 一并写入 `model_reasoning_effort`。
|
|
77
|
+
|
|
78
|
+
行为说明:
|
|
79
|
+
- 原始 `config.toml` 会先备份为 `config.toml.cpc.bak`。
|
|
80
|
+
- 只 upsert 顶层 `model_provider` / `model`(及可选 `model_reasoning_effort`)和 `[model_providers.<name>]` 段内字段,
|
|
81
|
+
不会误伤 `model_personality` 等同前缀 key,也不会破坏 `[features]` / `[mcp_servers.*]` / `[desktop]` 等段。
|
|
82
|
+
- `cpc list-installed --agent codex` 可查看当前 provider、model、wire_api、env_key。
|
|
83
|
+
|
|
61
84
|
### 测试连接
|
|
62
85
|
|
|
63
86
|
```bash
|
|
@@ -76,6 +99,17 @@ cpc test openai
|
|
|
76
99
|
- cursor
|
|
77
100
|
- opencode
|
|
78
101
|
|
|
102
|
+
### Codex 的特殊性
|
|
103
|
+
|
|
104
|
+
- 一次只能用一个模型:`cpc install --agent codex` 会把选中的模型设为全局默认,
|
|
105
|
+
多模型时强制让用户挑一个;不像 pi/openclaw 能一次装多个。
|
|
106
|
+
- provider 的 API key 通过 **环境变量** 引用(`env_key`),cpc 跨平台落地:
|
|
107
|
+
- Linux / macOS:按 `$SHELL` 写入 `~/.zshrc` 或 `~/.bashrc`(幂等,已存在则替换值)
|
|
108
|
+
- Windows:用 `setx` 写入用户环境变量
|
|
109
|
+
- 环境变量名规范为 `CPC_<NAME>_API_KEY`(大写、非字母数字转 `_`)
|
|
110
|
+
- 装完新开终端自动加载;当前终端需 `source ~/.bashrc`(或对应 rc 文件)。
|
|
111
|
+
`cpc install` 输出会明确提示该路径。
|
|
112
|
+
|
|
79
113
|
## 配置文件
|
|
80
114
|
|
|
81
115
|
- `~/.config/cpc/providers.json` - 提供商配置
|
package/package.json
CHANGED
package/registry.yaml
CHANGED
|
@@ -4,8 +4,10 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
|
4
4
|
import { resolve, join } from "node:path";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import yaml from "js-yaml";
|
|
7
|
-
import { loadRegistry, getAgents, resolveAgentPaths } from "../lib/agents.mjs";
|
|
7
|
+
import { loadRegistry, getAgents, resolveAgentPaths, detectCurrentAgent } from "../lib/agents.mjs";
|
|
8
8
|
import { enrichModelWithBaseline } from "../lib/baseline-models.mjs";
|
|
9
|
+
import { persistEnvVar, envKeyNameForProvider } from "../lib/env.mjs";
|
|
10
|
+
import { writeCodexProviderConfig, readCodexProviderInfo } from "../lib/codex-toml.mjs";
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* Get the config directory path (~/.config/cpc)
|
|
@@ -121,10 +123,10 @@ const MODEL_DEFAULTS = {
|
|
|
121
123
|
"gpt-4o": { contextWindow: 128000, maxTokens: 16384, reasoning: false },
|
|
122
124
|
"gpt-4o-mini": { contextWindow: 128000, maxTokens: 16384, reasoning: false },
|
|
123
125
|
"gpt-3.5-turbo": { contextWindow: 16385, maxTokens: 4096, reasoning: false },
|
|
124
|
-
o1: { contextWindow: 200000, maxTokens: 100000, reasoning:
|
|
125
|
-
"o1-mini": { contextWindow: 128000, maxTokens: 65536, reasoning:
|
|
126
|
-
"o1-preview": { contextWindow: 128000, maxTokens: 32768, reasoning:
|
|
127
|
-
"o3-mini": { contextWindow: 200000, maxTokens: 100000, reasoning:
|
|
126
|
+
o1: { contextWindow: 200000, maxTokens: 100000, reasoning: false },
|
|
127
|
+
"o1-mini": { contextWindow: 128000, maxTokens: 65536, reasoning: false },
|
|
128
|
+
"o1-preview": { contextWindow: 128000, maxTokens: 32768, reasoning: false },
|
|
129
|
+
"o3-mini": { contextWindow: 200000, maxTokens: 100000, reasoning: false },
|
|
128
130
|
|
|
129
131
|
// Anthropic
|
|
130
132
|
"claude-3-opus-20240229": {
|
|
@@ -159,7 +161,7 @@ const MODEL_DEFAULTS = {
|
|
|
159
161
|
"deepseek-reasoner": {
|
|
160
162
|
contextWindow: 65536,
|
|
161
163
|
maxTokens: 8192,
|
|
162
|
-
reasoning:
|
|
164
|
+
reasoning: false,
|
|
163
165
|
},
|
|
164
166
|
|
|
165
167
|
// Qwen
|
|
@@ -167,6 +169,10 @@ const MODEL_DEFAULTS = {
|
|
|
167
169
|
"qwen-plus": { contextWindow: 32768, maxTokens: 8192, reasoning: false },
|
|
168
170
|
"qwen-max": { contextWindow: 32768, maxTokens: 8192, reasoning: false },
|
|
169
171
|
|
|
172
|
+
// 智谱 GLM-5.2
|
|
173
|
+
"glm-5.2": { contextWindow: 1000000, maxTokens: 131072, reasoning: false },
|
|
174
|
+
"glm-5.2-anthropic": { contextWindow: 1000000, maxTokens: 131072, reasoning: false },
|
|
175
|
+
|
|
170
176
|
// 通用默认值
|
|
171
177
|
default: { contextWindow: 32768, maxTokens: 4096, reasoning: false },
|
|
172
178
|
};
|
|
@@ -188,16 +194,9 @@ function getModelDefaults(modelId) {
|
|
|
188
194
|
}
|
|
189
195
|
}
|
|
190
196
|
|
|
191
|
-
// 检测是否是推理模型
|
|
192
|
-
const isReasoning =
|
|
193
|
-
lowerId.includes("reason") ||
|
|
194
|
-
lowerId.includes("o1") ||
|
|
195
|
-
lowerId.includes("o3") ||
|
|
196
|
-
lowerId.includes("think");
|
|
197
|
-
|
|
198
197
|
return {
|
|
199
198
|
...MODEL_DEFAULTS["default"],
|
|
200
|
-
reasoning:
|
|
199
|
+
reasoning: false,
|
|
201
200
|
};
|
|
202
201
|
}
|
|
203
202
|
|
|
@@ -323,24 +322,26 @@ function getAgentProviderConfig(agentId, provider) {
|
|
|
323
322
|
},
|
|
324
323
|
};
|
|
325
324
|
|
|
326
|
-
case "codex":
|
|
325
|
+
case "codex": {
|
|
326
|
+
// codex 一次只能用一个模型(顶层 model + model_provider)。
|
|
327
|
+
// provider 段用 env_key 引用环境变量,key 本身由 cpc 跨平台落地。
|
|
328
|
+
const envKey = envKeyNameForProvider(provider.name);
|
|
327
329
|
return {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
+
codex: {
|
|
331
|
+
providerName: provider.name,
|
|
332
|
+
baseUrl: provider.baseUrl,
|
|
333
|
+
wireApi:
|
|
334
|
+
provider.api === "anthropic-messages"
|
|
335
|
+
? "chat_completions"
|
|
336
|
+
: provider.api === "openai-completions"
|
|
337
|
+
? "chat_completions"
|
|
338
|
+
: "responses",
|
|
339
|
+
envKey,
|
|
330
340
|
model: provider.models[0]?.id || "default",
|
|
331
|
-
|
|
332
|
-
[provider.name]: {
|
|
333
|
-
name: provider.name,
|
|
334
|
-
base_url: provider.baseUrl,
|
|
335
|
-
wire_api:
|
|
336
|
-
provider.api === "anthropic-messages"
|
|
337
|
-
? "chat_completions"
|
|
338
|
-
: "responses",
|
|
339
|
-
requires_openai_auth: false,
|
|
340
|
-
},
|
|
341
|
-
},
|
|
341
|
+
apiKey: provider.apiKey,
|
|
342
342
|
},
|
|
343
343
|
};
|
|
344
|
+
}
|
|
344
345
|
|
|
345
346
|
case "openclaw":
|
|
346
347
|
return {
|
|
@@ -417,7 +418,7 @@ function getAgentProviderConfig(agentId, provider) {
|
|
|
417
418
|
/**
|
|
418
419
|
* Install provider configuration to agent
|
|
419
420
|
*/
|
|
420
|
-
function installProviderToAgent(agentId, provider, agentHome) {
|
|
421
|
+
function installProviderToAgent(agentId, provider, agentHome, opts = {}) {
|
|
421
422
|
const config = getAgentProviderConfig(agentId, provider);
|
|
422
423
|
if (!config) {
|
|
423
424
|
console.log(chalk.yellow(` ⚠ 未知 agent: ${agentId}`));
|
|
@@ -442,7 +443,13 @@ function installProviderToAgent(agentId, provider, agentHome) {
|
|
|
442
443
|
const modelsPath = join(agentHome, "models.json");
|
|
443
444
|
let models = { providers: {} };
|
|
444
445
|
if (existsSync(modelsPath)) {
|
|
445
|
-
|
|
446
|
+
try {
|
|
447
|
+
const content = readFileSync(modelsPath, "utf8").trim();
|
|
448
|
+
if (content) models = JSON.parse(content);
|
|
449
|
+
} catch {
|
|
450
|
+
// 空文件或损坏的 JSON,重置为默认值
|
|
451
|
+
models = { providers: {} };
|
|
452
|
+
}
|
|
446
453
|
}
|
|
447
454
|
models.providers = {
|
|
448
455
|
...models.providers,
|
|
@@ -467,38 +474,55 @@ function installProviderToAgent(agentId, provider, agentHome) {
|
|
|
467
474
|
|
|
468
475
|
case "codex": {
|
|
469
476
|
const configPath = join(agentHome, "config.toml");
|
|
470
|
-
|
|
471
|
-
if (existsSync(configPath)) {
|
|
472
|
-
configContent = readFileSync(configPath, "utf8");
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
const providerSection = `\n[model_providers.${provider.name}]\nname = "${provider.name}"\nbase_url = "${provider.baseUrl}"\nwire_api = "${provider.api === "anthropic-messages" ? "chat_completions" : "responses"}"\nrequires_openai_auth = false\n`;
|
|
476
|
-
|
|
477
|
-
const modelLine = `model_provider = "${provider.name}"\n`;
|
|
478
|
-
const modelIdLine = `model = "${provider.models[0]?.id || "default"}"\n`;
|
|
477
|
+
const c = config.codex;
|
|
479
478
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
479
|
+
// 1) 跨平台落地 API key 到环境变量
|
|
480
|
+
let envResult;
|
|
481
|
+
try {
|
|
482
|
+
envResult = persistEnvVar(c.envKey, c.apiKey);
|
|
483
|
+
} catch (err) {
|
|
484
|
+
console.error(
|
|
485
|
+
chalk.red(` ✗ 落地环境变量 ${c.envKey} 失败: ${err.message}`),
|
|
484
486
|
);
|
|
485
|
-
|
|
486
|
-
configContent = modelLine + configContent;
|
|
487
|
+
return false;
|
|
487
488
|
}
|
|
488
489
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
490
|
+
// 2) 稳健写入 config.toml(顶层 + provider 段,备份原文件)
|
|
491
|
+
try {
|
|
492
|
+
const { backed } = writeCodexProviderConfig(configPath, {
|
|
493
|
+
providerName: c.providerName,
|
|
494
|
+
baseUrl: c.baseUrl,
|
|
495
|
+
wireApi: opts.wireApi || c.wireApi,
|
|
496
|
+
envKey: c.envKey,
|
|
497
|
+
model: c.model,
|
|
498
|
+
modelReasoningEffort: opts.modelReasoningEffort,
|
|
499
|
+
});
|
|
500
|
+
console.log(chalk.green(` ✓ 更新 Codex config.toml`));
|
|
501
|
+
if (backed) {
|
|
502
|
+
console.log(chalk.dim(` • 备份: ${backed}`));
|
|
503
|
+
}
|
|
504
|
+
console.log(
|
|
505
|
+
chalk.dim(
|
|
506
|
+
` • env_key=$${c.envKey} (${envResult.method}${envResult.target ? ": " + envResult.target : ""}${envResult.created ? ", 新增" : ", 已存在"})`,
|
|
507
|
+
),
|
|
508
|
+
);
|
|
509
|
+
if (process.platform !== "win32") {
|
|
510
|
+
console.log(
|
|
511
|
+
chalk.dim(
|
|
512
|
+
` • 新终端自动加载; 当前终端需执行: source ${envResult.target}`,
|
|
513
|
+
),
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
console.log(
|
|
517
|
+
chalk.dim(` • 启动: codex (全局默认即 ${c.providerName}/${c.model})`),
|
|
518
|
+
);
|
|
519
|
+
return true;
|
|
520
|
+
} catch (err) {
|
|
521
|
+
console.error(
|
|
522
|
+
chalk.red(` ✗ 更新 ${agentId} 失败: ${err.message}`),
|
|
523
|
+
);
|
|
524
|
+
return false;
|
|
497
525
|
}
|
|
498
|
-
|
|
499
|
-
writeFileSync(configPath, configContent);
|
|
500
|
-
console.log(chalk.green(` ✓ 更新 Codex config.toml`));
|
|
501
|
-
return true;
|
|
502
526
|
}
|
|
503
527
|
|
|
504
528
|
case "openclaw": {
|
|
@@ -563,15 +587,9 @@ function installProviderToAgent(agentId, provider, agentHome) {
|
|
|
563
587
|
* Check if an agent is installed by looking at its home directory
|
|
564
588
|
*/
|
|
565
589
|
function isAgentInstalled(agentId, agents) {
|
|
566
|
-
const agent = agents[agentId];
|
|
590
|
+
const agent = resolveAgentPaths(agents[agentId]);
|
|
567
591
|
if (!agent || !agent.home) return false;
|
|
568
|
-
|
|
569
|
-
// Expand ~ to home directory
|
|
570
|
-
const home = agent.home.replace(
|
|
571
|
-
/^~/,
|
|
572
|
-
process.env.HOME || process.env.USERPROFILE,
|
|
573
|
-
);
|
|
574
|
-
return existsSync(home);
|
|
592
|
+
return existsSync(agent.home);
|
|
575
593
|
}
|
|
576
594
|
|
|
577
595
|
export function registerProvider(program, repoRoot) {
|
|
@@ -1076,6 +1094,192 @@ export function registerProvider(program, repoRoot) {
|
|
|
1076
1094
|
);
|
|
1077
1095
|
});
|
|
1078
1096
|
|
|
1097
|
+
// ── cpc update ───────────────────────────────────────────────
|
|
1098
|
+
program
|
|
1099
|
+
.command("update [providerName]")
|
|
1100
|
+
.description("重新从 API 拉取模型并更新 providers.json + 已安装的 agent")
|
|
1101
|
+
.option("--all", "选择全部模型")
|
|
1102
|
+
.option("--agent <agent>", "指定目标 agent")
|
|
1103
|
+
.action(async (providerName, opts) => {
|
|
1104
|
+
const providersData = loadProviders();
|
|
1105
|
+
const providers = Object.entries(providersData.providers);
|
|
1106
|
+
|
|
1107
|
+
if (providers.length === 0) {
|
|
1108
|
+
console.log(chalk.yellow("No providers configured."));
|
|
1109
|
+
console.log(chalk.dim("Use `cpc add` to add a new provider first."));
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// Step 1: 选择 provider
|
|
1114
|
+
if (!providerName) {
|
|
1115
|
+
providerName = await select({
|
|
1116
|
+
message: "Select provider to update:",
|
|
1117
|
+
choices: providers.map(([name, config]) => ({
|
|
1118
|
+
name: `${name.padEnd(20)} (${config.models?.length || 0} models)`,
|
|
1119
|
+
value: name,
|
|
1120
|
+
})),
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
const provider = providersData.providers[providerName];
|
|
1125
|
+
if (!provider) {
|
|
1126
|
+
console.error(chalk.red(`Provider "${providerName}" not found.`));
|
|
1127
|
+
process.exit(1);
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// Step 2: 从 API 拉取模型
|
|
1131
|
+
console.log(chalk.dim(`\nFetching models from ${provider.baseUrl}...`));
|
|
1132
|
+
const result = await fetchModelsFromAPI(provider.baseUrl, provider.apiKey, provider.api);
|
|
1133
|
+
|
|
1134
|
+
if (!result.success) {
|
|
1135
|
+
console.log(chalk.red(`✗ Failed to fetch models: ${result.error}`));
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
console.log(chalk.green(`✓ Found ${result.models.length} models from API`));
|
|
1140
|
+
|
|
1141
|
+
// Step 3: 选择模型
|
|
1142
|
+
const oldIds = (provider.models || []).map((m) => m.id);
|
|
1143
|
+
const newIds = result.models.map((m) => m.id);
|
|
1144
|
+
|
|
1145
|
+
// 报告新增和移除的模型
|
|
1146
|
+
const addedIds = newIds.filter((id) => !oldIds.includes(id));
|
|
1147
|
+
const removedIds = oldIds.filter((id) => !newIds.includes(id));
|
|
1148
|
+
const keptIds = oldIds.filter((id) => newIds.includes(id));
|
|
1149
|
+
|
|
1150
|
+
if (addedIds.length > 0) {
|
|
1151
|
+
console.log(chalk.cyan(` + ${addedIds.length} new model(s): ${addedIds.join(", ")}`));
|
|
1152
|
+
}
|
|
1153
|
+
if (removedIds.length > 0) {
|
|
1154
|
+
console.log(chalk.yellow(` - ${removedIds.length} removed model(s): ${removedIds.join(", ")}`));
|
|
1155
|
+
}
|
|
1156
|
+
if (keptIds.length > 0) {
|
|
1157
|
+
console.log(chalk.dim(` = ${keptIds.length} kept model(s): ${keptIds.join(", ")}`));
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// 让用户选择要安装的模型
|
|
1161
|
+
let selectedIds;
|
|
1162
|
+
if (opts.all) {
|
|
1163
|
+
selectedIds = result.models.map((m) => m.id);
|
|
1164
|
+
console.log(chalk.dim(`Auto-selected all ${selectedIds.length} models`));
|
|
1165
|
+
} else {
|
|
1166
|
+
selectedIds = await checkbox({
|
|
1167
|
+
message: "Select models to install:",
|
|
1168
|
+
choices: result.models.map((m) => {
|
|
1169
|
+
const isNew = !oldIds.includes(m.id);
|
|
1170
|
+
return {
|
|
1171
|
+
name: `${isNew ? chalk.cyan("+ ") : chalk.dim(" ")}${m.id.padEnd(40)} ctx:${m.contextWindow || "?"}`,
|
|
1172
|
+
value: m.id,
|
|
1173
|
+
checked: true,
|
|
1174
|
+
};
|
|
1175
|
+
}),
|
|
1176
|
+
instructions: {
|
|
1177
|
+
navigator: "↑↓ navigate",
|
|
1178
|
+
select: "space select",
|
|
1179
|
+
all: "a toggle all",
|
|
1180
|
+
},
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
if (selectedIds.length === 0) {
|
|
1185
|
+
console.log(chalk.yellow("No models selected. Cancelled."));
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
// Step 4: 用 baseline + MODEL_DEFAULTS 补全模型属性
|
|
1190
|
+
const updatedModels = [];
|
|
1191
|
+
for (const modelId of selectedIds) {
|
|
1192
|
+
const apiModel = result.models.find((m) => m.id === modelId);
|
|
1193
|
+
const defaults = getModelDefaults(modelId);
|
|
1194
|
+
const configured = enrichModelWithBaseline({
|
|
1195
|
+
id: modelId,
|
|
1196
|
+
name: apiModel?.name || modelId,
|
|
1197
|
+
contextWindow: apiModel?.contextWindow || defaults.contextWindow,
|
|
1198
|
+
maxTokens: apiModel?.maxTokens || defaults.maxTokens,
|
|
1199
|
+
reasoning: false,
|
|
1200
|
+
input: apiModel?.input || defaults.input || ["text"],
|
|
1201
|
+
});
|
|
1202
|
+
if (configured.description) {
|
|
1203
|
+
console.log(chalk.dim(` ↪ ${modelId}: baseline 命中, contextWindow=${configured.contextWindow}, input=[${configured.input.join(",")}]`));
|
|
1204
|
+
}
|
|
1205
|
+
updatedModels.push(configured);
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// Step 5: 更新 providers.json
|
|
1209
|
+
provider.models = updatedModels;
|
|
1210
|
+
provider.updatedAt = new Date().toISOString();
|
|
1211
|
+
saveProviders(providersData);
|
|
1212
|
+
console.log(chalk.green(`\n✓ Provider "${providerName}" updated: ${updatedModels.length} model(s)`));
|
|
1213
|
+
|
|
1214
|
+
// Step 6: 更新已安装的 agent
|
|
1215
|
+
const registry = loadRegistry(repoRoot);
|
|
1216
|
+
const agents = getAgents(registry);
|
|
1217
|
+
const resolvedAgents = Object.fromEntries(
|
|
1218
|
+
Object.entries(agents).map(([id, a]) => [id, resolveAgentPaths(a)]),
|
|
1219
|
+
);
|
|
1220
|
+
const installProvider = { ...provider, models: updatedModels };
|
|
1221
|
+
|
|
1222
|
+
// 找出哪些 agent 之前安装过这个 provider
|
|
1223
|
+
let agentIds;
|
|
1224
|
+
if (opts.agent) {
|
|
1225
|
+
agentIds = opts.agent === "all" ? Object.keys(agents) : [opts.agent];
|
|
1226
|
+
} else {
|
|
1227
|
+
// 只显示 pi 和 opencode,默认不勾选
|
|
1228
|
+
const visibleAgentIds = Object.keys(resolvedAgents)
|
|
1229
|
+
.filter((id) => id === "pi" || id === "opencode");
|
|
1230
|
+
|
|
1231
|
+
if (visibleAgentIds.length === 0) {
|
|
1232
|
+
console.log(chalk.dim("\nNo pi or opencode agents found. Skipping agent update."));
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
agentIds = await checkbox({
|
|
1237
|
+
message: "Select target agents to update:",
|
|
1238
|
+
choices: visibleAgentIds.map((id) => {
|
|
1239
|
+
const a = resolvedAgents[id];
|
|
1240
|
+
const installed = isAgentInstalled(id, resolvedAgents);
|
|
1241
|
+
const mark = installed ? " (installed)" : "";
|
|
1242
|
+
return {
|
|
1243
|
+
name: `${id.padEnd(14)} ${a.description || ""}${mark}`,
|
|
1244
|
+
value: id,
|
|
1245
|
+
checked: false,
|
|
1246
|
+
};
|
|
1247
|
+
}),
|
|
1248
|
+
instructions: {
|
|
1249
|
+
navigator: "↑↓ navigate",
|
|
1250
|
+
select: "space select",
|
|
1251
|
+
all: "a toggle all",
|
|
1252
|
+
},
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
if (agentIds.length === 0) {
|
|
1257
|
+
console.log(chalk.yellow("No agents selected. Skipping agent update."));
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
console.log();
|
|
1262
|
+
let successCount = 0;
|
|
1263
|
+
for (const aid of agentIds) {
|
|
1264
|
+
const agent = resolvedAgents[aid];
|
|
1265
|
+
if (!agent) {
|
|
1266
|
+
console.log(chalk.red(`Unknown agent: ${aid}`));
|
|
1267
|
+
continue;
|
|
1268
|
+
}
|
|
1269
|
+
console.log(chalk.bold(`→ ${aid}`));
|
|
1270
|
+
console.log(chalk.dim(` ${updatedModels.length} model(s)`));
|
|
1271
|
+
const success = installProviderToAgent(aid, installProvider, agent.home);
|
|
1272
|
+
if (success) successCount++;
|
|
1273
|
+
console.log();
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
if (successCount > 0) {
|
|
1277
|
+
console.log(chalk.green.bold(`✓ Provider updated in ${successCount} agent(s)!`));
|
|
1278
|
+
} else {
|
|
1279
|
+
console.log(chalk.yellow("No agents were updated."));
|
|
1280
|
+
}
|
|
1281
|
+
});
|
|
1282
|
+
|
|
1079
1283
|
// ── cpc remove ───────────────────────────────────────────────
|
|
1080
1284
|
program
|
|
1081
1285
|
.command("remove [name]")
|
|
@@ -1128,6 +1332,8 @@ export function registerProvider(program, repoRoot) {
|
|
|
1128
1332
|
.option("--model <model>", "Install only specific model")
|
|
1129
1333
|
.option("--all", "Install all models")
|
|
1130
1334
|
.option("--first", "Install only first model")
|
|
1335
|
+
.option("--wire-api <api>", "Codex wire_api (responses | chat_completions)")
|
|
1336
|
+
.option("--reasoning-effort <level>", "Codex model_reasoning_effort (minimal|low|medium|high)")
|
|
1131
1337
|
.action(async (providerName, opts) => {
|
|
1132
1338
|
const registry = loadRegistry(repoRoot);
|
|
1133
1339
|
const agents = getAgents(registry);
|
|
@@ -1209,16 +1415,20 @@ export function registerProvider(program, repoRoot) {
|
|
|
1209
1415
|
if (opts.agent) {
|
|
1210
1416
|
agentIds = opts.agent === "all" ? Object.keys(agents) : [opts.agent];
|
|
1211
1417
|
} else {
|
|
1212
|
-
// Auto-detect
|
|
1418
|
+
// Auto-detect: 只显示 pi 和 opencode,默认不勾选
|
|
1419
|
+
const visibleAgentIds = Object.keys(agents)
|
|
1420
|
+
.filter((id) => id === "pi" || id === "opencode");
|
|
1421
|
+
const currentAgent = detectCurrentAgent(agents);
|
|
1213
1422
|
const selected = await checkbox({
|
|
1214
|
-
message: "Select target agents
|
|
1423
|
+
message: "Select target agents:",
|
|
1215
1424
|
choices: [
|
|
1216
|
-
...
|
|
1217
|
-
const
|
|
1425
|
+
...visibleAgentIds.map((id) => {
|
|
1426
|
+
const a = agents[id];
|
|
1427
|
+
const mark = id === currentAgent ? " ← current" : "";
|
|
1218
1428
|
return {
|
|
1219
|
-
name: `${id.padEnd(14)} ${a.description || ""}${
|
|
1429
|
+
name: `${id.padEnd(14)} ${a.description || ""}${mark}`,
|
|
1220
1430
|
value: id,
|
|
1221
|
-
checked:
|
|
1431
|
+
checked: false,
|
|
1222
1432
|
};
|
|
1223
1433
|
}),
|
|
1224
1434
|
],
|
|
@@ -1241,21 +1451,92 @@ export function registerProvider(program, repoRoot) {
|
|
|
1241
1451
|
console.log();
|
|
1242
1452
|
let successCount = 0;
|
|
1243
1453
|
|
|
1454
|
+
// codex 特殊处理:全局只能用一个 model,需确定 wire_api(协议)。
|
|
1455
|
+
// 如果目标含 codex 且选了多个模型,先让用户挑一个作为默认。
|
|
1456
|
+
const hasCodex = agentIds.includes("codex");
|
|
1457
|
+
let codexDefaultModel = null;
|
|
1458
|
+
let codexWireApi = opts.wireApi || null;
|
|
1459
|
+
let codexReasoningEffort = opts.reasoningEffort || null;
|
|
1460
|
+
if (hasCodex) {
|
|
1461
|
+
let codexModels = installProvider.models;
|
|
1462
|
+
if (codexModels.length > 1) {
|
|
1463
|
+
console.log(
|
|
1464
|
+
chalk.yellow(
|
|
1465
|
+
`⚠ codex 一次只能用一个模型(顶层 model),多模型时仅生效默认这一个`,
|
|
1466
|
+
),
|
|
1467
|
+
);
|
|
1468
|
+
const picked = await select({
|
|
1469
|
+
message: "选择 codex 的默认 model:",
|
|
1470
|
+
choices: codexModels.map((m) => ({
|
|
1471
|
+
name: `${m.id}${m.description ? " — " + m.description : ""}`,
|
|
1472
|
+
value: m.id,
|
|
1473
|
+
})),
|
|
1474
|
+
});
|
|
1475
|
+
codexDefaultModel = picked;
|
|
1476
|
+
} else {
|
|
1477
|
+
codexDefaultModel = codexModels[0]?.id;
|
|
1478
|
+
}
|
|
1479
|
+
// wire_api 选择(未传 --wire-api 时交互确认)
|
|
1480
|
+
if (!codexWireApi) {
|
|
1481
|
+
const inferred =
|
|
1482
|
+
installProvider.api === "responses"
|
|
1483
|
+
? "responses"
|
|
1484
|
+
: installProvider.api === "anthropic-messages"
|
|
1485
|
+
? "chat_completions"
|
|
1486
|
+
: "chat_completions";
|
|
1487
|
+
codexWireApi = await select({
|
|
1488
|
+
message: `codex wire_api(与 ${installProvider.name} 通信协议):`,
|
|
1489
|
+
choices: [
|
|
1490
|
+
{
|
|
1491
|
+
name: `responses — Codex Responses 原生协议(CCX 网关用这个)${inferred === "responses" ? " [推断]" : ""}`,
|
|
1492
|
+
value: "responses",
|
|
1493
|
+
},
|
|
1494
|
+
{
|
|
1495
|
+
name: `chat_completions — 标准 OpenAI 兼容协议(多数网关用这个)${inferred === "chat_completions" ? " [推断]" : ""}`,
|
|
1496
|
+
value: "chat_completions",
|
|
1497
|
+
},
|
|
1498
|
+
],
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1244
1503
|
for (const aid of agentIds) {
|
|
1245
1504
|
const agentDef = agents[aid];
|
|
1246
1505
|
if (!agentDef) {
|
|
1247
1506
|
console.log(chalk.red(`Unknown agent: ${aid}`));
|
|
1248
1507
|
continue;
|
|
1249
|
-
|
|
1508
|
+
}
|
|
1250
1509
|
|
|
1251
1510
|
const agent = resolveAgentPaths(agentDef);
|
|
1252
1511
|
console.log(chalk.bold(`→ ${aid}`));
|
|
1253
|
-
|
|
1512
|
+
|
|
1513
|
+
let targetProvider = installProvider;
|
|
1514
|
+
let targetOpts = {};
|
|
1515
|
+
if (aid === "codex") {
|
|
1516
|
+
const picked =
|
|
1517
|
+
installProvider.models.find(
|
|
1518
|
+
(m) => m.id === codexDefaultModel,
|
|
1519
|
+
) || installProvider.models[0];
|
|
1520
|
+
targetProvider = { ...installProvider, models: [picked] };
|
|
1521
|
+
targetOpts = {
|
|
1522
|
+
wireApi: codexWireApi,
|
|
1523
|
+
modelReasoningEffort: codexReasoningEffort,
|
|
1524
|
+
};
|
|
1525
|
+
console.log(
|
|
1526
|
+
chalk.dim(` model: ${picked.id} (codex 全局默认, 单模型)`),
|
|
1527
|
+
);
|
|
1528
|
+
console.log(chalk.dim(` wire_api: ${codexWireApi}`));
|
|
1529
|
+
} else {
|
|
1530
|
+
console.log(
|
|
1531
|
+
chalk.dim(` ${targetProvider.models.length} model(s)`),
|
|
1532
|
+
);
|
|
1533
|
+
}
|
|
1254
1534
|
|
|
1255
1535
|
const success = installProviderToAgent(
|
|
1256
1536
|
aid,
|
|
1257
|
-
|
|
1537
|
+
targetProvider,
|
|
1258
1538
|
agent.home,
|
|
1539
|
+
targetOpts,
|
|
1259
1540
|
);
|
|
1260
1541
|
if (success) successCount++;
|
|
1261
1542
|
console.log();
|
|
@@ -1529,7 +1810,13 @@ export function registerProvider(program, repoRoot) {
|
|
|
1529
1810
|
case "pi": {
|
|
1530
1811
|
const modelsPath = join(agent.home, "models.json");
|
|
1531
1812
|
if (existsSync(modelsPath)) {
|
|
1532
|
-
|
|
1813
|
+
let models = { providers: {} };
|
|
1814
|
+
try {
|
|
1815
|
+
const content = readFileSync(modelsPath, "utf8").trim();
|
|
1816
|
+
if (content) models = JSON.parse(content);
|
|
1817
|
+
} catch {
|
|
1818
|
+
models = { providers: {} };
|
|
1819
|
+
}
|
|
1533
1820
|
if (
|
|
1534
1821
|
models.providers &&
|
|
1535
1822
|
Object.keys(models.providers).length > 0
|
|
@@ -1578,20 +1865,15 @@ export function registerProvider(program, repoRoot) {
|
|
|
1578
1865
|
|
|
1579
1866
|
case "codex": {
|
|
1580
1867
|
const configPath = join(agent.home, "config.toml");
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
` Model: ${chalk.dim(modelMatch ? modelMatch[1] : "not set")}`,
|
|
1589
|
-
);
|
|
1590
|
-
} else {
|
|
1591
|
-
console.log(chalk.dim(" No provider configured"));
|
|
1592
|
-
}
|
|
1868
|
+
const info = readCodexProviderInfo(configPath);
|
|
1869
|
+
if (info.provider) {
|
|
1870
|
+
console.log(` Provider: ${chalk.dim(info.provider)}`);
|
|
1871
|
+
console.log(` Model: ${chalk.dim(info.model || "not set")}`);
|
|
1872
|
+
console.log(` Base URL: ${chalk.dim(info.baseUrl || "not set")}`);
|
|
1873
|
+
console.log(` Wire API: ${chalk.dim(info.wireApi || "not set")}`);
|
|
1874
|
+
console.log(` Env Key: ${chalk.dim(info.envKey || "(none)")}`);
|
|
1593
1875
|
} else {
|
|
1594
|
-
console.log(chalk.dim("
|
|
1876
|
+
console.log(chalk.dim(" No provider configured"));
|
|
1595
1877
|
}
|
|
1596
1878
|
break;
|
|
1597
1879
|
}
|
package/src/lib/agents.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
3
4
|
import yaml from "js-yaml";
|
|
4
5
|
import { expandHome } from "./fs.mjs";
|
|
5
6
|
|
|
@@ -27,10 +28,102 @@ export function getAgent(registry, agentId) {
|
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* Resolve ~ paths in agent definition.
|
|
31
|
+
*
|
|
32
|
+
* 如果 agent 声明了 home_env(如 codex 的 CODEX_HOME),且该环境变量已设置,
|
|
33
|
+
* 优先用它覆盖 home —— 因为 agent 运行时(如 orca 托管的 codex)会用这个
|
|
34
|
+
* 隔离的 home,而不是 registry 里的默认路径。否则 agent 会读到错误目录。
|
|
30
35
|
*/
|
|
31
36
|
export function resolveAgentPaths(agent) {
|
|
37
|
+
const resolvedHome =
|
|
38
|
+
agent.home_env && process.env[agent.home_env]
|
|
39
|
+
? process.env[agent.home_env]
|
|
40
|
+
: expandHome(agent.home);
|
|
32
41
|
return {
|
|
33
42
|
...agent,
|
|
34
|
-
home:
|
|
43
|
+
home: resolvedHome,
|
|
35
44
|
};
|
|
36
45
|
}
|
|
46
|
+
|
|
47
|
+
// ── 当前 agent 检测 ───────────────────────────────────────────
|
|
48
|
+
// 用于 install 时默认预选 cpc 自己跑在哪个 agent 内。
|
|
49
|
+
// 注意:orca 会把 CODEX_HOME / OPENCODE_CONFIG_DIR 等「workspace 级」env 注入
|
|
50
|
+
// 到工作区所有 shell(即便当前在 pi 内也会设 CODEX_HOME),所以 env 存在性
|
|
51
|
+
// 不能用来判断「当前在哪个 agent」。只有 PPID 链里的 agent 进程名才是 ground
|
|
52
|
+
// truth。env 信号仅作 PPID 不可用(非 Linux)时的兜底,且只用 agent 自身注入、
|
|
53
|
+
// 非 orca 注入的变量(如 PI_CODING_AGENT)。
|
|
54
|
+
|
|
55
|
+
// PPID 链里能识别的进程名前缀 → registry agentId
|
|
56
|
+
const COMM_TO_AGENT = [
|
|
57
|
+
["pi", "pi"],
|
|
58
|
+
["codex", "codex"],
|
|
59
|
+
["claude", "claude-code"],
|
|
60
|
+
["opencode", "opencode"],
|
|
61
|
+
["hermes", "hermes"],
|
|
62
|
+
["omp", "omp"],
|
|
63
|
+
["openclaw", "openclaw"],
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
// agent 自身注入(非 orca)的存在性 env —— 仅 PPID 不可用时兜底
|
|
67
|
+
const SELF_PRESENCE_ENV = {
|
|
68
|
+
pi: ["PI_CODING_AGENT"],
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
function readProcComm(pid) {
|
|
72
|
+
// Linux: /proc/<pid>/status
|
|
73
|
+
try {
|
|
74
|
+
const status = readFileSync(`/proc/${pid}/status`, "utf8");
|
|
75
|
+
const c = status.match(/^Name:\s*(\S+)/m);
|
|
76
|
+
const p = status.match(/^PPid:\s*(\d+)/m);
|
|
77
|
+
return { comm: c ? c[1] : null, ppid: p ? p[1] : null };
|
|
78
|
+
} catch {
|
|
79
|
+
return { comm: null, ppid: null };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function readCommViaPs(pid) {
|
|
84
|
+
// macOS / 其他 Unix 兜底
|
|
85
|
+
try {
|
|
86
|
+
const out = execSync(`ps -p ${pid} -o ppid=,comm=`, {
|
|
87
|
+
encoding: "utf8",
|
|
88
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
89
|
+
timeout: 1000,
|
|
90
|
+
}).trim();
|
|
91
|
+
const m = out.match(/^(\d+)\s+(.*)$/);
|
|
92
|
+
if (!m) return { comm: null, ppid: null };
|
|
93
|
+
return { comm: m[2].trim(), ppid: m[1] };
|
|
94
|
+
} catch {
|
|
95
|
+
return { comm: null, ppid: null };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 探测 cpc 当前跑在哪个 agent 内(用于 install 时默认预选)。
|
|
101
|
+
* 返回 agentId(registry key)或 null。
|
|
102
|
+
*
|
|
103
|
+
* 优先级:PPID 链进程名(ground truth)→ agent 自身注入 env(兜底)。
|
|
104
|
+
* 不使用 home_env / ORCA_* 存在性(orca 会 workspace 级注入,会误判)。
|
|
105
|
+
*/
|
|
106
|
+
export function detectCurrentAgent(agents) {
|
|
107
|
+
const ids = Object.keys(agents || {});
|
|
108
|
+
|
|
109
|
+
// 1) PPID 链进程名匹配(最可靠)
|
|
110
|
+
const reader = process.platform === "linux" ? readProcComm : readCommViaPs;
|
|
111
|
+
let pid = process.ppid;
|
|
112
|
+
for (let i = 0; i < 8 && pid; i++) {
|
|
113
|
+
const { comm, ppid } = reader(pid);
|
|
114
|
+
if (!comm) break;
|
|
115
|
+
const hit = COMM_TO_AGENT.find(
|
|
116
|
+
([c]) => comm === c || comm.startsWith(c),
|
|
117
|
+
);
|
|
118
|
+
if (hit) return hit[1];
|
|
119
|
+
if (!ppid || ppid === "0" || ppid === "1") break;
|
|
120
|
+
pid = Number(ppid);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 2) 兜底:agent 自身注入的 env(非 orca 注入)
|
|
124
|
+
for (const id of ids) {
|
|
125
|
+
const vars = SELF_PRESENCE_ENV[id];
|
|
126
|
+
if (vars?.some((v) => process.env[v])) return id;
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
* - contextWindow: 模型上下文窗口
|
|
7
7
|
* - input: 模型支持的输入类型 (text / image / ...)
|
|
8
8
|
* - description: 模型中文描述
|
|
9
|
+
* - reasoning: 是否为推理模型 (boolean)
|
|
9
10
|
* 然后随 model 一起持久化到 ~/.config/cpc/providers.json
|
|
10
11
|
*
|
|
11
|
-
* 注意: 本字典不包含 maxTokens
|
|
12
|
+
* 注意: 本字典不包含 maxTokens, 仍由 src/commands/provider.mjs
|
|
12
13
|
* 中的 MODEL_DEFAULTS 处理. 安装到 Agent 时, 各 agent schema 不消费
|
|
13
14
|
* description, 该字段仅作为 provider.json 内的元信息保留.
|
|
14
15
|
*/
|
|
@@ -19,54 +20,63 @@ export const BASELINE_MODELS = [
|
|
|
19
20
|
contextWindow: 32768,
|
|
20
21
|
input: ["text"],
|
|
21
22
|
description: "小米 MiMo V2 全模态旗舰,文本/图像/音频/视频统一理解与生成。",
|
|
23
|
+
reasoning: false,
|
|
22
24
|
},
|
|
23
25
|
{
|
|
24
26
|
modelId: "mimo-v2-pro",
|
|
25
27
|
contextWindow: 32768,
|
|
26
28
|
input: ["text"],
|
|
27
29
|
description: "小米 MiMo V2 推理增强版,强项为复杂推理与长程任务。",
|
|
30
|
+
reasoning: false,
|
|
28
31
|
},
|
|
29
32
|
{
|
|
30
33
|
modelId: "mimo-v2-tts",
|
|
31
34
|
contextWindow: 32768,
|
|
32
35
|
input: ["text"],
|
|
33
36
|
description: "小米 MiMo V2 文本转语音合成模型,多音色/多语种。",
|
|
37
|
+
reasoning: false,
|
|
34
38
|
},
|
|
35
39
|
{
|
|
36
40
|
modelId: "mimo-v2.5",
|
|
37
41
|
contextWindow: 32768,
|
|
38
42
|
input: ["text"],
|
|
39
43
|
description: "小米 MiMo V2.5 通用对话基座,性价比首选。",
|
|
44
|
+
reasoning: false,
|
|
40
45
|
},
|
|
41
46
|
{
|
|
42
47
|
modelId: "mimo-v2.5-asr",
|
|
43
48
|
contextWindow: 32768,
|
|
44
49
|
input: ["text"],
|
|
45
50
|
description: "小米 MiMo V2.5 语音识别模型,音频转文字。",
|
|
51
|
+
reasoning: false,
|
|
46
52
|
},
|
|
47
53
|
{
|
|
48
54
|
modelId: "mimo-v2.5-pro",
|
|
49
55
|
contextWindow: 32768,
|
|
50
56
|
input: ["text"],
|
|
51
57
|
description: "小米 MiMo V2.5 推理增强版,复杂任务与编程。",
|
|
58
|
+
reasoning: false,
|
|
52
59
|
},
|
|
53
60
|
{
|
|
54
61
|
modelId: "mimo-v2.5-tts",
|
|
55
62
|
contextWindow: 32768,
|
|
56
63
|
input: ["text"],
|
|
57
64
|
description: "小米 MiMo V2.5 文本转语音合成模型。",
|
|
65
|
+
reasoning: false,
|
|
58
66
|
},
|
|
59
67
|
{
|
|
60
68
|
modelId: "mimo-v2.5-tts-voiceclone",
|
|
61
69
|
contextWindow: 32768,
|
|
62
70
|
input: ["text"],
|
|
63
71
|
description: "小米 MiMo V2.5 声音克隆 TTS,参考音频复刻音色。",
|
|
72
|
+
reasoning: false,
|
|
64
73
|
},
|
|
65
74
|
{
|
|
66
75
|
modelId: "mimo-v2.5-tts-voicedesign",
|
|
67
76
|
contextWindow: 32768,
|
|
68
77
|
input: ["text"],
|
|
69
78
|
description: "小米 MiMo V2.5 声音设计 TTS,按描述/特征生成新音色。",
|
|
79
|
+
reasoning: false,
|
|
70
80
|
},
|
|
71
81
|
{
|
|
72
82
|
modelId: "gemma4:e4b",
|
|
@@ -74,6 +84,7 @@ export const BASELINE_MODELS = [
|
|
|
74
84
|
input: ["text"],
|
|
75
85
|
description:
|
|
76
86
|
"Google Gemma 4 边缘端模型(4.5B effective / 8B with embeddings),128K 上下文,支持文本+图像+音频。",
|
|
87
|
+
reasoning: false,
|
|
77
88
|
},
|
|
78
89
|
{
|
|
79
90
|
modelId: "qwen3.6:27b",
|
|
@@ -81,6 +92,7 @@ export const BASELINE_MODELS = [
|
|
|
81
92
|
input: ["text"],
|
|
82
93
|
description:
|
|
83
94
|
"Qwen3.5 架构的 27.8B dense 本地模型(Q4_K_M 17GB),本地多用途对话与编程。",
|
|
95
|
+
reasoning: false,
|
|
84
96
|
},
|
|
85
97
|
{
|
|
86
98
|
modelId: "qwen3.6-plus",
|
|
@@ -88,6 +100,7 @@ export const BASELINE_MODELS = [
|
|
|
88
100
|
input: ["text", "image"],
|
|
89
101
|
description:
|
|
90
102
|
"阿里 Qwen3.6 旗舰 VL 模型,主打 Agentic Coding、Vibe Coding 与 1M 多模态长上下文。",
|
|
103
|
+
reasoning: false,
|
|
91
104
|
},
|
|
92
105
|
{
|
|
93
106
|
modelId: "qwen3-vl-plus",
|
|
@@ -95,6 +108,7 @@ export const BASELINE_MODELS = [
|
|
|
95
108
|
input: ["text", "image"],
|
|
96
109
|
description:
|
|
97
110
|
"通义千问视觉旗舰,OS World 视觉 Agent SOTA,文档/视频/OCR 全能理解。",
|
|
111
|
+
reasoning: false,
|
|
98
112
|
},
|
|
99
113
|
{
|
|
100
114
|
modelId: "qwen3.5-122b-a10b",
|
|
@@ -102,6 +116,7 @@ export const BASELINE_MODELS = [
|
|
|
102
116
|
input: ["text", "image"],
|
|
103
117
|
description:
|
|
104
118
|
"Qwen3.5 开源 MoE 旗舰(122B 总参/10B 激活),262K 上下文,原生视觉语言模型。",
|
|
119
|
+
reasoning: false,
|
|
105
120
|
},
|
|
106
121
|
{
|
|
107
122
|
modelId: "qwen3-coder-plus",
|
|
@@ -109,6 +124,7 @@ export const BASELINE_MODELS = [
|
|
|
109
124
|
input: ["text"],
|
|
110
125
|
description:
|
|
111
126
|
"通义千问编码专项旗舰,1M 仓库级上下文,强工具调用与多文件重构。",
|
|
127
|
+
reasoning: false,
|
|
112
128
|
},
|
|
113
129
|
{
|
|
114
130
|
modelId: "text-embedding-v4",
|
|
@@ -116,6 +132,7 @@ export const BASELINE_MODELS = [
|
|
|
116
132
|
input: ["text"],
|
|
117
133
|
description:
|
|
118
134
|
"阿里 Qwen3-Embedding 旗舰,MTEB 多语言榜单领先,支持 8 种向量维度的语义检索首选。",
|
|
135
|
+
reasoning: false,
|
|
119
136
|
},
|
|
120
137
|
{
|
|
121
138
|
modelId: "GLM-5.0",
|
|
@@ -123,12 +140,14 @@ export const BASELINE_MODELS = [
|
|
|
123
140
|
input: ["text"],
|
|
124
141
|
description:
|
|
125
142
|
"智谱基座旗舰 744B MoE,编程对齐 Claude Opus 4.5,复杂系统工程与长程 Agent 基座。",
|
|
143
|
+
reasoning: false,
|
|
126
144
|
},
|
|
127
145
|
{
|
|
128
146
|
modelId: "GLM-5.0-anthropic",
|
|
129
147
|
contextWindow: 200000,
|
|
130
148
|
input: ["text"],
|
|
131
149
|
description: "GLM-5.0 的 Anthropic 协议兼容封装。",
|
|
150
|
+
reasoning: false,
|
|
132
151
|
},
|
|
133
152
|
{
|
|
134
153
|
modelId: "glm-5.1",
|
|
@@ -136,12 +155,29 @@ export const BASELINE_MODELS = [
|
|
|
136
155
|
input: ["text"],
|
|
137
156
|
description:
|
|
138
157
|
"智谱长程任务旗舰,可单任务持续自主工作 8 小时,编程对齐 Claude Opus 4.6。",
|
|
158
|
+
reasoning: false,
|
|
139
159
|
},
|
|
140
160
|
{
|
|
141
161
|
modelId: "glm-5.1-anthropic",
|
|
142
162
|
contextWindow: 200000,
|
|
143
163
|
input: ["text"],
|
|
144
164
|
description: "glm-5.1 的 Anthropic 协议兼容封装。",
|
|
165
|
+
reasoning: false,
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
modelId: "glm-5.2",
|
|
169
|
+
contextWindow: 1000000,
|
|
170
|
+
input: ["text"],
|
|
171
|
+
description:
|
|
172
|
+
"智谱 GLM-5 系列旗舰(7533.3亿/400亿激活),Solid 1M 无损上下文 + 128K 输出,面向长程 Agentic Coding 与多端可部署产物开发。",
|
|
173
|
+
reasoning: false,
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
modelId: "glm-5.2-anthropic",
|
|
177
|
+
contextWindow: 1000000,
|
|
178
|
+
input: ["text"],
|
|
179
|
+
description: "glm-5.2 的 Anthropic 协议兼容封装。",
|
|
180
|
+
reasoning: false,
|
|
145
181
|
},
|
|
146
182
|
{
|
|
147
183
|
modelId: "deepseek-v4-pro",
|
|
@@ -149,12 +185,14 @@ export const BASELINE_MODELS = [
|
|
|
149
185
|
input: ["text"],
|
|
150
186
|
description:
|
|
151
187
|
"DeepSeek V4 旗舰 1.6T MoE,开源 SOTA 世界知识与 Agent Coding,支持双思考模式。",
|
|
188
|
+
reasoning: false,
|
|
152
189
|
},
|
|
153
190
|
{
|
|
154
191
|
modelId: "deepseek-v4-pro-anthropic",
|
|
155
192
|
contextWindow: 1000000,
|
|
156
193
|
input: ["text"],
|
|
157
194
|
description: "DeepSeek V4-Pro 的 Anthropic 协议封装。",
|
|
195
|
+
reasoning: false,
|
|
158
196
|
},
|
|
159
197
|
{
|
|
160
198
|
modelId: "deepseek-v4-flash",
|
|
@@ -162,6 +200,7 @@ export const BASELINE_MODELS = [
|
|
|
162
200
|
input: ["text"],
|
|
163
201
|
description:
|
|
164
202
|
"DeepSeek V4 轻量版(284B/13B 激活),1M 上下文,主打高吞吐低成本日常 Agent。",
|
|
203
|
+
reasoning: false,
|
|
165
204
|
},
|
|
166
205
|
{
|
|
167
206
|
modelId: "DeepSeek-V3.2",
|
|
@@ -169,6 +208,7 @@ export const BASELINE_MODELS = [
|
|
|
169
208
|
input: ["text"],
|
|
170
209
|
description:
|
|
171
210
|
"DeepSeek V3 末代 DSA 稀疏注意力版本,平衡推理深度与输出长度,适合日常问答与 Agent。",
|
|
211
|
+
reasoning: false,
|
|
172
212
|
},
|
|
173
213
|
{
|
|
174
214
|
modelId: "Doubao-Seed-2.0-Pro",
|
|
@@ -176,6 +216,7 @@ export const BASELINE_MODELS = [
|
|
|
176
216
|
input: ["text", "image"],
|
|
177
217
|
description:
|
|
178
218
|
"字节豆包 2.0 旗舰,多模态视觉推理/空间推理 SOTA,全面对标 GPT-5.2 / Gemini 3 Pro。",
|
|
219
|
+
reasoning: false,
|
|
179
220
|
},
|
|
180
221
|
{
|
|
181
222
|
modelId: "Doubao-Seed-2.0-Code",
|
|
@@ -183,6 +224,7 @@ export const BASELINE_MODELS = [
|
|
|
183
224
|
input: ["text", "image"],
|
|
184
225
|
description:
|
|
185
226
|
"字节豆包 2.0 编程专版,前端强、支持 UI-to-Code 视觉理解,与 TRAE 集成最佳。",
|
|
227
|
+
reasoning: false,
|
|
186
228
|
},
|
|
187
229
|
{
|
|
188
230
|
modelId: "Kimi-K2.5",
|
|
@@ -190,6 +232,7 @@ export const BASELINE_MODELS = [
|
|
|
190
232
|
input: ["text", "image"],
|
|
191
233
|
description:
|
|
192
234
|
"月之暗面原生多模态旗舰,文本+视觉联合优化,Agent Swarm 与视觉编程(UI/视频生成代码)。",
|
|
235
|
+
reasoning: false,
|
|
193
236
|
},
|
|
194
237
|
{
|
|
195
238
|
modelId: "kimi-k2.6",
|
|
@@ -197,12 +240,14 @@ export const BASELINE_MODELS = [
|
|
|
197
240
|
input: ["text", "image"],
|
|
198
241
|
description:
|
|
199
242
|
"月之暗面 K2.6,开源 SOTA 长程编码(13 小时不间断)+ Agent Swarm 300 子智能体。",
|
|
243
|
+
reasoning: false,
|
|
200
244
|
},
|
|
201
245
|
{
|
|
202
246
|
modelId: "kimi-k2.6-anthropic",
|
|
203
247
|
contextWindow: 256000,
|
|
204
248
|
input: ["text", "image"],
|
|
205
249
|
description: "kimi-k2.6 的 Anthropic 协议兼容封装。",
|
|
250
|
+
reasoning: false,
|
|
206
251
|
},
|
|
207
252
|
{
|
|
208
253
|
modelId: "minimax-m3",
|
|
@@ -210,6 +255,7 @@ export const BASELINE_MODELS = [
|
|
|
210
255
|
input: ["text", "image"],
|
|
211
256
|
description:
|
|
212
257
|
"MiniMax 2026 旗舰,国内首个前沿 Coding + 1M 上下文 + 原生多模态三合一模型。",
|
|
258
|
+
reasoning: false,
|
|
213
259
|
},
|
|
214
260
|
{
|
|
215
261
|
modelId: "MiniMax-M2.7",
|
|
@@ -217,12 +263,14 @@ export const BASELINE_MODELS = [
|
|
|
217
263
|
input: ["text"],
|
|
218
264
|
description:
|
|
219
265
|
"MiniMax M2.7,模型自我进化路径,端到端软件工程交付与 Office 复杂编辑。",
|
|
266
|
+
reasoning: false,
|
|
220
267
|
},
|
|
221
268
|
{
|
|
222
269
|
modelId: "minimax-m2.7",
|
|
223
270
|
contextWindow: 204800,
|
|
224
271
|
input: ["text"],
|
|
225
272
|
description: "MiniMax M2.7 小写别名,与 MiniMax-M2.7 同款。",
|
|
273
|
+
reasoning: false,
|
|
226
274
|
},
|
|
227
275
|
];
|
|
228
276
|
|
|
@@ -257,9 +305,9 @@ export function lookupBaselineModel(modelId) {
|
|
|
257
305
|
|
|
258
306
|
/**
|
|
259
307
|
* 用 baseline 字典补全 model 字段
|
|
260
|
-
* 命中时, baseline 的 contextWindow / input / description 会覆盖传入值
|
|
308
|
+
* 命中时, baseline 的 contextWindow / input / description / reasoning 会覆盖传入值
|
|
261
309
|
* 命不中则原样返回
|
|
262
|
-
* maxTokens
|
|
310
|
+
* maxTokens 不在 baseline 范围, 由调用方自己处理
|
|
263
311
|
* @param {object} model 至少有 { id }
|
|
264
312
|
* @returns {object} 增强后的 model
|
|
265
313
|
*/
|
|
@@ -282,5 +330,9 @@ export function enrichModelWithBaseline(model) {
|
|
|
282
330
|
enriched.description = baseline.description;
|
|
283
331
|
}
|
|
284
332
|
|
|
333
|
+
if (typeof baseline.reasoning === "boolean") {
|
|
334
|
+
enriched.reasoning = baseline.reasoning;
|
|
335
|
+
}
|
|
336
|
+
|
|
285
337
|
return enriched;
|
|
286
338
|
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, copyFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Codex config.toml 稳健操作工具。
|
|
5
|
+
*
|
|
6
|
+
* 现有问题:provider.mjs 旧实现用 `/model = .*\n/` 简单正则替换顶层
|
|
7
|
+
* key,并把 provider 段整段字符串拼接,丢掉 env_key、可能误伤同前缀
|
|
8
|
+
* key(model_personality / model_reasoning_effort)。
|
|
9
|
+
*
|
|
10
|
+
* 本模块用"顶层区域隔离 + 段内逐字段 upsert"的方式重写:
|
|
11
|
+
* - 顶层 key 只在第一个 table section 之前查找/替换/插入
|
|
12
|
+
* - provider 段:已存在则段内逐字段更新(不丢用户手动加的字段),
|
|
13
|
+
* 不存在则追加
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
function escapeRegex(s) {
|
|
17
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function ensureNewline(s) {
|
|
21
|
+
return s === "" || s.endsWith("\n") ? s : s + "\n";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 在"顶层区域"(第一个 table section 之前)精确替换或插入一个字符串 key。
|
|
26
|
+
* 只匹配行首 `key =`,不会误伤 `key_suffix =` 同前缀 key。
|
|
27
|
+
*/
|
|
28
|
+
function setTopLevelString(content, key, value) {
|
|
29
|
+
const line = `${key} = "${value}"`;
|
|
30
|
+
const secMatch = content.match(/^[ \t]*\[/m);
|
|
31
|
+
const topEnd = secMatch ? secMatch.index : content.length;
|
|
32
|
+
const top = content.slice(0, topEnd);
|
|
33
|
+
const rest = content.slice(topEnd);
|
|
34
|
+
|
|
35
|
+
const re = new RegExp(`^[ \\t]*${escapeRegex(key)}[ \\t]*=.*$`, "m");
|
|
36
|
+
if (re.test(top)) {
|
|
37
|
+
return top.replace(re, line) + rest;
|
|
38
|
+
}
|
|
39
|
+
// 顶层没有该 key:插到顶层末尾(第一个 section 之前)
|
|
40
|
+
return ensureNewline(top) + line + "\n" + rest;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 找 [section] 段的 { start, headerEnd, bodyEnd }。
|
|
45
|
+
* bodyEnd = 下一个 section 开始处 或 文件尾。
|
|
46
|
+
*/
|
|
47
|
+
function findSectionRange(content, sectionHeader) {
|
|
48
|
+
const re = new RegExp(`^[ \\t]*${escapeRegex(sectionHeader)}[ \\t]*$`, "m");
|
|
49
|
+
const m = re.exec(content);
|
|
50
|
+
if (!m) return null;
|
|
51
|
+
const headerEnd = m.index + m[0].length;
|
|
52
|
+
const after = content.slice(headerEnd);
|
|
53
|
+
const next = after.match(/^[ \t]*\[/m);
|
|
54
|
+
const bodyEnd = next ? headerEnd + next.index : content.length;
|
|
55
|
+
return { start: m.index, headerEnd, bodyEnd };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 在 section 段体内 upsert 一个字符串字段。
|
|
60
|
+
* 已存在则替换该行,不存在则在段体末尾追加。
|
|
61
|
+
*/
|
|
62
|
+
function upsertFieldInSection(content, sectionHeader, key, value) {
|
|
63
|
+
const range = findSectionRange(content, sectionHeader);
|
|
64
|
+
if (!range) return content;
|
|
65
|
+
const line = `${key} = "${value}"`;
|
|
66
|
+
const body = content.slice(range.headerEnd, range.bodyEnd);
|
|
67
|
+
const re = new RegExp(`^[ \\t]*${escapeRegex(key)}[ \\t]*=.*$`, "m");
|
|
68
|
+
if (re.test(body)) {
|
|
69
|
+
const newBody = body.replace(re, line);
|
|
70
|
+
return content.slice(0, range.headerEnd) + newBody + content.slice(range.bodyEnd);
|
|
71
|
+
}
|
|
72
|
+
// 段体末尾追加(bodyEnd 之前)
|
|
73
|
+
const insertion = `${line}\n`;
|
|
74
|
+
return content.slice(0, range.bodyEnd) + insertion + content.slice(range.bodyEnd);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 备份 config.toml -> config.toml.cpc.bak
|
|
79
|
+
*/
|
|
80
|
+
export function backupCodexConfig(configPath) {
|
|
81
|
+
if (!existsSync(configPath)) return null;
|
|
82
|
+
const bak = `${configPath}.cpc.bak`;
|
|
83
|
+
copyFileSync(configPath, bak);
|
|
84
|
+
return bak;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 写入 codex provider 配置到 config.toml(全局默认方式)。
|
|
89
|
+
*
|
|
90
|
+
* - 顶层:model_provider, model(可选 model_reasoning_effort)
|
|
91
|
+
* - 段:[model_providers.NAME] 内 upsert name/base_url/wire_api/env_key
|
|
92
|
+
*
|
|
93
|
+
* @param {string} configPath ~/.codex/config.toml
|
|
94
|
+
* @param {object} opts
|
|
95
|
+
* @param {string} opts.providerName
|
|
96
|
+
* @param {string} opts.baseUrl
|
|
97
|
+
* @param {string} opts.wireApi "responses" | "chat_completions"
|
|
98
|
+
* @param {string} [opts.envKey] 环境变量名;不传则不写 env_key
|
|
99
|
+
* @param {string} opts.model 全局默认 model id
|
|
100
|
+
* @param {string} [opts.modelReasoningEffort]
|
|
101
|
+
* @returns {{backed: string|null, wrote: string}}
|
|
102
|
+
*/
|
|
103
|
+
export function writeCodexProviderConfig(configPath, opts) {
|
|
104
|
+
const backed = backupCodexConfig(configPath);
|
|
105
|
+
let content = existsSync(configPath)
|
|
106
|
+
? readFileSync(configPath, "utf8")
|
|
107
|
+
: "";
|
|
108
|
+
|
|
109
|
+
// 顶层 key
|
|
110
|
+
content = setTopLevelString(content, "model_provider", opts.providerName);
|
|
111
|
+
content = setTopLevelString(content, "model", opts.model);
|
|
112
|
+
if (opts.modelReasoningEffort) {
|
|
113
|
+
content = setTopLevelString(
|
|
114
|
+
content,
|
|
115
|
+
"model_reasoning_effort",
|
|
116
|
+
opts.modelReasoningEffort,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// provider 段
|
|
121
|
+
const header = `[model_providers.${opts.providerName}]`;
|
|
122
|
+
if (!findSectionRange(content, header)) {
|
|
123
|
+
// 追加新段,确保前导空行
|
|
124
|
+
const sep = content && !content.endsWith("\n\n")
|
|
125
|
+
? content.endsWith("\n") ? "\n" : "\n\n"
|
|
126
|
+
: "";
|
|
127
|
+
content = content + sep + header + "\n";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
content = upsertFieldInSection(content, header, "name", opts.providerName);
|
|
131
|
+
content = upsertFieldInSection(content, header, "base_url", opts.baseUrl);
|
|
132
|
+
content = upsertFieldInSection(content, header, "wire_api", opts.wireApi);
|
|
133
|
+
if (opts.envKey) {
|
|
134
|
+
content = upsertFieldInSection(content, header, "env_key", opts.envKey);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
writeFileSync(configPath, content);
|
|
138
|
+
return { backed, wrote: configPath };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 读取 codex config.toml 当前的 provider 配置摘要(用于 list-installed)。
|
|
143
|
+
* @returns {{provider: string|null, model: string|null, envKey: string|null, baseUrl: string|null, wireApi: string|null}}
|
|
144
|
+
*/
|
|
145
|
+
export function readCodexProviderInfo(configPath) {
|
|
146
|
+
if (!existsSync(configPath)) {
|
|
147
|
+
return { provider: null, model: null, envKey: null, baseUrl: null, wireApi: null };
|
|
148
|
+
}
|
|
149
|
+
const content = readFileSync(configPath, "utf8");
|
|
150
|
+
const get = (re) => {
|
|
151
|
+
const m = content.match(re);
|
|
152
|
+
return m ? m[1] : null;
|
|
153
|
+
};
|
|
154
|
+
const provider = get(/^[ \t]*model_provider[ \t]*=\s*"([^"]*)"/m);
|
|
155
|
+
const model = get(/^[ \t]*model[ \t]*=\s*"([^"]*)"/m);
|
|
156
|
+
let envKey = null;
|
|
157
|
+
let baseUrl = null;
|
|
158
|
+
let wireApi = null;
|
|
159
|
+
if (provider) {
|
|
160
|
+
const range = findSectionRange(content, `[model_providers.${provider}]`);
|
|
161
|
+
if (range) {
|
|
162
|
+
const body = content.slice(range.headerEnd, range.bodyEnd);
|
|
163
|
+
const pick = (key) => {
|
|
164
|
+
const m = body.match(
|
|
165
|
+
new RegExp(`^[ \\t]*${key}[ \\t]*=\\s*"([^"]*)"`, "m"),
|
|
166
|
+
);
|
|
167
|
+
return m ? m[1] : null;
|
|
168
|
+
};
|
|
169
|
+
envKey = pick("env_key");
|
|
170
|
+
baseUrl = pick("base_url");
|
|
171
|
+
wireApi = pick("wire_api");
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return { provider, model, envKey, baseUrl, wireApi };
|
|
175
|
+
}
|
package/src/lib/env.mjs
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
writeFileSync,
|
|
6
|
+
appendFileSync,
|
|
7
|
+
} from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { execSync } from "node:child_process";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 跨平台环境变量持久化模块。
|
|
13
|
+
*
|
|
14
|
+
* codex 的 model_providers 只能用 `env_key` 引用环境变量,
|
|
15
|
+
* 不能内联 key。cpc 安装到 codex 时,需要把 provider.apiKey
|
|
16
|
+
* 落地到一个环境变量里,并保证新开的终端/codex 进程能读到。
|
|
17
|
+
*
|
|
18
|
+
* 平台策略:
|
|
19
|
+
* - Linux / macOS: 按 $SHELL 检测写 ~/.zshrc 或 ~/.bashrc(幂等)
|
|
20
|
+
* - Windows: 用 setx 写入用户环境变量(注册表 HKCU\Environment)
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 推断当前 shell 的 rc 文件路径。
|
|
25
|
+
* @returns {string|null} rc 文件绝对路径;Windows 返回 null
|
|
26
|
+
*/
|
|
27
|
+
function detectShellRc() {
|
|
28
|
+
const platform = process.platform;
|
|
29
|
+
if (platform === "win32") return null;
|
|
30
|
+
|
|
31
|
+
const shell = process.env.SHELL || "";
|
|
32
|
+
const home = homedir();
|
|
33
|
+
if (shell.includes("zsh")) return join(home, ".zshrc");
|
|
34
|
+
if (shell.includes("bash")) return join(home, ".bashrc");
|
|
35
|
+
|
|
36
|
+
// $SHELL 缺失时按平台兜底:macOS 默认 zsh,其余默认 bash
|
|
37
|
+
if (platform === "darwin") return join(home, ".zshrc");
|
|
38
|
+
return join(home, ".bashrc");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 幂等地把 `export KEY="value"` 写入 rc 文件。
|
|
43
|
+
* 已存在该 export 行则替换值,不存在则追加。
|
|
44
|
+
*/
|
|
45
|
+
function persistToRc(rcPath, key, value) {
|
|
46
|
+
const exportLine = `export ${key}="${value}"`;
|
|
47
|
+
let content = "";
|
|
48
|
+
if (existsSync(rcPath)) {
|
|
49
|
+
content = readFileSync(rcPath, "utf8");
|
|
50
|
+
}
|
|
51
|
+
// 精确匹配 `export KEY=...` 整行
|
|
52
|
+
const regex = new RegExp(`^export ${key}=.*$`, "m");
|
|
53
|
+
if (regex.test(content)) {
|
|
54
|
+
const updated = content.replace(regex, exportLine);
|
|
55
|
+
if (updated !== content) {
|
|
56
|
+
writeFileSync(rcPath, updated);
|
|
57
|
+
}
|
|
58
|
+
return false; // 不是新增
|
|
59
|
+
}
|
|
60
|
+
const sep = content && !content.endsWith("\n") ? "\n" : "";
|
|
61
|
+
const block = `${sep}# Added by cpc (codex provider)\n${exportLine}\n`;
|
|
62
|
+
appendFileSync(rcPath, block);
|
|
63
|
+
return true; // 新增
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Windows: 用 setx 持久化用户环境变量。
|
|
68
|
+
* 注意 setx 设置的变量只在新进程生效,当前进程不会自动有。
|
|
69
|
+
*/
|
|
70
|
+
function persistWithSetx(key, value) {
|
|
71
|
+
// setx 对带空格的值需要引号;API key 一般是 token 串,安全起见仍加引号
|
|
72
|
+
execSync(`setx ${key} "${value}"`, {
|
|
73
|
+
shell: "cmd.exe",
|
|
74
|
+
stdio: "ignore",
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 跨平台持久化环境变量。
|
|
80
|
+
*
|
|
81
|
+
* @param {string} key 环境变量名
|
|
82
|
+
* @param {string} value 值
|
|
83
|
+
* @returns {{method: "rc"|"setx"|"skip", target: string, created: boolean}}
|
|
84
|
+
*/
|
|
85
|
+
export function persistEnvVar(key, value) {
|
|
86
|
+
if (!key || value == null) {
|
|
87
|
+
return { method: "skip", target: "", created: false };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const rc = detectShellRc();
|
|
91
|
+
if (rc) {
|
|
92
|
+
const created = persistToRc(rc, key, value);
|
|
93
|
+
return { method: "rc", target: rc, created };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Windows
|
|
97
|
+
try {
|
|
98
|
+
persistWithSetx(key, value);
|
|
99
|
+
return { method: "setx", target: "HKCU\\Environment", created: true };
|
|
100
|
+
} catch (err) {
|
|
101
|
+
throw new Error(`Failed to set env var on Windows: ${err.message}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 把 provider 名转成规范环境变量名:CPC_<NAME_UPPER>_API_KEY
|
|
107
|
+
* 非字母数字字符 -> _
|
|
108
|
+
*/
|
|
109
|
+
export function envKeyNameForProvider(providerName) {
|
|
110
|
+
const safe = String(providerName)
|
|
111
|
+
.toUpperCase()
|
|
112
|
+
.replace(/[^A-Z0-9]+/g, "_")
|
|
113
|
+
.replace(/^_+|_+$/g, "");
|
|
114
|
+
return `CPC_${safe}_API_KEY`;
|
|
115
|
+
}
|