dsh-agentone 1.0.0 → 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/index.js CHANGED
@@ -34,6 +34,10 @@ import { applyPlatformModel, removePlatformModel } from './model.js';
34
34
  import { installPlatformSkill, listPlatformSkills, removePlatformSkill } from './skill.js';
35
35
  import { applyPlatformSkillhub, removePlatformSkillhub } from './skillhub.js';
36
36
  import { createProfilePlugins, validatePackage, PROTECTED_PACKAGES } from './profile-plugins.js';
37
+ import {
38
+ createDriveService, withChineseLocale, describeDrivePermission, parseLarkJson, loadDriveCache,
39
+ } from './drive.js';
40
+ import { loadArchivePref, setArchiveEnabled, syncArchiveFeature } from './archive.js';
37
41
  import { isDesktopHost } from './desktop.js';
38
42
  import { createDefaultsSync } from './defaults-sync.js';
39
43
  import { canonicalSkillSlug, localSkillState } from './skill-state.js';
@@ -691,6 +695,42 @@ export function apply(ctx, config) {
691
695
  `dsh-agentone: route ${path}`,
692
696
  );
693
697
 
698
+ // 飞书云盘寻址(/agentone/api/drive/*)共享的 lark-cli 门与服务实例。
699
+ // 门返回非 null 表示不可用(state 给前端渲染引导),为 null 才继续寻址。
700
+ // withLatest=false:门是热路径(tab 每次打开都过),不查 npm latest。
701
+ const driveGate = async () => {
702
+ const detail = await probeCliDetail('lark-cli', { withLatest: false })
703
+ .then(withCliUpgradeGuard).catch(() => null);
704
+ if (!detail || detail.state === 'missing') return { state: 'cli_missing' };
705
+ if (detail.unconfigured) return { state: 'cli_unconfigured' };
706
+ if (detail.state !== 'authorized') return { state: 'cli_unauthorized' };
707
+ return null;
708
+ };
709
+ const driveService = createDriveService({
710
+ run: (args, options) => runCliTool('lark-cli', args, options),
711
+ });
712
+
713
+ /**
714
+ * 「云盘没权限」的第四态:前三态只管装/配/授权,已授权但缺云盘 scope 时
715
+ * 寻址会抛 LarkError。这里把它映射成独立 state + 责任方(用户可自助重新
716
+ * 授权 / 只能管理员开通),而不是把 lark-cli 的英文原文当 502 丢给用户。
717
+ * 返回 null 表示不是权限问题(按普通失败处理)。
718
+ */
719
+ const drivePermissionState = async (error) => {
720
+ const mapped = await describeDrivePermission(error, {
721
+ // auth scopes:应用侧已开通的 scope 清单(输出无 ok 壳,parseLarkJson
722
+ // 会原样返回对象);失败时 describeDrivePermission 按可自助处理。
723
+ runAppScopes: async () => {
724
+ const { stdout } = await runCliTool('lark-cli', ['auth', 'scopes', '--json']);
725
+ return parseLarkJson(stdout)?.userScopes || null;
726
+ },
727
+ });
728
+ if (mapped) {
729
+ log?.info?.(`[dsh-agentone] drive permission gap: ${mapped.state} (${mapped.missing_scopes.join(',') || 'unspecified'})`);
730
+ }
731
+ return mapped;
732
+ };
733
+
694
734
  register('/agentone/', (request, response) => {
695
735
  if (request.method !== 'GET') {
696
736
  response.writeHead(405, { allow: 'GET' });
@@ -1548,6 +1588,121 @@ export function apply(ctx, config) {
1548
1588
  }
1549
1589
  });
1550
1590
 
1591
+ register('/agentone/api/drive/home', async (request, response) => {
1592
+ if (request.method !== 'GET') {
1593
+ response.writeHead(405, { allow: 'GET' });
1594
+ response.end();
1595
+ return;
1596
+ }
1597
+ try {
1598
+ // lark-cli 三态门与 CLI 页同口径(missing / unconfigured / unauthorized),
1599
+ // 前端按 state 渲染引导;寻址细节见 lib/drive.js 头注释。
1600
+ const gate = await driveGate();
1601
+ if (gate) {
1602
+ sendJson(response, 200, gate);
1603
+ return;
1604
+ }
1605
+ const folder = await driveService.resolveAgentOneFolder({ config });
1606
+ // 带上中文界面参数:缓存里可能是修复前存的裸链接,统一在出口补
1607
+ sendJson(response, 200, { state: 'ready', ...folder, url: withChineseLocale(folder.url) });
1608
+ } catch (error) {
1609
+ const permission = await drivePermissionState(error);
1610
+ if (permission) {
1611
+ sendJson(response, 200, permission);
1612
+ return;
1613
+ }
1614
+ log?.warn?.(`[dsh-agentone] drive home failed: ${error?.message || error}`);
1615
+ sendJson(response, 502, { error: '获取云盘文件夹失败,请稍后重试' });
1616
+ }
1617
+ });
1618
+
1619
+ register('/agentone/api/drive/home/resolve', async (request, response) => {
1620
+ if (request.method !== 'POST') {
1621
+ response.writeHead(405, { allow: 'POST' });
1622
+ response.end();
1623
+ return;
1624
+ }
1625
+ try {
1626
+ const gate = await driveGate();
1627
+ if (gate) {
1628
+ sendJson(response, 200, gate);
1629
+ return;
1630
+ }
1631
+ const body = await readJsonBody(request);
1632
+ const adoptUrl = String(body.adopt_url || '').trim().slice(0, 1000);
1633
+ const folder = await driveService.resolveAgentOneFolder({ config, force: true, adoptUrl });
1634
+ sendJson(response, 200, { state: 'ready', ...folder, url: withChineseLocale(folder.url) });
1635
+ } catch (error) {
1636
+ const permission = await drivePermissionState(error);
1637
+ if (permission) {
1638
+ sendJson(response, 200, permission);
1639
+ return;
1640
+ }
1641
+ log?.warn?.(`[dsh-agentone] drive resolve failed: ${error?.message || error}`);
1642
+ sendJson(response, 502, { error: '定位云盘文件夹失败,请稍后重试' });
1643
+ }
1644
+ });
1645
+
1646
+ // 归档开关:AI 产出的飞书云文档是否自动归入云盘文件夹。
1647
+ // 开 = 注入 AGENTS.md 指令块 + 安装归档技能;关 = 撤销两者。
1648
+ register('/agentone/api/drive/archive', async (request, response) => {
1649
+ if (request.method === 'GET') {
1650
+ try {
1651
+ const pref = await loadArchivePref(config);
1652
+ sendJson(response, 200, { enabled: pref.enabled });
1653
+ } catch (error) {
1654
+ log?.warn?.(`[dsh-agentone] read archive pref failed: ${error?.message || error}`);
1655
+ sendJson(response, 502, { error: '读取归档开关失败,请稍后重试' });
1656
+ }
1657
+ return;
1658
+ }
1659
+ if (request.method !== 'POST') {
1660
+ response.writeHead(405, { allow: 'GET, POST' });
1661
+ response.end();
1662
+ return;
1663
+ }
1664
+ try {
1665
+ const body = await readJsonBody(request);
1666
+ const result = await setArchiveEnabled(config, body.enabled === true, log);
1667
+ if (!result.ok) {
1668
+ // 同名技能目录被用户占用:不改开关,明确告知处理方式
1669
+ sendJson(response, 409, { error: result.error });
1670
+ return;
1671
+ }
1672
+ sendJson(response, 200, { enabled: result.enabled });
1673
+ } catch (error) {
1674
+ log?.warn?.(`[dsh-agentone] set archive failed: ${error?.message || error}`);
1675
+ sendJson(response, 502, { error: '设置归档开关失败,请稍后重试' });
1676
+ }
1677
+ });
1678
+
1679
+ // 云盘页签自动刷新:轮询根清单指纹(10s 节流缓存,多个客户端共享)。
1680
+ // 指纹直接用 drive.json 缓存的 token,不做可用性探测(省一次 API 调用);
1681
+ // 缓存缺失 / lark-cli 不可用 / 任何失败都返回 rev:null,前端按「未知,不动」。
1682
+ let driveRevCheckedAt = 0;
1683
+ let driveRevValue = null;
1684
+ register('/agentone/api/drive/rev', async (request, response) => {
1685
+ if (request.method !== 'GET') {
1686
+ response.writeHead(405, { allow: 'GET' });
1687
+ response.end();
1688
+ return;
1689
+ }
1690
+ try {
1691
+ if (Date.now() - driveRevCheckedAt >= 10_000) {
1692
+ driveRevCheckedAt = Date.now();
1693
+ const cache = await loadDriveCache(config);
1694
+ const token = cache?.agentone_folder?.token;
1695
+ driveRevValue = token ? await driveService.fingerprintFolder(token) : null;
1696
+ }
1697
+ sendJson(response, 200, { rev: driveRevValue });
1698
+ } catch (error) {
1699
+ // 失败同样走 10s 节流(时间戳已前移),rev 归 null 让前端静默不动
1700
+ driveRevValue = null;
1701
+ log?.warn?.(`[dsh-agentone] drive rev failed: ${error?.message || error}`);
1702
+ sendJson(response, 200, { rev: null });
1703
+ }
1704
+ });
1705
+
1551
1706
  register('/agentone/api/plan', async (request, response) => {
1552
1707
  if (request.method !== 'GET') {
1553
1708
  response.writeHead(405, { allow: 'GET' });
@@ -1643,6 +1798,10 @@ export function apply(ctx, config) {
1643
1798
  ensureLanguageDefault(config, log).catch((error) => {
1644
1799
  log.warn(`[dsh-agentone] language default check failed: ${error?.message || error}`);
1645
1800
  });
1801
+ // 云盘归档开关落盘在 storeDir:启动按开关收敛(用户改过 AGENTS.md / 技能目录也对齐)
1802
+ syncArchiveFeature(config, log).catch((error) => {
1803
+ log.warn(`[dsh-agentone] drive archive sync failed: ${error?.message || error}`);
1804
+ });
1646
1805
  (async () => {
1647
1806
  // 托管实例先把 Hub 注入的凭据落盘,再走既有的「已登录」启动流程。
1648
1807
  await bootstrapInstanceCredentials(config, log).catch((error) => {
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
  });
@@ -851,6 +986,12 @@ const cliAuthDialog = async (tool) => {
851
986
  setTimeout(close, 800);
852
987
  if (d.tool) renderCliData(lastStatus, { [tool]: d.tool });
853
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
+ }
854
995
  return;
855
996
  }
856
997
  if (r.status === 202 && d.pending !== false) {
@@ -1037,9 +1178,77 @@ const cliInstall = async (tool, btn) => {
1037
1178
  }
1038
1179
  };
1039
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
+
1040
1250
  /** CLI 一键升级:官方 tool update(ccpg 下载二进制可达分钟级)。 */
1041
- const cliUpgrade = async (tool, btn) => {
1042
- btn.disabled = true;
1251
+ const cliUpgrade = async (tool, btn) => { btn.disabled = true;
1043
1252
  btn.textContent = '升级中,可能需要 1–2 分钟…';
1044
1253
  try {
1045
1254
  const resp = await fetch('api/cli/upgrade', {
@@ -1574,6 +1783,7 @@ export function statusPageHtml() {
1574
1783
  <button class="tab" role="tab" aria-selected="false" data-tab="skills">技能</button>
1575
1784
  <button class="tab" role="tab" aria-selected="false" data-tab="plugins">插件</button>
1576
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>
1577
1787
  <button class="tab" role="tab" aria-selected="false" data-tab="about">关于</button>
1578
1788
  </nav>
1579
1789
 
@@ -1643,6 +1853,42 @@ export function statusPageHtml() {
1643
1853
  </div>
1644
1854
  </div>
1645
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
+
1646
1892
  <div class="tabpane" id="pane-about" hidden>
1647
1893
  <div class="card">
1648
1894
  <h3>关于</h3>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-agentone",
3
- "version": "1.0.0",
4
- "description": "AgentOne 平台集成:平台模型、SkillHub 技能、套餐与 CLI;平台清单融入技能/插件双 Tab,无 Node 机器用桌面壳自带工具链装 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
  ],
@@ -0,0 +1,71 @@
1
+ ---
2
+ name: agentone-drive
3
+ description: 使用飞书云盘的 AgentOne 文件夹:查看和读取里面的资料,或把飞书云文档(文档 / 表格 / 多维表格 / 幻灯片)保存进去并按「项目或应用」建子文件夹。当需要查共享资料、读取云盘文件夹内容,或把新做的/已有的飞书文档统一存进云盘时使用。
4
+ version: 1.2.0
5
+ ---
6
+
7
+ # AgentOne 云盘文件夹 / agentone-drive
8
+
9
+ 飞书云盘的 AgentOne 文件夹是本机用户与 AI 共享的工作目录:既能**查资料、取材料**,
10
+ 也能**把文档存进去**。文件夹由 dsh-agentone 插件维护(设置 → 云盘),本技能只按约定
11
+ 调用脚本,不自己拼 lark-cli 命令。
12
+
13
+ ## 说法(对用户)
14
+
15
+ 用「云盘里的资料」「存进云盘」「已经存好了」这类日常说法,不要用「归档」「归位」
16
+ 「落库」等用户可能看不懂的词。
17
+
18
+ ## 文件夹规则
19
+
20
+ - 根文件夹:`{{DRIVE_CACHE}}` 里 `agentone_folder` 的 token(脚本自己读,不要手抄 token)。
21
+ - 子文件夹:`<项目或应用>`(只有这一层,不按日期切分),保存时自动创建,查看时不创建。
22
+ - 只管理云文档(docx / sheet / base / slides);普通附件上传用 lark-cli 的
23
+ `drive +upload`,不要用本技能。
24
+
25
+ ## 一:查看/读取云盘里的资料
26
+
27
+ ```bash
28
+ node "{{DRIVE_SCRIPT}}" list # 看根文件夹里有什么(各项目文件夹)
29
+ node "{{DRIVE_SCRIPT}}" list --project "项目或应用" # 看某个项目文件夹里的文档
30
+ ```
31
+
32
+ 输出 `{ok:true, files:[{name,type,token,url}...], has_more}`。`has_more:true` 表示还有
33
+ 更多(一页 200 条),需要翻页时直接用 lark-cli 的 `api GET /open-apis/drive/v1/files`。
34
+ 拿到 token/url 后按类型读取:
35
+
36
+ - 在线文档:`lark-cli docs +fetch --url <url>`
37
+ - 表格 / 多维表格:`lark-cli` 的 sheets / base 命令
38
+ - 普通文件:`lark-cli drive +download --file-token <token>`
39
+
40
+ ## 二:新建文档时直接存进项目文件夹
41
+
42
+ 先取子文件夹 token,再在创建时指定父文件夹,文档一步到位、无需二次移动:
43
+
44
+ ```bash
45
+ FOLDER=$(node "{{DRIVE_SCRIPT}}" resolve-folder --project "项目或应用" --token-only)
46
+ lark-cli docs +create --title "标题" --content @body.xml --parent-token "$FOLDER"
47
+ ```
48
+
49
+ 表格 / 多维表格 / 幻灯片:先用 `lark-cli drive +import --file x.xlsx --type sheet`
50
+ 生成云文档,再按下节存进去。
51
+
52
+ ## 三:把已经存在的文档存进去
53
+
54
+ ```bash
55
+ node "{{DRIVE_SCRIPT}}" archive --token <文档 token 或链接> --type docx --project "项目或应用"
56
+ ```
57
+
58
+ 输出 JSON:`{ok:true, archived:true|false, folder_token, folder_url, path}`。
59
+ `archived:false` 表示该文档已经在目标文件夹里(重复调用安全,不会重复移动)。
60
+
61
+ ## 约定
62
+
63
+ - **把飞书链接给用户时,一律在链接后追加 `?lang=zh-CN`**(脚本输出的链接已自动带)。
64
+ 原因:宿主的内嵌浏览器是独立登录会话,界面语言可能默认英文;`?lang=zh-CN` 会被
65
+ 飞书改写成该会话的语言偏好。`docs +create` 等 lark-cli 命令返回的链接不带参数,要自己补。
66
+ - `--project` 取当前任务所属的项目名或应用名;**同一个项目/应用固定用同一个名字**,
67
+ 文档才会聚在同一个文件夹里。保存时拿不准就省略,脚本会放进「未分类」,不要自创层级。
68
+ - 已经存进去的文档不要重复移动;同一次任务里只存一次。
69
+ - 脚本返回 `{ok:false, error}` 时,把 `error` 原文(已是中文)告诉用户,不要重试
70
+ 超过一次;提示里出现「请先在 dsh 设置 → 云盘 初始化文件夹」时,先请用户完成
71
+ 初始化再继续。