dsh-agentone 0.5.25 → 0.5.27

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/page.js +64 -8
  2. package/lib/proc.js +24 -6
  3. package/package.json +1 -1
package/lib/page.js CHANGED
@@ -44,6 +44,8 @@ body {
44
44
  .kv { display: flex; justify-content: space-between; gap: 12px; padding: 5px 0; }
45
45
  .kv .k { color: var(--muted); flex-shrink: 0; }
46
46
  .kv .v { text-align: right; word-break: break-all; }
47
+ .kv .v a { color: var(--brand); text-decoration: none; }
48
+ .kv .v a:hover { text-decoration: underline; }
47
49
  button {
48
50
  font: inherit; cursor: pointer; border-radius: 10px; transition: filter .15s, opacity .15s;
49
51
  border: 1px solid transparent;
@@ -153,7 +155,23 @@ const notify = (message, kind) => {
153
155
  // 错误信息可能较长(含平台原因),给足阅读时间;成功提示维持 4s
154
156
  toastTimer = setTimeout(() => { toast.className = 'banner'; }, kind === 'err' ? 10000 : 4000);
155
157
  };
156
- const withLoading = async (btn, action, okMessage) => {
158
+ // withLoading 收尾的刷新分两档:
159
+ // 'status'(默认)——拉 /api/status 并整页 refresh()。模型/SkillHub/登录类
160
+ // 操作后的语义(页面各卡都要对齐新状态)。
161
+ // 'quiet'——只拉一次 status 快照(不打散正在看的列表)+ 刷新技能列表
162
+ // (指纹保护下内容没变不会重绘)。技能/插件操作用这档:整页 refresh
163
+ // 会把当前列表闪一遍「读取中…」,还重建按钮节点打断二次确认。
164
+ // null——完全不动界面(调用方自己刷,如插件列表升级后 renderPlugins)。
165
+ const refreshStatusQuietly = async () => {
166
+ try {
167
+ const s = await (await fetch('api/status')).json();
168
+ lastStatus = s;
169
+ renderSkills(s);
170
+ } catch {
171
+ // 网络抖动:保持现状,下一轮 5s 轮询会自然对齐
172
+ }
173
+ };
174
+ const withLoading = async (btn, action, okMessage, refreshMode = 'status') => {
157
175
  const original = btn.innerHTML;
158
176
  btn.disabled = true;
159
177
  btn.innerHTML = '<span class="spin"></span>' + (btn.dataset.loading || '处理中…');
@@ -167,7 +185,8 @@ const withLoading = async (btn, action, okMessage) => {
167
185
  } finally {
168
186
  btn.disabled = false;
169
187
  btn.innerHTML = original;
170
- await refresh();
188
+ if (refreshMode === 'status') await refresh();
189
+ else if (refreshMode === 'quiet') await refreshStatusQuietly();
171
190
  }
172
191
  };
173
192
  // 卸载类破坏性操作的轻量二次确认:第一次点击把按钮变成「确认卸载?」,
@@ -256,6 +275,7 @@ const syncPlanSkills = (btn) => withLoading(
256
275
  btn,
257
276
  () => fetch('api/skill/sync', { method: 'POST' }),
258
277
  null,
278
+ 'quiet',
259
279
  );
260
280
  const skillAction = (slug, action, btn, successText) => withLoading(
261
281
  btn,
@@ -265,6 +285,8 @@ const skillAction = (slug, action, btn, successText) => withLoading(
265
285
  body: JSON.stringify({ slug }),
266
286
  }),
267
287
  successText || (action === 'install' ? '技能已安装' : '技能已卸载'),
288
+ // 技能装/卸后只需技能列表指纹更新;整页 refresh 会闪一遍各卡片
289
+ 'quiet',
268
290
  );
269
291
  const PLAN_CAPABILITY_LABELS = {
270
292
  image_understanding_enabled: '图片理解',
@@ -462,8 +484,13 @@ const renderCli = async (s) => {
462
484
  body.innerHTML = '<div class="skill-sub">平台未开启 CLI 功能。</div>';
463
485
  return;
464
486
  }
465
- // 真实探测本机安装与授权态(数秒),期间先展示读取中
466
- body.innerHTML = '<div class="skill-sub">检测中…</div>';
487
+ // 真实探测本机安装与授权态(数秒)。有旧数据(renderCli._tools)时先用
488
+ // 缓存渲染再后台刷新——不清屏闪「检测中…」,内容回来后整块替换一次。
489
+ if (renderCli._tools && Object.keys(renderCli._tools).length) {
490
+ renderCliData(s, {});
491
+ } else {
492
+ body.innerHTML = '<div class="skill-sub">检测中…</div>';
493
+ }
467
494
  let tools;
468
495
  let python;
469
496
  try {
@@ -473,7 +500,10 @@ const renderCli = async (s) => {
473
500
  tools = data.tools || {};
474
501
  python = data.python || null;
475
502
  } catch {
476
- body.innerHTML = '<div class="skill-sub" style="color:var(--err)">CLI 状态检测失败,请稍后重试。</div>';
503
+ // 探测失败:已有内容保持不动,首载才显示错误
504
+ if (!renderCli._tools || !Object.keys(renderCli._tools).length) {
505
+ body.innerHTML = '<div class="skill-sub" style="color:var(--err)">CLI 状态检测失败,请稍后重试。</div>';
506
+ }
477
507
  return;
478
508
  }
479
509
  renderCliData(s, tools, python);
@@ -744,17 +774,27 @@ const pluginAction = (name, action, btn) => withLoading(
744
774
  action === 'install' ? '插件已安装,重启 DSH 后生效'
745
775
  : action === 'remove' ? '插件已卸载'
746
776
  : '插件已升级,重启 DSH 后生效',
777
+ // 插件列表由下方 renderPlugins 定向刷新(自带指纹保护),不走整页 refresh
778
+ null,
747
779
  ).then(() => renderPlugins());
748
780
  const renderPlugins = async () => {
749
781
  const list = $('plugins-list');
750
- list.innerHTML = '<div class="skill-sub">读取中…</div>';
782
+ // 有旧内容时不闪「读取中…」:后台拉取,回来后整块替换(配合下方指纹
783
+ // 保护,内容没变连替换都省了)。首载(空容器)才显示占位。
784
+ if (!list.children.length) {
785
+ list.innerHTML = '<div class="skill-sub">读取中…</div>';
786
+ }
751
787
  let data;
752
788
  try {
753
789
  const resp = await fetch('api/plugin/list');
754
790
  data = await resp.json().catch(() => ({}));
755
791
  if (!resp.ok) throw new Error(data.error || '读取插件列表失败');
756
792
  } catch (error) {
757
- list.innerHTML = '<div class="skill-sub" style="color:var(--err)">' + esc(error.message) + '</div>';
793
+ // 拉取失败:已有内容保持不动(只是旧了),首载才显示错误
794
+ if (!list.children.length || list.dataset.loaded === '1') {
795
+ list.innerHTML = '<div class="skill-sub" style="color:var(--err)">' + esc(error.message) + '</div>';
796
+ delete list.dataset.loaded;
797
+ }
758
798
  return;
759
799
  }
760
800
  // 渲染成功即标记已加载:避免切 tab 重复拉取(outdated 查询有网络开销)
@@ -805,7 +845,21 @@ const renderPlugins = async () => {
805
845
  + (plugin.bundled && !plugin.active ? ' · 未在 bundle 层,重启 dsh 后生效' : '')
806
846
  + '</div></div>' + actionHtml + '</div>';
807
847
  });
808
- list.innerHTML = rows.join('');
848
+ // 指纹保护(与 renderSkills 同思路):内容没变不碰 innerHTML——升级后
849
+ // 的定向刷新、切 tab 重入都不会重建按钮节点(打断卸载二次确认)或闪动
850
+ const fingerprint = data.plugins.map((plugin) => [
851
+ plugin.name,
852
+ plugin.version || '',
853
+ plugin.latest || '',
854
+ plugin.active ? 1 : 0,
855
+ plugin.bundled ? 1 : 0,
856
+ plugin.self_managed ? 1 : 0,
857
+ ].join(':')).join('|');
858
+ if (list.dataset.fingerprint !== fingerprint) {
859
+ list.dataset.fingerprint = fingerprint;
860
+ list.dataset.loaded = '1';
861
+ list.innerHTML = rows.join('');
862
+ }
809
863
  list.querySelectorAll('[data-install-pkg]').forEach((btn) => {
810
864
  btn.dataset.loading = '安装中…';
811
865
  btn.onclick = () => pluginAction(btn.dataset.installPkg, 'install', btn);
@@ -889,6 +943,7 @@ const renderAbout = (s) => {
889
943
  '<div class="kv"><span class="k">登录账号</span><span class="v">' + esc(s.user_email || '未登录') + '</span></div>',
890
944
  '<div class="kv"><span class="k">所属套餐</span><span class="v">' + esc(s.capabilities?.plan_name || '—') + '</span></div>',
891
945
  '<div class="kv"><span class="k">平台地址</span><span class="v muted">' + esc(s.platform_url || '—') + '</span></div>',
946
+ '<div class="kv"><span class="k">开发者</span><span class="v"><a href="mailto:chumingjun@cc-pg.cn">chumingjun@cc-pg.cn</a></span></div>',
892
947
  ];
893
948
  $('about-body').innerHTML = lines.join('');
894
949
  wireAboutUpgrade();
@@ -1061,6 +1116,7 @@ $('skill-sync-btn').onclick = () => withLoading(
1061
1116
  return resp;
1062
1117
  },
1063
1118
  null,
1119
+ 'quiet',
1064
1120
  );
1065
1121
  logoutBtn.onclick = () => withLoading(
1066
1122
  logoutBtn,
package/lib/proc.js CHANGED
@@ -47,11 +47,13 @@ function extraBinDirs() {
47
47
  join(programFilesX86, 'nodejs'),
48
48
  join(localAppData, 'Programs', 'nodejs'),
49
49
  );
50
- // npm -g 默认 prefix、pnpm 默认 PNPM_HOME,以及 ccpg-cli Windows 官方
51
- // 安装器的落点 %USERPROFILE%\bin(本机实测 C:\Users\<user>\bin\
52
- // ccpg-cli.exe;装完不重启也能立即探测/自升级)。nodejs.org 安装器的
53
- // 系统级安装不在这些目录,但那种环境 PATH 里有 node,不需要兑底。
54
- dirs.push(join(appData, 'npm'), join(localAppData, 'pnpm'), join(home, 'bin'));
50
+ // npm -g 默认 prefix、pnpm 默认 PNPM_HOME(两种布局都兑底:pnpm 版本/
51
+ // 安装方式差异,全局 bin 可能在 %LOCALAPPDATA%\pnpm 或 ...\pnpm\bin——
52
+ // 后者还是 pnpm -g 的前置校验目录,缺了会拒绝安装),以及 ccpg-cli
53
+ // Windows 官方安装器的落点 %USERPROFILE%\bin(本机实测 C:\Users\<user>\
54
+ // bin\ccpg-cli.exe;装完不重启也能立即探测/自升级)。nodejs.org 安装器
55
+ // 的系统级安装不在这些目录,但那种环境 PATH 里有 node,不需要兑底。
56
+ dirs.push(join(appData, 'npm'), join(localAppData, 'pnpm'), join(localAppData, 'pnpm', 'bin'), join(home, 'bin'));
55
57
  // Python 常见安装位(python 探测兑底):python.org 每用户安装位
56
58
  // %LOCALAPPDATA%\Programs\Python\Python3xx、机器级 Program Files\
57
59
  // Python3xx、Miniconda/Anaconda 默认位。自定义安装位(如
@@ -612,7 +614,23 @@ export async function installCliTool(tool, { timeoutMs = 600_000 } = {}) {
612
614
  if (resolveCommandOrNull('npm')) {
613
615
  ({ stdout } = await runCliTool('npm', ['install', '-g', '@larksuite/cli@latest'], { timeoutMs }));
614
616
  } else if (resolveCommandOrNull('pnpm')) {
615
- ({ stdout } = await runCliTool('pnpm', ['add', '-g', '@larksuite/cli@latest'], { timeoutMs }));
617
+ const pnpmInstall = () => runCliTool('pnpm', ['add', '-g', '@larksuite/cli@latest'], { timeoutMs });
618
+ try {
619
+ ({ stdout } = await pnpmInstall());
620
+ } catch (error) {
621
+ // pnpm -g 前置校验:自己配置的全局 bin 目录必须在 PATH 里(真实
622
+ // 用户机报 The configured global bin directory "C:\Users\<u>\
623
+ // AppData\Local\pnpm\bin" is not in PATH)。自定义安装位不在兑底
624
+ // 目录里时,从报错原文解析出该目录补进 PATH 重试一次
625
+ const dir = /global bin directory "(.+?)" is not in PATH/i.exec(String(error?.message || ''))?.[1];
626
+ if (!dir) throw error;
627
+ extraBinDirsCache = [...extraBinDirs(), dir];
628
+ try {
629
+ ({ stdout } = await pnpmInstall());
630
+ } finally {
631
+ resetExtraBinDirsCache();
632
+ }
633
+ }
616
634
  } else if (process.platform === 'win32') {
617
635
  const nodeNote = await installNodeWindows(timeoutMs);
618
636
  ({ stdout } = await runCliTool('npm', ['install', '-g', '@larksuite/cli@latest'], { timeoutMs }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-agentone",
3
- "version": "0.5.25",
3
+ "version": "0.5.27",
4
4
  "description": "AgentOne 平台集成插件:飞书登录、平台模型(同步失败自愈重试)、SkillHub 技能(升级检测一键升级、装后即时刷新、套餐技能自动同步)、套餐申请、插件管理与自助升级、lark-cli / ccpg-cli 探测与一键安装(请求超时与错误码透传、令牌轮换 single-flight、凭据损坏容错与并发安全写入、状态页 UX 增强)",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",