dsh-agentone 1.0.0 → 1.0.3

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
  });
@@ -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.3",
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,100 @@
1
+ ---
2
+ name: agentone-drive
3
+ description: 使用飞书云盘的 AgentOne 文件夹:查看和读取里面的资料,或把飞书云文档(文档 / 表格 / 多维表格 / 幻灯片)保存进去并按「项目或应用」建子文件夹。当需要查共享资料、按关键词搜云盘里的文档、看最近有哪些新放进的文件、读取云盘文件夹内容,或把新做的/已有的飞书文档统一存进云盘时使用。
4
+ version: 1.3.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)走 archive/创建流程;普通附件(图片/压缩包/
23
+ xlsx 等本地文件)用本技能的 `upload`,不要自己拼 `drive +upload`(拿不到项目
24
+ 文件夹 token,会传错地方)。
25
+
26
+ ## 一:查看/读取云盘里的资料
27
+
28
+ ```bash
29
+ node "{{DRIVE_SCRIPT}}" list # 看根文件夹里有什么(各项目文件夹)
30
+ node "{{DRIVE_SCRIPT}}" list --project "项目或应用" # 看某个项目文件夹里的文档
31
+ ```
32
+
33
+ 输出 `{ok:true, files:[{name,type,token,url}...], has_more}`。`has_more:true` 表示还有
34
+ 更多(一页 200 条),需要翻页时直接用 lark-cli 的 `api GET /open-apis/drive/v1/files`。
35
+ 拿到 token/url 后按类型读取:
36
+
37
+ - 在线文档:`lark-cli docs +fetch --url <url>`
38
+ - 表格 / 多维表格:`lark-cli` 的 sheets / base 命令
39
+ - 普通文件:`lark-cli drive +download --file-token <token>`
40
+
41
+ ## 一·五:按关键词搜文档 / 看最近更新(不用逐个翻文件夹)
42
+
43
+ ```bash
44
+ node "{{DRIVE_SCRIPT}}" search "关键词" # 在全部项目文件夹里按文件名找(不区分大小写)
45
+ node "{{DRIVE_SCRIPT}}" changed # 上次查看之后新放进/有更新的文档
46
+ ```
47
+
48
+ - `search` 输出 `{ok:true, matches:[{name,type,token,url,project}...], total}`,
49
+ `project` 字段标明文档在哪个项目文件夹;找不到时 `total:0`,可以换关键词再试一次,
50
+ 也不要自创层级。
51
+ - `changed` 用于「用户说看看新放的文件 / 我手机上存了几篇」这类诉求:输出
52
+ `{ok:true, changes:[{name,type,token,url,project,modified_time}...], since, total}`,
53
+ 按更新时间新在前。**首次运行没有基准,返回空列表(first_run:true)属正常**;
54
+ 每次运行会自动把基准推进到当前时刻(看过即已读),不用额外操作。
55
+ - 用户提到「云盘里的资料」但不确定在哪时,先 `search` 再读取;拿到 token/url 后
56
+ 按上一节的方式按类型读取。
57
+
58
+ ## 二:新建文档时直接存进项目文件夹
59
+
60
+ 先取子文件夹 token,再在创建时指定父文件夹,文档一步到位、无需二次移动:
61
+
62
+ ```bash
63
+ FOLDER=$(node "{{DRIVE_SCRIPT}}" resolve-folder --project "项目或应用" --token-only)
64
+ lark-cli docs +create --title "标题" --content @body.xml --parent-token "$FOLDER"
65
+ ```
66
+
67
+ 表格 / 多维表格 / 幻灯片:先用 `lark-cli drive +import --file x.xlsx --type sheet`
68
+ 生成云文档,再按下节存进去。
69
+
70
+ ## 二·五:本地文件传进项目文件夹
71
+
72
+ ```bash
73
+ node "{{DRIVE_SCRIPT}}" upload --file /path/报告.xlsx --project "项目或应用"
74
+ ```
75
+
76
+ 输出 `{ok:true, file_token, url, path,...}`(链接已带 `?lang=zh-CN`)。用于 AI 生成的
77
+ xlsx / zip / 截图等**普通文件**;在线云文档(docx/sheet/base/slides)不要用它——
78
+ 它们走第二、三节的创建/存入流程,上传会多出一份重复副本。`--name` 可指定云盘里
79
+ 的文件名(默认用本地文件名)。
80
+
81
+ ## 三:把已经存在的文档存进去
82
+
83
+ ```bash
84
+ node "{{DRIVE_SCRIPT}}" archive --token <文档 token 或链接> --type docx --project "项目或应用"
85
+ ```
86
+
87
+ 输出 JSON:`{ok:true, archived:true|false, folder_token, folder_url, path}`。
88
+ `archived:false` 表示该文档已经在目标文件夹里(重复调用安全,不会重复移动)。
89
+
90
+ ## 约定
91
+
92
+ - **把飞书链接给用户时,一律在链接后追加 `?lang=zh-CN`**(脚本输出的链接已自动带)。
93
+ 原因:宿主的内嵌浏览器是独立登录会话,界面语言可能默认英文;`?lang=zh-CN` 会被
94
+ 飞书改写成该会话的语言偏好。`docs +create` 等 lark-cli 命令返回的链接不带参数,要自己补。
95
+ - `--project` 取当前任务所属的项目名或应用名;**同一个项目/应用固定用同一个名字**,
96
+ 文档才会聚在同一个文件夹里。保存时拿不准就省略,脚本会放进「未分类」,不要自创层级。
97
+ - 已经存进去的文档不要重复移动;同一次任务里只存一次。
98
+ - 脚本返回 `{ok:false, error}` 时,把 `error` 原文(已是中文)告诉用户,不要重试
99
+ 超过一次;提示里出现「请先在 dsh 设置 → 云盘 初始化文件夹」时,先请用户完成
100
+ 初始化再继续。