dsh-agentone 0.5.9 → 0.5.11

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 (3) hide show
  1. package/lib/index.js +14 -3
  2. package/lib/proc.js +70 -21
  3. package/package.json +1 -1
package/lib/index.js CHANGED
@@ -328,11 +328,17 @@ const BUILTIN_PLUGIN_ALLOW_BUILD = {
328
328
  'dsh-im': [],
329
329
  };
330
330
 
331
- /** 安装:白名单校验后转发(desktop 受管 pnpm / 官方 CLI;参数数组,无 shell)。 */
331
+ /** 安装:白名单校验后转发(desktop 受管 pnpm / 官方 CLI;参数数组,无 shell)。
332
+ * pnpm 11 在 profile 存在被忽略的构建脚本时 add 以 ERR_PNPM_IGNORED_BUILDS
333
+ * 非零退出,但包本身已装上(先装后报)——以落盘为准,装上即成功。 */
332
334
  async function installProfilePlugin(config, name, version) {
333
335
  assertValidPackage(name, version);
334
336
  const target = version ? `${name}@${version}` : name;
335
- await runPluginCli(['add', target, ...(BUILTIN_PLUGIN_ALLOW_BUILD[name] || [])]);
337
+ try {
338
+ await runPluginCli(['add', target, ...(BUILTIN_PLUGIN_ALLOW_BUILD[name] || [])]);
339
+ } catch (error) {
340
+ if ((await installedVersion(pluginProfileDir(config), name)) === null) throw error;
341
+ }
336
342
  return listProfilePlugins(config);
337
343
  }
338
344
 
@@ -377,7 +383,12 @@ async function upgradeProfilePlugin(config, name) {
377
383
  if (latest === current || (current && compareSemver(latest, current) <= 0)) {
378
384
  throw new Error(`已是最新版本(v${current ?? latest})`);
379
385
  }
380
- await runPluginCli(['add', `${name}@${latest}`, ...(BUILTIN_PLUGIN_ALLOW_BUILD[name] || [])]);
386
+ try {
387
+ await runPluginCli(['add', `${name}@${latest}`, ...(BUILTIN_PLUGIN_ALLOW_BUILD[name] || [])]);
388
+ } catch (error) {
389
+ // 同 install:ERR_PNPM_IGNORED_BUILDS 非零退出但包已装上,不阻断升级
390
+ if ((await installedVersion(dir, name)) !== latest) throw error;
391
+ }
381
392
  const after = await installedVersion(dir, name);
382
393
  if (after !== latest) {
383
394
  // 兑现「插件已升级」提示的前置校验:没升上去就明说,不再假报成功
package/lib/proc.js CHANGED
@@ -5,10 +5,66 @@
5
5
  // (execFile 参数数组、绝不经过 shell)与错误翻译;包名/版本白名单在
6
6
  // index.js 的调用侧执行。
7
7
  import { execFile } from 'node:child_process';
8
+ import { existsSync, readdirSync } from 'node:fs';
9
+ import { homedir } from 'node:os';
10
+ import { join, dirname, delimiter as pathDelimiter } from 'node:path';
8
11
  import { promisify } from 'node:util';
9
12
 
10
13
  const execFileAsync = promisify(execFile);
11
14
 
15
+ /** 常见安装目录兑底(环境 PATH 之外的补充):GUI 启动的 DSH Desktop 继承
16
+ * launchd 的 PATH(/usr/bin:/bin:…),不含 nvm/homebrew/.local —— 实测
17
+ * 从 Dock 启动时 pnpm/lark-cli 全部 ENOENT,插件页 latest 查不到、CLI
18
+ * 全显示未安装。nvm 取最高 node 版本的 bin;结果进程内缓存。 */
19
+ let extraBinDirsCache = null;
20
+ function extraBinDirs() {
21
+ if (extraBinDirsCache !== null) return extraBinDirsCache;
22
+ const dirs = [];
23
+ const home = homedir();
24
+ try {
25
+ // nvm 目录名形如 v24.18.0,用 compareSemver 取最高版(compareSemver
26
+ // 在本文件后文定义,函数提升可用)
27
+ const versions = readdirSync(join(home, '.nvm/versions/node'))
28
+ .filter((v) => /^v?\d/.test(v))
29
+ .sort((a, b) => compareSemver(a.replace(/^v/, ''), b.replace(/^v/, '')));
30
+ if (versions.length) dirs.push(join(home, '.nvm/versions/node', versions[versions.length - 1], 'bin'));
31
+ } catch {
32
+ // 无 nvm(Windows/其他包管理器):跳过,走后面的通用目录
33
+ }
34
+ dirs.push(join(home, '.local/bin'), '/opt/homebrew/bin', '/usr/local/bin');
35
+ extraBinDirsCache = dirs;
36
+ return dirs;
37
+ }
38
+
39
+ /** 命令解析:PATH 里找不到时从兑底目录解析绝对路径(找不到返回原名,
40
+ * 后续 ENOENT 走既有 missing/降级语义)。所有 execFile 调用统一过这里。 */
41
+ export function resolveCommand(tool) {
42
+ const pathDirs = String(process.env.PATH || '').split(pathDelimiter).filter(Boolean);
43
+ const candidates = [];
44
+ for (const dir of [...pathDirs, ...extraBinDirs()]) {
45
+ candidates.push(join(dir, tool));
46
+ if (process.platform === 'win32') candidates.push(join(dir, `${tool}.exe`), join(dir, `${tool}.cmd`));
47
+ }
48
+ return candidates.find((p) => existsSync(p)) || tool;
49
+ }
50
+
51
+ /** 统一的子进程执行:解析命令路径,并把解析到的目录前置进子进程 PATH
52
+ * (pnpm/lark-cli 等是 `#!/usr/bin/env node` 脚本,宿主 PATH 贫瘠时
53
+ * 找得到脚本也找不到 node,必须连带注入)。opts.env 只传增量变量。 */
54
+ function execResolved(tool, args, { timeoutMs, cwd, env } = {}) {
55
+ const resolved = resolveCommand(tool);
56
+ const childEnv = { ...process.env, ...(env || {}) };
57
+ if (resolved !== tool) {
58
+ childEnv.PATH = [dirname(resolved), childEnv.PATH || ''].filter(Boolean).join(pathDelimiter);
59
+ }
60
+ return execFileAsync(resolved, args, {
61
+ timeout: timeoutMs,
62
+ killSignal: 'SIGKILL',
63
+ cwd,
64
+ env: childEnv,
65
+ });
66
+ }
67
+
12
68
  /** 从 pnpm 冗长输出里提取对用户有意义的错误行。 */
13
69
  function firstErrorLines(raw) {
14
70
  const lines = String(raw).split('\n').map((line) => line.trim()).filter(Boolean);
@@ -25,13 +81,12 @@ function firstErrorLines(raw) {
25
81
  * async 函数;execFile 不经过 shell,参数数组中的每个元素都是独立参数,
26
82
  * 不存在拼接解释执行。
27
83
  */
28
- export async function runCliTool(tool, args, { timeoutMs = 300000, cwd } = {}) {
84
+ export async function runCliTool(tool, args, { timeoutMs = 300000, cwd, env } = {}) {
29
85
  try {
30
- const { stdout, stderr } = await execFileAsync(tool, args, {
31
- timeout: timeoutMs,
32
- killSignal: 'SIGKILL',
33
- env: process.env,
86
+ const { stdout, stderr } = await execResolved(tool, args, {
87
+ timeoutMs,
34
88
  cwd,
89
+ env,
35
90
  });
36
91
  return { stdout, stderr };
37
92
  } catch (error) {
@@ -147,10 +202,9 @@ export async function probeCliDetail(tool, { timeoutMs = 15000 } = {}) {
147
202
  let stdout;
148
203
  let state = 'unauthorized';
149
204
  try {
150
- ({ stdout } = await execFileAsync(tool, ['auth', 'status'], {
151
- timeout: timeoutMs,
152
- killSignal: 'SIGKILL',
153
- env: tool === 'ccpg-cli' ? { ...process.env, CCPG_FULL_CLI: '1' } : process.env,
205
+ ({ stdout } = await execResolved(tool, ['auth', 'status'], {
206
+ timeoutMs,
207
+ env: tool === 'ccpg-cli' ? { CCPG_FULL_CLI: '1' } : undefined,
154
208
  }));
155
209
  } catch (error) {
156
210
  if (error.code === 'ENOENT') return { state: 'missing', version: null, latest: null };
@@ -160,7 +214,7 @@ export async function probeCliDetail(tool, { timeoutMs = 15000 } = {}) {
160
214
  if (larkStyleAuthorized(stdout) || ccpgStyleAuthorized(stdout)) state = 'authorized';
161
215
 
162
216
  const [version, latest] = await Promise.all([
163
- execFileAsync(tool, ['--version'], { timeout: 10_000, killSignal: 'SIGKILL' })
217
+ execResolved(tool, ['--version'], { timeoutMs: 10_000 })
164
218
  .then((r) => parseCliVersion(r.stdout))
165
219
  .catch(() => null),
166
220
  cliLatestVersion(tool, timeoutMs).catch(() => null),
@@ -171,20 +225,15 @@ export async function probeCliDetail(tool, { timeoutMs = 15000 } = {}) {
171
225
  /** 最新版查询:ccpg-cli 官方 check 接口;lark-cli 查 npm 包 dist-tag。 */
172
226
  async function cliLatestVersion(tool, timeoutMs) {
173
227
  if (tool === 'ccpg-cli') {
174
- const { stdout } = await execFileAsync(
175
- tool,
176
- ['update', '--check', '--json'],
177
- { timeout: timeoutMs, killSignal: 'SIGKILL', env: { ...process.env, CCPG_FULL_CLI: '1' } },
178
- );
228
+ const { stdout } = await execResolved(tool, ['update', '--check', '--json'], {
229
+ timeoutMs,
230
+ env: { CCPG_FULL_CLI: '1' },
231
+ });
179
232
  const data = JSON.parse(String(stdout).trim());
180
233
  return typeof data?.latest_version === 'string' ? data.latest_version : null;
181
234
  }
182
235
  if (tool === 'lark-cli') {
183
- const { stdout } = await execFileAsync(
184
- 'npm',
185
- ['view', '@larksuite/cli', 'version'],
186
- { timeout: timeoutMs, killSignal: 'SIGKILL' },
187
- );
236
+ const { stdout } = await execResolved('npm', ['view', '@larksuite/cli', 'version'], { timeoutMs });
188
237
  return parseCliVersion(stdout);
189
238
  }
190
239
  return null;
@@ -198,7 +247,7 @@ async function cliLatestVersion(tool, timeoutMs) {
198
247
  export async function upgradeCliTool(tool, { timeoutMs = 300_000 } = {}) {
199
248
  const { stdout } = await runCliTool(tool, ['update'], {
200
249
  timeoutMs,
201
- env: tool === 'ccpg-cli' ? { ...process.env, CCPG_FULL_CLI: '1' } : process.env,
250
+ env: tool === 'ccpg-cli' ? { CCPG_FULL_CLI: '1' } : undefined,
202
251
  });
203
252
  const lines = String(stdout).split('\n').map((l) => l.trim()).filter(Boolean);
204
253
  return lines.slice(-6).join('\n');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-agentone",
3
- "version": "0.5.9",
3
+ "version": "0.5.11",
4
4
  "description": "AgentOne 平台集成插件:飞书登录、平台模型、SkillHub 技能、套餐申请、插件管理、lark-cli / ccpg-cli 探测(请求超时与错误码透传、令牌轮换 single-flight、凭据损坏容错、状态页 UX 增强)",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",