@yhong91/cpac 0.1.27 → 0.1.28
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 +3 -3
- package/dist/agents.js +8 -5
- package/dist/config.js +40 -1
- package/dist/proxy.js +2 -2
- package/dist/targets/codex.js +62 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
CPAC 将 Codex 和 Claude Code 接入远端 CLIProxyAPI(CPA):
|
|
4
4
|
|
|
5
|
-
- **Codex**:注入模型目录和 loopback 网关,保留内置 `openai` provider
|
|
5
|
+
- **Codex**:注入模型目录和 loopback 网关,保留内置 `openai` provider。无用户改动时逐字节恢复原配置;注入期间改过文件则只撤掉 CPAC 字段。
|
|
6
6
|
- **Claude Code**:通过临时环境变量启动,不修改 Claude Code 配置。
|
|
7
7
|
|
|
8
8
|
实现基于 TypeScript + Node.js,运行时零依赖。
|
|
@@ -41,7 +41,7 @@ cpac install --all # 注入所有检测到的目标(codex/
|
|
|
41
41
|
cpac install --target codex # 只注入 Codex(默认开启 multi-agent v2)
|
|
42
42
|
cpac install --target codex --v2_models # 先多选 spawn_agent 模型再注入
|
|
43
43
|
cpac install --target codex --max_context # 注入时把模型上下文提升到实测上限(opt-in,仅限 codex)
|
|
44
|
-
cpac restore --target codex #
|
|
44
|
+
cpac restore --target codex # 撤掉注入;注入期间的用户改动会留下
|
|
45
45
|
|
|
46
46
|
# 4b. 临时接管(退出无痕,不写任何配置文件)
|
|
47
47
|
cpac claude # Claude Code 走 CPA
|
|
@@ -248,7 +248,7 @@ Codex App 的长驻 `app-server` 可能缓存旧目录。`inject` 会删除 `mod
|
|
|
248
248
|
|
|
249
249
|
注入命令可选 `--max_context`(`cpac inject --max_context` 或 `cpac install --target codex --max_context`,默认不带):把 catalog 中 `max_context_window` 高于 `context_window` 的模型提升到上限,并把 `auto_compact_token_limit` 设为上限的 90%。Codex 把 `context_window` 当输入预算而非展示标签,上游默认保留保守运营值(如 GPT-5.6 家族 272k),带上该参数后可用到实测上限(约 921k,opencodex 实测);90% 压缩线确保在硬上限前触发 auto-compact。不带参数注入的仍是原本上下文的目录。该参数仅限 codex,与其他 target 组合使用会直接报错。
|
|
250
250
|
|
|
251
|
-
第一次注入时,原文件按原始 bytes 备份到 `state_dir/config.toml.backup`。重复注入不会覆盖首次备份,并会保留 CPAC 管理字段之外的用户编辑。`restore`
|
|
251
|
+
第一次注入时,原文件按原始 bytes 备份到 `state_dir/config.toml.backup`。重复注入不会覆盖首次备份,并会保留 CPAC 管理字段之外的用户编辑。`restore`:若 `config.toml` 相对首次注入未改,按原 mode 逐字节还原备份;若用户在注入期间改过文件,只撤掉 CPAC 写入的 catalog、url 和标记。原先没有 `config.toml` 且撤掉后为空则删除文件。关闭 CPAC loopback 代理。
|
|
252
252
|
|
|
253
253
|
loopback 代理是 detached 用户进程,不安装系统服务。机器重启或进程意外退出后,`cpac status` 会报告 `loopback proxy stopped`;重新执行 `cpac inject` 即可恢复。
|
|
254
254
|
|
package/dist/agents.js
CHANGED
|
@@ -3,7 +3,7 @@ import { existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { fetchCatalog, pickSpawnModels, proxyFingerprint, readState, saveSpawnModels, stateBytes, stateProxy, } from "./config.js";
|
|
6
|
+
import { fetchCatalog, injectedOwnership, pickSpawnModels, proxyFingerprint, readState, saveSpawnModels, stateBytes, stateProxy, } from "./config.js";
|
|
7
7
|
import { proxyIsHealthy, startProxyProcess, stopProxyProcess, } from "./proxy.js";
|
|
8
8
|
import { MANAGED_MARKER, inject, restore } from "./targets/codex.js";
|
|
9
9
|
import { grokConfigPath, grokHome, installGrokConfig, isGrokConfigInstalled, uninstallGrokConfig, } from "./targets/grok.js";
|
|
@@ -76,13 +76,16 @@ export async function runTargetLauncher(config, targetId, args, executable = tar
|
|
|
76
76
|
if (["kimi", "grok"].includes(targetId)) {
|
|
77
77
|
const state = readState(config.state_dir);
|
|
78
78
|
const proxy = state ? stateProxy(state) : null;
|
|
79
|
-
|
|
79
|
+
const apiKey = await resolveApiKey(config.api_key_env);
|
|
80
|
+
const fingerprint = proxyFingerprint(config, apiKey);
|
|
81
|
+
const reuse = proxy !== null &&
|
|
82
|
+
state?.proxy_fingerprint === fingerprint &&
|
|
83
|
+
(await proxyIsHealthy(proxy));
|
|
84
|
+
if (!reuse) {
|
|
80
85
|
if (proxy)
|
|
81
86
|
await stopProxyProcess(proxy);
|
|
82
|
-
const apiKey = await resolveApiKey(config.api_key_env);
|
|
83
87
|
const started = await startProxyProcess(config, apiKey);
|
|
84
88
|
try {
|
|
85
|
-
const fingerprint = proxyFingerprint(config, apiKey);
|
|
86
89
|
const catalog = await fetchCatalog(config.cpa_url, apiKey);
|
|
87
90
|
const catalogPath = join(config.state_dir, "codex-models.json");
|
|
88
91
|
mkdirSync(config.state_dir, { recursive: true, mode: 0o700 });
|
|
@@ -90,7 +93,7 @@ export async function runTargetLauncher(config, targetId, args, executable = tar
|
|
|
90
93
|
const originalMode = existsSync(config.codex_config)
|
|
91
94
|
? statSync(config.codex_config).mode & 0o7777
|
|
92
95
|
: 0o600;
|
|
93
|
-
atomicWrite(join(config.state_dir, "state.json"), stateBytes(config, state ? state.config_existed : false, state ? state.config_mode : originalMode, started, fingerprint));
|
|
96
|
+
atomicWrite(join(config.state_dir, "state.json"), stateBytes(config, state ? state.config_existed : false, state ? state.config_mode : originalMode, started, fingerprint, injectedOwnership(state)));
|
|
94
97
|
}
|
|
95
98
|
catch (error) {
|
|
96
99
|
await stopProxyProcess(started);
|
package/dist/config.js
CHANGED
|
@@ -31,6 +31,9 @@ const STATE_KEYS = new Set([
|
|
|
31
31
|
"proxy_fingerprint",
|
|
32
32
|
"proxy_pid",
|
|
33
33
|
"proxy_port",
|
|
34
|
+
"injected_config_hash",
|
|
35
|
+
"injected_openai_base_url",
|
|
36
|
+
"injected_catalog_path",
|
|
34
37
|
]);
|
|
35
38
|
export const STATE_FILES = [
|
|
36
39
|
"state.json",
|
|
@@ -388,6 +391,21 @@ export function readState(stateDir) {
|
|
|
388
391
|
!/^[a-f0-9]{64}$/.test(value.proxy_fingerprint))) {
|
|
389
392
|
throw new CPACError("invalid loopback proxy fingerprint");
|
|
390
393
|
}
|
|
394
|
+
if (value.injected_config_hash !== undefined &&
|
|
395
|
+
(typeof value.injected_config_hash !== "string" ||
|
|
396
|
+
!/^[a-f0-9]{64}$/.test(value.injected_config_hash))) {
|
|
397
|
+
throw new CPACError("invalid injected_config_hash in state");
|
|
398
|
+
}
|
|
399
|
+
if (value.injected_openai_base_url !== undefined &&
|
|
400
|
+
(typeof value.injected_openai_base_url !== "string" ||
|
|
401
|
+
!value.injected_openai_base_url.trim())) {
|
|
402
|
+
throw new CPACError("invalid injected_openai_base_url in state");
|
|
403
|
+
}
|
|
404
|
+
if (value.injected_catalog_path !== undefined &&
|
|
405
|
+
(typeof value.injected_catalog_path !== "string" ||
|
|
406
|
+
!value.injected_catalog_path.trim())) {
|
|
407
|
+
throw new CPACError("invalid injected_catalog_path in state");
|
|
408
|
+
}
|
|
391
409
|
return value;
|
|
392
410
|
}
|
|
393
411
|
export function stateProxy(state) {
|
|
@@ -395,7 +413,21 @@ export function stateProxy(state) {
|
|
|
395
413
|
? { id: state.proxy_id, pid: state.proxy_pid, port: state.proxy_port }
|
|
396
414
|
: null;
|
|
397
415
|
}
|
|
398
|
-
export function
|
|
416
|
+
export function injectedOwnership(state) {
|
|
417
|
+
if (!state?.injected_config_hash ||
|
|
418
|
+
!state.injected_openai_base_url ||
|
|
419
|
+
!state.injected_catalog_path)
|
|
420
|
+
return undefined;
|
|
421
|
+
return {
|
|
422
|
+
hash: state.injected_config_hash,
|
|
423
|
+
openaiBaseUrl: state.injected_openai_base_url,
|
|
424
|
+
catalogPath: state.injected_catalog_path,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
export function sha256Hex(bytes) {
|
|
428
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
429
|
+
}
|
|
430
|
+
export function stateBytes(config, existed, mode, proxy, proxyFingerprint, injected) {
|
|
399
431
|
return Buffer.from(`${JSON.stringify({
|
|
400
432
|
config_path: config.codex_config,
|
|
401
433
|
config_existed: existed,
|
|
@@ -404,6 +436,13 @@ export function stateBytes(config, existed, mode, proxy, proxyFingerprint) {
|
|
|
404
436
|
proxy_fingerprint: proxyFingerprint,
|
|
405
437
|
proxy_pid: proxy.pid,
|
|
406
438
|
proxy_port: proxy.port,
|
|
439
|
+
...(injected
|
|
440
|
+
? {
|
|
441
|
+
injected_config_hash: injected.hash,
|
|
442
|
+
injected_openai_base_url: injected.openaiBaseUrl,
|
|
443
|
+
injected_catalog_path: injected.catalogPath,
|
|
444
|
+
}
|
|
445
|
+
: {}),
|
|
407
446
|
}, null, 2)}\n`);
|
|
408
447
|
}
|
|
409
448
|
export function originalBytes(stateDir, state, expectedPath) {
|
package/dist/proxy.js
CHANGED
|
@@ -5,7 +5,7 @@ import { createServer, request as httpRequest, } from "node:http";
|
|
|
5
5
|
import { request as httpsRequest } from "node:https";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
-
import { apiBase, originalBytes, proxyFingerprint, readState, stateBytes, stateProxy, } from "./config.js";
|
|
8
|
+
import { apiBase, injectedOwnership, originalBytes, proxyFingerprint, readState, stateBytes, stateProxy, } from "./config.js";
|
|
9
9
|
import { CPACError, atomicWrite, objectValue, resolveApiKey } from "./util.js";
|
|
10
10
|
const HOP_BY_HOP_HEADERS = new Set([
|
|
11
11
|
"connection",
|
|
@@ -322,7 +322,7 @@ export async function runProxy(config) {
|
|
|
322
322
|
}
|
|
323
323
|
const proxy = await createLoopbackProxy(config.cpa_url, apiKey, recorded.id, recorded.port);
|
|
324
324
|
try {
|
|
325
|
-
atomicWrite(join(config.state_dir, "state.json"), stateBytes(config, state.config_existed, state.config_mode, { id: recorded.id, pid: process.pid, port: proxy.port }, proxyFingerprint(config, apiKey)));
|
|
325
|
+
atomicWrite(join(config.state_dir, "state.json"), stateBytes(config, state.config_existed, state.config_mode, { id: recorded.id, pid: process.pid, port: proxy.port }, proxyFingerprint(config, apiKey), injectedOwnership(state)));
|
|
326
326
|
}
|
|
327
327
|
catch (error) {
|
|
328
328
|
proxy.server.closeAllConnections?.();
|
package/dist/targets/codex.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
-
import { PROVIDER, STATE_FILES, cleanupStateFiles, fetchCatalog, liftContextWindows, originalBytes, proxyFingerprint, readState, reorderCatalog, stateBytes, stateProxy, } from "../config.js";
|
|
3
|
+
import { PROVIDER, STATE_FILES, cleanupStateFiles, fetchCatalog, injectedOwnership, liftContextWindows, originalBytes, proxyFingerprint, readState, reorderCatalog, sha256Hex, stateBytes, stateProxy, } from "../config.js";
|
|
4
4
|
import { proxyIsHealthy, startProxyProcess, stopProxyProcess, } from "../proxy.js";
|
|
5
5
|
import { CPACError, atomicWrite, dominantEol, tomlString } from "../util.js";
|
|
6
6
|
const MODEL_PROVIDER_KEY = /^\s*(?:model_provider|"model_provider"|'model_provider')\s*=/;
|
|
@@ -8,6 +8,37 @@ const MODEL_CATALOG_KEY = /^\s*(?:model_catalog_json|"model_catalog_json"|'model
|
|
|
8
8
|
const OPENAI_BASE_URL_KEY = /^\s*(?:openai_base_url|"openai_base_url"|'openai_base_url')\s*=/;
|
|
9
9
|
const ROOT_KEY = new RegExp(`(?:${MODEL_PROVIDER_KEY.source}|${MODEL_CATALOG_KEY.source}|${OPENAI_BASE_URL_KEY.source})`);
|
|
10
10
|
export const MANAGED_MARKER = "# CPAC managed; run CPAC 'restore' to restore the original file.";
|
|
11
|
+
function tomlQuotedValue(line) {
|
|
12
|
+
const eq = line.indexOf("=");
|
|
13
|
+
if (eq === -1)
|
|
14
|
+
return undefined;
|
|
15
|
+
const rest = line.slice(eq + 1).trimStart();
|
|
16
|
+
if (rest.startsWith('"')) {
|
|
17
|
+
const match = /^(?:"(?:\\.|[^"\\])*")/.exec(rest);
|
|
18
|
+
if (!match)
|
|
19
|
+
return undefined;
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(match[0]);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (rest.startsWith("'")) {
|
|
28
|
+
const end = rest.indexOf("'", 1);
|
|
29
|
+
return end === -1 ? undefined : rest.slice(1, end);
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
function ownedRootLine(line, owned) {
|
|
34
|
+
if (!owned)
|
|
35
|
+
return false;
|
|
36
|
+
const value = tomlQuotedValue(line);
|
|
37
|
+
if (value === undefined)
|
|
38
|
+
return false;
|
|
39
|
+
return ((MODEL_CATALOG_KEY.test(line) && value === owned.catalogPath) ||
|
|
40
|
+
(OPENAI_BASE_URL_KEY.test(line) && value === owned.openaiBaseUrl));
|
|
41
|
+
}
|
|
11
42
|
function providerPatterns(provider = PROVIDER) {
|
|
12
43
|
const escaped = provider.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
13
44
|
const modelProviders = `(?:model_providers|"model_providers"|'model_providers')`;
|
|
@@ -41,7 +72,7 @@ export function hasProviderTable(content, provider = PROVIDER) {
|
|
|
41
72
|
}
|
|
42
73
|
return false;
|
|
43
74
|
}
|
|
44
|
-
function stripManagedConfig(content) {
|
|
75
|
+
function stripManagedConfig(content, owned) {
|
|
45
76
|
let text;
|
|
46
77
|
try {
|
|
47
78
|
text = new TextDecoder("utf-8", { fatal: true }).decode(content);
|
|
@@ -75,6 +106,8 @@ function stripManagedConfig(content) {
|
|
|
75
106
|
managedRootKeys -= 1;
|
|
76
107
|
continue;
|
|
77
108
|
}
|
|
109
|
+
if (inRoot && ownedRootLine(line, owned))
|
|
110
|
+
continue;
|
|
78
111
|
kept.push(line);
|
|
79
112
|
}
|
|
80
113
|
const output = kept.join("\n");
|
|
@@ -225,7 +258,7 @@ export async function inject(config, v2Off = false, maxContext = false) {
|
|
|
225
258
|
const backup = originalBytes(config.state_dir, state, config.codex_config);
|
|
226
259
|
try {
|
|
227
260
|
original = existsSync(config.codex_config)
|
|
228
|
-
? stripManagedConfig(readFileSync(config.codex_config))
|
|
261
|
+
? stripManagedConfig(readFileSync(config.codex_config), injectedOwnership(state))
|
|
229
262
|
: backup;
|
|
230
263
|
}
|
|
231
264
|
catch (error) {
|
|
@@ -293,6 +326,11 @@ export async function inject(config, v2Off = false, maxContext = false) {
|
|
|
293
326
|
await stopProxyProcess(proxy);
|
|
294
327
|
throw error;
|
|
295
328
|
}
|
|
329
|
+
const ownership = {
|
|
330
|
+
hash: state?.injected_config_hash ?? sha256Hex(injected),
|
|
331
|
+
openaiBaseUrl: `http://127.0.0.1:${proxy.port}/v1`,
|
|
332
|
+
catalogPath,
|
|
333
|
+
};
|
|
296
334
|
mkdirSync(config.state_dir, { recursive: true, mode: 0o700 });
|
|
297
335
|
if (!stateDirExisted)
|
|
298
336
|
chmodSync(config.state_dir, 0o700);
|
|
@@ -300,7 +338,7 @@ export async function inject(config, v2Off = false, maxContext = false) {
|
|
|
300
338
|
try {
|
|
301
339
|
atomicWrite(catalogPath, catalog.bytes);
|
|
302
340
|
atomicWrite(config.codex_config, injected, originalMode);
|
|
303
|
-
atomicWrite(join(config.state_dir, "state.json"), stateBytes(config, existed, originalMode, proxy, fingerprint));
|
|
341
|
+
atomicWrite(join(config.state_dir, "state.json"), stateBytes(config, existed, originalMode, proxy, fingerprint, ownership));
|
|
304
342
|
}
|
|
305
343
|
catch (error) {
|
|
306
344
|
if (startedProxy)
|
|
@@ -316,7 +354,7 @@ export async function inject(config, v2Off = false, maxContext = false) {
|
|
|
316
354
|
atomicWrite(catalogPath, catalog.bytes);
|
|
317
355
|
atomicWrite(config.codex_config, injected, originalMode);
|
|
318
356
|
configWritten = true;
|
|
319
|
-
atomicWrite(join(config.state_dir, "state.json"), stateBytes(config, existed, originalMode, proxy, fingerprint));
|
|
357
|
+
atomicWrite(join(config.state_dir, "state.json"), stateBytes(config, existed, originalMode, proxy, fingerprint, ownership));
|
|
320
358
|
}
|
|
321
359
|
catch (error) {
|
|
322
360
|
if (configWritten) {
|
|
@@ -361,10 +399,25 @@ export async function restore(config) {
|
|
|
361
399
|
if (!state)
|
|
362
400
|
throw new CPACError("no active CPAC injection");
|
|
363
401
|
const original = originalBytes(config.state_dir, state, config.codex_config);
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
402
|
+
const current = existsSync(config.codex_config)
|
|
403
|
+
? readFileSync(config.codex_config)
|
|
404
|
+
: null;
|
|
405
|
+
const unchanged = !state.injected_config_hash ||
|
|
406
|
+
(current !== null && sha256Hex(current) === state.injected_config_hash);
|
|
407
|
+
if (!current || unchanged) {
|
|
408
|
+
if (state.config_existed)
|
|
409
|
+
atomicWrite(config.codex_config, original, state.config_mode);
|
|
410
|
+
else
|
|
411
|
+
rmSync(config.codex_config, { force: true });
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
const stripped = stripManagedConfig(current, injectedOwnership(state));
|
|
415
|
+
const empty = new TextDecoder("utf-8").decode(stripped).trim() === "";
|
|
416
|
+
if (!state.config_existed && empty)
|
|
417
|
+
rmSync(config.codex_config, { force: true });
|
|
418
|
+
else
|
|
419
|
+
atomicWrite(config.codex_config, stripped, state.config_mode);
|
|
420
|
+
}
|
|
368
421
|
const proxy = stateProxy(state);
|
|
369
422
|
if (proxy)
|
|
370
423
|
await stopProxyProcess(proxy);
|