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/README.md +23 -0
- package/lib/archive.js +323 -0
- package/lib/client.js +390 -0
- package/lib/drive.js +289 -0
- package/lib/index.js +159 -0
- package/lib/page.js +248 -2
- package/package.json +3 -2
- package/skills/agentone-drive/SKILL.md +100 -0
- package/skills/agentone-drive/scripts/drive.mjs +493 -0
package/lib/drive.js
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
// 飞书云盘「AgentOne」文件夹寻址(host 半区)。
|
|
2
|
+
//
|
|
3
|
+
// 侧边栏「飞书云盘」tab 的页面本体是飞书官方 Web UI(iframe 直嵌,飞书
|
|
4
|
+
// 不设 frame-ancestors——2026-10-13 spike 实测),插件只负责把用户落到
|
|
5
|
+
// 正确的层级:他的「我的空间 / AgentOne」根目录。iframe 加载的文件夹 URL
|
|
6
|
+
// 带每用户独立的 folder token(fldcn…),无法平台级写死,所以打开 tab 时
|
|
7
|
+
// 经本模块用 lark-cli 寻址:
|
|
8
|
+
// 1. 本地缓存的 folder token 校验仍可用 → 直接用(用户改文件夹名不
|
|
9
|
+
// 影响,token 不变);
|
|
10
|
+
// 2. 失效则按名字在「我的空间」根目录翻页找;
|
|
11
|
+
// 3. 找不到才创建(发生在用户点开云盘 tab / 设置页点初始化的明确意图
|
|
12
|
+
// 之下,不是后台自动写)。
|
|
13
|
+
// 设置页还支持「收养」:用户粘贴任意云盘文件夹链接,inspect 解出 token
|
|
14
|
+
// 校验可访问后作为文件夹(resolve 的 adoptUrl 路径)。
|
|
15
|
+
import { createHash } from 'node:crypto';
|
|
16
|
+
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
|
|
19
|
+
import { storeDir } from './store.js';
|
|
20
|
+
|
|
21
|
+
export const AGENTONE_FOLDER_NAME = 'AgentOne';
|
|
22
|
+
const DRIVE_CACHE_FILE = 'drive.json';
|
|
23
|
+
// 根目录翻页上限(每页 200):正常根目录远不会到 2000 项;超出宁可报错
|
|
24
|
+
// 也不新建——「找不到就建」在翻页盲区会造成两个同名文件夹分裂。
|
|
25
|
+
const MAX_LIST_PAGES = 10;
|
|
26
|
+
const LIST_PAGE_SIZE = 200;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* lark-cli JSON 输出解析。typed 命令与 `api` 子命令统一包 {ok, identity,
|
|
30
|
+
* data} 壳(ok=false 时 error 带结构化原因);解析失败与 ok=false 一律抛
|
|
31
|
+
* LarkError —— 上层的用户可见文案由 classifyMissingScopes 决定,不把
|
|
32
|
+
* lark-cli 的英文原文丢给用户。
|
|
33
|
+
*/
|
|
34
|
+
export class LarkError extends Error {
|
|
35
|
+
constructor(payload = {}) {
|
|
36
|
+
const raw = payload.message || payload.hint || '';
|
|
37
|
+
// message 只进日志/排查;用户可见文案由路由层按 kind 生成
|
|
38
|
+
super(`飞书接口调用失败${raw ? `:${raw}` : ''}`);
|
|
39
|
+
this.name = 'LarkError';
|
|
40
|
+
this.type = payload.type || '';
|
|
41
|
+
this.subtype = payload.subtype || '';
|
|
42
|
+
this.code = payload.code ?? null;
|
|
43
|
+
this.missingScopes = Array.isArray(payload.missing_scopes) ? payload.missing_scopes : [];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 令牌缺 scope(可自助重新授权);99991679 是 lark-cli 的 missing_scope 码。 */
|
|
47
|
+
get isScopeMissing() {
|
|
48
|
+
return this.subtype === 'missing_scope' || this.code === 99991679 || this.missingScopes.length > 0;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function parseLarkJson(text) {
|
|
53
|
+
let data;
|
|
54
|
+
try {
|
|
55
|
+
data = JSON.parse(String(text || '').trim());
|
|
56
|
+
} catch {
|
|
57
|
+
throw new Error('飞书命令返回解析失败,请稍后重试');
|
|
58
|
+
}
|
|
59
|
+
if (data && typeof data === 'object' && !Array.isArray(data) && 'ok' in data) {
|
|
60
|
+
if (data.ok === false) {
|
|
61
|
+
throw new LarkError(data.error || {});
|
|
62
|
+
}
|
|
63
|
+
return data.data ?? {};
|
|
64
|
+
}
|
|
65
|
+
return data ?? {};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 在一页文件列表里精确匹配指定名字的文件夹(同名文件不算)。 */
|
|
69
|
+
export function pickFolder(files, name) {
|
|
70
|
+
return (Array.isArray(files) ? files : []).find(
|
|
71
|
+
(item) => item?.type === 'folder' && item?.name === name,
|
|
72
|
+
) || null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 缺权限时分责任方——这是「云盘没权限」闭环的关键分支:
|
|
77
|
+
* - 应用已开通该 scope、只是当前令牌没授 → 用户点「重新授权」即可自助解决;
|
|
78
|
+
* - 应用本身没开通 → 重新授权也拿不到,只能管理员去开放平台开通并发布版本。
|
|
79
|
+
* appScopes 传 null(查不到)时按「用户可自助」处理,文案里保留管理员兜底路径。
|
|
80
|
+
*/
|
|
81
|
+
export function classifyMissingScopes({ missingScopes = [], appScopes = null } = {}) {
|
|
82
|
+
const missing = (Array.isArray(missingScopes) ? missingScopes : []).filter(Boolean);
|
|
83
|
+
const enabled = Array.isArray(appScopes) ? appScopes : null;
|
|
84
|
+
const appMissing = enabled ? missing.filter((scope) => !enabled.includes(scope)) : [];
|
|
85
|
+
return {
|
|
86
|
+
state: appMissing.length > 0 ? 'drive_app_forbidden' : 'drive_scope_missing',
|
|
87
|
+
missingScopes: missing,
|
|
88
|
+
appMissingScopes: appMissing,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 把 LarkError 映射成用户可见的权限缺口状态;不是权限问题返回 null。
|
|
94
|
+
* runAppScopes 注入(生产侧跑 `lark-cli auth scopes --json` 取应用已开通清单):
|
|
95
|
+
* 拿不到清单时按「用户可自助」处理,文案里保留管理员兜底路径。
|
|
96
|
+
*/
|
|
97
|
+
export async function describeDrivePermission(error, { runAppScopes } = {}) {
|
|
98
|
+
if (!(error instanceof LarkError)) return null;
|
|
99
|
+
if (!error.isScopeMissing && error.type !== 'authorization') return null;
|
|
100
|
+
let appScopes = null;
|
|
101
|
+
if (typeof runAppScopes === 'function') {
|
|
102
|
+
try {
|
|
103
|
+
appScopes = await runAppScopes();
|
|
104
|
+
} catch {
|
|
105
|
+
appScopes = null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const verdict = classifyMissingScopes({ missingScopes: error.missingScopes, appScopes });
|
|
109
|
+
return {
|
|
110
|
+
state: verdict.state,
|
|
111
|
+
missing_scopes: verdict.missingScopes,
|
|
112
|
+
app_missing_scopes: verdict.appMissingScopes,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** 寻址缓存路径。归档技能/指令块要把它交给 AI,故对外导出。 */
|
|
117
|
+
export function driveCachePath(config) {
|
|
118
|
+
return join(storeDir(config), DRIVE_CACHE_FILE);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** 读寻址缓存。损坏(截断/手改)不抛出:移开并返回 null,走重新寻址。 */
|
|
122
|
+
export async function loadDriveCache(config) {
|
|
123
|
+
const path = driveCachePath(config);
|
|
124
|
+
let raw;
|
|
125
|
+
try {
|
|
126
|
+
raw = await readFile(path, 'utf8');
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (error?.code === 'ENOENT') return null;
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
const data = JSON.parse(raw);
|
|
133
|
+
if (!data || typeof data !== 'object') throw new Error('not a drive cache');
|
|
134
|
+
return data;
|
|
135
|
+
} catch {
|
|
136
|
+
await rm(path, { force: true }).catch(() => {});
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** 写寻址缓存(tmp + rename 原子替换,0600——token 即文件夹地址,按凭据对待)。 */
|
|
142
|
+
export async function saveDriveCache(config, cache) {
|
|
143
|
+
const dir = storeDir(config);
|
|
144
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
145
|
+
const path = driveCachePath(config);
|
|
146
|
+
const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
147
|
+
await writeFile(tmp, JSON.stringify(cache, null, 2) + '\n', { mode: 0o600, flag: 'w' });
|
|
148
|
+
await rename(tmp, path);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* 给飞书链接带上中文界面参数。飞书 Web 套件按 `locale` cookie 决定界面语言,
|
|
153
|
+
* 而 `?lang=…` 会被网关消费并改写该 cookie(实测 zh-CN/en-US 双向生效);
|
|
154
|
+
* 桌面壳内嵌 webview 里这个 cookie 常是 en-US,界面就成了英文。iframe 跨域
|
|
155
|
+
* 改不了 cookie,只能在 URL 上带参数——链接经登录跳转也保留该参数。
|
|
156
|
+
*/
|
|
157
|
+
export function withChineseLocale(url) {
|
|
158
|
+
const raw = String(url || '').trim();
|
|
159
|
+
if (!raw) return raw;
|
|
160
|
+
try {
|
|
161
|
+
const parsed = new URL(raw);
|
|
162
|
+
if (!parsed.searchParams.has('lang')) parsed.searchParams.set('lang', 'zh-CN');
|
|
163
|
+
return parsed.toString();
|
|
164
|
+
} catch {
|
|
165
|
+
return raw;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* 寻址服务。run(args, options) → { stdout } 为 lark-cli 执行器(注入以便
|
|
171
|
+
* 测试;生产侧绑定 proc.runCliTool('lark-cli', …)——PATH shim、桌面壳自带
|
|
172
|
+
* Node、超时与中文错误包装都在那一层)。
|
|
173
|
+
*/
|
|
174
|
+
export function createDriveService({ run }) {
|
|
175
|
+
const listFolder = async (folderToken, pageSize, pageToken = '') => {
|
|
176
|
+
const params = { page_size: pageSize };
|
|
177
|
+
if (folderToken) params.folder_token = folderToken;
|
|
178
|
+
if (pageToken) params.page_token = pageToken;
|
|
179
|
+
const { stdout } = await run([
|
|
180
|
+
'api', 'GET', '/open-apis/drive/v1/files',
|
|
181
|
+
'--params', JSON.stringify(params),
|
|
182
|
+
'--format', 'json',
|
|
183
|
+
]);
|
|
184
|
+
return parseLarkJson(stdout);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const service = {
|
|
188
|
+
/** 根目录翻页找「AgentOne」文件夹;遍历完没找到返回 null。 */
|
|
189
|
+
async findAgentOneFolder(name = AGENTONE_FOLDER_NAME) {
|
|
190
|
+
let pageToken = '';
|
|
191
|
+
for (let page = 0; page < MAX_LIST_PAGES; page += 1) {
|
|
192
|
+
const data = await listFolder('', LIST_PAGE_SIZE, pageToken);
|
|
193
|
+
const hit = pickFolder(data.files, name);
|
|
194
|
+
if (hit) {
|
|
195
|
+
return { token: hit.token, url: hit.url || '', name: hit.name };
|
|
196
|
+
}
|
|
197
|
+
if (!data.has_more || !data.page_token) return null;
|
|
198
|
+
pageToken = data.page_token;
|
|
199
|
+
}
|
|
200
|
+
throw new Error(
|
|
201
|
+
`云盘根目录条目过多(超过 ${MAX_LIST_PAGES * LIST_PAGE_SIZE} 项),无法确认「${name}」文件夹是否存在,请先在飞书中整理根目录后再试`,
|
|
202
|
+
);
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
async createAgentOneFolder(name = AGENTONE_FOLDER_NAME) {
|
|
206
|
+
const { stdout } = await run([
|
|
207
|
+
'drive', '+create-folder', '--name', name, '--format', 'json',
|
|
208
|
+
]);
|
|
209
|
+
const data = parseLarkJson(stdout);
|
|
210
|
+
const token = data.folder_token || data.token;
|
|
211
|
+
if (!token) throw new Error('创建文件夹失败:飞书未返回文件夹标识,请稍后重试');
|
|
212
|
+
return { token, url: data.url || '', name: data.name || name };
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
/** 校验 token 仍指向可列出的文件夹(列一页子项试探)。任何失败→false。 */
|
|
216
|
+
async folderUsable(token) {
|
|
217
|
+
try {
|
|
218
|
+
await listFolder(String(token || ''), 1);
|
|
219
|
+
return true;
|
|
220
|
+
} catch {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* 文件夹首页清单指纹(token+modified_time+name 排序哈希,排序保证与
|
|
227
|
+
* 返回顺序无关)。给云盘页签做自动刷新:往子文件夹里增删文档会 bump 该
|
|
228
|
+
* 子文件夹在父清单中的 modified_time(2026-10-13 真机实测
|
|
229
|
+
* 1791876629 → 1791876675),因此**根清单指纹**即可覆盖一层深度的
|
|
230
|
+
* 变化——项目文件夹正是这一层。失败向上抛,由路由层兜底成 rev:null。
|
|
231
|
+
*/
|
|
232
|
+
async fingerprintFolder(token) {
|
|
233
|
+
const data = await listFolder(String(token || ''), LIST_PAGE_SIZE);
|
|
234
|
+
const files = Array.isArray(data.files) ? data.files : [];
|
|
235
|
+
const canonical = files
|
|
236
|
+
.map((item) => `${item?.token || ''}:${item?.modified_time || ''}:${item?.name || ''}`)
|
|
237
|
+
.sort()
|
|
238
|
+
.join('|');
|
|
239
|
+
return createHash('sha256').update(canonical).digest('hex').slice(0, 16);
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* 收养用户指定的文件夹链接(…/drive/folder/…):inspect 解出底层
|
|
244
|
+
* token(wiki 链也会解包),非文件夹/解不出即拒,再校验可访问。
|
|
245
|
+
*/
|
|
246
|
+
async adoptFolder(url) {
|
|
247
|
+
const raw = String(url || '').trim();
|
|
248
|
+
if (!/^https?:\/\//.test(raw)) {
|
|
249
|
+
throw new Error('请粘贴完整的飞书文件夹链接(以 https:// 开头)');
|
|
250
|
+
}
|
|
251
|
+
const { stdout } = await run([
|
|
252
|
+
'drive', '+inspect', '--url', raw, '--format', 'json',
|
|
253
|
+
]);
|
|
254
|
+
const data = parseLarkJson(stdout);
|
|
255
|
+
const token = data.obj_token || data.token;
|
|
256
|
+
const type = String(data.obj_type || data.type || '').toLowerCase();
|
|
257
|
+
if (!token || (type && type !== 'folder')) {
|
|
258
|
+
throw new Error('该链接不是有效的云盘文件夹链接,请使用「…/drive/folder/…」链接');
|
|
259
|
+
}
|
|
260
|
+
if (!(await service.folderUsable(token))) {
|
|
261
|
+
throw new Error('无法访问该文件夹:它可能已被删除,或你没有访问权限');
|
|
262
|
+
}
|
|
263
|
+
return { token, url: data.url || raw, name: data.title || data.name || '' };
|
|
264
|
+
},
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* 完整寻址。adoptUrl 优先(设置页「使用此文件夹」);否则缓存有效→
|
|
268
|
+
* 缓存,force(设置页「重新定位」)跳过缓存;失效→按名找→找不到建。
|
|
269
|
+
* 结果落盘缓存。注意:创建只发生在本函数,且必然先有「按名找不到」。
|
|
270
|
+
*/
|
|
271
|
+
async resolveAgentOneFolder({ config, force = false, adoptUrl = '' } = {}) {
|
|
272
|
+
if (adoptUrl) {
|
|
273
|
+
const adopted = await service.adoptFolder(adoptUrl);
|
|
274
|
+
await saveDriveCache(config, { agentone_folder: adopted });
|
|
275
|
+
return adopted;
|
|
276
|
+
}
|
|
277
|
+
const cache = await loadDriveCache(config);
|
|
278
|
+
const cached = cache?.agentone_folder;
|
|
279
|
+
if (!force && cached?.token && (await service.folderUsable(cached.token))) {
|
|
280
|
+
return cached;
|
|
281
|
+
}
|
|
282
|
+
const found = await service.findAgentOneFolder();
|
|
283
|
+
const folder = found || (await service.createAgentOneFolder());
|
|
284
|
+
await saveDriveCache(config, { agentone_folder: folder });
|
|
285
|
+
return folder;
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
return service;
|
|
289
|
+
}
|
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) => {
|