dsh-agentone 0.5.44 → 1.0.2

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/lib/page.js CHANGED
@@ -123,6 +123,24 @@ button:disabled { opacity: .55; cursor: default; }
123
123
  border: 1px solid var(--line); background: var(--card); color: var(--text); outline: none;
124
124
  }
125
125
  .search input:focus { border-color: var(--brand); }
126
+ /* 归档开关:原生 checkbox 外观改写,不引入组件依赖 */
127
+ .switch {
128
+ display: inline-flex; align-items: center; gap: 8px;
129
+ cursor: pointer; user-select: none; font-size: 13px;
130
+ }
131
+ .switch input {
132
+ appearance: none; -webkit-appearance: none; margin: 0; cursor: pointer;
133
+ width: 36px; height: 20px; border-radius: 999px; background: var(--line);
134
+ position: relative; transition: background .15s;
135
+ }
136
+ .switch input::after {
137
+ content: ''; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px;
138
+ border-radius: 50%; background: #fff; transition: transform .15s;
139
+ }
140
+ .switch input:checked { background: var(--brand); }
141
+ .switch input:checked::after { transform: translateX(16px); }
142
+ .switch input:disabled { opacity: .55; cursor: default; }
143
+ .switch input:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
126
144
  .footer { display: flex; justify-content: space-between; align-items: center; margin-top: 4px; }
127
145
  .spin {
128
146
  width: 13px; height: 13px; border: 2px solid currentColor; border-top-color: transparent;
@@ -242,12 +260,129 @@ const activateTab = (name) => {
242
260
  renderCli(lastStatus);
243
261
  }
244
262
  }
263
+ // 云盘文件夹状态按需探测(寻址要走 lark-cli 调飞书 API,同样 1 分钟节流;
264
+ // resolve 操作后由 renderDrive._probedAt=0 立即重探)
265
+ if (name === 'drive') {
266
+ const now = Date.now();
267
+ if (!renderDrive._probedAt || now - renderDrive._probedAt > 60000) {
268
+ renderDrive._probedAt = now;
269
+ renderDrive();
270
+ }
271
+ // 归档开关是本机状态(不走 lark-cli),无需节流
272
+ if (typeof renderDriveArchive === 'function') renderDriveArchive();
273
+ }
245
274
  };
246
275
  // CLI 页手动重新检测:清掉节流时间戳立即重探
247
276
  $('cli-recheck-btn').onclick = () => {
248
277
  renderCli._probedAt = 0;
249
278
  renderCli(lastStatus);
250
279
  };
280
+ // 云盘文件夹:打开云盘(宿主页监听 postMessage 后开侧边栏页签;无宿主时退回新窗口)
281
+ const DRIVE_OPEN_MESSAGE = 'agentone:open-feishu-drive';
282
+ const DRIVE_OPEN_RESULT = 'agentone:open-feishu-drive:result';
283
+
284
+ function openDriveSurface() {
285
+ const url = (renderDrive._data && renderDrive._data.url) || '';
286
+ const openExternal = () => {
287
+ if (!url) return false;
288
+ window.open(url, '_blank', 'noopener');
289
+ return true;
290
+ };
291
+ // 单独打开本页(无宿主框架)时没有侧边栏可开,直接新窗口
292
+ if (window.parent === window) {
293
+ if (!openExternal()) notify('还没有定位到文件夹,请先点「初始化 / 重新定位」', 'err');
294
+ return;
295
+ }
296
+ let settled = false;
297
+ const onReply = (event) => {
298
+ if (!event.data || event.data.type !== DRIVE_OPEN_RESULT) return;
299
+ settled = true;
300
+ window.removeEventListener('message', onReply);
301
+ if (!event.data.ok && !openExternal()) {
302
+ notify('侧边栏云盘页签不可用,请在 dsh 设置里确认 better-sidebar 已启用', 'err');
303
+ }
304
+ };
305
+ window.addEventListener('message', onReply);
306
+ // 目标 origin 用 '*':载荷只是「打开我的页签」这条指令,接收侧已校验来源
307
+ window.parent.postMessage({ type: DRIVE_OPEN_MESSAGE }, '*');
308
+ setTimeout(() => {
309
+ if (settled) return;
310
+ window.removeEventListener('message', onReply);
311
+ openExternal();
312
+ }, 800);
313
+ }
314
+
315
+ $('drive-open-btn').onclick = () => openDriveSurface();
316
+ // 云盘文件夹:初始化/重新定位(force 重新寻址:按名找,找不到建)
317
+ $('drive-resolve-btn').onclick = () => {
318
+ // readJsonBody 只认 application/json(跨站表单防线),POST 必须带该头
319
+ withLoading($('drive-resolve-btn'), async () => fetch('api/drive/home/resolve', {
320
+ method: 'POST',
321
+ headers: { 'Content-Type': 'application/json' },
322
+ body: JSON.stringify({}),
323
+ }),
324
+ '云盘文件夹已就绪', null)
325
+ .then(() => { renderDrive._probedAt = 0; renderDrive(); });
326
+ };
327
+ // 云盘文件夹:收养用户粘贴的文件夹链接
328
+ $('drive-adopt-btn').onclick = () => {
329
+ const input = $('drive-adopt-input');
330
+ const url = input.value.trim();
331
+ if (!/feishu\\.cn\\/drive\\/folder\\//.test(url) && !/larksuite\\.com\\/drive\\/folder\\//.test(url)) {
332
+ notify('请粘贴飞书云盘文件夹链接(…/drive/folder/…)', 'err');
333
+ return;
334
+ }
335
+ withLoading($('drive-adopt-btn'), async () => fetch('api/drive/home/resolve', {
336
+ method: 'POST',
337
+ headers: { 'Content-Type': 'application/json' },
338
+ body: JSON.stringify({ adopt_url: url }),
339
+ }), '已切换到指定文件夹', null)
340
+ .then(() => { input.value = ''; renderDrive._probedAt = 0; renderDrive(); });
341
+ };
342
+ // 云盘归档开关:读取当前状态(开 = AGENTS.md 指令块 + 归档技能都已就位)
343
+ const renderDriveArchive = async () => {
344
+ const toggle = $('drive-archive-toggle');
345
+ const stateEl = $('drive-archive-state');
346
+ try {
347
+ const resp = await fetch('api/drive/archive');
348
+ const data = await resp.json().catch(() => ({}));
349
+ if (!resp.ok) throw new Error(data.error || '读取失败,请稍后重试');
350
+ toggle.checked = data.enabled === true;
351
+ stateEl.textContent = data.enabled ? '已启用' : '未启用';
352
+ } catch (error) {
353
+ stateEl.textContent = error.message || '读取失败,请稍后重试';
354
+ }
355
+ };
356
+ $('drive-archive-toggle').onchange = async (event) => {
357
+ const toggle = event.target;
358
+ const stateEl = $('drive-archive-state');
359
+ const msgEl = $('drive-archive-msg');
360
+ const enabled = toggle.checked;
361
+ toggle.disabled = true;
362
+ stateEl.textContent = enabled ? '启用中…' : '停用中…';
363
+ msgEl.hidden = true;
364
+ try {
365
+ // readJsonBody 只认 application/json,POST 必须带该头
366
+ const resp = await fetch('api/drive/archive', {
367
+ method: 'POST',
368
+ headers: { 'Content-Type': 'application/json' },
369
+ body: JSON.stringify({ enabled }),
370
+ });
371
+ const data = await resp.json().catch(() => ({}));
372
+ if (!resp.ok) throw new Error(data.error || '设置失败,请稍后重试');
373
+ toggle.checked = data.enabled === true;
374
+ stateEl.textContent = data.enabled ? '已启用' : '未启用';
375
+ notify(data.enabled ? '已启用云盘,新开一个对话后生效' : '已停用云盘', 'ok');
376
+ } catch (error) {
377
+ // 失败回滚到操作前的状态,避免开关显示与实际不一致
378
+ toggle.checked = !enabled;
379
+ stateEl.textContent = toggle.checked ? '已启用' : '未启用';
380
+ msgEl.textContent = error.message || '设置失败,请稍后重试';
381
+ msgEl.hidden = false;
382
+ } finally {
383
+ toggle.disabled = false;
384
+ }
385
+ };
251
386
  document.querySelectorAll('.tab').forEach((tab) => {
252
387
  tab.onclick = () => activateTab(tab.dataset.tab);
253
388
  });
@@ -395,52 +530,87 @@ const renderPlan = async () => {
395
530
  ).then(() => renderPlan());
396
531
  });
397
532
  };
533
+ // 平台清单同步态与插件列表的同名合并:renderDefaults 记录待处理插件并排除
534
+ // 已列名条目,renderPlugins 把同步状态/原因/重试合并进同名行——同一个包只
535
+ // 出现一行(真实用户机 dsh-im:清单同步失败 + 本机未启用曾显示成两条)
536
+ let defaultsPendingPlugins = [];
537
+ let listedPluginNames = new Set();
538
+ let mergedPendingSig = '';
539
+ const DEFAULTS_STATE_LABELS = { current: '已符合', missing: '待安装', pending: '待应用', running: '同步中',
540
+ failed: '失败', conflict: '本机冲突', report_pending: '已安装,待回报', restart_required: '已安装,重启后生效' };
541
+ /** 同步动作按钮的统一绑定(待处理块与合并进插件行的按钮同一契约)。 */
542
+ const bindDefaultsActions = (el) => {
543
+ el.querySelectorAll('[data-default-action]').forEach((btn) => {
544
+ const request = () => fetch('api/defaults/' + btn.dataset.defaultAction, { method: 'POST',
545
+ headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ kind: btn.dataset.kind,
546
+ name: btn.dataset.name, confirm: btn.dataset.defaultAction === 'apply' }) });
547
+ btn.onclick = () => btn.dataset.defaultAction === 'apply'
548
+ ? confirmThen(btn, '替换当前版本?', request, '已开始应用,请查看同步状态')
549
+ : withLoading(btn, request, '已开始重试,请查看同步状态');
550
+ });
551
+ };
552
+ const defaultsPendingSig = () => defaultsPendingPlugins.map((item) => [item.name, item.state,
553
+ item.target_version || '', item.error || ''].join('~')).join('|');
554
+
398
555
  const renderDefaults = (s) => {
399
- const body = $('defaults-body');
400
556
  const config = s.defaults;
401
- const labels = { current: '已符合', missing: '待安装', pending: '待应用', running: '同步中',
402
- failed: '失败', conflict: '本机冲突', report_pending: '已安装,待回报', restart_required: '已安装,重启后生效' };
557
+ const syncBtn = $('defaults-sync-btn');
558
+ const labels = DEFAULTS_STATE_LABELS;
403
559
  const sources = { platform: '平台清单', compatibility: '旧平台兼容默认', cache: '本机缓存' };
404
- if (!config || !s.logged_in) { body.innerHTML = '<div class="muted">登录后检查平台默认能力</div>'; return; }
560
+ const clearBlock = (el) => { el.innerHTML = ''; delete el.dataset.fingerprint; };
561
+ if (!config || !s.logged_in) {
562
+ syncBtn.hidden = true;
563
+ for (const id of ['defaults-meta', 'defaults-plugins', 'defaults-skills']) clearBlock($(id));
564
+ return;
565
+ }
566
+ syncBtn.hidden = false;
567
+ syncBtn.disabled = Boolean(config.running);
405
568
  const entries = [...(config.skills || []), ...(config.plugins || [])];
406
569
  // legacy:0.5.39 之前装的技能只有平台台账、没有本机记录(见 defaults-sync)。
407
570
  // 这类条目单独给一个批量入口,不然升级后每台机器都要点 N 次「确认应用」。
408
571
  const legacyCount = entries.filter((item) => item.legacy).length;
409
- const summary = '<div class="skill-sub">' + esc(sources[config.source] || '尚未检查')
410
- + (config.revision !== null ? ' · 版本 ' + esc(config.revision) : '')
572
+ const pendingCount = entries.filter((item) => item.state !== 'current').length;
573
+ // 待处理插件交给 renderPlugins 合并进同名行(见顶部说明)
574
+ defaultsPendingPlugins = (config.plugins || []).filter((item) => item.state !== 'current');
575
+ // 清单状态收成插件卡头一行 meta(来源/版本/时间/符合统计/错误):健康项不
576
+ // 铺开——技能与插件分别在各自 Tab 的列表里以「平台默认/受管」标签呈现,
577
+ // 这里只按类型冒泡待处理项,长描述彻底退出列表。
578
+ const metaHtml = '<div class="skill-sub">' + esc(sources[config.source] || '尚未检查')
579
+ + (config.revision !== null ? ' · 清单版本 ' + esc(config.revision) : '')
411
580
  + (config.checked_at ? ' · ' + esc(new Date(config.checked_at).toLocaleString()) : '')
581
+ + ' · <span style="color:var(--ok)">' + (entries.length - pendingCount) + ' 项已符合</span>'
582
+ + (pendingCount ? ' · <span style="color:var(--warn)">' + pendingCount + ' 项待处理(见下)</span>' : '')
583
+ + (!entries.length ? ' · 当前清单为空' : '')
412
584
  + (config.running ? ' · 正在检查' : '') + '</div>'
413
- + (config.error || config.profile_error ? '<div class="skill-sub" style="color:var(--err)">' + esc(config.error || config.profile_error) + '</div>' : '')
414
- + (legacyCount ? '<div class="skill-sub"><button id="defaults-repair" class="btn small ghost"'
415
- + (config.running ? ' disabled' : '') + '>重新应用旧版安装的技能(' + legacyCount + ')</button>'
416
- + ' 技能目录会按平台当前版本覆盖</div>' : '');
417
- const html = summary + entries.map((item) => {
585
+ + (config.error || config.profile_error
586
+ ? '<div class="skill-sub" style="color:var(--err)">' + esc(config.error || config.profile_error) + '</div>' : '');
587
+ const meta = $('defaults-meta');
588
+ if (meta.dataset.fingerprint !== metaHtml) { meta.dataset.fingerprint = metaHtml; meta.innerHTML = metaHtml; }
589
+ const statePill = (state) => state === 'failed' || state === 'conflict'
590
+ ? '<span class="pill" style="font-size:11px;background:var(--err-weak);color:var(--err)">'
591
+ + esc(labels[state] || state) + '</span>'
592
+ : '<span class="pill warn" style="font-size:11px">' + esc(labels[state] || state) + '</span>';
593
+ // 单行紧凑条目:名称 · 类型徽标 · 状态徽标 · 版本(→ 目标版本)+ 行内操作
594
+ const rowHtml = (item) => {
418
595
  const action = ['failed', 'report_pending', 'missing'].includes(item.state) ? 'retry'
419
596
  : ['conflict', 'pending'].includes(item.state) ? 'apply' : null;
420
597
  const button = action ? '<button class="btn small ghost" data-default-action="' + action
421
598
  + '" data-kind="' + item.kind + '" data-name="' + esc(item.name) + '"'
422
599
  + (config.running ? ' disabled' : '') + '>' + (action === 'retry' ? '重试' : '确认应用') + '</button>' : '';
423
600
  return '<div class="skill-item"><div class="skill-info"><div class="skill-name">'
424
- + esc(item.description || item.name) + ' <span class="tag">' + (item.kind === 'skill' ? '技能' : '插件')
425
- + '</span></div><div class="skill-sub">' + esc(item.name) + ' · ' + esc(labels[item.state] || item.state)
426
- + (item.legacy ? '(旧版安装)' : '')
427
- + (item.version ? ' · ' + esc(item.version) : '')
601
+ + esc(item.name) + ' <span class="tag">' + (item.kind === 'skill' ? '技能' : '插件') + '</span>'
602
+ + statePill(item.state)
603
+ + (item.legacy ? ' <span class="tag">旧版安装</span>' : '')
604
+ + '</div><div class="skill-sub">'
605
+ + (item.version ? esc(item.version) : '')
428
606
  + (item.target_version && item.target_version !== item.version ? ' → ' + esc(item.target_version) : '')
429
607
  + (item.error ? '<br><span style="color:var(--err)">' + esc(item.error) + '</span>' : '')
430
608
  + '</div></div>' + button + '</div>';
431
- }).join('') + (!entries.length ? '<div class="skill-sub">当前清单为空</div>' : '');
432
- if (body.dataset.fingerprint !== html) {
433
- body.dataset.fingerprint = html; body.innerHTML = html;
434
- body.querySelectorAll('[data-default-action]').forEach((btn) => {
435
- const request = () => fetch('api/defaults/' + btn.dataset.defaultAction, { method: 'POST',
436
- headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ kind: btn.dataset.kind,
437
- name: btn.dataset.name, confirm: btn.dataset.defaultAction === 'apply' }) });
438
- btn.onclick = () => btn.dataset.defaultAction === 'apply'
439
- ? confirmThen(btn, '替换当前版本?', request, '已开始应用,请查看同步状态')
440
- : withLoading(btn, request, '已开始重试,请查看同步状态');
441
- });
442
- const repair = body.querySelector('#defaults-repair');
443
- if (repair) repair.onclick = async () => {
609
+ };
610
+ const bindRepair = (el) => {
611
+ const repair = el.querySelector('#defaults-repair');
612
+ if (!repair) return;
613
+ repair.onclick = async () => {
444
614
  if (repair.dataset.confirming !== '1') {
445
615
  repair.dataset.confirming = '1';
446
616
  repair.dataset.originalLabel = repair.textContent;
@@ -470,8 +640,31 @@ const renderDefaults = (s) => {
470
640
  await refresh();
471
641
  }
472
642
  };
473
- }
474
- $('defaults-sync-btn').disabled = config.running;
643
+ };
644
+ // 按类型各归其位:待处理插件进插件 Tab、待处理技能进技能 Tab,只列非已符合
645
+ // 项;5s 轮询高频调用,块内容没变时靠指纹跳过重绘(不打断进行中的点击)
646
+ const renderBlock = (id, list) => {
647
+ const el = $(id);
648
+ // 插件待处理项:已进插件列表的同名条目不在这里重复出现(合并进那一行)
649
+ const pending = (list || []).filter((item) => item.state !== 'current'
650
+ && !(id === 'defaults-plugins' && listedPluginNames.has(item.name)));
651
+ const legacy = id === 'defaults-skills' && legacyCount
652
+ ? '<div class="skill-sub"><button id="defaults-repair" class="btn small ghost"'
653
+ + (config.running ? ' disabled' : '') + '>重新应用旧版安装的技能(' + legacyCount + ')</button>'
654
+ + ' 技能目录会按平台当前版本覆盖</div>'
655
+ : '';
656
+ const html = (pending.length || legacy)
657
+ ? '<div class="skill-sub" style="margin-top:10px">平台同步待处理</div>'
658
+ + pending.map(rowHtml).join('') + legacy
659
+ : '';
660
+ if (el.dataset.fingerprint !== html) { el.dataset.fingerprint = html; el.innerHTML = html; bindDefaultsActions(el); bindRepair(el); }
661
+ };
662
+ renderBlock('defaults-plugins', config.plugins);
663
+ renderBlock('defaults-skills', config.skills);
664
+ // 待处理集合变化(新失败/恢复)→ 让已加载的插件列表行重新合并;
665
+ // renderPlugins 侧会先对齐 mergedPendingSig,不会互相触发死循环
666
+ const pendingSig = defaultsPendingSig();
667
+ if (renderPlugins._loaded && pendingSig !== mergedPendingSig) void renderPlugins();
475
668
  };
476
669
  const renderSkills = (s) => {
477
670
  const skillhubEnabled = s.capabilities?.channels?.skillhub?.enabled !== false;
@@ -793,6 +986,12 @@ const cliAuthDialog = async (tool) => {
793
986
  setTimeout(close, 800);
794
987
  if (d.tool) renderCliData(lastStatus, { [tool]: d.tool });
795
988
  else renderCli(lastStatus);
989
+ // 云盘文件夹探测结果可能随授权态变化(cli_unauthorized → ready):
990
+ // 清节流立即重探,正在看云盘页的用户不用等 60s
991
+ if (typeof renderDrive === 'function') {
992
+ renderDrive._probedAt = 0;
993
+ if ($('pane-drive') && !$('pane-drive').hidden) renderDrive();
994
+ }
796
995
  return;
797
996
  }
798
997
  if (r.status === 202 && d.pending !== false) {
@@ -979,9 +1178,77 @@ const cliInstall = async (tool, btn) => {
979
1178
  }
980
1179
  };
981
1180
 
1181
+ /**
1182
+ * 云盘文件夹状态探测:/api/drive/home 带回 state(ready / cli_missing /
1183
+ * cli_unconfigured / cli_unauthorized)与文件夹信息。非 ready 态给出对应
1184
+ * 引导动作(跳 CLI 页安装 / 初始化应用 / 打开授权弹窗——复用 CLI 页同套
1185
+ * 设备流,授权成功会自动重探本页)。
1186
+ */
1187
+ const renderDrive = async () => {
1188
+ const statusEl = $('drive-status');
1189
+ const kvEl = $('drive-folder-kv');
1190
+ const linkEl = $('drive-folder-link');
1191
+ const seq = (renderDrive._seq = (renderDrive._seq || 0) + 1);
1192
+ statusEl.textContent = '检测中…';
1193
+ kvEl.hidden = true;
1194
+ let data;
1195
+ try {
1196
+ const resp = await fetch('api/drive/home');
1197
+ data = await resp.json().catch(() => ({}));
1198
+ if (!resp.ok) throw new Error(data.error || '检测失败,请稍后重试');
1199
+ } catch (error) {
1200
+ if (seq === renderDrive._seq) statusEl.textContent = error.message || '检测失败,请稍后重试';
1201
+ return;
1202
+ }
1203
+ if (seq !== renderDrive._seq) return;
1204
+ renderDrive._data = data;
1205
+ if (data.state === 'ready') {
1206
+ statusEl.textContent = '已就绪:侧边栏「飞书云盘」页签会直接进入这个文件夹。';
1207
+ if (data.url) {
1208
+ linkEl.textContent = data.name || 'AgentOne';
1209
+ linkEl.href = data.url;
1210
+ kvEl.hidden = false;
1211
+ }
1212
+ return;
1213
+ }
1214
+ kvEl.hidden = true;
1215
+ if (data.state === 'cli_missing') {
1216
+ statusEl.innerHTML = '本机还没有安装 lark-cli(飞书命令行工具),无法定位云盘文件夹。'
1217
+ + '请到「CLI」页签安装后回来点「初始化 / 重新定位」。';
1218
+ } else if (data.state === 'cli_unconfigured') {
1219
+ statusEl.innerHTML = 'lark-cli 还没有完成飞书应用初始化。请到「CLI」页签点「初始化应用」,'
1220
+ + '完成后再回来点「初始化 / 重新定位」。';
1221
+ } else if (data.state === 'cli_unauthorized') {
1222
+ statusEl.innerHTML = 'lark-cli 还没有获得你的飞书授权。首次使用需要扫码一次:'
1223
+ + '<div class="row" style="margin-top:8px">'
1224
+ + '<button class="btn small primary" id="drive-auth-btn">去授权</button></div>';
1225
+ statusEl.querySelector('#drive-auth-btn').onclick = () => cliAuthDialog('lark-cli');
1226
+ } else if (data.state === 'drive_scope_missing') {
1227
+ // 授权在、但令牌里没有云盘权限:应用已开通,用户重新授权即可自助解决
1228
+ const list = Array.isArray(data.missing_scopes) ? data.missing_scopes : [];
1229
+ statusEl.innerHTML = '你的飞书授权里还没有云盘权限'
1230
+ + (list.length ? '(缺少:' + esc(list.join('、')) + ')' : '')
1231
+ + '。点「重新授权」用手机飞书扫码确认一次即可,授权成功后会自动重新检测。'
1232
+ + '<div class="row" style="margin-top:8px">'
1233
+ + '<button class="btn small primary" id="drive-auth-btn">重新授权</button></div>';
1234
+ statusEl.querySelector('#drive-auth-btn').onclick = () => cliAuthDialog('lark-cli');
1235
+ } else if (data.state === 'drive_app_forbidden') {
1236
+ // 应用本身没开通:重新授权也拿不到,只能管理员在开放平台开通并发布版本
1237
+ const list = (Array.isArray(data.app_missing_scopes) && data.app_missing_scopes.length)
1238
+ ? data.app_missing_scopes
1239
+ : (Array.isArray(data.missing_scopes) ? data.missing_scopes : []);
1240
+ statusEl.innerHTML = '飞书应用本身还没有开通云空间权限'
1241
+ + (list.length ? '(缺少:' + esc(list.join('、')) + ')' : '')
1242
+ + ',重新授权也拿不到。请联系管理员在飞书开放平台为应用开通云空间权限并发布版本,'
1243
+ + '然后回来点「初始化 / 重新定位」。';
1244
+ } else {
1245
+ statusEl.textContent = data.error || '检测失败,请稍后重试';
1246
+ }
1247
+ };
1248
+ // 授权成功后的云盘页重探挂在 cliAuthDialog 的轮询成功分支(见上)。
1249
+
982
1250
  /** CLI 一键升级:官方 tool update(ccpg 下载二进制可达分钟级)。 */
983
- const cliUpgrade = async (tool, btn) => {
984
- btn.disabled = true;
1251
+ const cliUpgrade = async (tool, btn) => { btn.disabled = true;
985
1252
  btn.textContent = '升级中,可能需要 1–2 分钟…';
986
1253
  try {
987
1254
  const resp = await fetch('api/cli/upgrade', {
@@ -1044,8 +1311,14 @@ const renderPlugins = async () => {
1044
1311
  }
1045
1312
  // 渲染成功即标记已加载:避免切 tab 重复拉取(outdated 查询有网络开销)
1046
1313
  renderPlugins._loaded = true;
1314
+ // 同名合并登记(见 renderDefaults 顶部说明):已列名的插件,其清单同步
1315
+ // 状态合并进本行;待处理块不再单列一条
1316
+ listedPluginNames = new Set(data.plugins.map((plugin) => plugin.name));
1317
+ const pendingByN = new Map(defaultsPendingPlugins.map((item) => [item.name, item]));
1047
1318
  if (!data.plugins?.length) {
1048
1319
  list.innerHTML = '<div class="skill-sub">暂无已安装插件;内置插件缺失时会在 dsh 启动时自动恢复。</div>';
1320
+ mergedPendingSig = defaultsPendingSig();
1321
+ if (lastStatus) renderDefaults(lastStatus);
1049
1322
  return;
1050
1323
  }
1051
1324
  const rows = data.plugins.map((plugin) => {
@@ -1065,6 +1338,15 @@ const renderPlugins = async () => {
1065
1338
  const builtinTag = plugin.builtin
1066
1339
  ? '<span class="tag">受管</span>'
1067
1340
  : '';
1341
+ // 清单同步态合并:同名待处理插件在本行直接给出状态/原因/动作
1342
+ const pending = pendingByN.get(plugin.name);
1343
+ const syncPill = pending
1344
+ ? (['failed', 'conflict'].includes(pending.state)
1345
+ ? '<span class="pill" style="font-size:11px;background:var(--err-weak);color:var(--err)">同步'
1346
+ + esc(DEFAULTS_STATE_LABELS[pending.state] || pending.state) + '</span>'
1347
+ : '<span class="pill warn" style="font-size:11px">同步'
1348
+ + esc(DEFAULTS_STATE_LABELS[pending.state] || pending.state) + '</span>')
1349
+ : '';
1068
1350
  const version = plugin.latest
1069
1351
  ? esc(plugin.version) + ' → <b>' + esc(plugin.latest) + '</b>'
1070
1352
  : esc(plugin.version || '—');
@@ -1084,17 +1366,25 @@ const renderPlugins = async () => {
1084
1366
  actions.push('<button class="btn small danger" data-remove="' + esc(plugin.name) + '">卸载</button>');
1085
1367
  }
1086
1368
  }
1369
+ if (pending && ['failed', 'report_pending', 'missing'].includes(pending.state)) {
1370
+ actions.push('<button class="btn small ghost" data-default-action="retry" data-kind="plugin" data-name="' + esc(plugin.name) + '">重试</button>');
1371
+ } else if (pending && ['conflict', 'pending'].includes(pending.state)) {
1372
+ actions.push('<button class="btn small ghost" data-default-action="apply" data-kind="plugin" data-name="' + esc(plugin.name) + '">确认应用</button>');
1373
+ }
1087
1374
  const actionHtml = actions.length
1088
1375
  ? '<div style="display:flex;gap:6px">' + actions.join('') + '</div>'
1089
1376
  : '';
1090
1377
  return '<div class="skill-item"><div class="skill-info"><div class="skill-name">'
1091
- + esc(plugin.name) + builtinTag + statePill
1378
+ + esc(plugin.name) + builtinTag + statePill + syncPill
1092
1379
  + '</div><div class="skill-sub">' + desc + esc(plugin.source) + ' · ' + version
1093
1380
  + (plugin.registered ? ' · 已启用,重启后生效' : plugin.bundled ? ' · 已安装,未启用当前 profile(「升级」或重装可启用)' : '')
1381
+ + (pending ? ' · 平台同步 ' + esc(pending.version || '') + (pending.target_version && pending.target_version !== pending.version ? ' → ' + esc(pending.target_version) : '') : '')
1382
+ + (pending?.error ? '<br><span style="color:var(--err)">' + esc(pending.error) + '</span>' : '')
1094
1383
  + '</div></div>' + actionHtml + '</div>';
1095
1384
  });
1096
1385
  // 指纹保护(与 renderSkills 同思路):内容没变不碰 innerHTML——升级后
1097
- // 的定向刷新、切 tab 重入都不会重建按钮节点(打断卸载二次确认)或闪动
1386
+ // 的定向刷新、切 tab 重入都不会重建按钮节点(打断卸载二次确认)或闪动。
1387
+ // 清单同步态参与指纹:失败/恢复会让行内容变化
1098
1388
  const fingerprint = data.plugins.map((plugin) => [
1099
1389
  plugin.name,
1100
1390
  plugin.version || '',
@@ -1102,6 +1392,10 @@ const renderPlugins = async () => {
1102
1392
  plugin.registered ? 1 : 0,
1103
1393
  plugin.bundled ? 1 : 0,
1104
1394
  plugin.self_managed ? 1 : 0,
1395
+ pendingByN.get(plugin.name)
1396
+ ? [pendingByN.get(plugin.name).state, pendingByN.get(plugin.name).target_version || '',
1397
+ pendingByN.get(plugin.name).error || ''].join('~')
1398
+ : '',
1105
1399
  ].join(':')).join('|');
1106
1400
  if (list.dataset.fingerprint !== fingerprint) {
1107
1401
  list.dataset.fingerprint = fingerprint;
@@ -1125,6 +1419,12 @@ const renderPlugins = async () => {
1125
1419
  body: JSON.stringify({ name: btn.dataset.remove }),
1126
1420
  }).then(() => renderPlugins()), '插件已卸载');
1127
1421
  });
1422
+ // 合并进插件行的同步动作按钮(与待处理块同一契约)
1423
+ bindDefaultsActions(list);
1424
+ // 对齐「已合并的待处理集合」:renderDefaults 的变化触发到此为止(不会回环);
1425
+ // 列名登记可能让待处理块的排除集变化,定向重绘一次
1426
+ mergedPendingSig = defaultsPendingSig();
1427
+ if (lastStatus) renderDefaults(lastStatus);
1128
1428
  };
1129
1429
  // ==about-upgrade-begin==
1130
1430
  // 关于页版本行:aboutSelf(GET api/plugin/self)有值时展示升级入口或
@@ -1403,7 +1703,7 @@ $('skillhub-sync-btn').onclick = () => withLoading(
1403
1703
  () => fetch('api/skillhub/sync', { method: 'POST' }),
1404
1704
  'SkillHub 配置已同步',
1405
1705
  );
1406
- // 手动同步套餐技能:文案按结果给(装了几个),失败由 withLoading 展示
1706
+ // 手动同步技能:文案按结果给(装了几个),失败由 withLoading 展示
1407
1707
  $('skill-sync-btn').onclick = () => withLoading(
1408
1708
  $('skill-sync-btn'),
1409
1709
  async () => {
@@ -1483,6 +1783,7 @@ export function statusPageHtml() {
1483
1783
  <button class="tab" role="tab" aria-selected="false" data-tab="skills">技能</button>
1484
1784
  <button class="tab" role="tab" aria-selected="false" data-tab="plugins">插件</button>
1485
1785
  <button class="tab" role="tab" aria-selected="false" data-tab="cli">CLI</button>
1786
+ <button class="tab" role="tab" aria-selected="false" data-tab="drive">云盘</button>
1486
1787
  <button class="tab" role="tab" aria-selected="false" data-tab="about">关于</button>
1487
1788
  </nav>
1488
1789
 
@@ -1514,9 +1815,10 @@ export function statusPageHtml() {
1514
1815
  <div class="card" id="skills-card">
1515
1816
  <h3>平台技能 <span class="muted">套餐自动同步</span></h3>
1516
1817
  <div id="skills-list"></div>
1818
+ <div id="defaults-skills"></div>
1517
1819
  <div class="hint" id="skills-meta"></div>
1518
1820
  <div class="row" style="margin-top:8px">
1519
- <button id="skill-sync-btn" class="btn ghost small" data-loading="同步中…">同步套餐技能</button>
1821
+ <button id="skill-sync-btn" class="btn ghost small" data-loading="同步中…">同步技能</button>
1520
1822
  </div>
1521
1823
  <div class="search" style="margin-top:12px">
1522
1824
  <input id="search-input" placeholder="搜索技能市场,如:数据 / 报告 / lark" />
@@ -1532,11 +1834,11 @@ export function statusPageHtml() {
1532
1834
 
1533
1835
  <div class="tabpane" id="pane-plugins" hidden>
1534
1836
  <div class="card">
1535
- <h3>平台默认能力 <button id="defaults-sync-btn" class="btn ghost small" style="margin-left:auto">检查并更新</button></h3>
1536
- <div id="defaults-body" aria-live="polite"><div class="skill-sub">检查中…</div></div>
1537
- </div>
1538
- <div class="card">
1539
- <h3>dsh 插件 <span class="muted">当前 profile 已安装的插件</span></h3>
1837
+ <h3>dsh 插件 <span class="muted">当前 profile 已安装的插件</span>
1838
+ <button id="defaults-sync-btn" class="btn ghost small" style="margin-left:auto" hidden>检查并更新</button>
1839
+ </h3>
1840
+ <div id="defaults-meta" aria-live="polite"></div>
1841
+ <div id="defaults-plugins"></div>
1540
1842
  <div id="plugins-list"></div>
1541
1843
  <div class="hint" id="plugins-hint">受管技能和插件始终更新到最新发布版本;仅管理员停用后可卸载受管插件。</div>
1542
1844
  </div>
@@ -1551,6 +1853,42 @@ export function statusPageHtml() {
1551
1853
  </div>
1552
1854
  </div>
1553
1855
 
1856
+ <div class="tabpane" id="pane-drive" hidden>
1857
+ <div class="card">
1858
+ <h3>飞书云盘文件夹 <span class="muted">侧边栏「飞书云盘」页签的落点</span></h3>
1859
+ <div id="drive-status" class="skill-sub">检测中…</div>
1860
+ <div class="kv" id="drive-folder-kv" hidden>
1861
+ <span class="k">当前文件夹</span>
1862
+ <span class="v"><a id="drive-folder-link" target="_blank" rel="noopener"></a></span>
1863
+ </div>
1864
+ <div class="row" style="margin-top:12px">
1865
+ <button id="drive-open-btn" class="btn small">打开云盘</button>
1866
+ <button id="drive-resolve-btn" class="btn small primary" data-loading="定位中…">初始化 / 重新定位</button>
1867
+ </div>
1868
+ <div class="hint" id="drive-firstuse-hint">首次使用:云盘需要飞书授权。点「打开云盘」后在侧边栏页签里按提示点「去授权」,用手机飞书扫描弹窗里的二维码确认一次即可(之后不再需要)。</div>
1869
+ <div class="hint" id="drive-hint">初始化会在你的飞书「我的空间」根目录找到(没有则创建)名为「AgentOne」的文件夹;之后侧边栏云盘页签直接进入这个文件夹,你看到的飞书云盘界面里做的上传、整理都发生在这里。</div>
1870
+ </div>
1871
+ <div class="card">
1872
+ <h3>启用云盘 <span class="muted">启用后,AI 可以使用这个文件夹:读得到资料,也存得进文档</span></h3>
1873
+ <label class="switch">
1874
+ <input type="checkbox" id="drive-archive-toggle" aria-label="启用云盘" />
1875
+ <span id="drive-archive-state">读取中…</span>
1876
+ </label>
1877
+ <div class="hint">启用后:AI 干活时可以查看、使用这个文件夹里的资料;做好的飞书文档
1878
+ (文档、表格、多维表格、幻灯片)也可以存进这个文件夹,按项目名分好类。</div>
1879
+ <div class="hint">新开一个对话后开始生效。停用后 AI 不能再读写这个文件夹,已经存进去的文件不受影响。</div>
1880
+ <div class="hint" id="drive-archive-msg" hidden></div>
1881
+ </div>
1882
+ <div class="card">
1883
+ <h3>更换文件夹</h3>
1884
+ <div class="skill-sub">已有想用的文件夹?粘贴它的飞书链接(…/drive/folder/…),云盘页签将改为进入该文件夹。</div>
1885
+ <div class="search" style="margin-top:8px">
1886
+ <input id="drive-adopt-input" placeholder="https://xxx.feishu.cn/drive/folder/…" aria-label="文件夹链接" />
1887
+ <button id="drive-adopt-btn" class="btn ghost small" data-loading="校验中…">使用此文件夹</button>
1888
+ </div>
1889
+ </div>
1890
+ </div>
1891
+
1554
1892
  <div class="tabpane" id="pane-about" hidden>
1555
1893
  <div class="card">
1556
1894
  <h3>关于</h3>
package/lib/proc.js CHANGED
@@ -288,7 +288,13 @@ export function scriptInvocation(resolved, args, {
288
288
  execPath = process.execPath,
289
289
  } = {}) {
290
290
  if (!/\.(?:mjs|cjs)$/i.test(String(resolved || ''))) return null;
291
- if (runtime?.node) return { file: runtime.node, args: [resolved, ...args], env: {} };
291
+ if (runtime?.node) {
292
+ // Windows 的 node 启动器可能是 .cmd shim(runtime/bin/node.cmd 要求
293
+ // DSH_DESKTOP_NODE_EXECUTABLE 指向真实可执行文件,与
294
+ // desktopShimEnvDefaults 同一约定);真实 node.exe 则无需任何变量
295
+ const env = /\.cmd$/i.test(runtime.node) ? { DSH_DESKTOP_NODE_EXECUTABLE: execPath } : {};
296
+ return { file: runtime.node, args: [resolved, ...args], env };
297
+ }
292
298
  if (electron) {
293
299
  return { file: execPath, args: [resolved, ...args], env: { ELECTRON_RUN_AS_NODE: '1' } };
294
300
  }
@@ -590,10 +596,14 @@ export async function runCliTool(tool, args, { timeoutMs = 300000, cwd, env, sig
590
596
  function missingCommandGuidance(tool) {
591
597
  if (tool === 'npm' || tool === 'npx') {
592
598
  // 触达这里的机器连桌面壳自带工具链都没有(scriptInvocation/兜底映射
593
- // 均未命中)——重启通常能恢复探测,再给手动安装出路
599
+ // 均未命中)——重启通常能恢复探测,再给手动安装出路。社区桌面端
600
+ // (DSH Desktop/NEXT)与官方壳分开点名,避免指错重启对象
601
+ const brand = /DSH (?:Desktop|NEXT)\.(?:app|exe)/i.test(process.execPath)
602
+ ? 'DSH Desktop'
603
+ : 'DeepSeek Harness';
594
604
  return process.platform === 'win32'
595
- ? '未找到 npm/npx 命令:lark-cli 依赖 Node.js 环境,请先从 https://nodejs.org 下载安装 Node.js(LTS 版,安装时保持默认选项),安装完成后重启 DeepSeek Harness 再点「安装」'
596
- : '未找到 npm/npx 命令:lark-cli 依赖 Node.js 环境,且未探测到 DeepSeek Harness 自带的 Node 运行时。请重启 DeepSeek Harness 后重试;仍失败时安装 Node.js(推荐 nvm:nvm install --lts)后再点「安装」';
605
+ ? `未找到 npm/npx 命令:lark-cli 依赖 Node.js 环境,请先从 https://nodejs.org 下载安装 Node.js(LTS 版,安装时保持默认选项),安装完成后重启 ${brand} 再点「安装」`
606
+ : `未找到 npm/npx 命令:lark-cli 依赖 Node.js 环境,且未探测到 ${brand} 自带的 Node 运行时。请重启 ${brand} 后重试;仍失败时安装 Node.js(推荐 nvm:nvm install --lts)后再点「安装」`;
597
607
  }
598
608
  if (tool === 'pnpm') {
599
609
  return '未找到 pnpm 命令,无法查询插件最新版本。可安装 pnpm(npm install -g pnpm)后重启 DSH Desktop;不影响插件正常使用,仅影响升级提示。';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-agentone",
3
- "version": "0.5.44",
4
- "description": "AgentOne 平台集成:平台模型、SkillHub 技能、套餐与 CLI;无 Node 的 macOS 用桌面壳自带工具链装 lark-cli",
3
+ "version": "1.0.2",
4
+ "description": "AgentOne 平台集成:平台模型、SkillHub 技能、套餐、CLI、飞书云盘共享文件夹(侧栏页签;「启用云盘」后 AI 可读取使用资料、把文档存进文件夹)",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "smoke": "node scripts/env-matrix-smoke.mjs"
@@ -15,6 +15,7 @@
15
15
  "license": "MIT",
16
16
  "files": [
17
17
  "lib/",
18
+ "skills/",
18
19
  "cordis.patch.yml",
19
20
  "README.md"
20
21
  ],