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.
@@ -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
- resolveDshBin,
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
- const bin = resolveDshBin();
101
- const env = { ...process.env, DSH_PERMISSION_MODE: this.permission };
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(process.execPath, [bin, '--profile', DSH_PROFILE], {
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)) reject(new Error(`ACP 请求超时: ${method}`));
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 { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
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
- * 在 npx 缓存目录里按通配找 DSH 入口。
30
- * npx 的缓存目录名带一个随机 hash(`_npx/<hash>/`),写死会随环境失效,所以这里筛一遍。
27
+ * DSH 的发现与拉起方式见 dsh-locate.mjs:自动识别桌面版(asar + 自带 Electron 运行时)、
28
+ * npm 全局与 npx 缓存,DSH_BIN 只作显式覆盖。这里转出给其余模块用。
31
29
  */
32
- function findInNpxCache() {
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
- * 解析 DSH CLI 入口。按顺序尝试:
101
- * 1) DSH_BIN 环境变量(最可靠,推荐显式设置)
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
- const explicit = process.env.DSH_BIN;
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.0-0',
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} 在已验证区间(0.1.7 ~ 0.2.0 之前)` };
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} 未经本版本验证(兼容性矩阵只覆盖到 0.1.x),ACP session/* 面可能有变化`,
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
- const bin = resolveDshBin();
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
  }