u1s1-cli 1.19.0 → 1.20.1
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/agents/agent-config.d.ts +1 -1
- package/dist/agents/agent-config.js +1 -1
- package/dist/agents/dsh.d.ts +90 -0
- package/dist/agents/dsh.js +249 -0
- package/dist/agents/launch.d.ts +21 -2
- package/dist/agents/launch.js +100 -12
- package/dist/agents/node-runtime.d.ts +91 -0
- package/dist/agents/node-runtime.js +223 -0
- package/dist/agents/runtime.d.ts +26 -3
- package/dist/agents/runtime.js +50 -14
- package/dist/agents/status.d.ts +4 -0
- package/dist/agents/status.js +32 -7
- package/dist/config-command.d.ts +1 -1
- package/dist/config-command.js +16 -10
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/device-auth.d.ts +1 -1
- package/dist/index.js +3 -2
- package/dist/launcher.d.ts +5 -1
- package/dist/launcher.js +27 -11
- package/package.json +1 -1
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* 契约(网关 workers/gateway/src/agent-config.ts):
|
|
5
5
|
* { agent, enabled, disabled_reason, runtime: { min_version, max_tested_version, blocked_versions },
|
|
6
6
|
* default_model, small_model, models: [...], settings }
|
|
7
|
-
* default_model / small_model 在没有模型时是 null;settings
|
|
7
|
+
* default_model / small_model 在没有模型时是 null;settings 在 claude-code / codex / dsh 启用时是对象(各客户端自己的形状),其余 null。
|
|
8
8
|
* 响应里没有任何凭证或端点:地址和本地 token 由启动器自己注入。
|
|
9
9
|
*
|
|
10
10
|
* 结果处理:
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { type McpLaunchEnv } from "../mcp-server.js";
|
|
2
|
+
import type { AgentConfig } from "./agent-config.js";
|
|
3
|
+
import { type RuntimePaths } from "./runtime.js";
|
|
4
|
+
/** patch 里我们的 provider 名(dsh 的路由名:小写加连字符)。 */
|
|
5
|
+
export declare const DSH_PROVIDER_ID = "u1s1";
|
|
6
|
+
/** provider 的 apiKeyEnv:子进程环境里放本地 token 的变量名。 */
|
|
7
|
+
export declare const DSH_KEY_ENV = "U1S1_LOCAL_KEY";
|
|
8
|
+
/** 签名代理地址(带 /v1),patch 里用 `!!js process.env.U1S1_DSH_BASE_URL` 读。 */
|
|
9
|
+
export declare const DSH_BASE_URL_ENV = "U1S1_DSH_BASE_URL";
|
|
10
|
+
export interface DshPaths extends RuntimePaths {
|
|
11
|
+
/** DSH_HOME */
|
|
12
|
+
configDir: string;
|
|
13
|
+
/** 启动器生成的 --patch 文件 */
|
|
14
|
+
patchFile: string;
|
|
15
|
+
/** 上次成功拉到的 /v1/agents/dsh/config */
|
|
16
|
+
configCache: string;
|
|
17
|
+
}
|
|
18
|
+
export declare function dshPaths(root?: string): DshPaths;
|
|
19
|
+
/**
|
|
20
|
+
* 子进程环境里要去掉的变量(依据 dsh 0.2.2-rc.1 的默认配置树):
|
|
21
|
+
* - DSH_HOME(我们自己设);
|
|
22
|
+
* - DEEPSEEK_API_KEY:官方 API Key 通道和官方网页搜索读它,有了就会绕开 u1s1 直连官方;
|
|
23
|
+
* - 遥测/产品分析的地址与模式(DSH_TELEMETRY_MODE、DSH_TELEMETRY_OTLP_URL、DSH_PRODUCT_ANALYTICS_OTLP_URL、
|
|
24
|
+
* DSH_CLIENT_VERSION),以及换搜索/抓取提供方的 DSH_WEB_SEARCH_PROVIDER、DSH_WEB_FETCH_PROVIDER;
|
|
25
|
+
* - 我们自己的变量:关掉 MCP 注入时也不留用户环境里的旧值。
|
|
26
|
+
* DSH_PERMISSION_MODE(用户自己选的沙箱模式)保留。
|
|
27
|
+
*/
|
|
28
|
+
export declare const DSH_ENV_DROP: readonly string[];
|
|
29
|
+
export interface DshLaunchInput {
|
|
30
|
+
baseEnv: NodeJS.ProcessEnv;
|
|
31
|
+
/** 签名代理的一次性本地 token */
|
|
32
|
+
localToken: string;
|
|
33
|
+
/** 签名代理地址,形如 http://127.0.0.1:12345/v1 */
|
|
34
|
+
proxyBaseUrl: string;
|
|
35
|
+
/** DSH_HOME */
|
|
36
|
+
home: string;
|
|
37
|
+
/** u1s1 工具服务要的变量(mcpLaunchEnv);不注入时省略 */
|
|
38
|
+
mcp?: McpLaunchEnv;
|
|
39
|
+
}
|
|
40
|
+
export declare function dshLaunchEnv(input: DshLaunchInput): NodeJS.ProcessEnv;
|
|
41
|
+
/**
|
|
42
|
+
* 用户参数 → dsh 参数。dsh 的语法是 `dsh [--profile] <名字> [启动器选项] [应用参数]`,`--patch` 是启动器选项,
|
|
43
|
+
* 要紧跟在 profile 名后面(第一个它不认识的参数起就是应用参数)。
|
|
44
|
+
* (无参数) → web --patch <文件>
|
|
45
|
+
* --no-open --port 1 → web --patch <文件> --no-open --port 1(以选项开头:默认进网页界面)
|
|
46
|
+
* headless "任务" → headless --patch <文件> "任务"
|
|
47
|
+
* --profile web … → --profile web --patch <文件> …
|
|
48
|
+
* --help / --version → 原样(dsh 自己的帮助和版本)
|
|
49
|
+
* plugin … → 原样(插件管理,不启动 profile)
|
|
50
|
+
* 用户自己再传的 --patch 排在我们的后面,后叠的为准。
|
|
51
|
+
*/
|
|
52
|
+
export declare function dshLaunchArgs(patchFile: string, userArgs: readonly string[]): string[];
|
|
53
|
+
/**
|
|
54
|
+
* 网关 settings.models(dsh-config.ts 下发的 dsh 原生形状)→ provider 模型表。没有 settings(老网关)时
|
|
55
|
+
* 退回 models 阵容里的 id、名字、窗口。
|
|
56
|
+
*/
|
|
57
|
+
export declare function dshProviderModels(config: Pick<AgentConfig, "settings" | "models">): Array<Record<string, unknown>>;
|
|
58
|
+
export interface DshPatchInput {
|
|
59
|
+
models: Array<Record<string, unknown>>;
|
|
60
|
+
/** 这次启动的默认模型 */
|
|
61
|
+
model: string;
|
|
62
|
+
/** 会话标题等后台小任务用的模型;null = 跟默认模型 */
|
|
63
|
+
smallModel: string | null;
|
|
64
|
+
/** u1s1 工具服务怎么拉起;null = 不注入 */
|
|
65
|
+
mcp: {
|
|
66
|
+
command: string;
|
|
67
|
+
args: string[];
|
|
68
|
+
envVars: string[];
|
|
69
|
+
} | null;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* 生成 --patch 文件。dsh 的 patch 是顶层数组:按 id 覆盖条目(config 整段替换、不深合并),`insert` 追加新条目。
|
|
73
|
+
* 文件里没有端点和凭证:baseURL 与 MCP 服务的变量都用 `!!js process.env.…` 在 dsh 进程里读。
|
|
74
|
+
*/
|
|
75
|
+
export declare function renderDshPatch(input: DshPatchInput): string;
|
|
76
|
+
/** MCP 服务:同一个 node、同一份 CLI;env 里只列变量名,值由 dsh 从自己的环境转过去。 */
|
|
77
|
+
export declare function dshMcpServer(command?: {
|
|
78
|
+
command: string;
|
|
79
|
+
args: string[];
|
|
80
|
+
}, env?: NodeJS.ProcessEnv): {
|
|
81
|
+
command: string;
|
|
82
|
+
args: string[];
|
|
83
|
+
envVars: string[];
|
|
84
|
+
};
|
|
85
|
+
export declare function writeDshPatch(paths: DshPaths, input: DshPatchInput): void;
|
|
86
|
+
/**
|
|
87
|
+
* `u1s1 dsh [参数…]`。返回子进程的退出码(启动前就失败返回 1)。
|
|
88
|
+
* 是否可用完全由网关配置的 enabled 决定(prepareAgentLaunch)。
|
|
89
|
+
*/
|
|
90
|
+
export declare function runDsh(userArgs: string[]): Promise<number>;
|
|
@@ -0,0 +1,249 @@
|
|
|
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
|
+
"# dsh's own web_search has no usable search provider, so turn it off (u1s1 search is over MCP); only affects profiles with top-level tool entries such as headless (the web profile's tools live in its agent preset)",
|
|
200
|
+
"- id: tool-web",
|
|
201
|
+
" config:",
|
|
202
|
+
" search: false",
|
|
203
|
+
" fetch: true",
|
|
204
|
+
];
|
|
205
|
+
if (input.mcp) {
|
|
206
|
+
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}`));
|
|
207
|
+
}
|
|
208
|
+
return lines.join("\n") + "\n";
|
|
209
|
+
}
|
|
210
|
+
/** MCP 服务:同一个 node、同一份 CLI;env 里只列变量名,值由 dsh 从自己的环境转过去。 */
|
|
211
|
+
export function dshMcpServer(command = mcpServerCommand(), env = process.env) {
|
|
212
|
+
const envVars = [...MCP_ENV_NAMES];
|
|
213
|
+
if (env[MCP_TRACE_ENV])
|
|
214
|
+
envVars.push(MCP_TRACE_ENV);
|
|
215
|
+
return { ...command, envVars };
|
|
216
|
+
}
|
|
217
|
+
export function writeDshPatch(paths, input) {
|
|
218
|
+
writePrivateFile(paths.patchFile, renderDshPatch(input));
|
|
219
|
+
}
|
|
220
|
+
// ---------------------------------------------------------------- run
|
|
221
|
+
/**
|
|
222
|
+
* `u1s1 dsh [参数…]`。返回子进程的退出码(启动前就失败返回 1)。
|
|
223
|
+
* 是否可用完全由网关配置的 enabled 决定(prepareAgentLaunch)。
|
|
224
|
+
*/
|
|
225
|
+
export async function runDsh(userArgs) {
|
|
226
|
+
const paths = dshPaths();
|
|
227
|
+
const prepared = await prepareAgentLaunch(AGENT_PROFILES.dsh, paths);
|
|
228
|
+
if (typeof prepared === "number")
|
|
229
|
+
return prepared;
|
|
230
|
+
const models = dshProviderModels(prepared.config);
|
|
231
|
+
const model = prepared.model ?? prepared.config.defaultModel ?? models[0]?.["id"];
|
|
232
|
+
if (typeof model !== "string") {
|
|
233
|
+
console.error("No model is available for DeepSeek Harness right now.");
|
|
234
|
+
return 1;
|
|
235
|
+
}
|
|
236
|
+
const mcp = mcpInjectionDisabled()
|
|
237
|
+
? undefined
|
|
238
|
+
: mcpLaunchEnv({ proxyBaseUrl: prepared.proxy.baseUrl, localToken: prepared.proxy.localKey, features: prepared.features, lang: lang() });
|
|
239
|
+
writeDshPatch(paths, { models, model, smallModel: prepared.config.smallModel, mcp: mcp ? dshMcpServer() : null });
|
|
240
|
+
const env = dshLaunchEnv({
|
|
241
|
+
baseEnv: process.env,
|
|
242
|
+
localToken: prepared.proxy.localKey,
|
|
243
|
+
proxyBaseUrl: prepared.proxy.baseUrl,
|
|
244
|
+
home: paths.configDir,
|
|
245
|
+
mcp,
|
|
246
|
+
});
|
|
247
|
+
const args = dshLaunchArgs(paths.patchFile, userArgs);
|
|
248
|
+
return runPreparedAgent({ agent: "dsh", paths, prepared, args, env, userArgs });
|
|
249
|
+
}
|
package/dist/agents/launch.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 官方原版客户端(`u1s1 claude` / `u1s1 codex`)共用的启动流程与文案:
|
|
2
|
+
* 官方原版客户端(`u1s1 claude` / `u1s1 codex` / `u1s1 dsh`)共用的启动流程与文案:
|
|
3
3
|
* 登录 → 经签名代理拉网关配置(开关、刹车、阵容)→ 找到/下载原版 → 起声明了客户端的签名代理 →
|
|
4
4
|
* 由各客户端模块写配置、拼环境 → 子进程继承终端 → 透传退出码;顺带每天查一次新版。
|
|
5
5
|
*
|
|
@@ -10,13 +10,14 @@ 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
12
|
import { type AgentCustomModel } from "./custom-models.js";
|
|
13
|
+
import { type FoundNode, type NodeDownloadInfo } from "./node-runtime.js";
|
|
13
14
|
import { RuntimePlanError, type DownloadInfo, type EnsuredRuntime, type InstallHooks, type ManagedAgent, type RuntimePaths, type SkippedRuntime } from "./runtime.js";
|
|
14
15
|
export declare function agentsRoot(): string;
|
|
15
16
|
export interface AgentProfile {
|
|
16
17
|
/** 子命令名,也是 ~/.u1s1/agents 下的目录名 */
|
|
17
18
|
agent: ManagedAgent;
|
|
18
19
|
/** 网关里的客户端名(/v1/agents/:agent/config、x-u1s1-agent) */
|
|
19
|
-
gatewayAgent: Extract<GatewayAgentName, "claude-code" | "codex">;
|
|
20
|
+
gatewayAgent: Extract<GatewayAgentName, "claude-code" | "codex" | "dsh">;
|
|
20
21
|
/** 产品名(只作事实陈述,如「官方 Claude Code」) */
|
|
21
22
|
product: string;
|
|
22
23
|
/** 下载前提示里的许可/条款一行 */
|
|
@@ -26,6 +27,7 @@ export interface AgentProfile {
|
|
|
26
27
|
}
|
|
27
28
|
export declare const CLAUDE_TERMS_URL = "https://code.claude.com/docs/en/legal-and-compliance";
|
|
28
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";
|
|
29
31
|
export declare const AGENT_PROFILES: Record<ManagedAgent, AgentProfile>;
|
|
30
32
|
/**
|
|
31
33
|
* 请求本机签名代理(http://127.0.0.1:<端口>)专用:直连,不走用户的 HTTP(S)_PROXY。
|
|
@@ -45,8 +47,18 @@ export declare function loopbackFetch(url: string, init: RequestInit): Promise<R
|
|
|
45
47
|
export declare function spawnAgent(bin: string, args: readonly string[], env: NodeJS.ProcessEnv, deps?: {
|
|
46
48
|
spawn?: typeof spawn;
|
|
47
49
|
proc?: Pick<NodeJS.Process, "on" | "off">;
|
|
50
|
+
}, opts?: {
|
|
51
|
+
/**
|
|
52
|
+
* 也转发 SIGINT。只在没有终端时用(dsh 被别的程序拉起、对启动器进程发 SIGINT):那时没有终端
|
|
53
|
+
* 把 Ctrl+C 同时发给子进程,不转发子进程就收不到。有终端时照旧不转发,免得收到两次。
|
|
54
|
+
*/
|
|
55
|
+
forwardInterrupt?: boolean;
|
|
48
56
|
}): Promise<number>;
|
|
49
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>;
|
|
50
62
|
export declare function cliInstallHooks(agent?: ManagedAgent): InstallHooks;
|
|
51
63
|
/** 已有的某份客户端高于 max_tested、这次不用它。 */
|
|
52
64
|
export declare function overMaxTestedText(s: SkippedRuntime, agent?: ManagedAgent): string;
|
|
@@ -79,7 +91,14 @@ export interface PreparedLaunch {
|
|
|
79
91
|
features: ApiFeatures | undefined;
|
|
80
92
|
/** 账号里的自定义端点模型(id = ep…/<模型>):接在客户端模型阵容后面,请求由签名代理在本机直连端点 */
|
|
81
93
|
customModels: AgentCustomModel[];
|
|
94
|
+
/** 入口是 JS 文件的客户端(dsh)用来运行它的 Node;其余为 undefined */
|
|
95
|
+
node?: FoundNode;
|
|
82
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* 网关刹车;dsh 没有 max_tested_version(老网关、老缓存)时也不越过实测过的版本——它每天发预发布版,
|
|
99
|
+
* npm 的 latest 随时会变成没测过的版本。
|
|
100
|
+
*/
|
|
101
|
+
export declare function effectiveBrake(agent: ManagedAgent, brake: AgentConfig["runtime"]): AgentConfig["runtime"];
|
|
83
102
|
/**
|
|
84
103
|
* 子进程启动前的公共部分。返回 number = 启动前就失败的退出码(已向用户说明)。
|
|
85
104
|
* 是否可用完全由网关配置的 enabled 决定:停用时显示 disabled_reason,不下载、不启动。
|
package/dist/agents/launch.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 官方原版客户端(`u1s1 claude` / `u1s1 codex`)共用的启动流程与文案:
|
|
2
|
+
* 官方原版客户端(`u1s1 claude` / `u1s1 codex` / `u1s1 dsh`)共用的启动流程与文案:
|
|
3
3
|
* 登录 → 经签名代理拉网关配置(开关、刹车、阵容)→ 找到/下载原版 → 起声明了客户端的签名代理 →
|
|
4
4
|
* 由各客户端模块写配置、拼环境 → 子进程继承终端 → 透传退出码;顺带每天查一次新版。
|
|
5
5
|
*
|
|
@@ -19,12 +19,14 @@ import { siteLinks, siteOf } from "../site.js";
|
|
|
19
19
|
import { loadAgentConfig } from "./agent-config.js";
|
|
20
20
|
import { agentCustomModels } from "./custom-models.js";
|
|
21
21
|
import { readAgentPrefs, resolveLaunchModel } from "./prefs.js";
|
|
22
|
-
import {
|
|
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";
|
|
23
24
|
export function agentsRoot() {
|
|
24
25
|
return join(u1s1Dir, "agents");
|
|
25
26
|
}
|
|
26
27
|
export const CLAUDE_TERMS_URL = "https://code.claude.com/docs/en/legal-and-compliance";
|
|
27
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";
|
|
28
30
|
export const AGENT_PROFILES = {
|
|
29
31
|
claude: {
|
|
30
32
|
agent: "claude",
|
|
@@ -40,6 +42,13 @@ export const AGENT_PROFILES = {
|
|
|
40
42
|
termsLine: () => tr(` Codex CLI 以 Apache-2.0 许可发布:${CODEX_LICENSE_URL}`, ` Codex CLI is released under the Apache-2.0 license: ${CODEX_LICENSE_URL}`),
|
|
41
43
|
locationLine: () => tr(" 位置:~/.u1s1/agents/codex(不影响你自己装的版本和 ~/.codex)", " Location: ~/.u1s1/agents/codex (your own install and ~/.codex are left alone)"),
|
|
42
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
|
+
},
|
|
43
52
|
};
|
|
44
53
|
// ---------------------------------------------------------------- loopback
|
|
45
54
|
let loopbackAgent;
|
|
@@ -66,18 +75,20 @@ export function loopbackFetch(url, init) {
|
|
|
66
75
|
* SIGTERM / SIGHUP 通常只发给启动器(kill、关窗口),转发给子进程。
|
|
67
76
|
* 签名代理的监听是 unref 的:必须等子进程退出后再返回。
|
|
68
77
|
*/
|
|
69
|
-
export function spawnAgent(bin, args, env, deps = {}) {
|
|
78
|
+
export function spawnAgent(bin, args, env, deps = {}, opts = {}) {
|
|
70
79
|
const proc = deps.proc ?? process;
|
|
71
80
|
const child = (deps.spawn ?? spawn)(bin, [...args], { env, stdio: "inherit" });
|
|
72
81
|
const swallow = () => { };
|
|
73
82
|
const forwarders = new Map();
|
|
74
|
-
|
|
83
|
+
const forwarded = opts.forwardInterrupt ? ["SIGTERM", "SIGHUP", "SIGINT"] : ["SIGTERM", "SIGHUP"];
|
|
84
|
+
for (const sig of forwarded) {
|
|
75
85
|
forwarders.set(sig, () => {
|
|
76
86
|
if (child.exitCode === null && child.signalCode === null)
|
|
77
87
|
child.kill(sig);
|
|
78
88
|
});
|
|
79
89
|
}
|
|
80
|
-
const
|
|
90
|
+
const swallowed = process.platform === "win32" ? ["SIGINT"] : ["SIGINT", "SIGQUIT"];
|
|
91
|
+
const ignored = swallowed.filter((sig) => !forwarders.has(sig));
|
|
81
92
|
for (const sig of ignored)
|
|
82
93
|
proc.on(sig, swallow);
|
|
83
94
|
for (const [sig, fn] of forwarders)
|
|
@@ -135,14 +146,27 @@ export function consentLines(info) {
|
|
|
135
146
|
profile.termsLine(),
|
|
136
147
|
];
|
|
137
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
|
+
}
|
|
138
159
|
async function askConsent(info) {
|
|
139
|
-
|
|
160
|
+
return askYesNo(consentLines(info), info.agent);
|
|
161
|
+
}
|
|
162
|
+
async function askYesNo(lines, agent) {
|
|
163
|
+
for (const line of lines)
|
|
140
164
|
console.error(line);
|
|
141
165
|
const preset = process.env["U1S1_AGENT_INSTALL"]?.trim().toLowerCase();
|
|
142
166
|
if (preset === "yes" || preset === "y" || preset === "1")
|
|
143
167
|
return true;
|
|
144
168
|
if (!process.stdin.isTTY) {
|
|
145
|
-
console.error(tr(` 当前没有可交互的终端。请在终端里运行一次 u1s1 ${
|
|
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.`));
|
|
146
170
|
return false;
|
|
147
171
|
}
|
|
148
172
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
@@ -154,6 +178,34 @@ async function askConsent(info) {
|
|
|
154
178
|
rl.close();
|
|
155
179
|
}
|
|
156
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
|
+
}
|
|
157
209
|
export function cliInstallHooks(agent = "claude") {
|
|
158
210
|
let progressShown = false;
|
|
159
211
|
const endProgress = () => {
|
|
@@ -265,6 +317,15 @@ function attestationSource(cfg, models) {
|
|
|
265
317
|
},
|
|
266
318
|
};
|
|
267
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
|
+
}
|
|
268
329
|
/**
|
|
269
330
|
* 子进程启动前的公共部分。返回 number = 启动前就失败的退出码(已向用户说明)。
|
|
270
331
|
* 是否可用完全由网关配置的 enabled 决定:停用时显示 disabled_reason,不下载、不启动。
|
|
@@ -322,7 +383,8 @@ export async function prepareAgentLaunch(profile, paths) {
|
|
|
322
383
|
const config = result.config;
|
|
323
384
|
// 自定义端点:拉不到时用上次的缓存(与启动 Pi 同一做法)
|
|
324
385
|
await loadCustomEndpoints(cfg).catch(() => undefined);
|
|
325
|
-
|
|
386
|
+
// dsh 发 /v1/chat/completions,签名代理的自定义端点路由只转 /v1/messages、/v1/responses:不接自定义模型
|
|
387
|
+
const customModels = profile.agent === "dsh" ? [] : agentCustomModels(CUSTOM_ENDPOINTS);
|
|
326
388
|
const prefs = readAgentPrefs(profile.agent);
|
|
327
389
|
// 用户在 u1s1 config 里选的模型可以是自定义模型
|
|
328
390
|
const lineup = {
|
|
@@ -334,15 +396,24 @@ export async function prepareAgentLaunch(profile, paths) {
|
|
|
334
396
|
notify: (missing, fallback) => console.error(modelMissingText(profile.agent, missing, fallback)),
|
|
335
397
|
});
|
|
336
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);
|
|
337
407
|
let runtime;
|
|
338
408
|
try {
|
|
339
409
|
runtime = await ensureAgentRuntime(profile.agent, {
|
|
340
|
-
brake
|
|
410
|
+
brake,
|
|
341
411
|
site: site.id,
|
|
342
412
|
agentsRoot: agentsRoot(),
|
|
343
413
|
hooks: cliInstallHooks(profile.agent),
|
|
344
414
|
probeConfigDir: paths.configDir,
|
|
345
415
|
autoUpdate: prefs.autoUpdate,
|
|
416
|
+
node: node?.bin,
|
|
346
417
|
});
|
|
347
418
|
}
|
|
348
419
|
catch (e) {
|
|
@@ -356,7 +427,17 @@ export async function prepareAgentLaunch(profile, paths) {
|
|
|
356
427
|
const proxy = await ensureSigningProxy(cfg, "terminal", attestation, { name: profile.gatewayAgent, version: runtime.version }, {
|
|
357
428
|
endpoints: [...CUSTOM_ENDPOINTS],
|
|
358
429
|
});
|
|
359
|
-
return {
|
|
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
|
+
};
|
|
360
441
|
}
|
|
361
442
|
/** 用户在 u1s1 config 里选的模型不在当前阵容里(只提示一次,见 prefs.ts)。 */
|
|
362
443
|
export function modelMissingText(agent, missing, fallback) {
|
|
@@ -379,6 +460,8 @@ const SESSION_LAYOUT = {
|
|
|
379
460
|
/** configDir 下这次运行(since 之后)写过的最新会话的 id。 */
|
|
380
461
|
export function latestSessionId(agent, configDir, since) {
|
|
381
462
|
const layout = SESSION_LAYOUT[agent];
|
|
463
|
+
if (!layout)
|
|
464
|
+
return undefined;
|
|
382
465
|
let best;
|
|
383
466
|
const walk = (dir, depth) => {
|
|
384
467
|
let names;
|
|
@@ -415,7 +498,8 @@ export function latestSessionId(agent, configDir, since) {
|
|
|
415
498
|
* 我们也不加。
|
|
416
499
|
*/
|
|
417
500
|
export function wantsResumeHint(agent, userArgs, interactive) {
|
|
418
|
-
|
|
501
|
+
// dsh 没有终端界面(会话在网页里续),不补提示
|
|
502
|
+
if (!interactive || agent === "dsh")
|
|
419
503
|
return false;
|
|
420
504
|
if (agent === "codex")
|
|
421
505
|
return !["exec", "e", "review", "login", "logout", "mcp", "mcp-server", "app-server", "completion", "apply", "a", "cloud", "sandbox", "debug", "features"].includes(userArgs[0] ?? "")
|
|
@@ -443,7 +527,11 @@ export async function runPreparedAgent(input) {
|
|
|
443
527
|
}).catch(() => undefined)
|
|
444
528
|
: Promise.resolve(undefined);
|
|
445
529
|
const startedAt = Date.now();
|
|
446
|
-
|
|
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 });
|
|
447
535
|
if (code === 0 && input.paths.configDir && input.userArgs
|
|
448
536
|
&& wantsResumeHint(input.agent, input.userArgs, !!process.stdin.isTTY && !!process.stdout.isTTY)) {
|
|
449
537
|
// 文件时间的精度与时钟差:往前放 2 秒
|