u1s1-cli 1.14.5 → 1.15.0
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/dist/agent-setup.d.ts +7 -0
- package/dist/agent-setup.js +14 -0
- package/dist/agents/agent-config.d.ts +83 -0
- package/dist/agents/agent-config.js +154 -0
- package/dist/agents/claude.d.ts +80 -0
- package/dist/agents/claude.js +233 -0
- package/dist/agents/codex.d.ts +111 -0
- package/dist/agents/codex.js +392 -0
- package/dist/agents/launch.d.ts +109 -0
- package/dist/agents/launch.js +444 -0
- package/dist/agents/pi.d.ts +9 -0
- package/dist/agents/pi.js +562 -0
- package/dist/agents/prefs.d.ts +35 -0
- package/dist/agents/prefs.js +96 -0
- package/dist/agents/runtime.d.ts +354 -0
- package/dist/agents/runtime.js +884 -0
- package/dist/agents/status.d.ts +20 -0
- package/dist/agents/status.js +241 -0
- package/dist/api.js +2 -1
- package/dist/config-command.d.ts +18 -0
- package/dist/config-command.js +335 -0
- package/dist/config.d.ts +17 -0
- package/dist/config.js +35 -1
- package/dist/device-auth.d.ts +39 -17
- package/dist/device-auth.js +61 -72
- package/dist/embed.d.ts +13 -0
- package/dist/embed.js +15 -0
- package/dist/gateway-route.d.ts +57 -0
- package/dist/gateway-route.js +157 -0
- package/dist/index.js +22 -556
- package/dist/launcher.d.ts +48 -0
- package/dist/launcher.js +235 -0
- package/dist/login.js +3 -2
- package/dist/mcp-server.d.ts +105 -0
- package/dist/mcp-server.js +449 -0
- package/dist/pi-ui-text.d.ts +57 -0
- package/dist/pi-ui-text.js +148 -0
- package/dist/search-tools.js +2 -1
- package/dist/usage.js +2 -2
- package/package.json +3 -2
- package/scripts/p0-claude-code/README.md +39 -0
- package/scripts/p0-claude-code/launch-claude.ts +94 -0
- package/scripts/p0-claude-code/replay.ts +74 -0
- package/scripts/p0-claude-code/seed-local.mjs +145 -0
- package/scripts/p0-claude-code/tap.mjs +105 -0
- package/scripts/patch-pi.js +76 -55
- package/scripts/pi-patch-report.mjs +224 -0
- package/scripts/pi-upgrade.mjs +829 -0
- package/scripts/render-regression.mjs +67 -5
package/dist/agent-setup.d.ts
CHANGED
|
@@ -106,6 +106,13 @@ export declare function writeAttributionExtension(site?: Site): void;
|
|
|
106
106
|
* 所以启动时要把残留文件删掉(Desktop App 共用同一个 agentDir,同样受益)。
|
|
107
107
|
*/
|
|
108
108
|
export declare function removeLegacyCompactUiExtension(outputDir?: string): void;
|
|
109
|
+
/**
|
|
110
|
+
* 「界面文案」(斜杠命令补全描述、思考中提示)上一版投影成 <agentDir>/extensions/u1s1-ui-text.js,
|
|
111
|
+
* 用户带 --no-extensions 或在设置里禁用它时就失效;现在改由 pi.ts 的进程内扩展调用
|
|
112
|
+
* pi-ui-text.ts 的 registerUiText。启动时删掉残留文件,免得和进程内扩展各包一层
|
|
113
|
+
* (两层同时生效结果也正确,只是多余);Desktop App 共用 agentDir,它那里本来就是空转。
|
|
114
|
+
*/
|
|
115
|
+
export declare function removeLegacyUiTextExtension(outputDir?: string): void;
|
|
109
116
|
/**
|
|
110
117
|
* pi-web-ui 的就绪检测只认 <agentDir>/auth.json 里有没有凭据条目(models.json
|
|
111
118
|
* 的 apiKey 它不看),空 {} 会在网页里弹「未检测到 pi agent 配置」引导装 pi。
|
package/dist/agent-setup.js
CHANGED
|
@@ -4,6 +4,7 @@ import { dirname, join } from "node:path";
|
|
|
4
4
|
import { activeSite, agentDir, CUSTOM_ENDPOINTS, ENDPOINT_ID_RE, effectiveContextWindow, endpointKeyEnvName, MODELS, readCompactionBudgetSetting, PROVIDER_ID, UNKNOWN_MODEL_COST, VERSION, } from "./config.js";
|
|
5
5
|
import { tr } from "./i18n.js";
|
|
6
6
|
import { migrateLegacyMcpConfig } from "./mcp/config.js";
|
|
7
|
+
import { LEGACY_UI_TEXT_EXTENSION_FILE } from "./pi-ui-text.js";
|
|
7
8
|
/**
|
|
8
9
|
* 系统提示里的品牌人设。中国站面向编程新手、兜底语言中文(保持原文一字不差);
|
|
9
10
|
* 国际站面向开发者、兜底语言英文。按站点而不是界面语言判定,用时求值(配置可能晚于 import 才读)。
|
|
@@ -518,6 +519,19 @@ export function removeLegacyCompactUiExtension(outputDir) {
|
|
|
518
519
|
}
|
|
519
520
|
catch { }
|
|
520
521
|
}
|
|
522
|
+
/**
|
|
523
|
+
* 「界面文案」(斜杠命令补全描述、思考中提示)上一版投影成 <agentDir>/extensions/u1s1-ui-text.js,
|
|
524
|
+
* 用户带 --no-extensions 或在设置里禁用它时就失效;现在改由 pi.ts 的进程内扩展调用
|
|
525
|
+
* pi-ui-text.ts 的 registerUiText。启动时删掉残留文件,免得和进程内扩展各包一层
|
|
526
|
+
* (两层同时生效结果也正确,只是多余);Desktop App 共用 agentDir,它那里本来就是空转。
|
|
527
|
+
*/
|
|
528
|
+
export function removeLegacyUiTextExtension(outputDir) {
|
|
529
|
+
const dir = outputDir ?? join(agentDir, "extensions");
|
|
530
|
+
try {
|
|
531
|
+
rmSync(join(dir, LEGACY_UI_TEXT_EXTENSION_FILE), { force: true });
|
|
532
|
+
}
|
|
533
|
+
catch { }
|
|
534
|
+
}
|
|
521
535
|
/**
|
|
522
536
|
* pi-web-ui 的就绪检测只认 <agentDir>/auth.json 里有没有凭据条目(models.json
|
|
523
537
|
* 的 apiKey 它不看),空 {} 会在网页里弹「未检测到 pi agent 配置」引导装 pi。
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { type RuntimeBrake } from "./runtime.js";
|
|
2
|
+
export type GatewayAgentName = "claude-code" | "codex" | "pi";
|
|
3
|
+
export interface AgentModel {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
contextWindow?: number;
|
|
7
|
+
}
|
|
8
|
+
export interface AgentConfig {
|
|
9
|
+
agent: GatewayAgentName;
|
|
10
|
+
enabled: boolean;
|
|
11
|
+
disabledReason: string | null;
|
|
12
|
+
runtime: RuntimeBrake;
|
|
13
|
+
defaultModel: string | null;
|
|
14
|
+
smallModel: string | null;
|
|
15
|
+
models: AgentModel[];
|
|
16
|
+
settings: Record<string, unknown> | null;
|
|
17
|
+
}
|
|
18
|
+
/** 校验并规范化响应体;形状不对返回 undefined(按网关故障处理,可退回缓存)。 */
|
|
19
|
+
export declare function parseAgentConfig(body: unknown, agent: GatewayAgentName): AgentConfig | undefined;
|
|
20
|
+
export type AgentConfigResult = {
|
|
21
|
+
status: "ok";
|
|
22
|
+
config: AgentConfig;
|
|
23
|
+
fromCache: boolean;
|
|
24
|
+
} | {
|
|
25
|
+
status: "disabled";
|
|
26
|
+
reason: string | null;
|
|
27
|
+
fromCache: boolean;
|
|
28
|
+
} | {
|
|
29
|
+
status: "unavailable";
|
|
30
|
+
} | {
|
|
31
|
+
status: "auth";
|
|
32
|
+
httpStatus: number;
|
|
33
|
+
} | {
|
|
34
|
+
status: "error";
|
|
35
|
+
detail: string;
|
|
36
|
+
};
|
|
37
|
+
interface CacheFile {
|
|
38
|
+
site: string;
|
|
39
|
+
fetchedAt: number;
|
|
40
|
+
body: unknown;
|
|
41
|
+
}
|
|
42
|
+
export declare function readConfigCache(file: string, site: string): CacheFile | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* @param fetchConfig 发出请求(经签名代理);抛错 = 网络失败
|
|
45
|
+
* @param site 缓存按站点隔离(用 API origin),换站后不会用到另一站的配置
|
|
46
|
+
*/
|
|
47
|
+
export declare function loadAgentConfig(input: {
|
|
48
|
+
agent: GatewayAgentName;
|
|
49
|
+
fetchConfig: () => Promise<Response>;
|
|
50
|
+
cacheFile: string;
|
|
51
|
+
site: string;
|
|
52
|
+
now?: () => number;
|
|
53
|
+
}): Promise<AgentConfigResult>;
|
|
54
|
+
/**
|
|
55
|
+
* 网关 `GET /public/agents/:agent/runtime`(免鉴权):只有开关与版本刹车,没有模型阵容。
|
|
56
|
+
* 给还没登录、也没有配置缓存的 `u1s1 agents install` 用——那时拿不到 /v1/agents/:agent/config,
|
|
57
|
+
* 又不能在没有版本上限的情况下下载。不写缓存:缓存里要的是完整配置(离线启动时要用阵容)。
|
|
58
|
+
*/
|
|
59
|
+
export interface PublicAgentRuntime {
|
|
60
|
+
enabled: boolean;
|
|
61
|
+
disabledReason: string | null;
|
|
62
|
+
runtime: RuntimeBrake;
|
|
63
|
+
}
|
|
64
|
+
export declare function parsePublicAgentRuntime(body: unknown, agent: GatewayAgentName): PublicAgentRuntime | undefined;
|
|
65
|
+
export type PublicAgentRuntimeResult = {
|
|
66
|
+
status: "ok";
|
|
67
|
+
brakes: PublicAgentRuntime;
|
|
68
|
+
}
|
|
69
|
+
/** 404:线上网关还没有这个接口 */
|
|
70
|
+
| {
|
|
71
|
+
status: "unavailable";
|
|
72
|
+
}
|
|
73
|
+
/** 网络失败、5xx、响应不成形 */
|
|
74
|
+
| {
|
|
75
|
+
status: "error";
|
|
76
|
+
detail: string;
|
|
77
|
+
};
|
|
78
|
+
export declare function loadPublicAgentRuntime(input: {
|
|
79
|
+
agent: GatewayAgentName;
|
|
80
|
+
/** 发出请求;抛错 = 网络失败 */
|
|
81
|
+
fetchRuntime: () => Promise<Response>;
|
|
82
|
+
}): Promise<PublicAgentRuntimeResult>;
|
|
83
|
+
export {};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 网关按客户端下发的配置:`GET /v1/agents/:agent/config`(经本机签名代理调用)。
|
|
3
|
+
*
|
|
4
|
+
* 契约(网关 workers/gateway/src/agent-config.ts):
|
|
5
|
+
* { agent, enabled, disabled_reason, runtime: { min_version, max_tested_version, blocked_versions },
|
|
6
|
+
* default_model, small_model, models: [...], settings }
|
|
7
|
+
* default_model / small_model 在没有模型时是 null;settings 只在 claude-code 启用时是对象,其余 null。
|
|
8
|
+
* 响应里没有任何凭证或端点:地址和本地 token 由启动器自己注入。
|
|
9
|
+
*
|
|
10
|
+
* 结果处理:
|
|
11
|
+
* 200 → 用并写缓存;enabled:false → 不启动,显示 disabled_reason;
|
|
12
|
+
* 404(老网关没有这个接口)→ 当作「暂未开放」;
|
|
13
|
+
* 网络失败 / 5xx / 超时 → 用同一站点上次成功拉到的缓存,没有缓存才报错。
|
|
14
|
+
*/
|
|
15
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { randomBytes } from "node:crypto";
|
|
17
|
+
import { dirname } from "node:path";
|
|
18
|
+
import { brakeFromApi } from "./runtime.js";
|
|
19
|
+
function isRecord(v) {
|
|
20
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
21
|
+
}
|
|
22
|
+
function nonEmptyString(v, max = 200) {
|
|
23
|
+
return typeof v === "string" && v.length > 0 && v.length <= max ? v : null;
|
|
24
|
+
}
|
|
25
|
+
/** 校验并规范化响应体;形状不对返回 undefined(按网关故障处理,可退回缓存)。 */
|
|
26
|
+
export function parseAgentConfig(body, agent) {
|
|
27
|
+
if (!isRecord(body) || body["agent"] !== agent || typeof body["enabled"] !== "boolean")
|
|
28
|
+
return undefined;
|
|
29
|
+
const models = [];
|
|
30
|
+
if (Array.isArray(body["models"])) {
|
|
31
|
+
for (const m of body["models"]) {
|
|
32
|
+
if (!isRecord(m))
|
|
33
|
+
continue;
|
|
34
|
+
const id = nonEmptyString(m["id"]);
|
|
35
|
+
if (!id)
|
|
36
|
+
continue;
|
|
37
|
+
const cw = m["context_window"];
|
|
38
|
+
models.push({
|
|
39
|
+
id,
|
|
40
|
+
name: nonEmptyString(m["name"]) ?? id,
|
|
41
|
+
contextWindow: typeof cw === "number" && Number.isFinite(cw) && cw > 0 ? cw : undefined,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const settings = isRecord(body["settings"]) ? body["settings"] : null;
|
|
46
|
+
return {
|
|
47
|
+
agent,
|
|
48
|
+
enabled: body["enabled"],
|
|
49
|
+
disabledReason: nonEmptyString(body["disabled_reason"], 2000),
|
|
50
|
+
runtime: brakeFromApi(body["runtime"]),
|
|
51
|
+
defaultModel: nonEmptyString(body["default_model"]),
|
|
52
|
+
smallModel: nonEmptyString(body["small_model"]),
|
|
53
|
+
models,
|
|
54
|
+
settings,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export function readConfigCache(file, site) {
|
|
58
|
+
try {
|
|
59
|
+
const raw = JSON.parse(readFileSync(file, "utf8"));
|
|
60
|
+
return raw && raw.site === site && typeof raw.fetchedAt === "number" ? raw : undefined;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function writeConfigCache(file, entry) {
|
|
67
|
+
try {
|
|
68
|
+
mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
|
|
69
|
+
const tmp = `${file}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
|
|
70
|
+
writeFileSync(tmp, JSON.stringify(entry) + "\n", { mode: 0o600 });
|
|
71
|
+
renameSync(tmp, file);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// 缓存写不进去不影响本次启动
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function fromConfig(config, fromCache) {
|
|
78
|
+
if (!config.enabled)
|
|
79
|
+
return { status: "disabled", reason: config.disabledReason, fromCache };
|
|
80
|
+
return { status: "ok", config, fromCache };
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* @param fetchConfig 发出请求(经签名代理);抛错 = 网络失败
|
|
84
|
+
* @param site 缓存按站点隔离(用 API origin),换站后不会用到另一站的配置
|
|
85
|
+
*/
|
|
86
|
+
export async function loadAgentConfig(input) {
|
|
87
|
+
const now = input.now ?? Date.now;
|
|
88
|
+
const fallback = (detail) => {
|
|
89
|
+
const cached = readConfigCache(input.cacheFile, input.site);
|
|
90
|
+
const config = cached ? parseAgentConfig(cached.body, input.agent) : undefined;
|
|
91
|
+
return config ? fromConfig(config, true) : { status: "error", detail };
|
|
92
|
+
};
|
|
93
|
+
let res;
|
|
94
|
+
try {
|
|
95
|
+
res = await input.fetchConfig();
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
return fallback(e instanceof Error ? e.message : String(e));
|
|
99
|
+
}
|
|
100
|
+
if (res.status === 404)
|
|
101
|
+
return { status: "unavailable" };
|
|
102
|
+
if (res.status === 401 || res.status === 403)
|
|
103
|
+
return { status: "auth", httpStatus: res.status };
|
|
104
|
+
if (!res.ok)
|
|
105
|
+
return fallback(`HTTP ${res.status}`);
|
|
106
|
+
let body;
|
|
107
|
+
try {
|
|
108
|
+
const text = await res.text();
|
|
109
|
+
if (text.length > 1024 * 1024)
|
|
110
|
+
return fallback("response too large");
|
|
111
|
+
body = JSON.parse(text);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return fallback("malformed response");
|
|
115
|
+
}
|
|
116
|
+
const config = parseAgentConfig(body, input.agent);
|
|
117
|
+
if (!config)
|
|
118
|
+
return fallback("malformed response");
|
|
119
|
+
writeConfigCache(input.cacheFile, { site: input.site, fetchedAt: now(), body });
|
|
120
|
+
return fromConfig(config, false);
|
|
121
|
+
}
|
|
122
|
+
export function parsePublicAgentRuntime(body, agent) {
|
|
123
|
+
if (!isRecord(body) || body["agent"] !== agent || typeof body["enabled"] !== "boolean" || !isRecord(body["runtime"])) {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
enabled: body["enabled"],
|
|
128
|
+
disabledReason: nonEmptyString(body["disabled_reason"], 2000),
|
|
129
|
+
runtime: brakeFromApi(body["runtime"]),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
export async function loadPublicAgentRuntime(input) {
|
|
133
|
+
let res;
|
|
134
|
+
try {
|
|
135
|
+
res = await input.fetchRuntime();
|
|
136
|
+
}
|
|
137
|
+
catch (e) {
|
|
138
|
+
return { status: "error", detail: e instanceof Error ? e.message : String(e) };
|
|
139
|
+
}
|
|
140
|
+
if (res.status === 404)
|
|
141
|
+
return { status: "unavailable" };
|
|
142
|
+
if (!res.ok)
|
|
143
|
+
return { status: "error", detail: `HTTP ${res.status}` };
|
|
144
|
+
try {
|
|
145
|
+
const text = await res.text();
|
|
146
|
+
if (text.length > 64 * 1024)
|
|
147
|
+
return { status: "error", detail: "response too large" };
|
|
148
|
+
const brakes = parsePublicAgentRuntime(JSON.parse(text), input.agent);
|
|
149
|
+
return brakes ? { status: "ok", brakes } : { status: "error", detail: "malformed response" };
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return { status: "error", detail: "malformed response" };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { type McpLaunchEnv } from "../mcp-server.js";
|
|
2
|
+
import { type AgentConfig } from "./agent-config.js";
|
|
3
|
+
import { type RuntimePaths } from "./runtime.js";
|
|
4
|
+
export { agentsRoot, CLAUDE_TERMS_URL, cliInstallHooks, consentLines, notOpenText, overMaxTestedText, printRuntimeFailure, runtimeErrorText, spawnAgent, } from "./launch.js";
|
|
5
|
+
export declare const CLAUDE_GATEWAY_AGENT: "claude-code";
|
|
6
|
+
export interface ClaudePaths extends RuntimePaths {
|
|
7
|
+
/** CLAUDE_CONFIG_DIR */
|
|
8
|
+
configDir: string;
|
|
9
|
+
/** --settings 文件 */
|
|
10
|
+
settingsFile: string;
|
|
11
|
+
/** 上次成功拉到的 /v1/agents/claude-code/config */
|
|
12
|
+
configCache: string;
|
|
13
|
+
/** --mcp-config 文件 */
|
|
14
|
+
mcpConfigFile: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function claudePaths(root?: string): ClaudePaths;
|
|
17
|
+
/**
|
|
18
|
+
* 子进程环境里要去掉的变量:会让它绕开本机代理(云厂商开关与地址)、换用别的凭证
|
|
19
|
+
* (API Key、OAuth token,其中 x-api-key 会被代理拒绝),或盖过网关下发设置的模型变量。
|
|
20
|
+
* 只影响这一次启动的子进程环境,用户自己的 shell 与配置不动。
|
|
21
|
+
*/
|
|
22
|
+
export declare const CLAUDE_ENV_DROP: readonly string[];
|
|
23
|
+
export interface ClaudeLaunchInput {
|
|
24
|
+
baseEnv: NodeJS.ProcessEnv;
|
|
25
|
+
/** 签名代理的 origin,形如 http://127.0.0.1:12345(不带 /v1) */
|
|
26
|
+
proxyOrigin: string;
|
|
27
|
+
/** 签名代理的一次性本地 token */
|
|
28
|
+
localToken: string;
|
|
29
|
+
configDir: string;
|
|
30
|
+
/** u1s1 工具服务要的变量(mcpLaunchEnv);不注入时省略 */
|
|
31
|
+
mcp?: McpLaunchEnv;
|
|
32
|
+
}
|
|
33
|
+
export declare function claudeLaunchEnv(input: ClaudeLaunchInput): NodeJS.ProcessEnv;
|
|
34
|
+
/**
|
|
35
|
+
* --mcp-config 是可变参数(后面不是选项的参数都会被当成配置文件),所以放在 --settings 前面,
|
|
36
|
+
* 让 --settings 把它截住;用户自己的参数(包括直接写的提示词)放最后。
|
|
37
|
+
*
|
|
38
|
+
* 例外:用户参数恰好只有一个 --version / -v / -V 时原样传,什么都不加。Claude Code 只在参数只有
|
|
39
|
+
* 版本开关时走快速返回;前面多了 --mcp-config / --settings 就会先跑完整初始化,断网时要等很久。
|
|
40
|
+
* 只打印版本用不到这两个配置。
|
|
41
|
+
*/
|
|
42
|
+
export declare function claudeLaunchArgs(settingsFile: string, userArgs: readonly string[], mcpConfigFile?: string): string[];
|
|
43
|
+
/** `u1s1 claude --version`(或 -v / -V)且没有别的参数。 */
|
|
44
|
+
export declare function isBareVersionQuery(userArgs: readonly string[]): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* 写给 --mcp-config 的内容:一个 stdio 服务,env 里只有 `${变量名}` 引用,由 Claude Code 在拉起时
|
|
47
|
+
* 用它自己的环境展开(值是启动器放进子进程环境的,见 claudeLaunchEnv),文件里没有 token。
|
|
48
|
+
*/
|
|
49
|
+
export declare function claudeMcpConfig(command?: {
|
|
50
|
+
command: string;
|
|
51
|
+
args: string[];
|
|
52
|
+
}, env?: NodeJS.ProcessEnv): Record<string, unknown>;
|
|
53
|
+
/**
|
|
54
|
+
* 搜索与读网页预先允许(与 Pi 一致,不逐次确认;读网页直连不计费,搜索按次计费但单价低,
|
|
55
|
+
* 工具说明里要求模型别重复搜)。生图单价高:放进 ask,每次都问用户——不放 ask 的话,Claude Code
|
|
56
|
+
* 默认的 auto mode 会让它自己的安全检查替用户放行(2.1.322 实测),用户不知情就扣费。
|
|
57
|
+
*/
|
|
58
|
+
export declare const CLAUDE_MCP_ALLOWED_TOOLS: readonly string[];
|
|
59
|
+
export declare const CLAUDE_MCP_ASK_TOOLS: readonly string[];
|
|
60
|
+
export declare function withMcpPermissions(settings: Record<string, unknown>): Record<string, unknown>;
|
|
61
|
+
/**
|
|
62
|
+
* 启动器自己钉在 settings.env 里的值(盖过网关下发的同名项)。CLAUDE_ENV_DROP 只管得到 shell 环境;
|
|
63
|
+
* 用户仓库里的 .claude/settings.json / settings.local.json 也能用 env 设这两个变量,改掉网关判定要核对的
|
|
64
|
+
* 请求形状(=0 时 system 里不带计费头;EXTRA_BODY 带 metadata 时盖掉 metadata.user_id),判成中转嫌疑。
|
|
65
|
+
* --settings 的优先级高于项目设置,在这里写回默认值。
|
|
66
|
+
*/
|
|
67
|
+
export declare const CLAUDE_SETTINGS_ENV_PINNED: Readonly<Record<string, string>>;
|
|
68
|
+
/**
|
|
69
|
+
* 网关的 settings → 写给 --settings 的对象:去掉连接/凭证相关项,钉上 CLAUDE_SETTINGS_ENV_PINNED;
|
|
70
|
+
* model 用调用方选好的(用户在 u1s1 config 里定的、仍在阵容里的模型),没给时保留网关的,
|
|
71
|
+
* 网关也没有就用 default_model。
|
|
72
|
+
*/
|
|
73
|
+
export declare function claudeSettingsFromConfig(config: AgentConfig, model?: string | null): Record<string, unknown>;
|
|
74
|
+
export declare function writeClaudeSettings(file: string, settings: Record<string, unknown>): void;
|
|
75
|
+
/** 桌面端复用:同一份注入逻辑(embed.ts 导出)。 */
|
|
76
|
+
export declare function agentLaunchEnv(agent: "claude", input: ClaudeLaunchInput): NodeJS.ProcessEnv;
|
|
77
|
+
/**
|
|
78
|
+
* `u1s1 claude [参数…]`。返回子进程的退出码(启动前就失败返回 1)。
|
|
79
|
+
*/
|
|
80
|
+
export declare function runClaude(userArgs: string[]): Promise<number>;
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `u1s1 claude`:以子进程启动用户机器上的官方原版客户端,经本机签名代理用设备凭证访问网关。
|
|
3
|
+
*
|
|
4
|
+
* 注入(规划 4.4):
|
|
5
|
+
* CLAUDE_CONFIG_DIR=~/.u1s1/agents/claude/config 与用户自己的 ~/.claude 互不影响
|
|
6
|
+
* ANTHROPIC_BASE_URL=http://127.0.0.1:<端口> 不带 /v1,客户端自己拼 /v1/messages
|
|
7
|
+
* ANTHROPIC_AUTH_TOKEN=<本地 token> 代理只认 Bearer;不能用 ANTHROPIC_API_KEY(发 x-api-key)
|
|
8
|
+
* DISABLE_AUTOUPDATER=1 更新由启动器管(runtime.ts)
|
|
9
|
+
* --settings <启动器写出的文件> 网关下发的模型阵容等;文件里没有任何凭证
|
|
10
|
+
* --mcp-config <启动器写出的文件> u1s1 工具服务(搜索、读网页、生图,见 mcp-server.ts);
|
|
11
|
+
* 文件里只引用环境变量名,代理地址和本地 token 的值在子进程环境里
|
|
12
|
+
*
|
|
13
|
+
* 真实凭证(设备令牌、设备私钥)只留在启动器进程里:子进程环境从 stripSecretEnv 起步、
|
|
14
|
+
* 再显式写入上面几项;本地 token 只对本进程的签名代理有效,进程退出即作废。
|
|
15
|
+
* 不改二进制,不删除也不禁用它自带的登录方式。
|
|
16
|
+
*/
|
|
17
|
+
import { rmSync } from "node:fs";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import { writePrivateFile } from "../config.js";
|
|
20
|
+
import { lang } from "../i18n.js";
|
|
21
|
+
import { MCP_ENV_NAMES, MCP_SERVER_NAME, MCP_TRACE_ENV, mcpInjectionDisabled, mcpLaunchEnv, mcpServerCommand, } from "../mcp-server.js";
|
|
22
|
+
import { AGENT_PROFILES, agentsRoot, prepareAgentLaunch, runPreparedAgent, } from "./launch.js";
|
|
23
|
+
import { runtimePaths } from "./runtime.js";
|
|
24
|
+
import { stripSecretEnv } from "../secret-env.js";
|
|
25
|
+
// 共用的启动流程与文案在 launch.ts;这里保留原来的导出名,调用方与测试不用改
|
|
26
|
+
export { agentsRoot, CLAUDE_TERMS_URL, cliInstallHooks, consentLines, notOpenText, overMaxTestedText, printRuntimeFailure, runtimeErrorText, spawnAgent, } from "./launch.js";
|
|
27
|
+
export const CLAUDE_GATEWAY_AGENT = "claude-code";
|
|
28
|
+
export function claudePaths(root = agentsRoot()) {
|
|
29
|
+
const base = runtimePaths(root, "claude");
|
|
30
|
+
return {
|
|
31
|
+
...base,
|
|
32
|
+
configDir: join(base.root, "config"),
|
|
33
|
+
settingsFile: join(base.root, "settings.json"),
|
|
34
|
+
configCache: join(base.root, "agent-config.json"),
|
|
35
|
+
mcpConfigFile: join(base.root, "mcp.json"),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* 子进程环境里要去掉的变量:会让它绕开本机代理(云厂商开关与地址)、换用别的凭证
|
|
40
|
+
* (API Key、OAuth token,其中 x-api-key 会被代理拒绝),或盖过网关下发设置的模型变量。
|
|
41
|
+
* 只影响这一次启动的子进程环境,用户自己的 shell 与配置不动。
|
|
42
|
+
*/
|
|
43
|
+
export const CLAUDE_ENV_DROP = [
|
|
44
|
+
"ANTHROPIC_API_KEY",
|
|
45
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
46
|
+
"ANTHROPIC_BASE_URL",
|
|
47
|
+
"ANTHROPIC_CUSTOM_HEADERS",
|
|
48
|
+
"ANTHROPIC_MODEL",
|
|
49
|
+
"ANTHROPIC_SMALL_FAST_MODEL",
|
|
50
|
+
"ANTHROPIC_DEFAULT_OPUS_MODEL",
|
|
51
|
+
"ANTHROPIC_DEFAULT_SONNET_MODEL",
|
|
52
|
+
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
|
|
53
|
+
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
54
|
+
"CLAUDE_CODE_USE_BEDROCK",
|
|
55
|
+
"CLAUDE_CODE_USE_VERTEX",
|
|
56
|
+
"CLAUDE_CODE_USE_FOUNDRY",
|
|
57
|
+
"ANTHROPIC_BEDROCK_BASE_URL",
|
|
58
|
+
"ANTHROPIC_VERTEX_BASE_URL",
|
|
59
|
+
"ANTHROPIC_FOUNDRY_BASE_URL",
|
|
60
|
+
"ANTHROPIC_FOUNDRY_API_KEY",
|
|
61
|
+
"AWS_BEARER_TOKEN_BEDROCK",
|
|
62
|
+
"CLAUDE_CONFIG_DIR",
|
|
63
|
+
// 下面两个会改掉网关客户端判定要核对的请求形状,判成中转嫌疑(只有赠送余额的账号直接 403):
|
|
64
|
+
// =0 时 system 里不再带计费头;
|
|
65
|
+
"CLAUDE_CODE_ATTRIBUTION_HEADER",
|
|
66
|
+
// 合并进请求体的 JSON,带 metadata 时会盖掉 metadata.user_id(会话标识核对不上)。
|
|
67
|
+
"CLAUDE_CODE_EXTRA_BODY",
|
|
68
|
+
// u1s1 工具服务的变量只由启动器设;关掉注入时也不留用户环境里的旧值
|
|
69
|
+
...MCP_ENV_NAMES.filter((name) => name !== "U1S1_LANG"),
|
|
70
|
+
];
|
|
71
|
+
export function claudeLaunchEnv(input) {
|
|
72
|
+
if (!/^http:\/\/127\.0\.0\.1:\d+$/.test(input.proxyOrigin)) {
|
|
73
|
+
throw new Error(`proxy origin must be a loopback origin without a path: ${input.proxyOrigin}`);
|
|
74
|
+
}
|
|
75
|
+
const env = stripSecretEnv(input.baseEnv);
|
|
76
|
+
for (const key of CLAUDE_ENV_DROP)
|
|
77
|
+
delete env[key];
|
|
78
|
+
// scrubForeignProviderEnv 会从启动器自己的环境里删 ANTHROPIC_AUTH_TOKEN,所以这里显式写进子进程环境
|
|
79
|
+
env["CLAUDE_CONFIG_DIR"] = input.configDir;
|
|
80
|
+
env["ANTHROPIC_BASE_URL"] = input.proxyOrigin;
|
|
81
|
+
env["ANTHROPIC_AUTH_TOKEN"] = input.localToken;
|
|
82
|
+
env["DISABLE_AUTOUPDATER"] = "1";
|
|
83
|
+
if (input.mcp)
|
|
84
|
+
Object.assign(env, input.mcp);
|
|
85
|
+
return env;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* --mcp-config 是可变参数(后面不是选项的参数都会被当成配置文件),所以放在 --settings 前面,
|
|
89
|
+
* 让 --settings 把它截住;用户自己的参数(包括直接写的提示词)放最后。
|
|
90
|
+
*
|
|
91
|
+
* 例外:用户参数恰好只有一个 --version / -v / -V 时原样传,什么都不加。Claude Code 只在参数只有
|
|
92
|
+
* 版本开关时走快速返回;前面多了 --mcp-config / --settings 就会先跑完整初始化,断网时要等很久。
|
|
93
|
+
* 只打印版本用不到这两个配置。
|
|
94
|
+
*/
|
|
95
|
+
export function claudeLaunchArgs(settingsFile, userArgs, mcpConfigFile) {
|
|
96
|
+
if (isBareVersionQuery(userArgs))
|
|
97
|
+
return [...userArgs];
|
|
98
|
+
return [...(mcpConfigFile ? ["--mcp-config", mcpConfigFile] : []), "--settings", settingsFile, ...userArgs];
|
|
99
|
+
}
|
|
100
|
+
const CLAUDE_VERSION_FLAGS = new Set(["--version", "-v", "-V"]);
|
|
101
|
+
/** `u1s1 claude --version`(或 -v / -V)且没有别的参数。 */
|
|
102
|
+
export function isBareVersionQuery(userArgs) {
|
|
103
|
+
return userArgs.length === 1 && CLAUDE_VERSION_FLAGS.has(userArgs[0]);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* 写给 --mcp-config 的内容:一个 stdio 服务,env 里只有 `${变量名}` 引用,由 Claude Code 在拉起时
|
|
107
|
+
* 用它自己的环境展开(值是启动器放进子进程环境的,见 claudeLaunchEnv),文件里没有 token。
|
|
108
|
+
*/
|
|
109
|
+
export function claudeMcpConfig(command = mcpServerCommand(), env = process.env) {
|
|
110
|
+
const names = [...MCP_ENV_NAMES];
|
|
111
|
+
if (env[MCP_TRACE_ENV])
|
|
112
|
+
names.push(MCP_TRACE_ENV);
|
|
113
|
+
return {
|
|
114
|
+
mcpServers: {
|
|
115
|
+
[MCP_SERVER_NAME]: {
|
|
116
|
+
type: "stdio",
|
|
117
|
+
command: command.command,
|
|
118
|
+
args: command.args,
|
|
119
|
+
env: Object.fromEntries(names.map((name) => [name, `\${${name}}`])),
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* 搜索与读网页预先允许(与 Pi 一致,不逐次确认;读网页直连不计费,搜索按次计费但单价低,
|
|
126
|
+
* 工具说明里要求模型别重复搜)。生图单价高:放进 ask,每次都问用户——不放 ask 的话,Claude Code
|
|
127
|
+
* 默认的 auto mode 会让它自己的安全检查替用户放行(2.1.322 实测),用户不知情就扣费。
|
|
128
|
+
*/
|
|
129
|
+
export const CLAUDE_MCP_ALLOWED_TOOLS = [
|
|
130
|
+
`mcp__${MCP_SERVER_NAME}__web_search`,
|
|
131
|
+
`mcp__${MCP_SERVER_NAME}__web_fetch`,
|
|
132
|
+
];
|
|
133
|
+
export const CLAUDE_MCP_ASK_TOOLS = [`mcp__${MCP_SERVER_NAME}__generate_image`];
|
|
134
|
+
function mergeRules(existing, rules) {
|
|
135
|
+
const out = Array.isArray(existing) ? [...existing] : [];
|
|
136
|
+
for (const rule of rules)
|
|
137
|
+
if (!out.includes(rule))
|
|
138
|
+
out.push(rule);
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
export function withMcpPermissions(settings) {
|
|
142
|
+
const raw = settings["permissions"];
|
|
143
|
+
const permissions = raw && typeof raw === "object" && !Array.isArray(raw) ? { ...raw } : {};
|
|
144
|
+
permissions["allow"] = mergeRules(permissions["allow"], CLAUDE_MCP_ALLOWED_TOOLS);
|
|
145
|
+
permissions["ask"] = mergeRules(permissions["ask"], CLAUDE_MCP_ASK_TOOLS);
|
|
146
|
+
return { ...settings, permissions };
|
|
147
|
+
}
|
|
148
|
+
/** settings.env 里绝不能出现的键(契约上网关也不会发,这里再兜一层)。 */
|
|
149
|
+
const SETTINGS_ENV_FORBIDDEN = new Set([
|
|
150
|
+
"ANTHROPIC_BASE_URL",
|
|
151
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
152
|
+
"ANTHROPIC_API_KEY",
|
|
153
|
+
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
154
|
+
"CLAUDE_CONFIG_DIR",
|
|
155
|
+
]);
|
|
156
|
+
/** 会让客户端去别处取凭证的设置项。 */
|
|
157
|
+
const SETTINGS_FORBIDDEN = new Set(["apiKeyHelper"]);
|
|
158
|
+
/**
|
|
159
|
+
* 启动器自己钉在 settings.env 里的值(盖过网关下发的同名项)。CLAUDE_ENV_DROP 只管得到 shell 环境;
|
|
160
|
+
* 用户仓库里的 .claude/settings.json / settings.local.json 也能用 env 设这两个变量,改掉网关判定要核对的
|
|
161
|
+
* 请求形状(=0 时 system 里不带计费头;EXTRA_BODY 带 metadata 时盖掉 metadata.user_id),判成中转嫌疑。
|
|
162
|
+
* --settings 的优先级高于项目设置,在这里写回默认值。
|
|
163
|
+
*/
|
|
164
|
+
export const CLAUDE_SETTINGS_ENV_PINNED = {
|
|
165
|
+
CLAUDE_CODE_ATTRIBUTION_HEADER: "1",
|
|
166
|
+
CLAUDE_CODE_EXTRA_BODY: "{}",
|
|
167
|
+
};
|
|
168
|
+
/**
|
|
169
|
+
* 网关的 settings → 写给 --settings 的对象:去掉连接/凭证相关项,钉上 CLAUDE_SETTINGS_ENV_PINNED;
|
|
170
|
+
* model 用调用方选好的(用户在 u1s1 config 里定的、仍在阵容里的模型),没给时保留网关的,
|
|
171
|
+
* 网关也没有就用 default_model。
|
|
172
|
+
*/
|
|
173
|
+
export function claudeSettingsFromConfig(config, model) {
|
|
174
|
+
const out = {};
|
|
175
|
+
for (const [key, value] of Object.entries(config.settings ?? {})) {
|
|
176
|
+
if (!SETTINGS_FORBIDDEN.has(key))
|
|
177
|
+
out[key] = value;
|
|
178
|
+
}
|
|
179
|
+
const rawEnv = out["env"];
|
|
180
|
+
if (rawEnv && typeof rawEnv === "object" && !Array.isArray(rawEnv)) {
|
|
181
|
+
const env = {};
|
|
182
|
+
for (const [key, value] of Object.entries(rawEnv)) {
|
|
183
|
+
if (!SETTINGS_ENV_FORBIDDEN.has(key))
|
|
184
|
+
env[key] = value;
|
|
185
|
+
}
|
|
186
|
+
out["env"] = { ...env, ...CLAUDE_SETTINGS_ENV_PINNED };
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
out["env"] = { ...CLAUDE_SETTINGS_ENV_PINNED };
|
|
190
|
+
}
|
|
191
|
+
if (model)
|
|
192
|
+
out["model"] = model;
|
|
193
|
+
else if (out["model"] === undefined && config.defaultModel)
|
|
194
|
+
out["model"] = config.defaultModel;
|
|
195
|
+
return out;
|
|
196
|
+
}
|
|
197
|
+
export function writeClaudeSettings(file, settings) {
|
|
198
|
+
writePrivateFile(file, JSON.stringify(settings, null, 2) + "\n");
|
|
199
|
+
}
|
|
200
|
+
/** 桌面端复用:同一份注入逻辑(embed.ts 导出)。 */
|
|
201
|
+
export function agentLaunchEnv(agent, input) {
|
|
202
|
+
if (agent !== "claude")
|
|
203
|
+
throw new Error(`unsupported agent: ${String(agent)}`);
|
|
204
|
+
return claudeLaunchEnv(input);
|
|
205
|
+
}
|
|
206
|
+
// ---------------------------------------------------------------- run
|
|
207
|
+
/**
|
|
208
|
+
* `u1s1 claude [参数…]`。返回子进程的退出码(启动前就失败返回 1)。
|
|
209
|
+
*/
|
|
210
|
+
export async function runClaude(userArgs) {
|
|
211
|
+
const paths = claudePaths();
|
|
212
|
+
const prepared = await prepareAgentLaunch(AGENT_PROFILES.claude, paths);
|
|
213
|
+
if (typeof prepared === "number")
|
|
214
|
+
return prepared;
|
|
215
|
+
const mcp = mcpInjectionDisabled()
|
|
216
|
+
? undefined
|
|
217
|
+
: mcpLaunchEnv({ proxyBaseUrl: prepared.proxy.baseUrl, localToken: prepared.proxy.localKey, features: prepared.features, lang: lang() });
|
|
218
|
+
const settings = claudeSettingsFromConfig(prepared.config, prepared.model);
|
|
219
|
+
writeClaudeSettings(paths.settingsFile, mcp ? withMcpPermissions(settings) : settings);
|
|
220
|
+
if (mcp)
|
|
221
|
+
writePrivateFile(paths.mcpConfigFile, JSON.stringify(claudeMcpConfig(), null, 2) + "\n");
|
|
222
|
+
else
|
|
223
|
+
rmSync(paths.mcpConfigFile, { force: true });
|
|
224
|
+
const env = claudeLaunchEnv({
|
|
225
|
+
baseEnv: process.env,
|
|
226
|
+
proxyOrigin: new URL(prepared.proxy.baseUrl).origin,
|
|
227
|
+
localToken: prepared.proxy.localKey,
|
|
228
|
+
configDir: paths.configDir,
|
|
229
|
+
mcp,
|
|
230
|
+
});
|
|
231
|
+
const args = claudeLaunchArgs(paths.settingsFile, userArgs, mcp ? paths.mcpConfigFile : undefined);
|
|
232
|
+
return runPreparedAgent({ agent: "claude", paths, prepared, args, env, userArgs });
|
|
233
|
+
}
|