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/README.md +23 -0
- package/lib/archive.js +322 -0
- package/lib/client.js +307 -0
- package/lib/defaults-sync.js +11 -1
- package/lib/drive.js +289 -0
- package/lib/index.js +160 -1
- package/lib/page.js +379 -41
- package/lib/proc.js +14 -4
- package/package.json +3 -2
- package/skills/agentone-drive/SKILL.md +71 -0
- package/skills/agentone-drive/scripts/drive.mjs +336 -0
|
@@ -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
|
+
初始化再继续。
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// AgentOne 云盘文件夹助手(随 dsh-agentone 的 agentone-drive 技能安装到本机技能目录)。
|
|
3
|
+
//
|
|
4
|
+
// 职责:这个文件夹是用户与 AI 共享的工作目录——
|
|
5
|
+
// list 查看里面有什么(只读,绝不创建);
|
|
6
|
+
// resolve-folder 取「<项目或应用>」子文件夹 token(没有则创建);
|
|
7
|
+
// archive 把一篇飞书云文档存进该子文件夹(已在则不动,重复调用安全)。
|
|
8
|
+
// 目标文件夹来自插件维护的 drive.json(设置 → 云盘),本脚本不接收文件夹 token 参数。
|
|
9
|
+
//
|
|
10
|
+
// 输出契约:成功 → stdout JSON {ok:true,...} 退出码 0;失败 → stdout
|
|
11
|
+
// {"ok":false,"error":"<中文>"} 退出码 1。--token-only 只打印 token,失败时
|
|
12
|
+
// 改为 stderr + 退出码 1(避免把错误 JSON 当成 token 用进命令行)。
|
|
13
|
+
// 飞书原始英文错误只进 stderr(日志侧),stdout 永远是中文。
|
|
14
|
+
import { execFile } from 'node:child_process';
|
|
15
|
+
import { readdirSync } from 'node:fs';
|
|
16
|
+
import { readFile } from 'node:fs/promises';
|
|
17
|
+
import { homedir } from 'node:os';
|
|
18
|
+
import { delimiter, join } from 'node:path';
|
|
19
|
+
|
|
20
|
+
const DEFAULT_STORE_DIR = join(process.env.DSH_HOME || join(homedir(), '.dsh'), 'agentone');
|
|
21
|
+
const DEFAULT_CACHE = join(DEFAULT_STORE_DIR, 'drive.json');
|
|
22
|
+
const LARK_BIN = process.platform === 'win32' ? 'lark-cli.cmd' : 'lark-cli';
|
|
23
|
+
const LIST_PAGE_SIZE = 200;
|
|
24
|
+
|
|
25
|
+
const USAGE = `用法:
|
|
26
|
+
node drive.mjs list [--project "项目或应用"] [--drive-cache <路径>]
|
|
27
|
+
node drive.mjs resolve-folder --project "项目或应用" [--token-only] [--drive-cache <路径>]
|
|
28
|
+
node drive.mjs archive --token <文档 token 或链接> [--type docx] [--project "项目或应用"]
|
|
29
|
+
|
|
30
|
+
说明:AgentOne 云盘文件夹的查看与保存。list 只查看不创建;保存进
|
|
31
|
+
「<项目或应用>」子文件夹(自动创建)。
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
function parseArgs(argv) {
|
|
35
|
+
const out = { _: [] };
|
|
36
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
37
|
+
const token = argv[i];
|
|
38
|
+
if (!token.startsWith('--')) { out._.push(token); continue; }
|
|
39
|
+
const key = token.slice(2);
|
|
40
|
+
const next = argv[i + 1];
|
|
41
|
+
if (next === undefined || next.startsWith('--')) { out[key] = true; continue; }
|
|
42
|
+
out[key] = next;
|
|
43
|
+
i += 1;
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function compareVersion(a, b) {
|
|
49
|
+
const left = a.split('.').map(Number);
|
|
50
|
+
const right = b.split('.').map(Number);
|
|
51
|
+
for (let i = 0; i < 3; i += 1) {
|
|
52
|
+
const diff = (left[i] || 0) - (right[i] || 0);
|
|
53
|
+
if (diff) return diff;
|
|
54
|
+
}
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** GUI 宿主 PATH 贫瘠(launchd 的 /usr/bin:/bin)时的兑底目录,口径同插件 proc.js。 */
|
|
59
|
+
function fallbackDirs() {
|
|
60
|
+
const home = homedir();
|
|
61
|
+
const dirs = [];
|
|
62
|
+
try {
|
|
63
|
+
const versions = readdirSync(join(home, '.nvm/versions/node'))
|
|
64
|
+
.filter((v) => /^v?\d/.test(v))
|
|
65
|
+
.sort((a, b) => compareVersion(a.replace(/^v/, ''), b.replace(/^v/, '')));
|
|
66
|
+
if (versions.length) dirs.push(join(home, '.nvm/versions/node', versions[versions.length - 1], 'bin'));
|
|
67
|
+
} catch {
|
|
68
|
+
// 没有 nvm:走后面的通用目录
|
|
69
|
+
}
|
|
70
|
+
dirs.push(
|
|
71
|
+
join(home, '.local/bin'), '/opt/homebrew/bin', '/usr/local/bin',
|
|
72
|
+
join(home, 'Library', 'pnpm'), join(home, 'Library', 'pnpm', 'bin'), join(home, '.local', 'share', 'pnpm'),
|
|
73
|
+
);
|
|
74
|
+
return dirs;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function childEnv() {
|
|
78
|
+
const path = [...fallbackDirs(), process.env.PATH || ''].filter(Boolean).join(delimiter);
|
|
79
|
+
return { ...process.env, PATH: path };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function runLark(args) {
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
execFile(LARK_BIN, args, {
|
|
85
|
+
env: childEnv(), timeout: 120_000, maxBuffer: 16 * 1024 * 1024,
|
|
86
|
+
windowsHide: true, shell: process.platform === 'win32',
|
|
87
|
+
}, (error, stdout, stderr) => {
|
|
88
|
+
if (error?.code === 'ENOENT') {
|
|
89
|
+
reject(new Error('未找到 lark-cli,请先在 dsh 设置 → CLI 里安装并完成飞书授权'));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
resolve({ stdout: String(stdout || ''), stderr: String(stderr || ''), failed: Boolean(error) });
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 缺权限/授权失效给出可自助的中文动作,其余只给中性提示(原文进 stderr)。 */
|
|
98
|
+
function larkErrorText(error = {}) {
|
|
99
|
+
const missing = Array.isArray(error.missing_scopes) ? error.missing_scopes : [];
|
|
100
|
+
if (error.subtype === 'missing_scope' || error.code === 99991679 || missing.length) {
|
|
101
|
+
return '你的飞书授权里还没有云盘权限,请到 dsh 设置 → 云盘 点「重新授权」后重试';
|
|
102
|
+
}
|
|
103
|
+
if (error.type === 'authorization') {
|
|
104
|
+
return '飞书授权已失效,请到 dsh 设置 → 云盘 重新授权后重试';
|
|
105
|
+
}
|
|
106
|
+
return '飞书接口调用失败,请稍后重试';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** lark-cli 的 {ok, data|error} 壳解析(口径同插件 lib/drive.js)。 */
|
|
110
|
+
function parseLarkJson(text) {
|
|
111
|
+
let data;
|
|
112
|
+
try {
|
|
113
|
+
data = JSON.parse(String(text || '').trim());
|
|
114
|
+
} catch {
|
|
115
|
+
throw new Error('飞书命令返回解析失败,请稍后重试');
|
|
116
|
+
}
|
|
117
|
+
if (data && typeof data === 'object' && !Array.isArray(data) && 'ok' in data) {
|
|
118
|
+
if (data.ok === false) {
|
|
119
|
+
const error = data.error || {};
|
|
120
|
+
process.stderr.write(`[archive] lark error: ${JSON.stringify(error)}\n`);
|
|
121
|
+
throw new Error(larkErrorText(error));
|
|
122
|
+
}
|
|
123
|
+
return data.data ?? {};
|
|
124
|
+
}
|
|
125
|
+
return data ?? {};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function larkJson(args) {
|
|
129
|
+
const { stdout, stderr, failed } = await runLark(args);
|
|
130
|
+
const text = stdout.trim();
|
|
131
|
+
if (!text) {
|
|
132
|
+
if (failed) {
|
|
133
|
+
const reason = stderr.trim().split('\n')[0] || '';
|
|
134
|
+
process.stderr.write(`[archive] lark failed: ${reason}\n`);
|
|
135
|
+
throw new Error('飞书命令执行失败,请稍后重试');
|
|
136
|
+
}
|
|
137
|
+
throw new Error('飞书命令没有返回内容,请稍后重试');
|
|
138
|
+
}
|
|
139
|
+
return parseLarkJson(text);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function readRootFolder(cachePath) {
|
|
143
|
+
let data;
|
|
144
|
+
try {
|
|
145
|
+
data = JSON.parse(await readFile(cachePath, 'utf8'));
|
|
146
|
+
} catch {
|
|
147
|
+
throw new Error('还没有找到云盘文件夹,请先在 dsh 设置 → 云盘 点「初始化 / 重新定位」');
|
|
148
|
+
}
|
|
149
|
+
const folder = data?.agentone_folder;
|
|
150
|
+
if (!folder?.token) {
|
|
151
|
+
throw new Error('云盘文件夹记录里没有可用文件夹,请先在 dsh 设置 → 云盘 重新定位');
|
|
152
|
+
}
|
|
153
|
+
return { token: String(folder.token), url: String(folder.url || ''), name: String(folder.name || 'AgentOne') };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function listFolder(folderToken) {
|
|
157
|
+
const data = await larkJson([
|
|
158
|
+
'api', 'GET', '/open-apis/drive/v1/files',
|
|
159
|
+
'--params', JSON.stringify({ folder_token: folderToken, page_size: LIST_PAGE_SIZE }),
|
|
160
|
+
'--format', 'json',
|
|
161
|
+
]);
|
|
162
|
+
return data;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* 给飞书链接带上中文界面参数。宿主的内嵌浏览器是按账号分区的独立会话
|
|
167
|
+
* (persist:dsh-platform-…),locale cookie 跟云盘页签不共用、可能是英文;
|
|
168
|
+
* `?lang=zh-CN` 会被飞书网关消费并改写该分区的 locale cookie(口径同
|
|
169
|
+
* lib/drive.js 的 withChineseLocale,脚本自包含故复制一份)。
|
|
170
|
+
*/
|
|
171
|
+
function withChineseLocale(raw) {
|
|
172
|
+
const url = String(raw || '').trim();
|
|
173
|
+
if (!url) return url;
|
|
174
|
+
try {
|
|
175
|
+
// 只探测、不重建:URL.toString() 会把路径里的非 ASCII 百分号编码,改变
|
|
176
|
+
// 返回给用户的链接字形;这里保持原样,仅在 hash 前追加参数。
|
|
177
|
+
const parsed = new URL(url);
|
|
178
|
+
if (parsed.searchParams.has('lang')) return url;
|
|
179
|
+
const hash = parsed.hash || '';
|
|
180
|
+
const base = hash ? url.slice(0, url.length - hash.length) : url;
|
|
181
|
+
return `${base}${parsed.search ? '&' : '?'}lang=zh-CN${hash}`;
|
|
182
|
+
} catch {
|
|
183
|
+
return url;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function folderUrl(root, token, raw) {
|
|
188
|
+
if (raw) return withChineseLocale(raw);
|
|
189
|
+
try {
|
|
190
|
+
return withChineseLocale(`${new URL(root.url).origin}/drive/folder/${token}`);
|
|
191
|
+
} catch {
|
|
192
|
+
return '';
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** 只查找不创建(读取路径专用):返回 {token,url,name} 或 null。 */
|
|
197
|
+
async function findFolder(parent, name) {
|
|
198
|
+
const listed = await listFolder(parent.token);
|
|
199
|
+
const hit = (Array.isArray(listed.files) ? listed.files : [])
|
|
200
|
+
.find((item) => item?.type === 'folder' && item?.name === name);
|
|
201
|
+
return hit ? { token: String(hit.token), url: folderUrl(parent, hit.token, hit.url), name } : null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function findOrCreateFolder(root, parent, name) {
|
|
205
|
+
const hit = await findFolder(parent, name);
|
|
206
|
+
if (hit) return hit;
|
|
207
|
+
const created = await larkJson([
|
|
208
|
+
'drive', '+create-folder', '--name', name, '--folder-token', parent.token, '--format', 'json',
|
|
209
|
+
]);
|
|
210
|
+
const token = created.folder_token || created.token;
|
|
211
|
+
if (!token) throw new Error(`创建子文件夹「${name}」失败:飞书未返回文件夹标识,请稍后重试`);
|
|
212
|
+
return { token: String(token), url: folderUrl(root, token, created.url), name };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** 子目录名净化:去掉路径分隔符与控制字符,避免在云盘里造出意外的层级。 */
|
|
216
|
+
function sanitizeName(value) {
|
|
217
|
+
const cleaned = String(value || '')
|
|
218
|
+
.replace(/[\\/:*?"<>|\u0000-\u001f]/g, '-')
|
|
219
|
+
.replace(/\s+/g, ' ')
|
|
220
|
+
.trim()
|
|
221
|
+
.replace(/^[.\-\s]+/, '')
|
|
222
|
+
.slice(0, 40)
|
|
223
|
+
.trim();
|
|
224
|
+
return cleaned || '未分类';
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function resolveTarget(root, project) {
|
|
228
|
+
// 只按「项目/应用」分组一层:同一项目的产物聚在一个目录里,便于按项目找,
|
|
229
|
+
// 也不引入按时间切分的层级(用户明确要求不要日期层)。
|
|
230
|
+
const groupName = sanitizeName(project);
|
|
231
|
+
const group = await findOrCreateFolder(root, root, groupName);
|
|
232
|
+
return { ...group, path: `${root.name}/${groupName}` };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function report(root, target) {
|
|
236
|
+
// 输出的链接一律带 ?lang=zh-CN:AI 会把这些链接直接给用户,在宿主内嵌浏览器
|
|
237
|
+
// (独立会话)里打开时界面语言才不会被该分区的 locale cookie 重置成英文。
|
|
238
|
+
return {
|
|
239
|
+
folder_token: target.token,
|
|
240
|
+
folder_url: withChineseLocale(target.url),
|
|
241
|
+
path: target.path,
|
|
242
|
+
root_url: withChineseLocale(root.url),
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function resolveFolderCommand(args) {
|
|
247
|
+
const root = await readRootFolder(args['drive-cache'] || DEFAULT_CACHE);
|
|
248
|
+
const target = await resolveTarget(root, args.project);
|
|
249
|
+
if (args['token-only']) return { tokenOnly: target.token };
|
|
250
|
+
return { ok: true, ...report(root, target) };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function archiveCommand(args) {
|
|
254
|
+
const root = await readRootFolder(args['drive-cache'] || DEFAULT_CACHE);
|
|
255
|
+
let fileToken = String(args.token || '').trim();
|
|
256
|
+
let fileType = String(args.type || '').trim().toLowerCase();
|
|
257
|
+
if (!fileToken || fileToken === 'true') throw new Error('缺少 --token(文档 token 或飞书链接)');
|
|
258
|
+
if (/^https?:\/\//i.test(fileToken)) {
|
|
259
|
+
const info = await larkJson(['drive', '+inspect', '--url', fileToken, '--format', 'json']);
|
|
260
|
+
fileToken = String(info.obj_token || info.token || '');
|
|
261
|
+
fileType = fileType || String(info.obj_type || info.type || '').toLowerCase();
|
|
262
|
+
if (!fileToken) throw new Error('无法从链接解析出文档标识,请改用文档 token 重试');
|
|
263
|
+
}
|
|
264
|
+
const target = await resolveTarget(root, args.project);
|
|
265
|
+
// 只翻第一页判断「是否已归档」:命中则不动(幂等);漏判时移动到自己所在的
|
|
266
|
+
// 目录同样无副作用,不为此翻完整个目录。
|
|
267
|
+
const listed = await listFolder(target.token);
|
|
268
|
+
const archived = !(Array.isArray(listed.files) ? listed.files : []).some((item) => item?.token === fileToken);
|
|
269
|
+
if (archived) {
|
|
270
|
+
await larkJson([
|
|
271
|
+
'drive', '+move', '--file-token', fileToken, '--type', fileType || 'file',
|
|
272
|
+
'--folder-token', target.token, '--format', 'json',
|
|
273
|
+
]);
|
|
274
|
+
}
|
|
275
|
+
return { ok: true, archived, ...report(root, target) };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* 列出文件夹里的内容(读取路径,绝不创建)。不带 --project 列根文件夹;
|
|
280
|
+
* 带 --project 只列该子文件夹(不存在返回空列表,不新建)。
|
|
281
|
+
*/
|
|
282
|
+
async function listCommand(args) {
|
|
283
|
+
const root = await readRootFolder(args['drive-cache'] || DEFAULT_CACHE);
|
|
284
|
+
let folder = root;
|
|
285
|
+
let path = root.name;
|
|
286
|
+
if (args.project !== undefined) {
|
|
287
|
+
const groupName = sanitizeName(args.project);
|
|
288
|
+
const found = await findFolder(root, groupName);
|
|
289
|
+
if (!found) {
|
|
290
|
+
return { ok: true, folder_token: '', folder_url: '', path: `${root.name}/${groupName}`, files: [], has_more: false };
|
|
291
|
+
}
|
|
292
|
+
folder = { ...found, url: found.url || root.url };
|
|
293
|
+
path = `${root.name}/${groupName}`;
|
|
294
|
+
}
|
|
295
|
+
const listed = await listFolder(folder.token);
|
|
296
|
+
const files = (Array.isArray(listed.files) ? listed.files : []).map((item) => ({
|
|
297
|
+
name: item?.name || '',
|
|
298
|
+
type: item?.type || '',
|
|
299
|
+
token: item?.token || '',
|
|
300
|
+
url: withChineseLocale(item?.url),
|
|
301
|
+
}));
|
|
302
|
+
return {
|
|
303
|
+
ok: true, folder_token: folder.token, folder_url: withChineseLocale(folder.url), path,
|
|
304
|
+
files, has_more: Boolean(listed.has_more),
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function main() {
|
|
309
|
+
const args = parseArgs(process.argv.slice(2));
|
|
310
|
+
const command = args._[0];
|
|
311
|
+
if (!command || args.help) {
|
|
312
|
+
process.stdout.write(USAGE);
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
if (command === 'list') return listCommand(args);
|
|
316
|
+
if (command === 'resolve-folder') return resolveFolderCommand(args);
|
|
317
|
+
if (command === 'archive') return archiveCommand(args);
|
|
318
|
+
throw new Error(`未知子命令:${command}(可用:list、resolve-folder、archive)`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
main().then((result) => {
|
|
322
|
+
if (!result) return;
|
|
323
|
+
if (result.tokenOnly) {
|
|
324
|
+
process.stdout.write(`${result.tokenOnly}\n`);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
328
|
+
}).catch((error) => {
|
|
329
|
+
const message = error?.message || '保存失败,请稍后重试';
|
|
330
|
+
if (process.argv.includes('--token-only')) {
|
|
331
|
+
process.stderr.write(`保存失败:${message}\n`);
|
|
332
|
+
} else {
|
|
333
|
+
process.stdout.write(`${JSON.stringify({ ok: false, error: message })}\n`);
|
|
334
|
+
}
|
|
335
|
+
process.exitCode = 1;
|
|
336
|
+
});
|