dsh-any-agent 0.5.2 → 0.6.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/CHANGELOG.md +126 -1
- package/USAGE-NOTES.en.md +3 -1
- package/USAGE-NOTES.md +9 -2
- package/bin/dsh-any-agent-workspaces.mjs +12 -5
- package/bin/dsh-any-agent.mjs +4 -1
- package/bin/restore.mjs +1 -1
- package/docs/en/compatibility.md +27 -6
- package/docs/en/reference.md +2 -1
- package/docs/en/release.md +4 -0
- package/docs/en/tests.md +57 -15
- package/docs/zh/compatibility.md +25 -6
- package/docs/zh/reference.md +2 -1
- package/docs/zh/release.md +3 -0
- package/docs/zh/tests.md +57 -15
- package/package.json +4 -1
- package/scripts/detect-dsh.mjs +55 -0
- package/scripts/lint.mjs +595 -0
- package/scripts/sync-test-counts.mjs +699 -0
- package/src/acp-session.mjs +63 -0
- package/src/acp.mjs +14 -5
- package/src/config.mjs +14 -111
- package/src/conversation.mjs +1600 -0
- package/src/doctor.mjs +203 -0
- package/src/dsh-locate.mjs +560 -0
- package/src/git-snapshot.mjs +196 -0
- package/src/hub-store.mjs +108 -0
- package/src/hub.mjs +26 -1972
- package/src/mcp.mjs +21 -75
- package/src/residue.mjs +278 -0
- package/src/sentinel.mjs +97 -0
- package/src/tool-defs.mjs +399 -0
- package/src/tool-render.mjs +262 -0
- package/src/tools.mjs +18 -767
- package/src/workspace-guard.mjs +89 -0
- package/src/workspace.mjs +295 -59
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话与 ACP 之间的"线格式"小函数:权限应答选项、审批上下文、会话级配置下发。
|
|
3
|
+
*
|
|
4
|
+
* 从 hub.mjs 拆出来:它们只读入参、通过传入的 `proc.request` 说话,不碰 Conversation/Hub
|
|
5
|
+
* 的状态,所以放在一起方便对照 ACP 的规范形状。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { sanitizeApprovalContext } from './redact.mjs';
|
|
9
|
+
|
|
10
|
+
/** 按 ACP 权限应答的规范形状挑选项。 */
|
|
11
|
+
export function pickOption(options, want) {
|
|
12
|
+
const list = Array.isArray(options) ? options : [];
|
|
13
|
+
const match = list.find((o) => typeof o?.kind === 'string' && o.kind.startsWith(want));
|
|
14
|
+
return match?.optionId;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function approvalContext(params, toolCall, desc, options) {
|
|
18
|
+
const input = toolCall.rawInput ?? toolCall.input ?? null;
|
|
19
|
+
return sanitizeApprovalContext({
|
|
20
|
+
tool: toolCall.name ?? desc,
|
|
21
|
+
title: desc,
|
|
22
|
+
tool_call_id: toolCall.toolCallId ?? null,
|
|
23
|
+
kind: toolCall.kind ?? null,
|
|
24
|
+
description: input?.description ?? null,
|
|
25
|
+
reason: params?.reason ?? input?.justification ?? input?.reason ?? null,
|
|
26
|
+
input,
|
|
27
|
+
locations: toolCall.locations ?? [],
|
|
28
|
+
options,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 把会话级配置套到 ACP 会话上。
|
|
34
|
+
*
|
|
35
|
+
* 两件必须知道的事:
|
|
36
|
+
* 1) ACP 的 `reasoning_effort` 默认值是**空串**(Provider default),不是最大档,
|
|
37
|
+
* 所以要显式设。传 'default' 即还原成空串(不设置)。
|
|
38
|
+
* 2) **新进程不会记住**这些选择,因此 session/new 之后、以及每次 resume 之后都要重套一遍。
|
|
39
|
+
*
|
|
40
|
+
* 失败不致命:记进 applied/failed 供上层展示,避免因为某个模型不支持某一项而整个建会话失败。
|
|
41
|
+
*/
|
|
42
|
+
export async function applySessionConfig(proc, sessionId, cfg) {
|
|
43
|
+
const applied = [];
|
|
44
|
+
const failed = [];
|
|
45
|
+
const set = async (configId, value, label) => {
|
|
46
|
+
if (value === undefined || value === null || value === '') return;
|
|
47
|
+
try {
|
|
48
|
+
// 注意方法名是 ACP 的 snake_case:session/set_config_option
|
|
49
|
+
// (写成 session/setConfigOption 会得到 -32601 Method not found,且很容易被忽略)
|
|
50
|
+
await proc.request('session/set_config_option', { sessionId, configId, value }, 30_000);
|
|
51
|
+
applied.push(label);
|
|
52
|
+
} catch (e) {
|
|
53
|
+
failed.push(`${label} 设置失败: ${e.message}`);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
const effort = cfg.reasoningEffort === 'default' ? '' : cfg.reasoningEffort;
|
|
57
|
+
// Selecting a model resets its dependent reasoning option in ACP. Apply effort last.
|
|
58
|
+
if (cfg.provider && cfg.model) {
|
|
59
|
+
await set('model', JSON.stringify([cfg.provider, cfg.model]), `模型=${cfg.provider}/${cfg.model}`);
|
|
60
|
+
}
|
|
61
|
+
await set('reasoning_effort', effort, `思考深度=${effort}`);
|
|
62
|
+
return { applied, failed };
|
|
63
|
+
}
|
package/src/acp.mjs
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
DSH_PROFILE,
|
|
17
17
|
PKG_VERSION,
|
|
18
18
|
REQUEST_TIMEOUT_MS,
|
|
19
|
-
|
|
19
|
+
resolveDsh,
|
|
20
20
|
} from './config.mjs';
|
|
21
21
|
import { LineDecoder } from './line-decoder.mjs';
|
|
22
22
|
|
|
@@ -97,17 +97,19 @@ export class AcpProcess {
|
|
|
97
97
|
} else if (ready.skipped) {
|
|
98
98
|
this.#log.debug(`[acp] profile 引导跳过: ${ready.skipped}`);
|
|
99
99
|
}
|
|
100
|
-
|
|
101
|
-
const
|
|
100
|
+
// 桌面版:用它自带的 Electron 以 Node 模式跑 asar 里的 CLI;npm/npx:用本进程的 node。
|
|
101
|
+
const dsh = resolveDsh();
|
|
102
|
+
const env = { ...process.env, ...dsh.env, DSH_PERMISSION_MODE: this.permission };
|
|
102
103
|
// 0.4.0:可选的子进程堆上限。只加参数、不覆盖用户已有的 NODE_OPTIONS。
|
|
103
104
|
if (CHILD_MAX_OLD_SPACE_MB > 0) {
|
|
104
105
|
const prev = String(env.NODE_OPTIONS ?? '').trim();
|
|
105
106
|
env.NODE_OPTIONS = `${prev} --max-old-space-size=${CHILD_MAX_OLD_SPACE_MB}`.trim();
|
|
106
107
|
}
|
|
107
|
-
this.#child = spawn(
|
|
108
|
+
this.#child = spawn(dsh.command, [...dsh.args, '--profile', DSH_PROFILE], {
|
|
108
109
|
cwd: this.cwd,
|
|
109
110
|
env,
|
|
110
111
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
112
|
+
windowsHide: true,
|
|
111
113
|
});
|
|
112
114
|
|
|
113
115
|
this.#child.stdout.on('data', (d) => this.#onData(d));
|
|
@@ -243,7 +245,14 @@ export class AcpProcess {
|
|
|
243
245
|
const timer =
|
|
244
246
|
timeoutMs > 0
|
|
245
247
|
? setTimeout(() => {
|
|
246
|
-
if (this.#pending.delete(id))
|
|
248
|
+
if (!this.#pending.delete(id)) return;
|
|
249
|
+
// 带上机器可读的 code/method:上层要区分"DSH 没回话(超时)"与"DSH 明确拒绝",
|
|
250
|
+
// 前者可能是瞬时卡顿(可重试),后者重试只会重复同一个错误,不能靠匹配中文报错文本来分。
|
|
251
|
+
const err = new Error(`ACP 请求超时: ${method}`);
|
|
252
|
+
err.code = 'ACP_TIMEOUT';
|
|
253
|
+
err.method = method;
|
|
254
|
+
err.timeoutMs = timeoutMs;
|
|
255
|
+
reject(err);
|
|
247
256
|
}, timeoutMs)
|
|
248
257
|
: null;
|
|
249
258
|
this.#pending.set(id, { resolve, reject, timer });
|
package/src/config.mjs
CHANGED
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
* 因为本服务是被 Codex/Claude 以未知环境拉起的。
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
9
9
|
import { homedir } from 'node:os';
|
|
10
10
|
import { join, dirname } from 'node:path';
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { cmpVersion, resolveDsh } from './dsh-locate.mjs';
|
|
12
13
|
|
|
13
14
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
14
15
|
export const PKG_ROOT = join(HERE, '..');
|
|
@@ -22,121 +23,31 @@ export const PKG_VERSION = (() => {
|
|
|
22
23
|
}
|
|
23
24
|
})();
|
|
24
25
|
|
|
25
|
-
/** 相对 DSH 包入口的固定尾巴。 */
|
|
26
|
-
const DSH_ENTRY = join('@deepseek-ai', 'dsh', 'lib', 'bin.js');
|
|
27
|
-
|
|
28
26
|
/**
|
|
29
|
-
*
|
|
30
|
-
* npx
|
|
27
|
+
* DSH 的发现与拉起方式见 dsh-locate.mjs:自动识别桌面版(asar + 自带 Electron 运行时)、
|
|
28
|
+
* npm 全局与 npx 缓存,DSH_BIN 只作显式覆盖。这里转出给其余模块用。
|
|
31
29
|
*/
|
|
32
|
-
|
|
33
|
-
const roots = [
|
|
34
|
-
join(homedir(), 'AppData', 'Local', 'npm-cache', '_npx'),
|
|
35
|
-
join(homedir(), '.npm', '_npx'),
|
|
36
|
-
];
|
|
37
|
-
for (const root of roots) {
|
|
38
|
-
if (!existsSync(root)) continue;
|
|
39
|
-
let entries;
|
|
40
|
-
try {
|
|
41
|
-
entries = readdirSync(root, { withFileTypes: true });
|
|
42
|
-
} catch {
|
|
43
|
-
continue;
|
|
44
|
-
}
|
|
45
|
-
const found = [];
|
|
46
|
-
for (const e of entries) {
|
|
47
|
-
if (!e.isDirectory()) continue;
|
|
48
|
-
const p = join(root, e.name, 'node_modules', DSH_ENTRY);
|
|
49
|
-
if (!existsSync(p)) continue;
|
|
50
|
-
let version = '0.0.0';
|
|
51
|
-
try {
|
|
52
|
-
version =
|
|
53
|
-
JSON.parse(readFileSync(join(dirname(p), '..', 'package.json'), 'utf8')).version ?? version;
|
|
54
|
-
} catch {
|
|
55
|
-
// 版本读不到就当 0.0.0,仍可作为候选
|
|
56
|
-
}
|
|
57
|
-
found.push({ p, version });
|
|
58
|
-
}
|
|
59
|
-
if (found.length) {
|
|
60
|
-
// ★ 取**最高版本**,不是目录枚举顺序。本机可以同时缓存 0.1.5 和 0.1.7,
|
|
61
|
-
// 按目录顺序会随机选中旧版,导致实验性能力(如 Agent Teams)根本加载不到。
|
|
62
|
-
found.sort((a, b) => cmpVersion(b.version, a.version));
|
|
63
|
-
return found[0].p;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return undefined;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** 极简 semver 比较(够用于 npx 缓存里挑最新);返回 a-b 的符号。 */
|
|
70
|
-
export function cmpVersion(a, b) {
|
|
71
|
-
const [ac, ap] = String(a).split('-', 2);
|
|
72
|
-
const [bc, bp] = String(b).split('-', 2);
|
|
73
|
-
const an = ac.split('.').map(Number);
|
|
74
|
-
const bn = bc.split('.').map(Number);
|
|
75
|
-
for (let i = 0; i < 3; i++) {
|
|
76
|
-
const d = (an[i] || 0) - (bn[i] || 0);
|
|
77
|
-
if (d) return d;
|
|
78
|
-
}
|
|
79
|
-
if (!ap && bp) return 1; // release > prerelease
|
|
80
|
-
if (ap && !bp) return -1;
|
|
81
|
-
if (ap && bp) {
|
|
82
|
-
const ar = ap.split('.');
|
|
83
|
-
const br = bp.split('.');
|
|
84
|
-
for (let i = 0; i < Math.max(ar.length, br.length); i++) {
|
|
85
|
-
const x = ar[i] ?? '';
|
|
86
|
-
const y = br[i] ?? '';
|
|
87
|
-
const xn = x !== '' && Number.isFinite(Number(x));
|
|
88
|
-
const yn = y !== '' && Number.isFinite(Number(y));
|
|
89
|
-
if (xn && yn) {
|
|
90
|
-
if (Number(x) !== Number(y)) return Number(x) - Number(y);
|
|
91
|
-
} else if (xn) return -1;
|
|
92
|
-
else if (yn) return 1;
|
|
93
|
-
else if (x !== y) return x < y ? -1 : 1;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
return 0;
|
|
97
|
-
}
|
|
30
|
+
export { cmpVersion, discoverDsh, isNpxCachePath, resolveDsh } from './dsh-locate.mjs';
|
|
98
31
|
|
|
99
32
|
/**
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
* 2) npx 缓存里的通配搜索(`_npx/<hash>/node_modules/@deepseek-ai/dsh/lib/bin.js`)
|
|
103
|
-
* 3) npm 全局安装目录
|
|
104
|
-
*
|
|
105
|
-
* 用 `node <bin.js>` 而不是 `dsh`/`dsh.cmd`/`dsh.ps1`:Windows 的 PowerShell 执行策略
|
|
106
|
-
* 会拦住 `.ps1` shim,而直接跑入口文件在所有平台上都稳。
|
|
33
|
+
* 只要 DSH 入口路径(兼容旧调用方;桌面版时是 asar 内部路径,普通 fs 读不到)。
|
|
34
|
+
* 要拉起 DSH 请用 resolveDsh():它同时给出运行时与环境变量。
|
|
107
35
|
*/
|
|
108
36
|
export function resolveDshBin() {
|
|
109
|
-
|
|
110
|
-
if (explicit) {
|
|
111
|
-
if (!existsSync(explicit)) {
|
|
112
|
-
throw new Error(`DSH_BIN 指向的文件不存在: ${explicit}`);
|
|
113
|
-
}
|
|
114
|
-
return explicit;
|
|
115
|
-
}
|
|
116
|
-
const candidates = [
|
|
117
|
-
findInNpxCache(),
|
|
118
|
-
join(homedir(), 'AppData', 'Roaming', 'npm', 'node_modules', DSH_ENTRY),
|
|
119
|
-
join('/usr', 'local', 'lib', 'node_modules', DSH_ENTRY),
|
|
120
|
-
join('/usr', 'lib', 'node_modules', DSH_ENTRY),
|
|
121
|
-
].filter(Boolean);
|
|
122
|
-
for (const c of candidates) {
|
|
123
|
-
if (existsSync(c)) return c;
|
|
124
|
-
}
|
|
125
|
-
throw new Error(
|
|
126
|
-
'DSH CLI 入口未找到。请设置环境变量 DSH_BIN 指向 @deepseek-ai/dsh/lib/bin.js\n' +
|
|
127
|
-
' 例如: DSH_BIN=/path/to/node_modules/@deepseek-ai/dsh/lib/bin.js',
|
|
128
|
-
);
|
|
37
|
+
return resolveDsh().bin;
|
|
129
38
|
}
|
|
130
39
|
|
|
131
40
|
/**
|
|
132
41
|
* 兼容性矩阵(docs/zh/compatibility.md)的机器可读版本,供 dsh_doctor 使用。
|
|
133
42
|
* 改矩阵时同步改这里:`[verifiedMin, verifiedBelow)` 是“已验证”区间。
|
|
134
43
|
* 用 `-0` 后缀是为了把同号的 rc 版本也算进去(semver 里 `0.1.7-rc.1 < 0.1.7`)。
|
|
44
|
+
* 0.2.x:2026-11-06 用 0.2.1-rc.5(npm)与 0.2.2-rc.1(桌面版自带运行时)跑过全量套件。
|
|
135
45
|
*/
|
|
136
46
|
export const DSH_COMPAT = Object.freeze({
|
|
137
47
|
partialMin: '0.1.5-0',
|
|
138
48
|
verifiedMin: '0.1.7-0',
|
|
139
|
-
verifiedBelow: '0.2.
|
|
49
|
+
verifiedBelow: '0.2.3-0',
|
|
50
|
+
verifiedLabel: '0.1.7 ~ 0.2.2',
|
|
140
51
|
});
|
|
141
52
|
|
|
142
53
|
/**
|
|
@@ -154,20 +65,15 @@ export function dshVersionStatus(version) {
|
|
|
154
65
|
return { status: 'warn', level: 'partial', note: `DSH ${version} 部分可用:ACP 基本路径可用,缺 team 包` };
|
|
155
66
|
}
|
|
156
67
|
if (cmpVersion(version, DSH_COMPAT.verifiedBelow) < 0) {
|
|
157
|
-
return { status: 'ok', level: 'verified', note: `DSH ${version}
|
|
68
|
+
return { status: 'ok', level: 'verified', note: `DSH ${version} 在已验证区间(${DSH_COMPAT.verifiedLabel})` };
|
|
158
69
|
}
|
|
159
70
|
return {
|
|
160
71
|
status: 'warn',
|
|
161
72
|
level: 'unverified',
|
|
162
|
-
note: `DSH ${version}
|
|
73
|
+
note: `DSH ${version} 未经本版本验证(兼容性矩阵覆盖 ${DSH_COMPAT.verifiedLabel}),ACP session/* 面可能有变化`,
|
|
163
74
|
};
|
|
164
75
|
}
|
|
165
76
|
|
|
166
|
-
/** DSH 入口是不是落在 npx 缓存里(`_npx/<hash>/…`):那里随 `npm cache clean` 或 npx 自身淘汰随时消失。 */
|
|
167
|
-
export function isNpxCachePath(p) {
|
|
168
|
-
return /[\\/]_npx[\\/]/.test(String(p ?? ''));
|
|
169
|
-
}
|
|
170
|
-
|
|
171
77
|
/**
|
|
172
78
|
* DSH 主目录。**默认就是本机用户自己的 ~/.dsh** —— 本服务不拥有独立的 home,
|
|
173
79
|
* 因此凭据(.credentials.yaml)、会话历史、工作区登记与 GUI 完全共用一份。
|
|
@@ -213,10 +119,7 @@ export const TEAM_MODE = (() => {
|
|
|
213
119
|
/** 运行中的 DSH 版本(读不到为 null)。 */
|
|
214
120
|
export const DSH_VERSION = (() => {
|
|
215
121
|
try {
|
|
216
|
-
|
|
217
|
-
return (
|
|
218
|
-
JSON.parse(readFileSync(join(dirname(bin), '..', 'package.json'), 'utf8')).version ?? null
|
|
219
|
-
);
|
|
122
|
+
return resolveDsh().version ?? null;
|
|
220
123
|
} catch {
|
|
221
124
|
return null;
|
|
222
125
|
}
|