vps-work 2.5.0 → 2.5.1
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/dist/packages/vps-work/src/bin/vps-work.js +271 -0
- package/dist/packages/vps-work/src/commands/agent/index.js +264 -0
- package/dist/packages/vps-work/src/commands/approval/index.js +7 -0
- package/dist/packages/vps-work/src/commands/approval/shortcuts.js +259 -0
- package/dist/packages/vps-work/src/commands/attendance/applications.js +21 -0
- package/dist/packages/vps-work/src/commands/attendance/index.js +9 -0
- package/dist/packages/vps-work/src/commands/attendance/scheduling.js +183 -0
- package/dist/packages/vps-work/src/commands/attendance/shortcuts.js +131 -0
- package/dist/packages/vps-work/src/commands/auth/check.js +34 -0
- package/dist/packages/vps-work/src/commands/auth/index.js +21 -0
- package/dist/packages/vps-work/src/commands/auth/list.js +50 -0
- package/dist/packages/vps-work/src/commands/auth/scopes.js +17 -0
- package/dist/packages/vps-work/src/commands/auth/status.js +127 -0
- package/dist/packages/vps-work/src/commands/auth/tenants.js +35 -0
- package/dist/packages/vps-work/src/commands/auth/use.js +40 -0
- package/dist/packages/vps-work/src/commands/calendar/index.js +7 -0
- package/dist/packages/vps-work/src/commands/calendar/shortcuts.js +598 -0
- package/dist/packages/vps-work/src/commands/catalog.js +29 -0
- package/dist/packages/vps-work/src/commands/cli-install.js +70 -0
- package/dist/packages/vps-work/src/commands/collab-invoke.js +19 -0
- package/dist/packages/vps-work/src/commands/corporate/index.js +204 -0
- package/dist/packages/vps-work/src/commands/creative/index.js +369 -0
- package/dist/packages/vps-work/src/commands/documents/index.js +991 -0
- package/dist/packages/vps-work/src/commands/drive/index.js +435 -0
- package/dist/packages/vps-work/src/commands/exec.js +46 -0
- package/dist/packages/vps-work/src/commands/hr/business.js +22 -0
- package/dist/packages/vps-work/src/commands/hr/data-view-shortcuts.js +519 -0
- package/dist/packages/vps-work/src/commands/hr/index.js +15 -0
- package/dist/packages/vps-work/src/commands/hr/job-responsibilities.js +266 -0
- package/dist/packages/vps-work/src/commands/hr/payslip.js +45 -0
- package/dist/packages/vps-work/src/commands/hr/shortcuts.js +146 -0
- package/dist/packages/vps-work/src/commands/im/im-message-payload.js +150 -0
- package/dist/packages/vps-work/src/commands/im/index.js +7 -0
- package/dist/packages/vps-work/src/commands/im/shortcuts.js +856 -0
- package/dist/packages/vps-work/src/commands/knowledge/index.js +36 -0
- package/dist/packages/vps-work/src/commands/login.js +52 -0
- package/dist/packages/vps-work/src/commands/logout.js +12 -0
- package/dist/packages/vps-work/src/commands/mail/email.js +132 -0
- package/dist/packages/vps-work/src/commands/mail/index.js +9 -0
- package/dist/packages/vps-work/src/commands/mail/shortcuts.js +43 -0
- package/dist/packages/vps-work/src/commands/meeting/index.js +7 -0
- package/dist/packages/vps-work/src/commands/meeting/shortcuts.js +120 -0
- package/dist/packages/vps-work/src/commands/odoo-invoke.js +21 -0
- package/dist/packages/vps-work/src/commands/odoo-leaf-registry.js +13 -0
- package/dist/packages/vps-work/src/commands/office/index.js +59 -0
- package/dist/packages/vps-work/src/commands/okr/index.js +7 -0
- package/dist/packages/vps-work/src/commands/okr/shortcuts.js +75 -0
- package/dist/packages/vps-work/src/commands/org-structure.js +347 -0
- package/dist/packages/vps-work/src/commands/popup/index.js +147 -0
- package/dist/packages/vps-work/src/commands/product/abnormal-statistics.js +250 -0
- package/dist/packages/vps-work/src/commands/product/data-view-shortcuts.js +253 -0
- package/dist/packages/vps-work/src/commands/product/index.js +11 -0
- package/dist/packages/vps-work/src/commands/purchase/index.js +45 -0
- package/dist/packages/vps-work/src/commands/reauth.js +127 -0
- package/dist/packages/vps-work/src/commands/report/index.js +7 -0
- package/dist/packages/vps-work/src/commands/report/missing-daily-reports.js +9 -0
- package/dist/packages/vps-work/src/commands/report/shortcuts.js +251 -0
- package/dist/packages/vps-work/src/commands/sales/index.js +11 -0
- package/dist/packages/vps-work/src/commands/sales/period-params.js +75 -0
- package/dist/packages/vps-work/src/commands/sales/shortcuts-personnel.js +124 -0
- package/dist/packages/vps-work/src/commands/sales/shortcuts.js +800 -0
- package/dist/packages/vps-work/src/commands/shortcut-common.js +88 -0
- package/dist/packages/vps-work/src/commands/shortcut-params.js +22 -0
- package/dist/packages/vps-work/src/commands/skill/index.js +136 -0
- package/dist/packages/vps-work/src/commands/skill/personal.js +167 -0
- package/dist/packages/vps-work/src/commands/store/index.js +82 -0
- package/dist/packages/vps-work/src/commands/task/index.js +7 -0
- package/dist/packages/vps-work/src/commands/task/task-center.js +595 -0
- package/dist/packages/vps-work/src/commands/tools/index.js +127 -0
- package/dist/packages/vps-work/src/commands/vemory/index.js +7 -0
- package/dist/packages/vps-work/src/commands/vemory/shortcuts.js +53 -0
- package/dist/packages/vps-work/src/commands/vertu-aisales.js +30 -0
- package/dist/packages/vps-work/src/commands/vertu-erp.js +30 -0
- package/dist/packages/vps-work/src/core/aisales-mcp-client.js +35 -0
- package/dist/packages/vps-work/src/core/auth.js +240 -0
- package/dist/packages/vps-work/src/core/cli-bin.js +11 -0
- package/dist/packages/vps-work/src/core/cli-root-help.js +190 -0
- package/dist/packages/vps-work/src/core/client.js +158 -0
- package/dist/packages/vps-work/src/core/config.js +183 -0
- package/dist/packages/vps-work/src/core/corporate-client.js +52 -0
- package/dist/packages/vps-work/src/core/creative-attachment-download.js +48 -0
- package/dist/packages/vps-work/src/core/documents-client.js +57 -0
- package/dist/packages/vps-work/src/core/endpoints.js +46 -0
- package/dist/packages/vps-work/src/core/erp-mcp-client.js +11 -0
- package/dist/packages/vps-work/src/core/errors.js +107 -0
- package/dist/packages/vps-work/src/core/execute-catalog-client.js +42 -0
- package/dist/packages/vps-work/src/core/hermes-client.js +70 -0
- package/dist/packages/vps-work/src/core/im-attachment-download.js +109 -0
- package/dist/packages/vps-work/src/core/im-client.js +64 -0
- package/dist/packages/vps-work/src/core/keychain.js +58 -0
- package/dist/packages/vps-work/src/core/mail-client.js +82 -0
- package/dist/packages/vps-work/src/core/mcp-client.js +129 -0
- package/dist/packages/vps-work/src/core/office-client.js +25 -0
- package/dist/packages/vps-work/src/core/office-files.js +82 -0
- package/dist/packages/vps-work/src/core/paths.js +70 -0
- package/dist/packages/vps-work/src/core/public-result.js +43 -0
- package/dist/packages/vps-work/src/core/sales-invoice-attachments-client.js +75 -0
- package/dist/packages/vps-work/src/core/sales-query-client.js +452 -0
- package/dist/packages/vps-work/src/core/skill-catalog.js +158 -0
- package/dist/packages/vps-work/src/core/skill-key.js +19 -0
- package/dist/packages/vps-work/src/core/v-drive-store.js +392 -0
- package/dist/packages/vps-work/src/core/vertu-agent-env.js +32 -0
- package/dist/packages/vps-work/src/core/vps-auth-scopes.js +73 -0
- package/dist/packages/vps-work/src/core/vps-service-auth.js +382 -0
- package/dist/packages/vps-work/src/core/vps-service-config.js +83 -0
- package/dist/packages/vps-work/src/core/vps-service-http.js +187 -0
- package/dist/packages/vps-work/src/utils/at-file.js +34 -0
- package/dist/packages/vps-work/src/utils/cli-option-helpers.js +52 -0
- package/dist/packages/vps-work/src/utils/frontmatter.js +26 -0
- package/dist/packages/vps-work/src/utils/normalize-argv.js +51 -0
- package/dist/packages/vps-work/src/utils/parse-json-arg.js +85 -0
- package/dist/packages/vps-work/src/utils/prompt.js +46 -0
- package/dist/packages/vps-work/src/utils/read-text.js +44 -0
- package/dist/packages/vps-work/src/utils/schema-to-options.js +307 -0
- package/dist/packages/vps-work/src/utils/unwrap-envelope.js +31 -0
- package/dist/shared/im-org-tree/index.js +200 -0
- package/dist/vps-work.cjs +3 -3
- package/package.json +1 -1
- package/skills/vertu-aisales/SKILL.md +4 -2
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import { createReadStream, createWriteStream, existsSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { mkdir } from 'node:fs/promises';
|
|
3
|
+
import { basename, dirname, extname, join, resolve } from 'node:path';
|
|
4
|
+
import { pipeline } from 'node:stream/promises';
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
import { request } from 'undici';
|
|
7
|
+
import { VertuError } from '../../core/errors.js';
|
|
8
|
+
import { imRequest } from '../../core/im-client.js';
|
|
9
|
+
import { VDriveStore } from '../../core/v-drive-store.js';
|
|
10
|
+
import { readStdinText, readUtf8File } from '../../utils/read-text.js';
|
|
11
|
+
function required(value, flag) {
|
|
12
|
+
const normalized = value?.trim();
|
|
13
|
+
if (!normalized)
|
|
14
|
+
throw new VertuError(`${flag} 为必填。`);
|
|
15
|
+
return normalized;
|
|
16
|
+
}
|
|
17
|
+
function parentId(value) {
|
|
18
|
+
const normalized = value?.trim();
|
|
19
|
+
return !normalized || normalized === 'root' ? null : normalized;
|
|
20
|
+
}
|
|
21
|
+
function record(value, message = 'V盘服务返回格式无效。') {
|
|
22
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
23
|
+
throw new VertuError(message);
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
function records(value) {
|
|
27
|
+
return Array.isArray(value) ? value.filter((item) => Boolean(item) && typeof item === 'object' && !Array.isArray(item)) : [];
|
|
28
|
+
}
|
|
29
|
+
function itemFrom(value) {
|
|
30
|
+
return record(record(value).item, 'V盘服务未返回项目。');
|
|
31
|
+
}
|
|
32
|
+
function print(payload) {
|
|
33
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
34
|
+
}
|
|
35
|
+
function attachBaseOption(command) {
|
|
36
|
+
return command.option('--base-url <url>', 'Override Vertu service URL');
|
|
37
|
+
}
|
|
38
|
+
function collect(value, previous) {
|
|
39
|
+
return [...previous, value];
|
|
40
|
+
}
|
|
41
|
+
function positiveInteger(value, flag) {
|
|
42
|
+
const normalized = value?.trim();
|
|
43
|
+
if (!normalized || !/^\d+$/.test(normalized))
|
|
44
|
+
throw new VertuError(`${flag} 需为正整数。`);
|
|
45
|
+
const parsed = Number(normalized);
|
|
46
|
+
if (!Number.isSafeInteger(parsed) || parsed <= 0)
|
|
47
|
+
throw new VertuError(`${flag} 需为正整数。`);
|
|
48
|
+
return parsed;
|
|
49
|
+
}
|
|
50
|
+
async function resolveContent(opts, requiredInput = false) {
|
|
51
|
+
if (opts.content !== undefined && opts.contentFile !== undefined) {
|
|
52
|
+
throw new VertuError('--content 和 --content-file 不能同时使用。');
|
|
53
|
+
}
|
|
54
|
+
if (requiredInput && opts.content === undefined && opts.contentFile === undefined) {
|
|
55
|
+
throw new VertuError('--content 或 --content-file 至少传一个。');
|
|
56
|
+
}
|
|
57
|
+
if (opts.contentFile === '-')
|
|
58
|
+
return readStdinText();
|
|
59
|
+
if (opts.contentFile !== undefined)
|
|
60
|
+
return readUtf8File(opts.contentFile);
|
|
61
|
+
return opts.content ?? '';
|
|
62
|
+
}
|
|
63
|
+
function mimeType(name) {
|
|
64
|
+
const types = {
|
|
65
|
+
'.csv': 'text/csv', '.doc': 'application/msword',
|
|
66
|
+
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
67
|
+
'.gif': 'image/gif', '.jpeg': 'image/jpeg', '.jpg': 'image/jpeg', '.md': 'text/markdown',
|
|
68
|
+
'.mp3': 'audio/mpeg', '.mp4': 'video/mp4', '.pdf': 'application/pdf', '.png': 'image/png',
|
|
69
|
+
'.ppt': 'application/vnd.ms-powerpoint',
|
|
70
|
+
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
71
|
+
'.txt': 'text/plain', '.webp': 'image/webp', '.xls': 'application/vnd.ms-excel',
|
|
72
|
+
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', '.zip': 'application/zip',
|
|
73
|
+
};
|
|
74
|
+
return types[extname(name).toLowerCase()] ?? 'application/octet-stream';
|
|
75
|
+
}
|
|
76
|
+
async function driveGet(opts, view) {
|
|
77
|
+
return record(await imRequest('GET', '/v1/v-drive', {
|
|
78
|
+
baseUrl: opts.baseUrl,
|
|
79
|
+
query: view ? { view } : undefined,
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
async function driveAction(opts, action, body = {}) {
|
|
83
|
+
return record(await imRequest('POST', '/v1/v-drive', {
|
|
84
|
+
baseUrl: opts.baseUrl,
|
|
85
|
+
body: { action, ...body },
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
async function putOss(uploadUrl, body, size, type, signedHeaders = {}) {
|
|
89
|
+
const response = await request(uploadUrl, {
|
|
90
|
+
method: 'PUT',
|
|
91
|
+
headers: { 'content-type': type, 'content-length': String(size), ...Object.fromEntries(Object.entries(signedHeaders).map(([key, value]) => [key, String(value)])) },
|
|
92
|
+
body,
|
|
93
|
+
});
|
|
94
|
+
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
95
|
+
const message = await response.body.text();
|
|
96
|
+
throw new VertuError(`OSS 上传失败:HTTP ${response.statusCode}${message ? ` ${message.slice(0, 200)}` : ''}`);
|
|
97
|
+
}
|
|
98
|
+
await response.body.dump();
|
|
99
|
+
}
|
|
100
|
+
async function uploadContent(opts, input) {
|
|
101
|
+
const initialized = await driveAction(opts, 'upload-init', {
|
|
102
|
+
name: input.name,
|
|
103
|
+
parent_id: input.parentId ?? null,
|
|
104
|
+
mime_type: input.type,
|
|
105
|
+
size_bytes: input.size,
|
|
106
|
+
});
|
|
107
|
+
const uploadUrl = String(initialized.upload_url ?? '');
|
|
108
|
+
const storageKey = String(initialized.storage_key ?? '');
|
|
109
|
+
if (!storageKey)
|
|
110
|
+
throw new VertuError('V盘服务未返回 OSS 存储路径。');
|
|
111
|
+
try {
|
|
112
|
+
if (initialized.upload_type === 'multipart') {
|
|
113
|
+
const partSize = Number(initialized.part_size);
|
|
114
|
+
const urls = initialized.part_urls;
|
|
115
|
+
if (!Number.isSafeInteger(partSize) || partSize <= 0 || !Array.isArray(urls)
|
|
116
|
+
|| urls.length !== Math.ceil(input.size / partSize)
|
|
117
|
+
|| urls.some((url) => typeof url !== 'string' || !url)) {
|
|
118
|
+
throw new VertuError('V盘服务返回的 OSS 分片上传参数无效。');
|
|
119
|
+
}
|
|
120
|
+
for (let index = 0; index < urls.length; index += 1) {
|
|
121
|
+
const start = index * partSize;
|
|
122
|
+
const size = Math.min(partSize, input.size - start);
|
|
123
|
+
const body = Buffer.isBuffer(input.body)
|
|
124
|
+
? input.body.subarray(start, start + size)
|
|
125
|
+
: createReadStream(input.body, { start, end: start + size - 1 });
|
|
126
|
+
await putOss(String(urls[index]), body, size, 'application/octet-stream', initialized.part_headers ? record(initialized.part_headers) : {});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
if (!uploadUrl)
|
|
131
|
+
throw new VertuError('V盘服务未返回 OSS 上传地址。');
|
|
132
|
+
await putOss(uploadUrl, Buffer.isBuffer(input.body) ? input.body : createReadStream(input.body), input.size, input.type, initialized.headers ? record(initialized.headers) : {});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
// Release the reservation and any uploaded parts without masking the original error.
|
|
137
|
+
await driveAction(opts, 'upload-abort', { storage_key: storageKey }).catch(() => undefined);
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
return driveAction(opts, input.itemId ? 'replace-complete' : 'upload-complete', {
|
|
141
|
+
...(input.itemId ? { item_id: input.itemId } : { name: input.name, parent_id: input.parentId ?? null }),
|
|
142
|
+
mime_type: input.type,
|
|
143
|
+
size_bytes: input.size,
|
|
144
|
+
storage_key: storageKey,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
async function uploadFile(opts, path, targetParentId) {
|
|
148
|
+
const source = resolve(path);
|
|
149
|
+
if (!existsSync(source) || !statSync(source).isFile())
|
|
150
|
+
throw new VertuError(`源文件不存在:${path}`);
|
|
151
|
+
const size = statSync(source).size;
|
|
152
|
+
const name = basename(source);
|
|
153
|
+
if (size === 0) {
|
|
154
|
+
return driveAction(opts, 'create-file', { name, parent_id: targetParentId });
|
|
155
|
+
}
|
|
156
|
+
return uploadContent(opts, {
|
|
157
|
+
name,
|
|
158
|
+
parentId: targetParentId,
|
|
159
|
+
type: mimeType(name),
|
|
160
|
+
size,
|
|
161
|
+
body: source,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
async function downloadResponse(opts, id) {
|
|
165
|
+
const result = await driveAction(opts, 'download', { item_id: id });
|
|
166
|
+
const item = itemFrom(result);
|
|
167
|
+
if (result.empty_text_file === true)
|
|
168
|
+
return { item, response: null };
|
|
169
|
+
const url = String(result.download_url ?? '');
|
|
170
|
+
if (!url)
|
|
171
|
+
throw new VertuError('V盘服务未返回 OSS 下载地址。');
|
|
172
|
+
const response = await request(url);
|
|
173
|
+
if (response.statusCode >= 400) {
|
|
174
|
+
await response.body.dump();
|
|
175
|
+
throw new VertuError(`OSS 下载失败:HTTP ${response.statusCode}`);
|
|
176
|
+
}
|
|
177
|
+
return { item, response };
|
|
178
|
+
}
|
|
179
|
+
function scopedItems(items, parent, recursive, query) {
|
|
180
|
+
const descendants = new Set();
|
|
181
|
+
if (recursive) {
|
|
182
|
+
let changed = true;
|
|
183
|
+
while (changed) {
|
|
184
|
+
changed = false;
|
|
185
|
+
for (const item of items) {
|
|
186
|
+
const id = String(item.id ?? '');
|
|
187
|
+
const itemParent = item.parentId == null ? null : String(item.parentId);
|
|
188
|
+
if (id && (parent === null || itemParent === parent || (itemParent && descendants.has(itemParent))) && !descendants.has(id)) {
|
|
189
|
+
descendants.add(id);
|
|
190
|
+
changed = true;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const normalizedQuery = query?.trim().toLocaleLowerCase();
|
|
196
|
+
return items.filter((item) => {
|
|
197
|
+
const inScope = recursive
|
|
198
|
+
? descendants.has(String(item.id ?? ''))
|
|
199
|
+
: (item.parentId == null ? null : String(item.parentId)) === parent;
|
|
200
|
+
return inScope && (!normalizedQuery || String(item.name ?? '').toLocaleLowerCase().includes(normalizedQuery));
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
function migrationState(path) {
|
|
204
|
+
if (!existsSync(path))
|
|
205
|
+
return { items: {}, spaces: {} };
|
|
206
|
+
try {
|
|
207
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
208
|
+
return { items: parsed.items ?? {}, spaces: parsed.spaces ?? {} };
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
throw new VertuError(`迁移状态文件无法解析:${path}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
async function migrateLocalVDrive(opts) {
|
|
215
|
+
const local = new VDriveStore(opts.root?.trim() || undefined);
|
|
216
|
+
const items = local.list(null, true);
|
|
217
|
+
let spaces = [];
|
|
218
|
+
const legacyIndexPath = join(local.root, 'index.json');
|
|
219
|
+
if (existsSync(legacyIndexPath)) {
|
|
220
|
+
const parsed = record(JSON.parse(readFileSync(legacyIndexPath, 'utf8')), '旧 V盘索引无法解析。');
|
|
221
|
+
spaces = records(parsed.spaces).map((space) => ({
|
|
222
|
+
id: String(space.id ?? ''),
|
|
223
|
+
name: String(space.name ?? '团队共享空间'),
|
|
224
|
+
members: Array.isArray(space.members) ? space.members : [],
|
|
225
|
+
security: space.security && typeof space.security === 'object' && !Array.isArray(space.security)
|
|
226
|
+
? space.security
|
|
227
|
+
: {},
|
|
228
|
+
deletedAt: typeof space.deletedAt === 'string' ? space.deletedAt : null,
|
|
229
|
+
})).filter((space) => space.id && !space.deletedAt);
|
|
230
|
+
}
|
|
231
|
+
const statePath = join(local.root, 'oss-migration-v1.json');
|
|
232
|
+
const state = migrationState(statePath);
|
|
233
|
+
const save = () => writeFileSync(statePath, JSON.stringify(state, null, 2), 'utf8');
|
|
234
|
+
let migratedItems = 0;
|
|
235
|
+
let migratedSpaces = 0;
|
|
236
|
+
const pendingFolders = items.filter((item) => item.kind === 'folder' && !state.items[item.id]);
|
|
237
|
+
while (pendingFolders.length) {
|
|
238
|
+
const index = pendingFolders.findIndex((item) => !item.parentId || state.items[item.parentId]);
|
|
239
|
+
if (index < 0)
|
|
240
|
+
throw new VertuError('旧 V盘文件夹层级无法迁移。');
|
|
241
|
+
const item = pendingFolders.splice(index, 1)[0];
|
|
242
|
+
const result = await driveAction(opts, 'create-folder', {
|
|
243
|
+
name: item.name,
|
|
244
|
+
parent_id: item.parentId ? state.items[item.parentId] : null,
|
|
245
|
+
});
|
|
246
|
+
state.items[item.id] = String(itemFrom(result).id);
|
|
247
|
+
migratedItems += 1;
|
|
248
|
+
save();
|
|
249
|
+
}
|
|
250
|
+
for (const item of items.filter((candidate) => candidate.kind === 'file')) {
|
|
251
|
+
if (state.items[item.id])
|
|
252
|
+
continue;
|
|
253
|
+
const bytes = local.readFile(item.id).bytes;
|
|
254
|
+
const result = bytes.length === 0
|
|
255
|
+
? await driveAction(opts, 'create-file', { name: item.name, parent_id: item.parentId ? state.items[item.parentId] : null })
|
|
256
|
+
: await uploadContent(opts, {
|
|
257
|
+
name: item.name,
|
|
258
|
+
parentId: item.parentId ? state.items[item.parentId] : null,
|
|
259
|
+
type: item.mimeType || mimeType(item.name),
|
|
260
|
+
size: bytes.length,
|
|
261
|
+
body: bytes,
|
|
262
|
+
});
|
|
263
|
+
state.items[item.id] = String(itemFrom(result).id);
|
|
264
|
+
migratedItems += 1;
|
|
265
|
+
save();
|
|
266
|
+
}
|
|
267
|
+
for (const space of spaces) {
|
|
268
|
+
if (state.spaces[space.id])
|
|
269
|
+
continue;
|
|
270
|
+
const result = await driveAction(opts, 'create-space', {
|
|
271
|
+
name: space.name,
|
|
272
|
+
members: space.members.filter((member) => Boolean(member)
|
|
273
|
+
&& typeof member === 'object'
|
|
274
|
+
&& !Array.isArray(member)
|
|
275
|
+
&& member.role !== 'owner'),
|
|
276
|
+
security: space.security,
|
|
277
|
+
});
|
|
278
|
+
const createdSpace = result.space && typeof result.space === 'object'
|
|
279
|
+
? record(result.space)
|
|
280
|
+
: records(result.spaces).find((candidate) => candidate.name === space.name
|
|
281
|
+
&& !Object.values(state.spaces).includes(String(candidate.id ?? '')));
|
|
282
|
+
if (!createdSpace)
|
|
283
|
+
throw new VertuError('V盘服务未返回新建共享空间。');
|
|
284
|
+
state.spaces[space.id] = String(createdSpace.id);
|
|
285
|
+
migratedSpaces += 1;
|
|
286
|
+
save();
|
|
287
|
+
}
|
|
288
|
+
for (const item of items) {
|
|
289
|
+
for (const oldSpaceId of item.sharedSpaceIds) {
|
|
290
|
+
const newItemId = state.items[item.id];
|
|
291
|
+
const newSpaceId = state.spaces[oldSpaceId];
|
|
292
|
+
if (newItemId && newSpaceId) {
|
|
293
|
+
await driveAction(opts, 'share', { item_id: newItemId, space_id: newSpaceId, enabled: true });
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
storage_provider: 'aliyun-oss',
|
|
299
|
+
source_root: local.root,
|
|
300
|
+
state_file: statePath,
|
|
301
|
+
migrated_items: migratedItems,
|
|
302
|
+
migrated_spaces: migratedSpaces,
|
|
303
|
+
total_items: items.length,
|
|
304
|
+
local_source_preserved: true,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
export function buildDriveCommand() {
|
|
308
|
+
const drive = new Command('drive')
|
|
309
|
+
.aliases(['vdrive', 'v-drive'])
|
|
310
|
+
.description('账号级云端 V盘:文件内容存储于 OSS。');
|
|
311
|
+
attachBaseOption(drive.command('+list').description('列出 V盘文件和文件夹')
|
|
312
|
+
.option('--parent-id <id>', '父文件夹 ID;省略或 root 表示根目录')
|
|
313
|
+
.option('--recursive', '递归列出全部后代').option('--query <text>', '按名称过滤')).action(async (opts) => {
|
|
314
|
+
const snapshot = await driveGet(opts);
|
|
315
|
+
const parent = parentId(opts.parentId);
|
|
316
|
+
print({ storage_provider: 'aliyun-oss', parent_id: parent, recursive: opts.recursive === true,
|
|
317
|
+
items: scopedItems(records(snapshot.items), parent, opts.recursive === true, opts.query), used_bytes: snapshot.usedBytes ?? 0 });
|
|
318
|
+
});
|
|
319
|
+
attachBaseOption(drive.command('+info').description('读取单个项目元数据').requiredOption('--item-id <id>')).action(async (opts) => {
|
|
320
|
+
print(await driveAction(opts, 'info', { item_id: required(opts.itemId, '--item-id') }));
|
|
321
|
+
});
|
|
322
|
+
attachBaseOption(drive.command('+mkdir').description('新建文件夹').requiredOption('--name <name>')
|
|
323
|
+
.option('--parent-id <id>')).action(async (opts) => {
|
|
324
|
+
print(await driveAction(opts, 'create-folder', { name: required(opts.name, '--name'), parent_id: parentId(opts.parentId) }));
|
|
325
|
+
});
|
|
326
|
+
attachBaseOption(drive.command('+create-file').description('新建 UTF-8 文本文件').requiredOption('--name <name>')
|
|
327
|
+
.option('--parent-id <id>').option('--content <text>').option('--content-file <path>')).action(async (opts) => {
|
|
328
|
+
const name = required(opts.name, '--name');
|
|
329
|
+
const content = Buffer.from(await resolveContent(opts));
|
|
330
|
+
const result = content.length === 0
|
|
331
|
+
? await driveAction(opts, 'create-file', { name, parent_id: parentId(opts.parentId) })
|
|
332
|
+
: await uploadContent(opts, { name, parentId: parentId(opts.parentId), type: mimeType(name), size: content.length, body: content });
|
|
333
|
+
print(result);
|
|
334
|
+
});
|
|
335
|
+
attachBaseOption(drive.command('+upload').description('把本地文件上传到 OSS V盘')
|
|
336
|
+
.requiredOption('--source <path>', '源文件路径;可重复传入', collect, []).option('--parent-id <id>')).action(async (opts) => {
|
|
337
|
+
const sources = opts.source ?? [];
|
|
338
|
+
if (!sources.length)
|
|
339
|
+
throw new VertuError('--source 至少传一次。');
|
|
340
|
+
const items = [];
|
|
341
|
+
for (const source of sources)
|
|
342
|
+
items.push(itemFrom(await uploadFile(opts, source, parentId(opts.parentId))));
|
|
343
|
+
print({ storage_provider: 'aliyun-oss', items });
|
|
344
|
+
});
|
|
345
|
+
attachBaseOption(drive.command('+read').description('读取 OSS V盘文件内容').requiredOption('--item-id <id>')
|
|
346
|
+
.option('--encoding <encoding>', 'utf8 或 base64', 'utf8').option('--max-bytes <number>', '', '1048576')).action(async (opts) => {
|
|
347
|
+
const encoding = opts.encoding?.trim().toLowerCase();
|
|
348
|
+
if (encoding !== 'utf8' && encoding !== 'base64')
|
|
349
|
+
throw new VertuError('--encoding 需为 utf8 或 base64。');
|
|
350
|
+
const maxBytes = positiveInteger(opts.maxBytes, '--max-bytes');
|
|
351
|
+
const { item, response } = await downloadResponse(opts, required(opts.itemId, '--item-id'));
|
|
352
|
+
if (Number(item.sizeBytes ?? 0) > maxBytes) {
|
|
353
|
+
if (response)
|
|
354
|
+
await response.body.dump();
|
|
355
|
+
throw new VertuError(`文件超过 --max-bytes=${maxBytes};请用 +download。`);
|
|
356
|
+
}
|
|
357
|
+
const bytes = response ? Buffer.from(await response.body.arrayBuffer()) : Buffer.alloc(0);
|
|
358
|
+
if (bytes.length > maxBytes)
|
|
359
|
+
throw new VertuError(`文件超过 --max-bytes=${maxBytes};请用 +download。`);
|
|
360
|
+
print({ item, encoding, content: bytes.toString(encoding) });
|
|
361
|
+
});
|
|
362
|
+
attachBaseOption(drive.command('+write').description('覆盖 OSS V盘文件内容').requiredOption('--item-id <id>')
|
|
363
|
+
.option('--content <text>').option('--content-file <path>').option('--source <path>', '以二进制文件替换原项目,保留项目 ID')).action(async (opts) => {
|
|
364
|
+
if ([opts.content, opts.contentFile, opts.source].filter(value => value !== undefined).length !== 1) {
|
|
365
|
+
throw new VertuError('--content / --content-file / --source 必须且只能指定一个。');
|
|
366
|
+
}
|
|
367
|
+
const id = required(opts.itemId, '--item-id');
|
|
368
|
+
const current = itemFrom(await driveAction(opts, 'info', { item_id: id }));
|
|
369
|
+
if (opts.source) {
|
|
370
|
+
const source = resolve(opts.source);
|
|
371
|
+
const info = statSync(source);
|
|
372
|
+
if (!info.isFile())
|
|
373
|
+
throw new VertuError('--source 必须是文件。');
|
|
374
|
+
const result = info.size === 0
|
|
375
|
+
? await driveAction(opts, 'replace-empty', { item_id: id, mime_type: current.mimeType ?? mimeType(source) })
|
|
376
|
+
: await uploadContent(opts, { name: String(current.name ?? basename(source)), itemId: id,
|
|
377
|
+
type: String(current.mimeType ?? mimeType(source)), size: info.size, body: source });
|
|
378
|
+
print(result);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
const content = Buffer.from(await resolveContent(opts, true));
|
|
382
|
+
const result = content.length === 0
|
|
383
|
+
? await driveAction(opts, 'replace-empty', { item_id: id, mime_type: current.mimeType ?? 'text/plain' })
|
|
384
|
+
: await uploadContent(opts, { name: String(current.name ?? 'file'), itemId: id,
|
|
385
|
+
type: String(current.mimeType ?? mimeType(String(current.name ?? 'file'))), size: content.length, body: content });
|
|
386
|
+
print(result);
|
|
387
|
+
});
|
|
388
|
+
attachBaseOption(drive.command('+download').description('下载 OSS V盘文件').requiredOption('--item-id <id>')
|
|
389
|
+
.requiredOption('--output <path>').option('--overwrite')).action(async (opts) => {
|
|
390
|
+
const { item, response } = await downloadResponse(opts, required(opts.itemId, '--item-id'));
|
|
391
|
+
const requested = resolve(required(opts.output, '--output'));
|
|
392
|
+
const target = existsSync(requested) && statSync(requested).isDirectory() ? join(requested, basename(String(item.name ?? 'file'))) : requested;
|
|
393
|
+
if (existsSync(target) && !opts.overwrite) {
|
|
394
|
+
if (response)
|
|
395
|
+
await response.body.dump();
|
|
396
|
+
throw new VertuError('目标文件已存在;如需覆盖请显式传 --overwrite。');
|
|
397
|
+
}
|
|
398
|
+
await mkdir(dirname(target), { recursive: true });
|
|
399
|
+
if (response)
|
|
400
|
+
await pipeline(response.body, createWriteStream(target));
|
|
401
|
+
else
|
|
402
|
+
writeFileSync(target, Buffer.alloc(0));
|
|
403
|
+
print({ item, file_path: target });
|
|
404
|
+
});
|
|
405
|
+
attachBaseOption(drive.command('+rename').description('重命名项目').requiredOption('--item-id <id>').requiredOption('--name <name>')).action(async (opts) => {
|
|
406
|
+
const id = required(opts.itemId, '--item-id');
|
|
407
|
+
await driveAction(opts, 'rename', { item_id: id, name: required(opts.name, '--name') });
|
|
408
|
+
print(await driveAction(opts, 'info', { item_id: id }));
|
|
409
|
+
});
|
|
410
|
+
attachBaseOption(drive.command('+move').description('移动项目').requiredOption('--item-id <id>').requiredOption('--parent-id <id>')).action(async (opts) => {
|
|
411
|
+
const id = required(opts.itemId, '--item-id');
|
|
412
|
+
await driveAction(opts, 'move', { item_id: id, parent_id: parentId(required(opts.parentId, '--parent-id')) });
|
|
413
|
+
print(await driveAction(opts, 'info', { item_id: id }));
|
|
414
|
+
});
|
|
415
|
+
attachBaseOption(drive.command('+remove').description('移入回收站').requiredOption('--item-id <id>')).action(async (opts) => {
|
|
416
|
+
const id = required(opts.itemId, '--item-id');
|
|
417
|
+
const item = itemFrom(await driveAction(opts, 'info', { item_id: id }));
|
|
418
|
+
await driveAction(opts, 'remove', { item_id: id });
|
|
419
|
+
print({ trashed_item: item });
|
|
420
|
+
});
|
|
421
|
+
attachBaseOption(drive.command('+trash').description('列出回收站')).action(async (opts) => print(await driveGet(opts, 'trash')));
|
|
422
|
+
attachBaseOption(drive.command('+restore').description('恢复回收站项目').requiredOption('--item-id <id>')).action(async (opts) => {
|
|
423
|
+
const id = required(opts.itemId, '--item-id');
|
|
424
|
+
await driveAction(opts, 'restore', { item_id: id });
|
|
425
|
+
print(await driveAction(opts, 'info', { item_id: id }));
|
|
426
|
+
});
|
|
427
|
+
attachBaseOption(drive.command('+purge').description('永久删除回收站项目').requiredOption('--item-id <id>')).action(async (opts) => {
|
|
428
|
+
print(await driveAction(opts, 'purge', { item_id: required(opts.itemId, '--item-id') }));
|
|
429
|
+
});
|
|
430
|
+
attachBaseOption(drive.command('+empty-trash').description('永久清空回收站')).action(async (opts) => print(await driveAction(opts, 'empty-trash')));
|
|
431
|
+
attachBaseOption(drive.command('+transfers').description('列出传输记录')).action(async (opts) => print(await driveGet(opts, 'transfers')));
|
|
432
|
+
attachBaseOption(drive.command('+migrate-local').description('把旧版本地 V盘迁移到 OSS,保留本地源文件')
|
|
433
|
+
.option('--root <path>', '旧版 v-drive 目录;默认自动定位')).action(async (opts) => print(await migrateLocalVDrive(opts)));
|
|
434
|
+
return drive;
|
|
435
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { executeVpsServiceScript, resolveVpsServiceUrl } from '../core/sales-query-client.js';
|
|
3
|
+
import { VertuError } from '../core/errors.js';
|
|
4
|
+
import { resolveParamsArg } from './shortcut-params.js';
|
|
5
|
+
import { attachSharedShortcutOptions } from './shortcut-common.js';
|
|
6
|
+
function parseParamAssignments(raw) {
|
|
7
|
+
const out = {};
|
|
8
|
+
for (const item of raw ?? []) {
|
|
9
|
+
const eq = item.indexOf('=');
|
|
10
|
+
if (eq <= 0) {
|
|
11
|
+
throw new VertuError(`无效 --param "${item}",需要 key=value`);
|
|
12
|
+
}
|
|
13
|
+
out[item.slice(0, eq).trim()] = item.slice(eq + 1);
|
|
14
|
+
}
|
|
15
|
+
return out;
|
|
16
|
+
}
|
|
17
|
+
/** 通用执行入口:新快捷命令只需服务端登记 script,不必再发 npm。 */
|
|
18
|
+
export function buildExecCommand() {
|
|
19
|
+
const exec = new Command('exec')
|
|
20
|
+
.description('按 catalog 中的 skill_key / script_key 执行查询')
|
|
21
|
+
.requiredOption('--skill <key>', 'catalog.skill_key,如 vps-daily-sales-report / vps-dealer-ops')
|
|
22
|
+
.requiredOption('--script <key>', 'catalog.script_key')
|
|
23
|
+
.option('--param <assignment>', '追加单个参数,格式 key=value,可重复', (value, previous) => {
|
|
24
|
+
previous.push(value);
|
|
25
|
+
return previous;
|
|
26
|
+
}, []);
|
|
27
|
+
attachSharedShortcutOptions(exec);
|
|
28
|
+
return exec.action(async (opts, cmd) => {
|
|
29
|
+
const merged = cmd.optsWithGlobals();
|
|
30
|
+
const extra = await resolveParamsArg(opts);
|
|
31
|
+
const params = { ...parseParamAssignments(opts.param), ...extra };
|
|
32
|
+
if (opts.validateOnly === true || opts.dryRun === true) {
|
|
33
|
+
const payload = {
|
|
34
|
+
validateOnly: true,
|
|
35
|
+
source: 'vps-service-execute',
|
|
36
|
+
skill_key: opts.skill,
|
|
37
|
+
script_key: opts.script,
|
|
38
|
+
params,
|
|
39
|
+
};
|
|
40
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const result = await executeVpsServiceScript(opts.skill, opts.script, params, opts.baseUrl ?? merged.baseUrl ?? resolveVpsServiceUrl());
|
|
44
|
+
console.log(typeof result === 'string' && opts.json === false ? result : JSON.stringify(result, null, 2));
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { attachSharedShortcutOptions, runSkillShortcutExecute } from '../shortcut-common.js';
|
|
2
|
+
const SKILL = 'vps-hr-business';
|
|
3
|
+
export function attachHrBusinessShortcuts(parent) {
|
|
4
|
+
const entries = [
|
|
5
|
+
['+business-list', '读取原生新人事人员及办理状态', 'list'],
|
|
6
|
+
['+create-employee', '创建企业员工档案', 'create_employee'],
|
|
7
|
+
['+onboarding', '发起入职办理', 'onboarding'],
|
|
8
|
+
['+confirm-onboarding', '确认员工入职', 'confirm_onboarding'],
|
|
9
|
+
['+transfer', '办理调岗、晋升或降级', 'transfer'],
|
|
10
|
+
['+resignation', '创建离职申请草稿(本人或HR代办)', 'resignation'],
|
|
11
|
+
['+resignations', '查看离职申请和核对清单', 'resignations'],
|
|
12
|
+
['+resignation-action', '补资料、核验、提交或撤回离职申请', 'resignation_action'],
|
|
13
|
+
['+resignation-file', '管理离职申请附件', 'resignation_file'],
|
|
14
|
+
['+confirm-regular', '确认员工转正', 'confirm_regular'],
|
|
15
|
+
['+confirm-resignation', '复核并生效已批准的离职申请', 'confirm_resignation'],
|
|
16
|
+
['+business-action', '执行 catalog 公布的新人事动作', 'mutate'],
|
|
17
|
+
];
|
|
18
|
+
for (const [name, description, script] of entries) {
|
|
19
|
+
attachSharedShortcutOptions(parent.command(name).description(`${description};业务参数通过 --params 或 --params-file 传入`))
|
|
20
|
+
.action(async (opts, cmd) => runSkillShortcutExecute(SKILL, script, {}, opts, cmd));
|
|
21
|
+
}
|
|
22
|
+
}
|