vps-work 2.4.1 → 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.
Files changed (125) hide show
  1. package/.well-known/skills/index.json +11 -0
  2. package/README.md +10 -0
  3. package/dist/packages/vps-work/src/bin/vps-work.js +271 -0
  4. package/dist/packages/vps-work/src/commands/agent/index.js +264 -0
  5. package/dist/packages/vps-work/src/commands/approval/index.js +7 -0
  6. package/dist/packages/vps-work/src/commands/approval/shortcuts.js +259 -0
  7. package/dist/packages/vps-work/src/commands/attendance/applications.js +21 -0
  8. package/dist/packages/vps-work/src/commands/attendance/index.js +9 -0
  9. package/dist/packages/vps-work/src/commands/attendance/scheduling.js +183 -0
  10. package/dist/packages/vps-work/src/commands/attendance/shortcuts.js +131 -0
  11. package/dist/packages/vps-work/src/commands/auth/check.js +34 -0
  12. package/dist/packages/vps-work/src/commands/auth/index.js +21 -0
  13. package/dist/packages/vps-work/src/commands/auth/list.js +50 -0
  14. package/dist/packages/vps-work/src/commands/auth/scopes.js +17 -0
  15. package/dist/packages/vps-work/src/commands/auth/status.js +127 -0
  16. package/dist/packages/vps-work/src/commands/auth/tenants.js +35 -0
  17. package/dist/packages/vps-work/src/commands/auth/use.js +40 -0
  18. package/dist/packages/vps-work/src/commands/calendar/index.js +7 -0
  19. package/dist/packages/vps-work/src/commands/calendar/shortcuts.js +598 -0
  20. package/dist/packages/vps-work/src/commands/catalog.js +29 -0
  21. package/dist/packages/vps-work/src/commands/cli-install.js +70 -0
  22. package/dist/packages/vps-work/src/commands/collab-invoke.js +19 -0
  23. package/dist/packages/vps-work/src/commands/corporate/index.js +204 -0
  24. package/dist/packages/vps-work/src/commands/creative/index.js +369 -0
  25. package/dist/packages/vps-work/src/commands/documents/index.js +991 -0
  26. package/dist/packages/vps-work/src/commands/drive/index.js +435 -0
  27. package/dist/packages/vps-work/src/commands/exec.js +46 -0
  28. package/dist/packages/vps-work/src/commands/hr/business.js +22 -0
  29. package/dist/packages/vps-work/src/commands/hr/data-view-shortcuts.js +519 -0
  30. package/dist/packages/vps-work/src/commands/hr/index.js +15 -0
  31. package/dist/packages/vps-work/src/commands/hr/job-responsibilities.js +266 -0
  32. package/dist/packages/vps-work/src/commands/hr/payslip.js +45 -0
  33. package/dist/packages/vps-work/src/commands/hr/shortcuts.js +146 -0
  34. package/dist/packages/vps-work/src/commands/im/im-message-payload.js +150 -0
  35. package/dist/packages/vps-work/src/commands/im/index.js +7 -0
  36. package/dist/packages/vps-work/src/commands/im/shortcuts.js +856 -0
  37. package/dist/packages/vps-work/src/commands/knowledge/index.js +36 -0
  38. package/dist/packages/vps-work/src/commands/login.js +52 -0
  39. package/dist/packages/vps-work/src/commands/logout.js +12 -0
  40. package/dist/packages/vps-work/src/commands/mail/email.js +132 -0
  41. package/dist/packages/vps-work/src/commands/mail/index.js +9 -0
  42. package/dist/packages/vps-work/src/commands/mail/shortcuts.js +43 -0
  43. package/dist/packages/vps-work/src/commands/meeting/index.js +7 -0
  44. package/dist/packages/vps-work/src/commands/meeting/shortcuts.js +120 -0
  45. package/dist/packages/vps-work/src/commands/odoo-invoke.js +21 -0
  46. package/dist/packages/vps-work/src/commands/odoo-leaf-registry.js +13 -0
  47. package/dist/packages/vps-work/src/commands/office/index.js +59 -0
  48. package/dist/packages/vps-work/src/commands/okr/index.js +7 -0
  49. package/dist/packages/vps-work/src/commands/okr/shortcuts.js +75 -0
  50. package/dist/packages/vps-work/src/commands/org-structure.js +347 -0
  51. package/dist/packages/vps-work/src/commands/popup/index.js +147 -0
  52. package/dist/packages/vps-work/src/commands/product/abnormal-statistics.js +250 -0
  53. package/dist/packages/vps-work/src/commands/product/data-view-shortcuts.js +253 -0
  54. package/dist/packages/vps-work/src/commands/product/index.js +11 -0
  55. package/dist/packages/vps-work/src/commands/purchase/index.js +45 -0
  56. package/dist/packages/vps-work/src/commands/reauth.js +127 -0
  57. package/dist/packages/vps-work/src/commands/report/index.js +7 -0
  58. package/dist/packages/vps-work/src/commands/report/missing-daily-reports.js +9 -0
  59. package/dist/packages/vps-work/src/commands/report/shortcuts.js +251 -0
  60. package/dist/packages/vps-work/src/commands/sales/index.js +11 -0
  61. package/dist/packages/vps-work/src/commands/sales/period-params.js +75 -0
  62. package/dist/packages/vps-work/src/commands/sales/shortcuts-personnel.js +124 -0
  63. package/dist/packages/vps-work/src/commands/sales/shortcuts.js +800 -0
  64. package/dist/packages/vps-work/src/commands/shortcut-common.js +88 -0
  65. package/dist/packages/vps-work/src/commands/shortcut-params.js +22 -0
  66. package/dist/packages/vps-work/src/commands/skill/index.js +136 -0
  67. package/dist/packages/vps-work/src/commands/skill/personal.js +167 -0
  68. package/dist/packages/vps-work/src/commands/store/index.js +82 -0
  69. package/dist/packages/vps-work/src/commands/task/index.js +7 -0
  70. package/dist/packages/vps-work/src/commands/task/task-center.js +595 -0
  71. package/dist/packages/vps-work/src/commands/tools/index.js +127 -0
  72. package/dist/packages/vps-work/src/commands/vemory/index.js +7 -0
  73. package/dist/packages/vps-work/src/commands/vemory/shortcuts.js +53 -0
  74. package/dist/packages/vps-work/src/commands/vertu-aisales.js +30 -0
  75. package/dist/packages/vps-work/src/commands/vertu-erp.js +30 -0
  76. package/dist/packages/vps-work/src/core/aisales-mcp-client.js +35 -0
  77. package/dist/packages/vps-work/src/core/auth.js +240 -0
  78. package/dist/packages/vps-work/src/core/cli-bin.js +11 -0
  79. package/dist/packages/vps-work/src/core/cli-root-help.js +190 -0
  80. package/dist/packages/vps-work/src/core/client.js +158 -0
  81. package/dist/packages/vps-work/src/core/config.js +183 -0
  82. package/dist/packages/vps-work/src/core/corporate-client.js +52 -0
  83. package/dist/packages/vps-work/src/core/creative-attachment-download.js +48 -0
  84. package/dist/packages/vps-work/src/core/documents-client.js +57 -0
  85. package/dist/packages/vps-work/src/core/endpoints.js +46 -0
  86. package/dist/packages/vps-work/src/core/erp-mcp-client.js +11 -0
  87. package/dist/packages/vps-work/src/core/errors.js +107 -0
  88. package/dist/packages/vps-work/src/core/execute-catalog-client.js +42 -0
  89. package/dist/packages/vps-work/src/core/hermes-client.js +70 -0
  90. package/dist/packages/vps-work/src/core/im-attachment-download.js +109 -0
  91. package/dist/packages/vps-work/src/core/im-client.js +64 -0
  92. package/dist/packages/vps-work/src/core/keychain.js +58 -0
  93. package/dist/packages/vps-work/src/core/mail-client.js +82 -0
  94. package/dist/packages/vps-work/src/core/mcp-client.js +129 -0
  95. package/dist/packages/vps-work/src/core/office-client.js +25 -0
  96. package/dist/packages/vps-work/src/core/office-files.js +82 -0
  97. package/dist/packages/vps-work/src/core/paths.js +70 -0
  98. package/dist/packages/vps-work/src/core/public-result.js +43 -0
  99. package/dist/packages/vps-work/src/core/sales-invoice-attachments-client.js +75 -0
  100. package/dist/packages/vps-work/src/core/sales-query-client.js +452 -0
  101. package/dist/packages/vps-work/src/core/skill-catalog.js +158 -0
  102. package/dist/packages/vps-work/src/core/skill-key.js +19 -0
  103. package/dist/packages/vps-work/src/core/v-drive-store.js +392 -0
  104. package/dist/packages/vps-work/src/core/vertu-agent-env.js +32 -0
  105. package/dist/packages/vps-work/src/core/vps-auth-scopes.js +73 -0
  106. package/dist/packages/vps-work/src/core/vps-service-auth.js +382 -0
  107. package/dist/packages/vps-work/src/core/vps-service-config.js +83 -0
  108. package/dist/packages/vps-work/src/core/vps-service-http.js +187 -0
  109. package/dist/packages/vps-work/src/utils/at-file.js +34 -0
  110. package/dist/packages/vps-work/src/utils/cli-option-helpers.js +52 -0
  111. package/dist/packages/vps-work/src/utils/frontmatter.js +26 -0
  112. package/dist/packages/vps-work/src/utils/normalize-argv.js +51 -0
  113. package/dist/packages/vps-work/src/utils/parse-json-arg.js +85 -0
  114. package/dist/packages/vps-work/src/utils/prompt.js +46 -0
  115. package/dist/packages/vps-work/src/utils/read-text.js +44 -0
  116. package/dist/packages/vps-work/src/utils/schema-to-options.js +307 -0
  117. package/dist/packages/vps-work/src/utils/unwrap-envelope.js +31 -0
  118. package/dist/shared/im-org-tree/index.js +200 -0
  119. package/dist/vps-work.cjs +246 -44
  120. package/package.json +1 -1
  121. package/scripts/package-desktop-runtime.mjs +43 -0
  122. package/skills/README.md +1 -0
  123. package/skills/vertu-aisales/SKILL.md +18 -0
  124. package/skills/vertu-erp/SKILL.md +17 -0
  125. package/skills/vps-dealer-ops/SKILL.md +92 -0
@@ -0,0 +1,392 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync, } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { basename, dirname, extname, join, resolve } from 'node:path';
5
+ import { VertuError } from './errors.js';
6
+ const EMPTY_DATABASE = { items: [], spaces: [], transfers: [] };
7
+ function cleanName(value, fallback) {
8
+ const name = String(value ?? '').trim().replaceAll('/', '-').replaceAll('\\', '-');
9
+ return name.slice(0, 160) || fallback;
10
+ }
11
+ function mimeFromName(name) {
12
+ const extension = extname(name).toLowerCase();
13
+ const types = {
14
+ '.csv': 'text/csv',
15
+ '.doc': 'application/msword',
16
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
17
+ '.gif': 'image/gif',
18
+ '.jpeg': 'image/jpeg',
19
+ '.jpg': 'image/jpeg',
20
+ '.json': 'application/json',
21
+ '.md': 'text/markdown',
22
+ '.mkv': 'video/x-matroska',
23
+ '.mov': 'video/quicktime',
24
+ '.mp4': 'video/mp4',
25
+ '.pdf': 'application/pdf',
26
+ '.png': 'image/png',
27
+ '.ppt': 'application/vnd.ms-powerpoint',
28
+ '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
29
+ '.txt': 'text/plain',
30
+ '.webp': 'image/webp',
31
+ '.webm': 'video/webm',
32
+ '.avi': 'video/x-msvideo',
33
+ '.xls': 'application/vnd.ms-excel',
34
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
35
+ '.zip': 'application/zip',
36
+ '.rar': 'application/vnd.rar',
37
+ '.7z': 'application/x-7z-compressed',
38
+ '.tar': 'application/x-tar',
39
+ '.gz': 'application/gzip',
40
+ '.bz2': 'application/x-bzip2',
41
+ '.xz': 'application/x-xz',
42
+ };
43
+ return types[extension] ?? 'application/octet-stream';
44
+ }
45
+ export function defaultVDriveRoot() {
46
+ const override = process.env.VERTU_V_DRIVE_ROOT?.trim();
47
+ if (override)
48
+ return resolve(override);
49
+ if (process.platform === 'darwin') {
50
+ return join(homedir(), 'Library', 'Application Support', 'vertu-im-desktop', 'v-drive');
51
+ }
52
+ if (process.platform === 'win32') {
53
+ const appData = process.env.APPDATA?.trim() || join(homedir(), 'AppData', 'Roaming');
54
+ return join(appData, 'vertu-im-desktop', 'v-drive');
55
+ }
56
+ const configRoot = process.env.XDG_CONFIG_HOME?.trim() || join(homedir(), '.config');
57
+ return join(configRoot, 'vertu-im-desktop', 'v-drive');
58
+ }
59
+ export class VDriveStore {
60
+ root;
61
+ filesDir;
62
+ databasePath;
63
+ constructor(root = defaultVDriveRoot()) {
64
+ this.root = resolve(root);
65
+ this.filesDir = join(this.root, 'files');
66
+ this.databasePath = join(this.root, 'index.json');
67
+ }
68
+ list(parentId, recursive, query) {
69
+ const database = this.read();
70
+ this.assertFolder(database, parentId);
71
+ const normalizedQuery = query?.trim().toLocaleLowerCase();
72
+ const descendantIds = recursive && parentId ? this.descendantIds(database, parentId) : null;
73
+ return database.items
74
+ .filter((item) => {
75
+ if (item.deletedAt)
76
+ return false;
77
+ const inScope = recursive
78
+ ? parentId === null || descendantIds?.has(item.id)
79
+ : item.parentId === parentId;
80
+ return inScope && (!normalizedQuery || item.name.toLocaleLowerCase().includes(normalizedQuery));
81
+ })
82
+ .map((item) => this.publicItem(item));
83
+ }
84
+ info(itemId) {
85
+ return this.publicItem(this.requireItem(this.read(), itemId));
86
+ }
87
+ usedBytes() {
88
+ return this.read().items.reduce((total, item) => total + (item.kind === 'file' ? item.sizeBytes : 0), 0);
89
+ }
90
+ trash() {
91
+ return this.read().items
92
+ .filter((item) => item.deletedAt && item.trashRootId === item.id)
93
+ .map((item) => ({
94
+ ...this.publicItem(item),
95
+ deletedAt: item.deletedAt,
96
+ }));
97
+ }
98
+ transfers() {
99
+ return this.read().transfers;
100
+ }
101
+ createFolder(parentId, name, actor) {
102
+ const database = this.read();
103
+ this.assertFolder(database, parentId);
104
+ const now = new Date().toISOString();
105
+ const item = {
106
+ id: randomUUID(),
107
+ name: cleanName(name, '新建文件夹'),
108
+ kind: 'folder',
109
+ parentId,
110
+ mimeType: null,
111
+ sizeBytes: 0,
112
+ createdAt: now,
113
+ updatedAt: now,
114
+ updatedBy: actor,
115
+ sharedSpaceIds: [],
116
+ storedName: null,
117
+ };
118
+ database.items.push(item);
119
+ this.write(database);
120
+ return this.publicItem(item);
121
+ }
122
+ createTextFile(parentId, name, content, actor) {
123
+ const database = this.read();
124
+ this.assertFolder(database, parentId);
125
+ const id = randomUUID();
126
+ const now = new Date().toISOString();
127
+ const fileName = cleanName(name, '新建文本.txt');
128
+ const storedName = `${id}${extname(fileName) || '.txt'}`;
129
+ this.ensureRoot();
130
+ writeFileSync(join(this.filesDir, storedName), content, 'utf8');
131
+ const item = {
132
+ id,
133
+ name: fileName,
134
+ kind: 'file',
135
+ parentId,
136
+ mimeType: mimeFromName(fileName),
137
+ sizeBytes: Buffer.byteLength(content),
138
+ createdAt: now,
139
+ updatedAt: now,
140
+ updatedBy: actor,
141
+ sharedSpaceIds: [],
142
+ storedName,
143
+ };
144
+ database.items.push(item);
145
+ this.write(database);
146
+ return this.publicItem(item);
147
+ }
148
+ importFiles(parentId, paths, actor) {
149
+ const database = this.read();
150
+ this.assertFolder(database, parentId);
151
+ const sourcePaths = paths.map((rawPath) => {
152
+ const sourcePath = resolve(rawPath);
153
+ if (!existsSync(sourcePath) || !statSync(sourcePath).isFile()) {
154
+ throw new VertuError(`上传源文件不存在或不是普通文件:${rawPath}`);
155
+ }
156
+ return sourcePath;
157
+ });
158
+ const imported = [];
159
+ for (const sourcePath of sourcePaths) {
160
+ const id = randomUUID();
161
+ const now = new Date().toISOString();
162
+ const name = cleanName(basename(sourcePath), '未命名文件');
163
+ const storedName = `${id}${extname(name)}`;
164
+ this.ensureRoot();
165
+ copyFileSync(sourcePath, join(this.filesDir, storedName));
166
+ const sizeBytes = statSync(sourcePath).size;
167
+ const item = {
168
+ id,
169
+ name,
170
+ kind: 'file',
171
+ parentId,
172
+ mimeType: mimeFromName(name),
173
+ sizeBytes,
174
+ createdAt: now,
175
+ updatedAt: now,
176
+ updatedBy: actor,
177
+ sharedSpaceIds: [],
178
+ storedName,
179
+ };
180
+ database.items.push(item);
181
+ imported.push(item);
182
+ database.transfers.unshift({
183
+ id: randomUUID(),
184
+ direction: 'upload',
185
+ name,
186
+ sizeBytes,
187
+ completedAt: now,
188
+ itemId: id,
189
+ });
190
+ }
191
+ database.transfers = database.transfers.slice(0, 100);
192
+ this.write(database);
193
+ return imported.map((item) => this.publicItem(item));
194
+ }
195
+ readFile(itemId) {
196
+ const database = this.read();
197
+ const item = this.requireFile(database, itemId);
198
+ return { item: this.publicItem(item), bytes: readFileSync(this.filePathFor(item)) };
199
+ }
200
+ writeText(itemId, content, actor) {
201
+ const database = this.read();
202
+ const item = this.requireFile(database, itemId);
203
+ writeFileSync(this.filePathFor(item), content, 'utf8');
204
+ item.sizeBytes = Buffer.byteLength(content);
205
+ item.updatedAt = new Date().toISOString();
206
+ item.updatedBy = actor;
207
+ this.write(database);
208
+ return this.publicItem(item);
209
+ }
210
+ download(itemId, outputPath, overwrite) {
211
+ const database = this.read();
212
+ const item = this.requireFile(database, itemId);
213
+ const requested = resolve(outputPath);
214
+ const target = existsSync(requested) && statSync(requested).isDirectory()
215
+ ? join(requested, item.name)
216
+ : requested;
217
+ if (existsSync(target) && !overwrite) {
218
+ throw new VertuError(`目标文件已存在:${target};如需覆盖请显式传 --overwrite。`);
219
+ }
220
+ mkdirSync(dirname(target), { recursive: true });
221
+ copyFileSync(this.filePathFor(item), target);
222
+ database.transfers.unshift({
223
+ id: randomUUID(),
224
+ direction: 'download',
225
+ name: item.name,
226
+ sizeBytes: item.sizeBytes,
227
+ completedAt: new Date().toISOString(),
228
+ filePath: target,
229
+ });
230
+ database.transfers = database.transfers.slice(0, 100);
231
+ this.write(database);
232
+ return { item: this.publicItem(item), filePath: target };
233
+ }
234
+ rename(itemId, name, actor) {
235
+ const database = this.read();
236
+ const item = this.requireItem(database, itemId);
237
+ item.name = cleanName(name, item.name);
238
+ item.updatedAt = new Date().toISOString();
239
+ item.updatedBy = actor;
240
+ this.write(database);
241
+ return this.publicItem(item);
242
+ }
243
+ move(itemId, parentId, actor) {
244
+ const database = this.read();
245
+ const item = this.requireItem(database, itemId);
246
+ this.assertFolder(database, parentId);
247
+ if (item.id === parentId || this.descendantIds(database, item.id).has(parentId ?? '')) {
248
+ throw new VertuError('不能将文件夹移动到自身或其子文件夹中。');
249
+ }
250
+ item.parentId = parentId;
251
+ item.updatedAt = new Date().toISOString();
252
+ item.updatedBy = actor;
253
+ this.write(database);
254
+ return this.publicItem(item);
255
+ }
256
+ remove(itemId) {
257
+ const database = this.read();
258
+ const root = this.requireItem(database, itemId);
259
+ const removedIds = this.descendantIds(database, itemId);
260
+ removedIds.add(itemId);
261
+ const deletedAt = new Date().toISOString();
262
+ for (const item of database.items) {
263
+ if (!removedIds.has(item.id))
264
+ continue;
265
+ item.deletedAt = deletedAt;
266
+ item.trashRootId = itemId;
267
+ }
268
+ this.write(database);
269
+ return { ...this.publicItem(root), deletedAt };
270
+ }
271
+ restore(itemId) {
272
+ const database = this.read();
273
+ const root = database.items.find((item) => item.id === itemId && item.deletedAt && item.trashRootId === item.id);
274
+ if (!root)
275
+ throw new VertuError('回收站项目不存在。');
276
+ const activeParent = root.parentId
277
+ ? database.items.some((item) => item.id === root.parentId && !item.deletedAt)
278
+ : true;
279
+ if (!activeParent)
280
+ root.parentId = null;
281
+ for (const item of database.items) {
282
+ if (item.trashRootId !== itemId)
283
+ continue;
284
+ item.deletedAt = null;
285
+ item.trashRootId = null;
286
+ }
287
+ this.write(database);
288
+ return this.publicItem(root);
289
+ }
290
+ purge(itemId) {
291
+ const database = this.read();
292
+ const root = database.items.find((item) => item.id === itemId && item.deletedAt && item.trashRootId === item.id);
293
+ if (!root)
294
+ throw new VertuError('回收站项目不存在。');
295
+ const removed = {
296
+ ...this.publicItem(root),
297
+ deletedAt: root.deletedAt,
298
+ };
299
+ for (const item of database.items) {
300
+ if (item.trashRootId === itemId && item.storedName) {
301
+ rmSync(join(this.filesDir, item.storedName), { force: true });
302
+ }
303
+ }
304
+ database.items = database.items.filter((item) => item.trashRootId !== itemId);
305
+ this.write(database);
306
+ return removed;
307
+ }
308
+ emptyTrash() {
309
+ const database = this.read();
310
+ const purgedItems = database.items.filter((item) => item.deletedAt).length;
311
+ for (const item of database.items) {
312
+ if (item.deletedAt && item.storedName) {
313
+ rmSync(join(this.filesDir, item.storedName), { force: true });
314
+ }
315
+ }
316
+ database.items = database.items.filter((item) => !item.deletedAt);
317
+ this.write(database);
318
+ return { purgedItems };
319
+ }
320
+ ensureRoot() {
321
+ mkdirSync(this.filesDir, { recursive: true });
322
+ }
323
+ read() {
324
+ this.ensureRoot();
325
+ if (!existsSync(this.databasePath)) {
326
+ return { ...EMPTY_DATABASE, items: [], spaces: [], transfers: [] };
327
+ }
328
+ try {
329
+ const parsed = JSON.parse(readFileSync(this.databasePath, 'utf8'));
330
+ return {
331
+ items: Array.isArray(parsed.items) ? parsed.items : [],
332
+ spaces: Array.isArray(parsed.spaces) ? parsed.spaces : [],
333
+ transfers: Array.isArray(parsed.transfers) ? parsed.transfers : [],
334
+ };
335
+ }
336
+ catch {
337
+ throw new VertuError(`V盘索引无法解析:${this.databasePath}`);
338
+ }
339
+ }
340
+ write(database) {
341
+ this.ensureRoot();
342
+ writeFileSync(this.databasePath, JSON.stringify(database, null, 2), 'utf8');
343
+ }
344
+ publicItem(item) {
345
+ const { storedName: _storedName, deletedAt: _deletedAt, trashRootId: _trashRootId, ...publicItem } = item;
346
+ return publicItem;
347
+ }
348
+ requireItem(database, itemId) {
349
+ const item = database.items.find((candidate) => candidate.id === itemId && !candidate.deletedAt);
350
+ if (!item)
351
+ throw new VertuError('文件或文件夹不存在。');
352
+ return item;
353
+ }
354
+ requireFile(database, itemId) {
355
+ const item = this.requireItem(database, itemId);
356
+ if (item.kind !== 'file' || !item.storedName) {
357
+ throw new VertuError('所选项目不是文件。');
358
+ }
359
+ return item;
360
+ }
361
+ assertFolder(database, parentId) {
362
+ if (parentId === null)
363
+ return;
364
+ const parent = this.requireItem(database, parentId);
365
+ if (parent.kind !== 'folder')
366
+ throw new VertuError('目标位置不是文件夹。');
367
+ }
368
+ filePathFor(item) {
369
+ if (!item.storedName)
370
+ throw new VertuError('文件内容不存在。');
371
+ const filePath = join(this.filesDir, item.storedName);
372
+ if (!existsSync(filePath))
373
+ throw new VertuError('文件内容不存在。');
374
+ return filePath;
375
+ }
376
+ descendantIds(database, itemId) {
377
+ const result = new Set();
378
+ let changed = true;
379
+ while (changed) {
380
+ changed = false;
381
+ for (const item of database.items) {
382
+ if (!item.deletedAt
383
+ && (item.parentId === itemId || (item.parentId && result.has(item.parentId)))
384
+ && !result.has(item.id)) {
385
+ result.add(item.id);
386
+ changed = true;
387
+ }
388
+ }
389
+ }
390
+ return result;
391
+ }
392
+ }
@@ -0,0 +1,32 @@
1
+ /** Agent 宿主注入的环境变量(人类 login 后由 CLI 签发 app_key,写入宿主配置)。 */
2
+ export const VERTU_APP_ID_ENV = 'VERTU_APP_ID';
3
+ export const VERTU_APP_KEY_ENV = 'VERTU_APP_KEY';
4
+ export const VERTU_USER_LOGIN_ENV = 'VERTU_USER_LOGIN';
5
+ export const VERTU_VPS_SERVICE_URL_ENV = 'VERTU_VPS_SERVICE_URL';
6
+ export const VERTU_ACCESS_TOKEN_ENV = 'VERTU_ACCESS_TOKEN';
7
+ export const VERTU_EXPECTED_USER_ID_ENV = 'VERTU_EXPECTED_USER_ID';
8
+ export const DEFAULT_AGENT_APP_ID = 'cursor';
9
+ /** 服务端 vps_auth_trusted_apps 中登记的智能体宿主(非 vertu-cli 本身)。 */
10
+ export const TRUSTED_AGENT_APP_IDS = ['cursor', 'openclaw', 'hermes'];
11
+ export function readVertuAgentEnvFromProcess() {
12
+ const trim = (key) => {
13
+ const v = process.env[key]?.trim();
14
+ return v || undefined;
15
+ };
16
+ return {
17
+ serviceUrl: trim(VERTU_VPS_SERVICE_URL_ENV),
18
+ appId: trim(VERTU_APP_ID_ENV),
19
+ appKey: trim(VERTU_APP_KEY_ENV),
20
+ login: trim(VERTU_USER_LOGIN_ENV),
21
+ };
22
+ }
23
+ export function formatAgentEnvExports(serviceUrl, appId, appKey, login) {
24
+ const lines = [
25
+ `# Vertu Agent — 注入到 Cursor / OpenClaw 等宿主环境`,
26
+ `export ${VERTU_VPS_SERVICE_URL_ENV}='${serviceUrl.replace(/'/g, "'\\''")}'`,
27
+ `export ${VERTU_APP_ID_ENV}='${appId.replace(/'/g, "'\\''")}'`,
28
+ `export ${VERTU_APP_KEY_ENV}='${appKey.replace(/'/g, "'\\''")}'`,
29
+ `export ${VERTU_USER_LOGIN_ENV}='${login.replace(/'/g, "'\\''")}'`,
30
+ ];
31
+ return `${lines.join('\n')}\n`;
32
+ }
@@ -0,0 +1,73 @@
1
+ import { request } from 'undici';
2
+ import { VertuError } from './errors.js';
3
+ import { resolveVpsServiceUrl } from './sales-query-client.js';
4
+ import { vpsServiceAuthHeaders } from './vps-service-http.js';
5
+ export async function fetchVpsUserScopes(serviceBaseUrl, scopeCheck) {
6
+ const base = resolveVpsServiceUrl(serviceBaseUrl);
7
+ const headers = vpsServiceAuthHeaders(base);
8
+ if (!headers.authorization) {
9
+ throw new VertuError('未认证。Agent/Bot/Hermes 请检查宿主注入的 app_key;仅人工用户可交互执行 `vps-work auth login`。');
10
+ }
11
+ const query = scopeCheck?.trim() ? `?scope=${encodeURIComponent(scopeCheck.trim())}` : '';
12
+ const res = await request(`${base}/v1/auth/scopes${query}`, {
13
+ method: 'GET',
14
+ headers: {
15
+ accept: 'application/json',
16
+ ...headers,
17
+ },
18
+ });
19
+ const text = await res.body.text();
20
+ let data;
21
+ try {
22
+ data = JSON.parse(text);
23
+ }
24
+ catch {
25
+ throw new VertuError(`查询 scope 失败:非 JSON(HTTP ${res.statusCode})`);
26
+ }
27
+ if (res.statusCode >= 400) {
28
+ const errMsg = data && typeof data === 'object' && 'error' in data
29
+ ? String(data.error)
30
+ : `HTTP ${res.statusCode}`;
31
+ throw new VertuError(`查询 scope 失败:${errMsg}`);
32
+ }
33
+ if (!data || typeof data !== 'object') {
34
+ throw new VertuError('查询 scope 失败:响应无效');
35
+ }
36
+ return data;
37
+ }
38
+ export function userHasVpsScope(scopes, required) {
39
+ const scope = required.trim();
40
+ if (!scope) {
41
+ return false;
42
+ }
43
+ if (scope === 'hr.payslip.dept:read') {
44
+ return scopes.includes(scope) || scopes.includes('hr.payslip.dept:*');
45
+ }
46
+ if (scope === 'sales.performance.all:read') {
47
+ return scopes.includes(scope) || scopes.includes('sales.performance.all:*');
48
+ }
49
+ if (scope === 'sales.pii:read') {
50
+ return scopes.includes(scope) || scopes.includes('sales.pii:*');
51
+ }
52
+ if (scope === 'sales.customer_region.all:read') {
53
+ return scopes.includes(scope) || scopes.includes('sales.customer_region.all:*');
54
+ }
55
+ if (scope === 'product.device_abnormal:read') {
56
+ return scopes.includes(scope) || scopes.includes('product.device_abnormal:*');
57
+ }
58
+ if (scope.startsWith('hr.profile.') || scope.startsWith('corporate.')) {
59
+ const resource = scope.split(':')[0];
60
+ return scopes.includes(scope) || scopes.includes(`${resource}:*`);
61
+ }
62
+ if (scopes.includes(scope)) {
63
+ return true;
64
+ }
65
+ const [resource] = scope.split(':');
66
+ if (resource && scopes.includes(`${resource}:*`)) {
67
+ return true;
68
+ }
69
+ if (scopes.includes('admin:*')) {
70
+ return true;
71
+ }
72
+ return false;
73
+ }