u1s1-cli-cn 1.18.0 → 1.20.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.
Files changed (43) hide show
  1. package/dist/agents/agent-config.d.ts +1 -1
  2. package/dist/agents/agent-config.js +1 -1
  3. package/dist/agents/claude.d.ts +4 -1
  4. package/dist/agents/claude.js +6 -3
  5. package/dist/agents/codex.d.ts +4 -1
  6. package/dist/agents/codex.js +6 -3
  7. package/dist/agents/custom-models.d.ts +61 -0
  8. package/dist/agents/custom-models.js +175 -0
  9. package/dist/agents/dsh.d.ts +90 -0
  10. package/dist/agents/dsh.js +243 -0
  11. package/dist/agents/launch.d.ts +24 -2
  12. package/dist/agents/launch.js +115 -15
  13. package/dist/agents/models-list.d.ts +65 -0
  14. package/dist/agents/models-list.js +297 -0
  15. package/dist/agents/node-runtime.d.ts +91 -0
  16. package/dist/agents/node-runtime.js +223 -0
  17. package/dist/agents/runtime.d.ts +26 -3
  18. package/dist/agents/runtime.js +50 -14
  19. package/dist/agents/status.d.ts +4 -0
  20. package/dist/agents/status.js +32 -7
  21. package/dist/api.d.ts +2 -0
  22. package/dist/chat-protocol/anthropic.d.ts +29 -0
  23. package/dist/chat-protocol/anthropic.js +484 -0
  24. package/dist/chat-protocol/json.d.ts +20 -0
  25. package/dist/chat-protocol/json.js +22 -0
  26. package/dist/chat-protocol/responses.d.ts +97 -0
  27. package/dist/chat-protocol/responses.js +1076 -0
  28. package/dist/chat-protocol/sse.d.ts +19 -0
  29. package/dist/chat-protocol/sse.js +67 -0
  30. package/dist/chat-protocol/thinking.d.ts +22 -0
  31. package/dist/chat-protocol/thinking.js +58 -0
  32. package/dist/config-command.d.ts +1 -1
  33. package/dist/config-command.js +20 -10
  34. package/dist/config.d.ts +23 -1
  35. package/dist/config.js +52 -1
  36. package/dist/custom-endpoint-proxy.d.ts +92 -0
  37. package/dist/custom-endpoint-proxy.js +461 -0
  38. package/dist/device-auth.d.ts +14 -3
  39. package/dist/device-auth.js +36 -2
  40. package/dist/index.js +12 -3
  41. package/dist/launcher.d.ts +5 -1
  42. package/dist/launcher.js +35 -11
  43. package/package.json +1 -1
@@ -0,0 +1,243 @@
1
+ /**
2
+ * `u1s1 dsh`:以子进程启动 DeepSeek Harness(npm `@deepseek-ai/dsh`,MIT,开发者预览),经本机签名代理
3
+ * 用设备凭证访问网关。dsh 没有终端界面:不带子命令时进网页界面(`dsh web`)。
4
+ *
5
+ * 注入(对着 dsh 0.2.2-rc.1 的 `--dump-config` / `--dump-config-schema` 与实跑核对过):
6
+ * DSH_HOME=~/.u1s1/agents/dsh profiles、会话、凭据都在这里,与用户自己的 ~/.dsh 互不影响
7
+ * --patch ~/.u1s1/agents/dsh/u1s1.patch.yml 在 profile 各层之后再叠一层(见 renderDshPatch):
8
+ * llm-pi-ai 的 provider `u1s1`(api: openai-completions,baseURL 读 U1S1_DSH_BASE_URL,
9
+ * apiKeyEnv: U1S1_LOCAL_KEY,模型表来自 /v1/agents/dsh/config);默认模型、会话标题模型落在它上面;
10
+ * 关掉 DeepSeek 官方账号、官方 API Key 通道、官方网页搜索、遥测上报;注入 u1s1 工具服务(MCP)
11
+ * U1S1_DSH_BASE_URL=http://127.0.0.1:<端口>/v1 签名代理;端口每次随机,patch 里用 !!js 读,文件内容不随端口变
12
+ * U1S1_LOCAL_KEY=<本地 token> provider 的 apiKeyEnv 读它,发 Authorization: Bearer
13
+ * DSH_TELEMETRY_DISABLED=1 dsh 自己的遥测总开关
14
+ * NO_PROXY / no_proxy 补上 127.0.0.1、localhost
15
+ *
16
+ * 真实凭证(设备令牌、设备私钥)只留在启动器进程里;本地 token 只对本进程的签名代理有效,进程退出即作废,
17
+ * 不写进任何文件。不改 dsh 的任何文件。
18
+ */
19
+ import { join } from "node:path";
20
+ import { writePrivateFile } from "../config.js";
21
+ import { lang } from "../i18n.js";
22
+ import { MCP_ENV_NAMES, MCP_SERVER_NAME, MCP_TRACE_ENV, mcpInjectionDisabled, mcpLaunchEnv, mcpServerCommand, } from "../mcp-server.js";
23
+ import { stripSecretEnv } from "../secret-env.js";
24
+ import { withLoopbackNoProxy } from "./codex.js";
25
+ import { AGENT_PROFILES, agentsRoot, prepareAgentLaunch, runPreparedAgent } from "./launch.js";
26
+ import { runtimePaths } from "./runtime.js";
27
+ /** patch 里我们的 provider 名(dsh 的路由名:小写加连字符)。 */
28
+ export const DSH_PROVIDER_ID = "u1s1";
29
+ /** provider 的 apiKeyEnv:子进程环境里放本地 token 的变量名。 */
30
+ export const DSH_KEY_ENV = "U1S1_LOCAL_KEY";
31
+ /** 签名代理地址(带 /v1),patch 里用 `!!js process.env.U1S1_DSH_BASE_URL` 读。 */
32
+ export const DSH_BASE_URL_ENV = "U1S1_DSH_BASE_URL";
33
+ export function dshPaths(root = agentsRoot()) {
34
+ const base = runtimePaths(root, "dsh");
35
+ return {
36
+ ...base,
37
+ configDir: base.root,
38
+ patchFile: join(base.root, "u1s1.patch.yml"),
39
+ configCache: join(base.root, "agent-config.json"),
40
+ };
41
+ }
42
+ /**
43
+ * 子进程环境里要去掉的变量(依据 dsh 0.2.2-rc.1 的默认配置树):
44
+ * - DSH_HOME(我们自己设);
45
+ * - DEEPSEEK_API_KEY:官方 API Key 通道和官方网页搜索读它,有了就会绕开 u1s1 直连官方;
46
+ * - 遥测/产品分析的地址与模式(DSH_TELEMETRY_MODE、DSH_TELEMETRY_OTLP_URL、DSH_PRODUCT_ANALYTICS_OTLP_URL、
47
+ * DSH_CLIENT_VERSION),以及换搜索/抓取提供方的 DSH_WEB_SEARCH_PROVIDER、DSH_WEB_FETCH_PROVIDER;
48
+ * - 我们自己的变量:关掉 MCP 注入时也不留用户环境里的旧值。
49
+ * DSH_PERMISSION_MODE(用户自己选的沙箱模式)保留。
50
+ */
51
+ export const DSH_ENV_DROP = [
52
+ "DSH_HOME",
53
+ "DEEPSEEK_API_KEY",
54
+ "DSH_TELEMETRY_MODE",
55
+ "DSH_TELEMETRY_OTLP_URL",
56
+ "DSH_PRODUCT_ANALYTICS_OTLP_URL",
57
+ "DSH_CLIENT_VERSION",
58
+ "DSH_WEB_SEARCH_PROVIDER",
59
+ "DSH_WEB_FETCH_PROVIDER",
60
+ DSH_KEY_ENV,
61
+ DSH_BASE_URL_ENV,
62
+ ...MCP_ENV_NAMES.filter((name) => name !== "U1S1_LANG"),
63
+ ];
64
+ export function dshLaunchEnv(input) {
65
+ if (!/^http:\/\/127\.0\.0\.1:\d+\/v1$/.test(input.proxyBaseUrl)) {
66
+ throw new Error(`proxy base URL must be the local signing proxy: ${input.proxyBaseUrl}`);
67
+ }
68
+ const env = stripSecretEnv(input.baseEnv);
69
+ for (const key of DSH_ENV_DROP)
70
+ delete env[key];
71
+ env["DSH_HOME"] = input.home;
72
+ env["DSH_TELEMETRY_DISABLED"] = "1";
73
+ env[DSH_KEY_ENV] = input.localToken;
74
+ env[DSH_BASE_URL_ENV] = input.proxyBaseUrl;
75
+ if (input.mcp)
76
+ Object.assign(env, input.mcp);
77
+ const noProxy = withLoopbackNoProxy([env["NO_PROXY"], env["no_proxy"]].filter(Boolean).join(","));
78
+ env["NO_PROXY"] = noProxy;
79
+ env["no_proxy"] = noProxy;
80
+ return env;
81
+ }
82
+ // ---------------------------------------------------------------- args
83
+ /** dsh 启动器自己的、不需要 profile 的选项:原样交给它,不加 patch。 */
84
+ const LAUNCHER_ONLY_FLAGS = new Set(["-h", "--help", "-V", "--version"]);
85
+ /**
86
+ * 用户参数 → dsh 参数。dsh 的语法是 `dsh [--profile] <名字> [启动器选项] [应用参数]`,`--patch` 是启动器选项,
87
+ * 要紧跟在 profile 名后面(第一个它不认识的参数起就是应用参数)。
88
+ * (无参数) → web --patch <文件>
89
+ * --no-open --port 1 → web --patch <文件> --no-open --port 1(以选项开头:默认进网页界面)
90
+ * headless "任务" → headless --patch <文件> "任务"
91
+ * --profile web … → --profile web --patch <文件> …
92
+ * --help / --version → 原样(dsh 自己的帮助和版本)
93
+ * plugin … → 原样(插件管理,不启动 profile)
94
+ * 用户自己再传的 --patch 排在我们的后面,后叠的为准。
95
+ */
96
+ export function dshLaunchArgs(patchFile, userArgs) {
97
+ const [first, ...rest] = userArgs;
98
+ if (first === undefined)
99
+ return ["web", "--patch", patchFile];
100
+ if (first === "plugin" || LAUNCHER_ONLY_FLAGS.has(first))
101
+ return [...userArgs];
102
+ if (first === "--profile") {
103
+ if (rest.length === 0)
104
+ return [...userArgs];
105
+ return ["--profile", rest[0], "--patch", patchFile, ...rest.slice(1)];
106
+ }
107
+ if (first.startsWith("--profile="))
108
+ return [first, "--patch", patchFile, ...rest];
109
+ if (first.startsWith("-"))
110
+ return ["web", "--patch", patchFile, ...userArgs];
111
+ return [first, "--patch", patchFile, ...rest];
112
+ }
113
+ // ---------------------------------------------------------------- patch
114
+ /** provider 模型条目里我们认的字段(dsh llm-pi-ai 的 providers.*.models[]);其余丢掉。 */
115
+ const MODEL_KEYS = ["id", "name", "contextWindow", "maxTokens", "input", "reasoningEfforts", "compat"];
116
+ function isRecord(v) {
117
+ return typeof v === "object" && v !== null && !Array.isArray(v);
118
+ }
119
+ /**
120
+ * 网关 settings.models(dsh-config.ts 下发的 dsh 原生形状)→ provider 模型表。没有 settings(老网关)时
121
+ * 退回 models 阵容里的 id、名字、窗口。
122
+ */
123
+ export function dshProviderModels(config) {
124
+ const raw = config.settings?.["models"];
125
+ if (Array.isArray(raw)) {
126
+ const out = [];
127
+ for (const m of raw) {
128
+ if (!isRecord(m) || typeof m["id"] !== "string" || !m["id"])
129
+ continue;
130
+ const entry = {};
131
+ for (const key of MODEL_KEYS)
132
+ if (m[key] !== undefined)
133
+ entry[key] = m[key];
134
+ out.push(entry);
135
+ }
136
+ if (out.length)
137
+ return out;
138
+ }
139
+ return config.models.map((m) => ({ id: m.id, name: m.name, ...(m.contextWindow ? { contextWindow: m.contextWindow } : {}) }));
140
+ }
141
+ /** YAML 标量:JSON 字符串字面量也是合法的 YAML 双引号标量;对象/数组用 JSON 流式写法。 */
142
+ const y = (value) => JSON.stringify(value);
143
+ /**
144
+ * 生成 --patch 文件。dsh 的 patch 是顶层数组:按 id 覆盖条目(config 整段替换、不深合并),`insert` 追加新条目。
145
+ * 文件里没有端点和凭证:baseURL 与 MCP 服务的变量都用 `!!js process.env.…` 在 dsh 进程里读。
146
+ */
147
+ export function renderDshPatch(input) {
148
+ const lines = [
149
+ "# u1s1: generated by `u1s1 dsh` on every launch; edits here are overwritten.",
150
+ "# Your own overrides go in profiles/<profile>/cordis.patch.yml or cordis.patch.yml in this directory.",
151
+ "",
152
+ "# u1s1 provider: the local signing proxy started by u1s1 (address and one-time token come from the environment)",
153
+ "- id: llm-pi-ai",
154
+ " config:",
155
+ " providers:",
156
+ ` ${DSH_PROVIDER_ID}:`,
157
+ ` displayName: ${y("u1s1")}`,
158
+ ` api: ${y("openai-completions")}`,
159
+ ` baseURL: !!js process.env.${DSH_BASE_URL_ENV}`,
160
+ ` apiKeyEnv: ${y(DSH_KEY_ENV)}`,
161
+ " models:",
162
+ ...input.models.map((m) => ` - ${y(m)}`),
163
+ "",
164
+ "# default model on the u1s1 provider",
165
+ "- id: agent-default-model",
166
+ " config:",
167
+ ` provider: ${y(DSH_PROVIDER_ID)}`,
168
+ ` model: ${y(input.model)}`,
169
+ "",
170
+ "# session titles: the small model on the u1s1 provider (other values are dsh 0.2.2-rc.1 defaults)",
171
+ "- id: session-title-llm",
172
+ " config:",
173
+ " targetWords: 5",
174
+ " targetCjkCharacters: 10",
175
+ " maxInputBytes: 4096",
176
+ " maxOutputTokens: 4096",
177
+ " timeoutMs: 60000",
178
+ ` provider: ${y(DSH_PROVIDER_ID)}`,
179
+ ` model: ${y(input.smallModel ?? input.model)}`,
180
+ "",
181
+ "# no direct traffic to the vendor's own services: account sign-in, API-key model route, web search, telemetry",
182
+ ...[
183
+ "deepseek-account",
184
+ "account-controller",
185
+ "ui-settings-account",
186
+ "llm-deepseek",
187
+ "llm-deepseek-account",
188
+ "web-search-deepseek",
189
+ "web-deepseek-api",
190
+ "session-telemetry-otel",
191
+ "desktop-product-telemetry",
192
+ "product-analytics",
193
+ ].flatMap((id) => [`- id: ${id}`, " disabled: true"]),
194
+ "- id: web",
195
+ " config:",
196
+ " searchProvider: null",
197
+ ` fetchProvider: ${y("http")}`,
198
+ ];
199
+ if (input.mcp) {
200
+ lines.push("", "# u1s1 tools (web search, page fetch, image generation) over MCP, through the same signing proxy", "- insert:", ` - id: ${y(`mcp-${MCP_SERVER_NAME}`)}`, ` name: ${y("@deepseek-ai/dsh-mcp-client")}`, " config:", ` serverName: ${y(MCP_SERVER_NAME)}`, ` transport: ${y("stdio")}`, ` command: ${y(input.mcp.command)}`, ` args: ${y(input.mcp.args)}`, " env:", ...input.mcp.envVars.map((name) => ` ${name}: !!js process.env.${name}`));
201
+ }
202
+ return lines.join("\n") + "\n";
203
+ }
204
+ /** MCP 服务:同一个 node、同一份 CLI;env 里只列变量名,值由 dsh 从自己的环境转过去。 */
205
+ export function dshMcpServer(command = mcpServerCommand(), env = process.env) {
206
+ const envVars = [...MCP_ENV_NAMES];
207
+ if (env[MCP_TRACE_ENV])
208
+ envVars.push(MCP_TRACE_ENV);
209
+ return { ...command, envVars };
210
+ }
211
+ export function writeDshPatch(paths, input) {
212
+ writePrivateFile(paths.patchFile, renderDshPatch(input));
213
+ }
214
+ // ---------------------------------------------------------------- run
215
+ /**
216
+ * `u1s1 dsh [参数…]`。返回子进程的退出码(启动前就失败返回 1)。
217
+ * 是否可用完全由网关配置的 enabled 决定(prepareAgentLaunch)。
218
+ */
219
+ export async function runDsh(userArgs) {
220
+ const paths = dshPaths();
221
+ const prepared = await prepareAgentLaunch(AGENT_PROFILES.dsh, paths);
222
+ if (typeof prepared === "number")
223
+ return prepared;
224
+ const models = dshProviderModels(prepared.config);
225
+ const model = prepared.model ?? prepared.config.defaultModel ?? models[0]?.["id"];
226
+ if (typeof model !== "string") {
227
+ console.error("No model is available for DeepSeek Harness right now.");
228
+ return 1;
229
+ }
230
+ const mcp = mcpInjectionDisabled()
231
+ ? undefined
232
+ : mcpLaunchEnv({ proxyBaseUrl: prepared.proxy.baseUrl, localToken: prepared.proxy.localKey, features: prepared.features, lang: lang() });
233
+ writeDshPatch(paths, { models, model, smallModel: prepared.config.smallModel, mcp: mcp ? dshMcpServer() : null });
234
+ const env = dshLaunchEnv({
235
+ baseEnv: process.env,
236
+ localToken: prepared.proxy.localKey,
237
+ proxyBaseUrl: prepared.proxy.baseUrl,
238
+ home: paths.configDir,
239
+ mcp,
240
+ });
241
+ const args = dshLaunchArgs(paths.patchFile, userArgs);
242
+ return runPreparedAgent({ agent: "dsh", paths, prepared, args, env, userArgs });
243
+ }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 官方原版客户端(`u1s1 claude` / `u1s1 codex`)共用的启动流程与文案:
2
+ * 官方原版客户端(`u1s1 claude` / `u1s1 codex` / `u1s1 dsh`)共用的启动流程与文案:
3
3
  * 登录 → 经签名代理拉网关配置(开关、刹车、阵容)→ 找到/下载原版 → 起声明了客户端的签名代理 →
4
4
  * 由各客户端模块写配置、拼环境 → 子进程继承终端 → 透传退出码;顺带每天查一次新版。
5
5
  *
@@ -9,13 +9,15 @@ import { spawn } from "node:child_process";
9
9
  import { type ApiFeatures } from "../api.js";
10
10
  import { type SiteId } from "../site.js";
11
11
  import { type AgentConfig, type GatewayAgentName } from "./agent-config.js";
12
+ import { type AgentCustomModel } from "./custom-models.js";
13
+ import { type FoundNode, type NodeDownloadInfo } from "./node-runtime.js";
12
14
  import { RuntimePlanError, type DownloadInfo, type EnsuredRuntime, type InstallHooks, type ManagedAgent, type RuntimePaths, type SkippedRuntime } from "./runtime.js";
13
15
  export declare function agentsRoot(): string;
14
16
  export interface AgentProfile {
15
17
  /** 子命令名,也是 ~/.u1s1/agents 下的目录名 */
16
18
  agent: ManagedAgent;
17
19
  /** 网关里的客户端名(/v1/agents/:agent/config、x-u1s1-agent) */
18
- gatewayAgent: Extract<GatewayAgentName, "claude-code" | "codex">;
20
+ gatewayAgent: Extract<GatewayAgentName, "claude-code" | "codex" | "dsh">;
19
21
  /** 产品名(只作事实陈述,如「官方 Claude Code」) */
20
22
  product: string;
21
23
  /** 下载前提示里的许可/条款一行 */
@@ -25,6 +27,7 @@ export interface AgentProfile {
25
27
  }
26
28
  export declare const CLAUDE_TERMS_URL = "https://code.claude.com/docs/en/legal-and-compliance";
27
29
  export declare const CODEX_LICENSE_URL = "https://github.com/openai/codex/blob/main/LICENSE";
30
+ export declare const DSH_LICENSE_URL = "https://www.npmjs.com/package/@deepseek-ai/dsh";
28
31
  export declare const AGENT_PROFILES: Record<ManagedAgent, AgentProfile>;
29
32
  /**
30
33
  * 请求本机签名代理(http://127.0.0.1:<端口>)专用:直连,不走用户的 HTTP(S)_PROXY。
@@ -44,8 +47,18 @@ export declare function loopbackFetch(url: string, init: RequestInit): Promise<R
44
47
  export declare function spawnAgent(bin: string, args: readonly string[], env: NodeJS.ProcessEnv, deps?: {
45
48
  spawn?: typeof spawn;
46
49
  proc?: Pick<NodeJS.Process, "on" | "off">;
50
+ }, opts?: {
51
+ /**
52
+ * 也转发 SIGINT。只在没有终端时用(dsh 被别的程序拉起、对启动器进程发 SIGINT):那时没有终端
53
+ * 把 Ctrl+C 同时发给子进程,不转发子进程就收不到。有终端时照旧不转发,免得收到两次。
54
+ */
55
+ forwardInterrupt?: boolean;
47
56
  }): Promise<number>;
48
57
  export declare function consentLines(info: DownloadInfo): string[];
58
+ /** dsh 要的 Node 24:下载前的说明(与客户端本身的下载分开问,两样都要用户点头)。 */
59
+ export declare function nodeConsentLines(info: NodeDownloadInfo): string[];
60
+ /** dsh 用的 Node:找不到就征得同意后下载(与 u1s1 agents install dsh 共用)。失败已向用户说明,返回 undefined。 */
61
+ export declare function ensureDshNode(site: SiteId): Promise<FoundNode | undefined>;
49
62
  export declare function cliInstallHooks(agent?: ManagedAgent): InstallHooks;
50
63
  /** 已有的某份客户端高于 max_tested、这次不用它。 */
51
64
  export declare function overMaxTestedText(s: SkippedRuntime, agent?: ManagedAgent): string;
@@ -76,7 +89,16 @@ export interface PreparedLaunch {
76
89
  site: SiteId;
77
90
  /** /v1/models 的能力开关(搜索、云端渲染、生图);没拉到时为 undefined。MCP 工具服务按它决定提供哪些工具 */
78
91
  features: ApiFeatures | undefined;
92
+ /** 账号里的自定义端点模型(id = ep…/<模型>):接在客户端模型阵容后面,请求由签名代理在本机直连端点 */
93
+ customModels: AgentCustomModel[];
94
+ /** 入口是 JS 文件的客户端(dsh)用来运行它的 Node;其余为 undefined */
95
+ node?: FoundNode;
79
96
  }
97
+ /**
98
+ * 网关刹车;dsh 没有 max_tested_version(老网关、老缓存)时也不越过实测过的版本——它每天发预发布版,
99
+ * npm 的 latest 随时会变成没测过的版本。
100
+ */
101
+ export declare function effectiveBrake(agent: ManagedAgent, brake: AgentConfig["runtime"]): AgentConfig["runtime"];
80
102
  /**
81
103
  * 子进程启动前的公共部分。返回 number = 启动前就失败的退出码(已向用户说明)。
82
104
  * 是否可用完全由网关配置的 enabled 决定:停用时显示 disabled_reason,不下载、不启动。
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 官方原版客户端(`u1s1 claude` / `u1s1 codex`)共用的启动流程与文案:
2
+ * 官方原版客户端(`u1s1 claude` / `u1s1 codex` / `u1s1 dsh`)共用的启动流程与文案:
3
3
  * 登录 → 经签名代理拉网关配置(开关、刹车、阵容)→ 找到/下载原版 → 起声明了客户端的签名代理 →
4
4
  * 由各客户端模块写配置、拼环境 → 子进程继承终端 → 透传退出码;顺带每天查一次新版。
5
5
  *
@@ -11,19 +11,22 @@ import { constants as osConstants } from "node:os";
11
11
  import { join } from "node:path";
12
12
  import { createInterface } from "node:readline/promises";
13
13
  import { Agent, fetch as undiciFetch } from "undici";
14
- import { AccessDeniedError, AuthError, fetchModels } from "../api.js";
15
- import { u1s1Dir } from "../config.js";
14
+ import { AccessDeniedError, AuthError, fetchModels, loadCustomEndpoints } from "../api.js";
15
+ import { CUSTOM_ENDPOINTS, u1s1Dir } from "../config.js";
16
16
  import { ensureSigningProxy } from "../device-auth.js";
17
17
  import { tr } from "../i18n.js";
18
18
  import { siteLinks, siteOf } from "../site.js";
19
19
  import { loadAgentConfig } from "./agent-config.js";
20
+ import { agentCustomModels } from "./custom-models.js";
20
21
  import { readAgentPrefs, resolveLaunchModel } from "./prefs.js";
21
- import { dailyUpdateCheck, ensureAgentRuntime, fetchLatestVersion, npmRegistryFor, RuntimeInstallError, RuntimePlanError, runtimeSpec, } from "./runtime.js";
22
+ import { cleanNodeStaging, DSH_MIN_NODE, ensureNode, NodeRuntimeError } from "./node-runtime.js";
23
+ import { dailyUpdateCheck, DSH_TESTED_VERSION, ensureAgentRuntime, fetchLatestVersion, npmRegistryFor, RuntimeInstallError, RuntimePlanError, runtimeSpec, } from "./runtime.js";
22
24
  export function agentsRoot() {
23
25
  return join(u1s1Dir, "agents");
24
26
  }
25
27
  export const CLAUDE_TERMS_URL = "https://code.claude.com/docs/en/legal-and-compliance";
26
28
  export const CODEX_LICENSE_URL = "https://github.com/openai/codex/blob/main/LICENSE";
29
+ export const DSH_LICENSE_URL = "https://www.npmjs.com/package/@deepseek-ai/dsh";
27
30
  export const AGENT_PROFILES = {
28
31
  claude: {
29
32
  agent: "claude",
@@ -39,6 +42,13 @@ export const AGENT_PROFILES = {
39
42
  termsLine: () => tr(` Codex CLI 以 Apache-2.0 许可发布:${CODEX_LICENSE_URL}`, ` Codex CLI is released under the Apache-2.0 license: ${CODEX_LICENSE_URL}`),
40
43
  locationLine: () => tr(" 位置:~/.u1s1/agents/codex(不影响你自己装的版本和 ~/.codex)", " Location: ~/.u1s1/agents/codex (your own install and ~/.codex are left alone)"),
41
44
  },
45
+ dsh: {
46
+ agent: "dsh",
47
+ gatewayAgent: "dsh",
48
+ product: "DeepSeek Harness",
49
+ termsLine: () => tr(` DeepSeek Harness 以 MIT 许可发布(开发者预览版):${DSH_LICENSE_URL}`, ` DeepSeek Harness is released under the MIT license (developer preview): ${DSH_LICENSE_URL}`),
50
+ locationLine: () => tr(" 位置:~/.u1s1/agents/dsh(不影响你自己装的版本和 ~/.dsh)", " Location: ~/.u1s1/agents/dsh (your own install and ~/.dsh are left alone)"),
51
+ },
42
52
  };
43
53
  // ---------------------------------------------------------------- loopback
44
54
  let loopbackAgent;
@@ -65,18 +75,20 @@ export function loopbackFetch(url, init) {
65
75
  * SIGTERM / SIGHUP 通常只发给启动器(kill、关窗口),转发给子进程。
66
76
  * 签名代理的监听是 unref 的:必须等子进程退出后再返回。
67
77
  */
68
- export function spawnAgent(bin, args, env, deps = {}) {
78
+ export function spawnAgent(bin, args, env, deps = {}, opts = {}) {
69
79
  const proc = deps.proc ?? process;
70
80
  const child = (deps.spawn ?? spawn)(bin, [...args], { env, stdio: "inherit" });
71
81
  const swallow = () => { };
72
82
  const forwarders = new Map();
73
- for (const sig of ["SIGTERM", "SIGHUP"]) {
83
+ const forwarded = opts.forwardInterrupt ? ["SIGTERM", "SIGHUP", "SIGINT"] : ["SIGTERM", "SIGHUP"];
84
+ for (const sig of forwarded) {
74
85
  forwarders.set(sig, () => {
75
86
  if (child.exitCode === null && child.signalCode === null)
76
87
  child.kill(sig);
77
88
  });
78
89
  }
79
- const ignored = process.platform === "win32" ? ["SIGINT"] : ["SIGINT", "SIGQUIT"];
90
+ const swallowed = process.platform === "win32" ? ["SIGINT"] : ["SIGINT", "SIGQUIT"];
91
+ const ignored = swallowed.filter((sig) => !forwarders.has(sig));
80
92
  for (const sig of ignored)
81
93
  proc.on(sig, swallow);
82
94
  for (const [sig, fn] of forwarders)
@@ -134,14 +146,27 @@ export function consentLines(info) {
134
146
  profile.termsLine(),
135
147
  ];
136
148
  }
149
+ /** dsh 要的 Node 24:下载前的说明(与客户端本身的下载分开问,两样都要用户点头)。 */
150
+ export function nodeConsentLines(info) {
151
+ return [
152
+ "",
153
+ tr(` DeepSeek Harness 需要 Node.js ${DSH_MIN_NODE} 或更新版本,这台机器上没有。需要先下载官方 Node.js ${info.version}(长期支持版):`, ` DeepSeek Harness needs Node.js ${DSH_MIN_NODE} or newer, which this machine doesn't have. The official Node.js ${info.version} (LTS) needs to be downloaded first:`),
154
+ tr(` 来源:${info.url}`, ` Source: ${info.url}`),
155
+ tr(` 大小:下载约 ${mb(info.downloadBytes ?? 0)} MB,按官方 SHA-256 校验;只留 node 可执行文件,约 120 MB`, ` Size: about ${mb(info.downloadBytes ?? 0)} MB to download, checked against the official SHA-256; only the node binary is kept (about 120 MB)`),
156
+ tr(" 位置:~/.u1s1/agents/node(只给 u1s1 dsh 用,不改你的 PATH 和已装的 Node)", " Location: ~/.u1s1/agents/node (used only by u1s1 dsh; your PATH and existing Node installs are left alone)"),
157
+ ];
158
+ }
137
159
  async function askConsent(info) {
138
- for (const line of consentLines(info))
160
+ return askYesNo(consentLines(info), info.agent);
161
+ }
162
+ async function askYesNo(lines, agent) {
163
+ for (const line of lines)
139
164
  console.error(line);
140
165
  const preset = process.env["U1S1_AGENT_INSTALL"]?.trim().toLowerCase();
141
166
  if (preset === "yes" || preset === "y" || preset === "1")
142
167
  return true;
143
168
  if (!process.stdin.isTTY) {
144
- console.error(tr(` 当前没有可交互的终端。请在终端里运行一次 u1s1 ${info.agent} 确认下载,或设置 U1S1_AGENT_INSTALL=yes。`, ` No interactive terminal. Run u1s1 ${info.agent} in a terminal once to confirm, or set U1S1_AGENT_INSTALL=yes.`));
169
+ console.error(tr(` 当前没有可交互的终端。请在终端里运行一次 u1s1 ${agent} 确认下载,或设置 U1S1_AGENT_INSTALL=yes。`, ` No interactive terminal. Run u1s1 ${agent} in a terminal once to confirm, or set U1S1_AGENT_INSTALL=yes.`));
145
170
  return false;
146
171
  }
147
172
  const rl = createInterface({ input: process.stdin, output: process.stderr });
@@ -153,6 +178,34 @@ async function askConsent(info) {
153
178
  rl.close();
154
179
  }
155
180
  }
181
+ /** dsh 用的 Node:找不到就征得同意后下载(与 u1s1 agents install dsh 共用)。失败已向用户说明,返回 undefined。 */
182
+ export async function ensureDshNode(site) {
183
+ cleanNodeStaging(agentsRoot());
184
+ try {
185
+ return await ensureNode({
186
+ agentsRoot: agentsRoot(),
187
+ site,
188
+ consent: (info) => askYesNo(nodeConsentLines(info), "dsh"),
189
+ log: (line) => console.error(line),
190
+ text: {
191
+ declined: () => tr("已取消下载。", "Download cancelled."),
192
+ unsupported: () => tr(`这台机器的系统/架构(${process.platform}-${process.arch})没有官方 Node.js 24。`, `There's no official Node.js 24 build for this system (${process.platform}-${process.arch}).`),
193
+ downloadFailed: (d) => tr(`Node.js 下载失败(${d}),检查一下网络后再试。`, `The Node.js download failed (${d}). Check your network and try again.`),
194
+ verifyFailed: (d) => tr(`下载的 Node.js 没通过校验(${d}),已丢弃。`, `The Node.js download failed verification (${d}) and was discarded.`),
195
+ downloading: (v) => tr(` 正在下载 Node.js ${v} …`, ` Downloading Node.js ${v}…`),
196
+ installed: (v) => tr(` 已安装 Node.js ${v}`, ` Installed Node.js ${v}`),
197
+ },
198
+ });
199
+ }
200
+ catch (e) {
201
+ if (!(e instanceof NodeRuntimeError))
202
+ throw e;
203
+ console.error(e.code === "env_unusable"
204
+ ? tr(`${e.message}:DeepSeek Harness 需要 Node.js ${DSH_MIN_NODE} 或更新版本。`, `${e.message}: DeepSeek Harness needs Node.js ${DSH_MIN_NODE} or newer.`)
205
+ : e.message);
206
+ return undefined;
207
+ }
208
+ }
156
209
  export function cliInstallHooks(agent = "claude") {
157
210
  let progressShown = false;
158
211
  const endProgress = () => {
@@ -264,6 +317,15 @@ function attestationSource(cfg, models) {
264
317
  },
265
318
  };
266
319
  }
320
+ /**
321
+ * 网关刹车;dsh 没有 max_tested_version(老网关、老缓存)时也不越过实测过的版本——它每天发预发布版,
322
+ * npm 的 latest 随时会变成没测过的版本。
323
+ */
324
+ export function effectiveBrake(agent, brake) {
325
+ if (agent !== "dsh" || brake.maxTestedVersion)
326
+ return brake;
327
+ return { ...brake, maxTestedVersion: DSH_TESTED_VERSION };
328
+ }
267
329
  /**
268
330
  * 子进程启动前的公共部分。返回 number = 启动前就失败的退出码(已向用户说明)。
269
331
  * 是否可用完全由网关配置的 enabled 决定:停用时显示 disabled_reason,不下载、不启动。
@@ -319,21 +381,39 @@ export async function prepareAgentLaunch(profile, paths) {
319
381
  return 1;
320
382
  }
321
383
  const config = result.config;
384
+ // 自定义端点:拉不到时用上次的缓存(与启动 Pi 同一做法)
385
+ await loadCustomEndpoints(cfg).catch(() => undefined);
386
+ // dsh 发 /v1/chat/completions,签名代理的自定义端点路由只转 /v1/messages、/v1/responses:不接自定义模型
387
+ const customModels = profile.agent === "dsh" ? [] : agentCustomModels(CUSTOM_ENDPOINTS);
322
388
  const prefs = readAgentPrefs(profile.agent);
323
- const model = resolveLaunchModel(profile.agent, config, {
389
+ // 用户在 u1s1 config 里选的模型可以是自定义模型
390
+ const lineup = {
391
+ ...config,
392
+ models: [...config.models, ...customModels.map((c) => ({ id: c.id, name: c.model.name, contextWindow: c.model.contextWindow }))],
393
+ };
394
+ const model = resolveLaunchModel(profile.agent, lineup, {
324
395
  prefs,
325
396
  notify: (missing, fallback) => console.error(modelMissingText(profile.agent, missing, fallback)),
326
397
  });
327
398
  mkdirSync(paths.configDir, { recursive: true, mode: 0o700 });
399
+ // 入口是 JS 文件的客户端先准备好够新的 Node(dsh 要 ≥24.9,便携包只带 22)
400
+ let node;
401
+ if (runtimeSpec(profile.agent).minNode) {
402
+ node = await ensureDshNode(site.id);
403
+ if (!node)
404
+ return 1;
405
+ }
406
+ const brake = effectiveBrake(profile.agent, config.runtime);
328
407
  let runtime;
329
408
  try {
330
409
  runtime = await ensureAgentRuntime(profile.agent, {
331
- brake: config.runtime,
410
+ brake,
332
411
  site: site.id,
333
412
  agentsRoot: agentsRoot(),
334
413
  hooks: cliInstallHooks(profile.agent),
335
414
  probeConfigDir: paths.configDir,
336
415
  autoUpdate: prefs.autoUpdate,
416
+ node: node?.bin,
337
417
  });
338
418
  }
339
419
  catch (e) {
@@ -343,8 +423,21 @@ export async function prepareAgentLaunch(profile, paths) {
343
423
  }
344
424
  printRuntimeNotes(runtime, profile.agent);
345
425
  // 子进程用的代理:带上客户端名与版本,网关据此核对请求形状
346
- const proxy = await ensureSigningProxy(cfg, "terminal", attestation, { name: profile.gatewayAgent, version: runtime.version });
347
- return { config, model, autoUpdate: prefs.autoUpdate, runtime, proxy, site: site.id, features: models?.features };
426
+ // 自定义模型的请求在这个代理里本机直连端点(custom-endpoint-proxy.ts),不经网关
427
+ const proxy = await ensureSigningProxy(cfg, "terminal", attestation, { name: profile.gatewayAgent, version: runtime.version }, {
428
+ endpoints: [...CUSTOM_ENDPOINTS],
429
+ });
430
+ return {
431
+ config: { ...config, runtime: brake },
432
+ model,
433
+ autoUpdate: prefs.autoUpdate,
434
+ runtime,
435
+ proxy,
436
+ site: site.id,
437
+ features: models?.features,
438
+ customModels,
439
+ ...(node ? { node } : {}),
440
+ };
348
441
  }
349
442
  /** 用户在 u1s1 config 里选的模型不在当前阵容里(只提示一次,见 prefs.ts)。 */
350
443
  export function modelMissingText(agent, missing, fallback) {
@@ -367,6 +460,8 @@ const SESSION_LAYOUT = {
367
460
  /** configDir 下这次运行(since 之后)写过的最新会话的 id。 */
368
461
  export function latestSessionId(agent, configDir, since) {
369
462
  const layout = SESSION_LAYOUT[agent];
463
+ if (!layout)
464
+ return undefined;
370
465
  let best;
371
466
  const walk = (dir, depth) => {
372
467
  let names;
@@ -403,7 +498,8 @@ export function latestSessionId(agent, configDir, since) {
403
498
  * 我们也不加。
404
499
  */
405
500
  export function wantsResumeHint(agent, userArgs, interactive) {
406
- if (!interactive)
501
+ // dsh 没有终端界面(会话在网页里续),不补提示
502
+ if (!interactive || agent === "dsh")
407
503
  return false;
408
504
  if (agent === "codex")
409
505
  return !["exec", "e", "review", "login", "logout", "mcp", "mcp-server", "app-server", "completion", "apply", "a", "cloud", "sandbox", "debug", "features"].includes(userArgs[0] ?? "")
@@ -431,7 +527,11 @@ export async function runPreparedAgent(input) {
431
527
  }).catch(() => undefined)
432
528
  : Promise.resolve(undefined);
433
529
  const startedAt = Date.now();
434
- const code = await spawnAgent(prepared.runtime.bin, input.args, input.env);
530
+ // JS 入口(dsh):用准备好的 Node 跑;没有终端时 SIGINT 也转发(被别的程序拉起时靠它停下)
531
+ const [command, args] = prepared.node
532
+ ? [prepared.node.bin, [prepared.runtime.bin, ...input.args]]
533
+ : [prepared.runtime.bin, [...input.args]];
534
+ const code = await spawnAgent(command, args, input.env, {}, { forwardInterrupt: !!prepared.node && !process.stdin.isTTY });
435
535
  if (code === 0 && input.paths.configDir && input.userArgs
436
536
  && wantsResumeHint(input.agent, input.userArgs, !!process.stdin.isTTY && !!process.stdout.isTTY)) {
437
537
  // 文件时间的精度与时钟差:往前放 2 秒
@@ -0,0 +1,65 @@
1
+ import type { ApiEndpoint, ApiModel } from "../api.js";
2
+ import { type CustomEndpoint, type ModelDef, type ModelRef } from "../config.js";
3
+ import { type AgentConfig, type GatewayAgentName } from "./agent-config.js";
4
+ export type ModelsClient = "pi" | "claude" | "codex";
5
+ export interface ModelEntry {
6
+ /** 该客户端 --model 要的值 */
7
+ id: string;
8
+ /** 显示名;拿不到时等于 id */
9
+ name: string;
10
+ source: "u1s1" | "custom";
11
+ /** source = custom 时:端点名(后台填的) */
12
+ endpoint?: string;
13
+ }
14
+ export interface ClientModels {
15
+ default: string | null;
16
+ /** 网关是否开放这个客户端(Pi 恒开放,不带这个字段) */
17
+ enabled?: boolean;
18
+ models: ModelEntry[];
19
+ }
20
+ export interface ModelsListing {
21
+ loggedIn: boolean;
22
+ clients: Partial<Record<ModelsClient, ClientModels>>;
23
+ }
24
+ /** Pi 的 --model 写法:provider/model(和 `u1s1 pi --list-models` 两列拼起来一致)。 */
25
+ export declare function piModelId(provider: string, modelId: string): string;
26
+ /**
27
+ * 自定义端点的模型 → 列表条目。idFor 决定该客户端认的 id:Pi 是 provider/model;
28
+ * Claude Code / Codex 接上本地路由后(A2)按各自的写法传进来。
29
+ */
30
+ export declare function customModelEntries(endpoints: readonly CustomEndpoint[], idFor: (endpoint: CustomEndpoint, model: ModelDef) => string): ModelEntry[];
31
+ export declare function piClientModels(official: readonly ModelDef[], endpoints: readonly CustomEndpoint[], preferred: ModelRef): ClientModels;
32
+ /**
33
+ * Claude Code / Codex:网关阵容(agent-config 缓存)+ 自定义端点的模型(只收本地路由转得了的
34
+ * openai-completions 端点,id = ep…/<模型>,见 custom-models.ts)。default 是启动时真正会用的模型:
35
+ * 用户在 u1s1 config 里选的(网关模型或自定义模型)还在就是它,否则网关默认——与 launch.ts 同一判断。
36
+ */
37
+ export declare function gatewayClientModels(config: AgentConfig, preferredModel: string | undefined, endpoints?: readonly CustomEndpoint[]): ClientModels;
38
+ /**
39
+ * 上次启动 Pi 时写进 <agentDir>/models.json 的 u1s1 provider。那里的 baseUrl 通常是本机签名代理,
40
+ * 认不出站点;只排除明确指向别的站点的条目(换站后第一次启动 Pi 之前,离线时可能列出上一站的名字)。
41
+ * 那里的 name 拼了「 · 提示」(见 toProviderModels),这里去掉,只留显示名。
42
+ */
43
+ export declare function officialModelsFromProviderFile(path: string, baseUrl: string): ModelDef[] | undefined;
44
+ /** endpoints.json 只取列表要用的字段,形状不对的条目跳过。 */
45
+ export declare function endpointsFromCache(raw: unknown): CustomEndpoint[];
46
+ export declare function cachedAgentConfig(cacheFile: string, site: string, agent: GatewayAgentName): AgentConfig | undefined;
47
+ export interface LiveCatalog {
48
+ models?: ApiModel[];
49
+ endpoints?: ApiEndpoint[];
50
+ }
51
+ export declare const LIVE_REFRESH_TIMEOUT_MS = 2500;
52
+ /**
53
+ * 并行拉 /v1/models 与 /v1/endpoints,整体不超过 timeoutMs;到点一起中止,
54
+ * 任何失败(断网、401、5xx、格式不对)都只是让对应字段缺席,调用方退回缓存。
55
+ */
56
+ export declare function fetchLiveCatalog(get: (path: "/models" | "/endpoints", signal: AbortSignal) => Promise<Response>, timeoutMs?: number): Promise<LiveCatalog>;
57
+ export interface ModelsCommandDeps {
58
+ offline?: boolean;
59
+ /** 测试注入;默认经 authorizedFetch 请求当前站点 */
60
+ fetchLive?: () => Promise<LiveCatalog>;
61
+ }
62
+ export declare function buildModelsListing(deps?: ModelsCommandDeps): Promise<ModelsListing>;
63
+ /** 人看的表。 */
64
+ export declare function formatModelsTable(listing: ModelsListing): string[];
65
+ export declare function modelsCommand(args: readonly string[], deps?: ModelsCommandDeps): Promise<number>;