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.
Files changed (119) hide show
  1. package/dist/packages/vps-work/src/bin/vps-work.js +271 -0
  2. package/dist/packages/vps-work/src/commands/agent/index.js +264 -0
  3. package/dist/packages/vps-work/src/commands/approval/index.js +7 -0
  4. package/dist/packages/vps-work/src/commands/approval/shortcuts.js +259 -0
  5. package/dist/packages/vps-work/src/commands/attendance/applications.js +21 -0
  6. package/dist/packages/vps-work/src/commands/attendance/index.js +9 -0
  7. package/dist/packages/vps-work/src/commands/attendance/scheduling.js +183 -0
  8. package/dist/packages/vps-work/src/commands/attendance/shortcuts.js +131 -0
  9. package/dist/packages/vps-work/src/commands/auth/check.js +34 -0
  10. package/dist/packages/vps-work/src/commands/auth/index.js +21 -0
  11. package/dist/packages/vps-work/src/commands/auth/list.js +50 -0
  12. package/dist/packages/vps-work/src/commands/auth/scopes.js +17 -0
  13. package/dist/packages/vps-work/src/commands/auth/status.js +127 -0
  14. package/dist/packages/vps-work/src/commands/auth/tenants.js +35 -0
  15. package/dist/packages/vps-work/src/commands/auth/use.js +40 -0
  16. package/dist/packages/vps-work/src/commands/calendar/index.js +7 -0
  17. package/dist/packages/vps-work/src/commands/calendar/shortcuts.js +598 -0
  18. package/dist/packages/vps-work/src/commands/catalog.js +29 -0
  19. package/dist/packages/vps-work/src/commands/cli-install.js +70 -0
  20. package/dist/packages/vps-work/src/commands/collab-invoke.js +19 -0
  21. package/dist/packages/vps-work/src/commands/corporate/index.js +204 -0
  22. package/dist/packages/vps-work/src/commands/creative/index.js +369 -0
  23. package/dist/packages/vps-work/src/commands/documents/index.js +991 -0
  24. package/dist/packages/vps-work/src/commands/drive/index.js +435 -0
  25. package/dist/packages/vps-work/src/commands/exec.js +46 -0
  26. package/dist/packages/vps-work/src/commands/hr/business.js +22 -0
  27. package/dist/packages/vps-work/src/commands/hr/data-view-shortcuts.js +519 -0
  28. package/dist/packages/vps-work/src/commands/hr/index.js +15 -0
  29. package/dist/packages/vps-work/src/commands/hr/job-responsibilities.js +266 -0
  30. package/dist/packages/vps-work/src/commands/hr/payslip.js +45 -0
  31. package/dist/packages/vps-work/src/commands/hr/shortcuts.js +146 -0
  32. package/dist/packages/vps-work/src/commands/im/im-message-payload.js +150 -0
  33. package/dist/packages/vps-work/src/commands/im/index.js +7 -0
  34. package/dist/packages/vps-work/src/commands/im/shortcuts.js +856 -0
  35. package/dist/packages/vps-work/src/commands/knowledge/index.js +36 -0
  36. package/dist/packages/vps-work/src/commands/login.js +52 -0
  37. package/dist/packages/vps-work/src/commands/logout.js +12 -0
  38. package/dist/packages/vps-work/src/commands/mail/email.js +132 -0
  39. package/dist/packages/vps-work/src/commands/mail/index.js +9 -0
  40. package/dist/packages/vps-work/src/commands/mail/shortcuts.js +43 -0
  41. package/dist/packages/vps-work/src/commands/meeting/index.js +7 -0
  42. package/dist/packages/vps-work/src/commands/meeting/shortcuts.js +120 -0
  43. package/dist/packages/vps-work/src/commands/odoo-invoke.js +21 -0
  44. package/dist/packages/vps-work/src/commands/odoo-leaf-registry.js +13 -0
  45. package/dist/packages/vps-work/src/commands/office/index.js +59 -0
  46. package/dist/packages/vps-work/src/commands/okr/index.js +7 -0
  47. package/dist/packages/vps-work/src/commands/okr/shortcuts.js +75 -0
  48. package/dist/packages/vps-work/src/commands/org-structure.js +347 -0
  49. package/dist/packages/vps-work/src/commands/popup/index.js +147 -0
  50. package/dist/packages/vps-work/src/commands/product/abnormal-statistics.js +250 -0
  51. package/dist/packages/vps-work/src/commands/product/data-view-shortcuts.js +253 -0
  52. package/dist/packages/vps-work/src/commands/product/index.js +11 -0
  53. package/dist/packages/vps-work/src/commands/purchase/index.js +45 -0
  54. package/dist/packages/vps-work/src/commands/reauth.js +127 -0
  55. package/dist/packages/vps-work/src/commands/report/index.js +7 -0
  56. package/dist/packages/vps-work/src/commands/report/missing-daily-reports.js +9 -0
  57. package/dist/packages/vps-work/src/commands/report/shortcuts.js +251 -0
  58. package/dist/packages/vps-work/src/commands/sales/index.js +11 -0
  59. package/dist/packages/vps-work/src/commands/sales/period-params.js +75 -0
  60. package/dist/packages/vps-work/src/commands/sales/shortcuts-personnel.js +124 -0
  61. package/dist/packages/vps-work/src/commands/sales/shortcuts.js +800 -0
  62. package/dist/packages/vps-work/src/commands/shortcut-common.js +88 -0
  63. package/dist/packages/vps-work/src/commands/shortcut-params.js +22 -0
  64. package/dist/packages/vps-work/src/commands/skill/index.js +136 -0
  65. package/dist/packages/vps-work/src/commands/skill/personal.js +167 -0
  66. package/dist/packages/vps-work/src/commands/store/index.js +82 -0
  67. package/dist/packages/vps-work/src/commands/task/index.js +7 -0
  68. package/dist/packages/vps-work/src/commands/task/task-center.js +595 -0
  69. package/dist/packages/vps-work/src/commands/tools/index.js +127 -0
  70. package/dist/packages/vps-work/src/commands/vemory/index.js +7 -0
  71. package/dist/packages/vps-work/src/commands/vemory/shortcuts.js +53 -0
  72. package/dist/packages/vps-work/src/commands/vertu-aisales.js +30 -0
  73. package/dist/packages/vps-work/src/commands/vertu-erp.js +30 -0
  74. package/dist/packages/vps-work/src/core/aisales-mcp-client.js +35 -0
  75. package/dist/packages/vps-work/src/core/auth.js +240 -0
  76. package/dist/packages/vps-work/src/core/cli-bin.js +11 -0
  77. package/dist/packages/vps-work/src/core/cli-root-help.js +190 -0
  78. package/dist/packages/vps-work/src/core/client.js +158 -0
  79. package/dist/packages/vps-work/src/core/config.js +183 -0
  80. package/dist/packages/vps-work/src/core/corporate-client.js +52 -0
  81. package/dist/packages/vps-work/src/core/creative-attachment-download.js +48 -0
  82. package/dist/packages/vps-work/src/core/documents-client.js +57 -0
  83. package/dist/packages/vps-work/src/core/endpoints.js +46 -0
  84. package/dist/packages/vps-work/src/core/erp-mcp-client.js +11 -0
  85. package/dist/packages/vps-work/src/core/errors.js +107 -0
  86. package/dist/packages/vps-work/src/core/execute-catalog-client.js +42 -0
  87. package/dist/packages/vps-work/src/core/hermes-client.js +70 -0
  88. package/dist/packages/vps-work/src/core/im-attachment-download.js +109 -0
  89. package/dist/packages/vps-work/src/core/im-client.js +64 -0
  90. package/dist/packages/vps-work/src/core/keychain.js +58 -0
  91. package/dist/packages/vps-work/src/core/mail-client.js +82 -0
  92. package/dist/packages/vps-work/src/core/mcp-client.js +129 -0
  93. package/dist/packages/vps-work/src/core/office-client.js +25 -0
  94. package/dist/packages/vps-work/src/core/office-files.js +82 -0
  95. package/dist/packages/vps-work/src/core/paths.js +70 -0
  96. package/dist/packages/vps-work/src/core/public-result.js +43 -0
  97. package/dist/packages/vps-work/src/core/sales-invoice-attachments-client.js +75 -0
  98. package/dist/packages/vps-work/src/core/sales-query-client.js +452 -0
  99. package/dist/packages/vps-work/src/core/skill-catalog.js +158 -0
  100. package/dist/packages/vps-work/src/core/skill-key.js +19 -0
  101. package/dist/packages/vps-work/src/core/v-drive-store.js +392 -0
  102. package/dist/packages/vps-work/src/core/vertu-agent-env.js +32 -0
  103. package/dist/packages/vps-work/src/core/vps-auth-scopes.js +73 -0
  104. package/dist/packages/vps-work/src/core/vps-service-auth.js +382 -0
  105. package/dist/packages/vps-work/src/core/vps-service-config.js +83 -0
  106. package/dist/packages/vps-work/src/core/vps-service-http.js +187 -0
  107. package/dist/packages/vps-work/src/utils/at-file.js +34 -0
  108. package/dist/packages/vps-work/src/utils/cli-option-helpers.js +52 -0
  109. package/dist/packages/vps-work/src/utils/frontmatter.js +26 -0
  110. package/dist/packages/vps-work/src/utils/normalize-argv.js +51 -0
  111. package/dist/packages/vps-work/src/utils/parse-json-arg.js +85 -0
  112. package/dist/packages/vps-work/src/utils/prompt.js +46 -0
  113. package/dist/packages/vps-work/src/utils/read-text.js +44 -0
  114. package/dist/packages/vps-work/src/utils/schema-to-options.js +307 -0
  115. package/dist/packages/vps-work/src/utils/unwrap-envelope.js +31 -0
  116. package/dist/shared/im-org-tree/index.js +200 -0
  117. package/dist/vps-work.cjs +3 -3
  118. package/package.json +1 -1
  119. package/skills/vertu-aisales/SKILL.md +4 -2
@@ -0,0 +1,127 @@
1
+ import { readFile, stat } from 'node:fs/promises';
2
+ import { basename, extname, resolve } from 'node:path';
3
+ import { Command } from 'commander';
4
+ import { FormData } from 'undici';
5
+ import { VertuError } from '../../core/errors.js';
6
+ import { vpsServiceGet, vpsServiceMultipartPost } from '../../core/sales-query-client.js';
7
+ import { attachSharedShortcutOptions, parseOptionalInt, resolveShortcutVpsServiceUrl, } from '../shortcut-common.js';
8
+ const MAX_OSS_UPLOAD_BYTES = 50 * 1024 * 1024;
9
+ /** 平台公共工具域;后续非业务域能力统一在 `tools +…` 下扩展。 */
10
+ export function buildToolsCommand() {
11
+ const command = new Command('tools')
12
+ .description('平台公共工具:OSS 上传、地址签名等跨业务能力。');
13
+ attachSharedShortcutOptions(command
14
+ .command('+oss-upload')
15
+ .description('上传文件到 OSS,返回公开地址或限时签名地址')
16
+ .requiredOption('--file <path>', '本地文件路径')
17
+ .option('--visibility <private|public>', '访问方式;默认 private', 'private')
18
+ .option('--object-name <path>', '身份隔离目录内的相对对象名')
19
+ .option('--content-type <mime>', '显式 MIME;默认按扩展名推断')
20
+ .option('--signed-url-expires <seconds>', '签名 URL 有效秒数(60~604800)', '3600')).action(async (opts, parent) => {
21
+ const filePath = resolve(opts.file?.trim() ?? '');
22
+ const fileStat = await stat(filePath).catch(() => null);
23
+ if (!fileStat?.isFile())
24
+ throw new VertuError(`文件不存在或不是普通文件:${filePath}`);
25
+ if (fileStat.size <= 0)
26
+ throw new VertuError('文件内容为空。');
27
+ if (fileStat.size > MAX_OSS_UPLOAD_BYTES)
28
+ throw new VertuError('文件超过 50 MiB 上限。');
29
+ const visibility = normalizeVisibility(opts.visibility);
30
+ const expires = parseOptionalInt(opts.signedUrlExpires, '--signed-url-expires') ?? 3600;
31
+ if (expires < 60 || expires > 7 * 24 * 60 * 60) {
32
+ throw new VertuError('--signed-url-expires 必须在 60 到 604800 秒之间。');
33
+ }
34
+ const baseUrl = resolveShortcutVpsServiceUrl(opts, parent);
35
+ const contentType = opts.contentType?.trim() || mimeType(filePath);
36
+ if (opts.validateOnly === true || opts.dryRun === true) {
37
+ print(opts, {
38
+ content_type: contentType,
39
+ endpoint: `${baseUrl}/v1/oss/upload`,
40
+ file: filePath,
41
+ object_name: opts.objectName?.trim() || null,
42
+ signed_url_expires: expires,
43
+ size: fileStat.size,
44
+ validateOnly: true,
45
+ visibility,
46
+ });
47
+ return;
48
+ }
49
+ const form = new FormData();
50
+ form.append('file', new Blob([await readFile(filePath)], { type: contentType }), basename(filePath));
51
+ form.append('visibility', visibility);
52
+ form.append('signed_url_expires', String(expires));
53
+ if (opts.objectName?.trim())
54
+ form.append('object_name', opts.objectName.trim());
55
+ const response = await vpsServiceMultipartPost('/v1/oss/upload', form, baseUrl, 'OSS 文件上传');
56
+ const upload = response && typeof response === 'object' && 'upload' in response
57
+ ? response.upload
58
+ : response;
59
+ print(opts, upload);
60
+ });
61
+ attachSharedShortcutOptions(command
62
+ .command('+attachment-parse')
63
+ .description('按 documentId 启动并读取附件解析;复杂 PDF/Office/扫描件按需调用')
64
+ .requiredOption('--document-id <uuid>', '附件登记返回的 documentId')
65
+ .option('--no-wait', '只触发/查询一次,不等待解析完成')
66
+ .option('--timeout <seconds>', '等待上限秒数', '600')
67
+ .option('--poll-interval <seconds>', '轮询间隔秒数', '2')).action(async (opts, parent) => {
68
+ const documentId = opts.documentId?.trim() || '';
69
+ if (!/^[0-9a-f-]{36}$/i.test(documentId))
70
+ throw new VertuError('--document-id 必须是 UUID。');
71
+ const timeout = parseOptionalInt(opts.timeout, '--timeout') ?? 600;
72
+ const pollInterval = parseOptionalInt(opts.pollInterval, '--poll-interval') ?? 2;
73
+ if (timeout < 1 || timeout > 600)
74
+ throw new VertuError('--timeout 必须在 1 到 600 秒之间。');
75
+ if (pollInterval < 1 || pollInterval > 30)
76
+ throw new VertuError('--poll-interval 必须在 1 到 30 秒之间。');
77
+ const baseUrl = resolveShortcutVpsServiceUrl(opts, parent);
78
+ if (opts.validateOnly === true || opts.dryRun === true) {
79
+ print(opts, { document_id: documentId, endpoint: `${baseUrl}/v1/im/agents/documents/${documentId}`, wait: opts.wait !== false, validateOnly: true });
80
+ return;
81
+ }
82
+ const deadline = Date.now() + timeout * 1000;
83
+ while (true) {
84
+ const result = await vpsServiceGet(`/v1/im/agents/documents/${encodeURIComponent(documentId)}`, { include_content: 1 }, baseUrl, '附件解析');
85
+ const status = result.document?.status || '';
86
+ if (status === 'ready') {
87
+ print(opts, result);
88
+ return;
89
+ }
90
+ if (status === 'failed')
91
+ throw new VertuError(result.document?.error_message || '附件解析失败。');
92
+ if (opts.wait === false || Date.now() >= deadline) {
93
+ if (opts.wait !== false)
94
+ throw new VertuError(`附件解析等待超过 ${timeout} 秒。`);
95
+ print(opts, result);
96
+ return;
97
+ }
98
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, pollInterval * 1000));
99
+ }
100
+ });
101
+ return command;
102
+ }
103
+ function normalizeVisibility(value) {
104
+ const normalized = value?.trim().toLowerCase() || 'private';
105
+ if (normalized !== 'private' && normalized !== 'public') {
106
+ throw new VertuError('--visibility 仅支持 private 或 public。');
107
+ }
108
+ return normalized;
109
+ }
110
+ function print(opts, value) {
111
+ console.log(opts.json === false && typeof value === 'string' ? value : JSON.stringify(value, null, 2));
112
+ }
113
+ function mimeType(name) {
114
+ const types = {
115
+ '.csv': 'text/csv', '.doc': 'application/msword',
116
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
117
+ '.gif': 'image/gif', '.html': 'text/html', '.jpeg': 'image/jpeg', '.jpg': 'image/jpeg',
118
+ '.json': 'application/json', '.md': 'text/markdown', '.mp3': 'audio/mpeg',
119
+ '.mp4': 'video/mp4', '.pdf': 'application/pdf', '.png': 'image/png',
120
+ '.ppt': 'application/vnd.ms-powerpoint',
121
+ '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
122
+ '.svg': 'image/svg+xml', '.txt': 'text/plain', '.wav': 'audio/wav',
123
+ '.webm': 'video/webm', '.webp': 'image/webp', '.xls': 'application/vnd.ms-excel',
124
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', '.zip': 'application/zip',
125
+ };
126
+ return types[extname(name).toLowerCase()] ?? 'application/octet-stream';
127
+ }
@@ -0,0 +1,7 @@
1
+ import { Command } from 'commander';
2
+ import { attachVemoryShortcuts } from './shortcuts.js';
3
+ export function buildVemoryCommand() {
4
+ const vemory = new Command('vemory').description('Legacy Vemory meeting commands (prefer: meeting +list).');
5
+ attachVemoryShortcuts(vemory);
6
+ return vemory;
7
+ }
@@ -0,0 +1,53 @@
1
+ import { VertuError } from '../../core/errors.js';
2
+ import { vpsServiceGet } from '../../core/sales-query-client.js';
3
+ import { attachSharedShortcutOptions, resolveShortcutVpsServiceUrl, } from '../shortcut-common.js';
4
+ function assertYmd(raw, flag) {
5
+ const value = raw?.trim();
6
+ if (!value) {
7
+ return undefined;
8
+ }
9
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
10
+ throw new VertuError(`${flag} 需为 YYYY-MM-DD。`);
11
+ }
12
+ return value;
13
+ }
14
+ function resolveScope(raw) {
15
+ const value = raw?.trim().toLowerCase();
16
+ if (value === 'team') {
17
+ return 'team';
18
+ }
19
+ if (value && value !== 'mine') {
20
+ throw new VertuError('--scope 仅支持 mine 或 team。');
21
+ }
22
+ return 'mine';
23
+ }
24
+ function printResult(opts, out) {
25
+ if (opts.json !== false) {
26
+ console.log(JSON.stringify(out, null, 2));
27
+ }
28
+ else {
29
+ console.log(typeof out === 'string' ? out : JSON.stringify(out));
30
+ }
31
+ }
32
+ export function attachVemoryShortcuts(parent) {
33
+ attachSharedShortcutOptions(parent
34
+ .command('+meetings')
35
+ .description('兼容入口:会议列表(推荐改用 meeting +list)')
36
+ .option('--scope <scope>', 'mine(本人)或 team(递归下属)', 'mine')
37
+ .option('--employee-name <name>', '按员工姓名精确查询会议')
38
+ .option('--start-date <ymd>', 'YYYY-MM-DD,省略=近 30 天')
39
+ .option('--end-date <ymd>', 'YYYY-MM-DD,省略=today')
40
+ .option('--template-type <type>', '按纪要模版类型精确筛选(如:团队内部会议)')
41
+ .option('--keyword <text>', '主题/摘要/章节/待办子串')).action(async (opts, cmd) => {
42
+ const serviceBaseUrl = resolveShortcutVpsServiceUrl(opts, cmd);
43
+ const result = await vpsServiceGet('/v1/meeting-center/meetings', {
44
+ scope: opts.employeeName?.trim() ? undefined : resolveScope(opts.scope),
45
+ employee_name: opts.employeeName?.trim() || undefined,
46
+ start_date: assertYmd(opts.startDate, '--start-date'),
47
+ end_date: assertYmd(opts.endDate, '--end-date'),
48
+ template_type: opts.templateType?.trim() || undefined,
49
+ keyword: opts.keyword?.trim() || undefined,
50
+ }, serviceBaseUrl, 'Vertu 会议中心');
51
+ printResult(opts, result);
52
+ });
53
+ }
@@ -0,0 +1,30 @@
1
+ import { Command } from 'commander';
2
+ import { VertuError } from '../core/errors.js';
3
+ import { listAiSalesServices, runAiSalesMcp } from '../core/aisales-mcp-client.js';
4
+ import { expandAtFileToken } from '../utils/at-file.js';
5
+ export function buildVertuAiSalesCommand() {
6
+ const command = new Command('vertu-aisales').description('VERTU 客户沟通查询(当前企业和员工数据授权)');
7
+ command.command('services').description('列出当前账号可用的 AI Sales 服务实例')
8
+ .action(async () => console.log(JSON.stringify({ items: await listAiSalesServices() }, null, 2)));
9
+ command.command('tools').description('发现可调用工具和参数 schema')
10
+ .option('--service-id <id>', '企业 MCP 服务实例 ID')
11
+ .action(async (opts) => console.log(JSON.stringify(await runAiSalesMcp(undefined, opts.serviceId), null, 2)));
12
+ command.command('call').description('调用 AINativeSales 销售只读工具;先发现业务对象与指标')
13
+ .requiredOption('--tool <name>', 'tools 返回的工具名')
14
+ .option('--args <json>', 'JSON 对象,支持 @file / @-', '{}')
15
+ .option('--service-id <id>', '企业 MCP 服务实例 ID')
16
+ .action(async (opts) => {
17
+ const raw = await expandAtFileToken(opts.args, '--args');
18
+ let args;
19
+ try {
20
+ args = JSON.parse(raw);
21
+ }
22
+ catch {
23
+ throw new VertuError('--args 必须是 JSON 对象。');
24
+ }
25
+ if (!args || typeof args !== 'object' || Array.isArray(args))
26
+ throw new VertuError('--args 必须是 JSON 对象。');
27
+ console.log(JSON.stringify(await runAiSalesMcp({ tool: opts.tool, arguments: args }, opts.serviceId), null, 2));
28
+ });
29
+ return command;
30
+ }
@@ -0,0 +1,30 @@
1
+ import { Command } from 'commander';
2
+ import { VertuError } from '../core/errors.js';
3
+ import { runErpMcp } from '../core/erp-mcp-client.js';
4
+ import { expandAtFileToken } from '../utils/at-file.js';
5
+ export function buildVertuErpCommand() {
6
+ const command = new Command('vertu-erp').description('使用当前账号获授权的 ERP MCP(只读,不自动提权)');
7
+ command.command('tools').description('列出 ERP 工具及实时参数 schema')
8
+ .option('--hermes-base-url <url>', 'Hermes 服务根地址')
9
+ .action(async (opts) => {
10
+ console.log(JSON.stringify(await runErpMcp(undefined, opts.hermesBaseUrl), null, 2));
11
+ });
12
+ command.command('call').description('调用 ERP 只读工具;参数以 tools 返回的 schema 为准')
13
+ .requiredOption('--tool <name>', 'erp-query / erp-list-tables / erp-table-columns')
14
+ .option('--args <json>', 'JSON 参数对象,支持 @file 或 @-(标准输入)', '{}')
15
+ .option('--hermes-base-url <url>', 'Hermes 服务根地址')
16
+ .action(async (opts) => {
17
+ const raw = await expandAtFileToken(opts.args, '--args');
18
+ let args;
19
+ try {
20
+ args = JSON.parse(raw);
21
+ }
22
+ catch {
23
+ throw new VertuError('--args 必须是 JSON 对象。');
24
+ }
25
+ if (!args || typeof args !== 'object' || Array.isArray(args))
26
+ throw new VertuError('--args 必须是 JSON 对象。');
27
+ console.log(JSON.stringify(await runErpMcp({ tool: opts.tool, arguments: args }, opts.hermesBaseUrl), null, 2));
28
+ });
29
+ return command;
30
+ }
@@ -0,0 +1,35 @@
1
+ import { VertuError } from './errors.js';
2
+ import { runMcp } from './mcp-client.js';
3
+ import { resolveVpsServiceUrl } from './sales-query-client.js';
4
+ import { vpsServiceAuthHeaders } from './vps-service-http.js';
5
+ const TENANT = '00000000-0000-4000-8000-000000000001';
6
+ const TOOL_NAMES = new Set(['business.resolve_subject', 'business.catalog', 'business.query', 'business.compare', 'business.get_evidence', 'business.explain_metric', 'sales.operation_summary', 'sales.customer_cohort', 'sales.customer_journey']);
7
+ export async function listAiSalesServices() {
8
+ const base = resolveVpsServiceUrl().replace(/\/+$/, '');
9
+ const headers = vpsServiceAuthHeaders(base);
10
+ if (!headers.authorization)
11
+ throw new VertuError('需要当前账号的 VPS 登录凭据。');
12
+ const response = await fetch(`${base}/v1/tenant/mcp/services`, {
13
+ headers, redirect: 'error', signal: AbortSignal.timeout(15_000),
14
+ });
15
+ if (!response.ok) {
16
+ await response.body?.cancel();
17
+ throw new VertuError(`无法读取企业 MCP 授权(HTTP ${response.status})。`);
18
+ }
19
+ const catalog = await response.json();
20
+ if (catalog.tenant_id !== TENANT)
21
+ throw new VertuError('vertu-aisales 仅对获授权的 VERTU 企业成员开放。');
22
+ return (Array.isArray(catalog.items) ? catalog.items : []).filter(item => item.capability === 'vertu-aisales'
23
+ && /^[0-9a-f-]{36}$/i.test(item.id)).map(item => ({ id: item.id, name: item.name || 'VERTU AI Sales' }));
24
+ }
25
+ export async function runAiSalesMcp(operation, serviceId) {
26
+ if (operation && !TOOL_NAMES.has(operation.tool))
27
+ throw new VertuError('未知的 VERTU AI Sales 工具,请先运行 vps-work vertu-aisales tools。');
28
+ const services = (await listAiSalesServices()).filter(item => !serviceId || item.id === serviceId);
29
+ if (services.length !== 1)
30
+ throw new VertuError(services.length
31
+ ? '存在多个 AI Sales 服务,请运行 vps-work vertu-aisales services,再用 --service-id 指定实例。'
32
+ : '当前账号没有可用的 vertu-aisales 服务,请由管理员在企业 MCP 后台开通。');
33
+ const base = resolveVpsServiceUrl().replace(/\/+$/, '');
34
+ return runMcp(`${base}/v1/tenant/mcp/services/${services[0].id}`, vpsServiceAuthHeaders(base), TOOL_NAMES, operation);
35
+ }
@@ -0,0 +1,240 @@
1
+ import { OdooClient } from './client.js';
2
+ import { AuthRequiredError } from './errors.js';
3
+ import { bearerAccount, deleteSecret, getSecret, isKeychainAvailable, passwordAccount, sessionAccount, setSecret, } from './keychain.js';
4
+ import { EMPTY_CONFIG, PRIMARY_ALIAS, deleteProfile, getEndpointBaseUrl, getProfile, readConfig, resolveEffectiveBaseUrl, setDefaultBaseUrl, upsertProfile, writeConfig, } from './config.js';
5
+ function envBearer() {
6
+ return process.env.VERTU_BOT_INBOUND_KEY?.trim() || undefined;
7
+ }
8
+ function envBaseUrl() {
9
+ return process.env.VERTU_BASE_URL?.trim() || undefined;
10
+ }
11
+ function pickBaseUrl(config, override) {
12
+ return resolveEffectiveBaseUrl(config, { override, envBaseUrl: envBaseUrl() });
13
+ }
14
+ export async function loadCredentials(opts = {}) {
15
+ const config = await readConfig();
16
+ const baseUrl = pickBaseUrl(config, opts.baseUrlOverride);
17
+ const profile = getProfile(config, baseUrl) ?? { baseUrl };
18
+ let sessionId;
19
+ let sessionSource = 'none';
20
+ if (await isKeychainAvailable()) {
21
+ const fromKey = await getSecret(sessionAccount(baseUrl));
22
+ if (fromKey) {
23
+ sessionId = fromKey;
24
+ sessionSource = 'keychain';
25
+ }
26
+ }
27
+ if (!sessionId && profile.sessionId) {
28
+ sessionId = profile.sessionId;
29
+ sessionSource = 'config';
30
+ }
31
+ let bearer = envBearer();
32
+ let bearerSource = bearer ? 'env' : 'none';
33
+ if (!bearer && (await isKeychainAvailable())) {
34
+ const fromKey = await getSecret(bearerAccount(baseUrl));
35
+ if (fromKey) {
36
+ bearer = fromKey;
37
+ bearerSource = 'keychain';
38
+ }
39
+ }
40
+ if (!bearer && profile.bearer) {
41
+ bearer = profile.bearer;
42
+ bearerSource = 'config';
43
+ }
44
+ if (opts.requireAuth && !sessionId && !bearer) {
45
+ throw new AuthRequiredError();
46
+ }
47
+ return {
48
+ baseUrl,
49
+ profile,
50
+ auth: { sessionId, bearer },
51
+ sessionSource,
52
+ bearerSource,
53
+ };
54
+ }
55
+ export async function persistSession(baseUrl, sessionId, meta, opts) {
56
+ const config = await readConfig();
57
+ const next = {
58
+ baseUrl,
59
+ db: meta?.db ?? getProfile(config, baseUrl)?.db,
60
+ login: meta?.login ?? getProfile(config, baseUrl)?.login,
61
+ bearer: getProfile(config, baseUrl)?.bearer,
62
+ };
63
+ // A successful login should always become the active default profile;
64
+ // otherwise other commands keep targeting whichever host happened to be
65
+ // first registered.
66
+ // skipDefaultUpdate=true 时(联动登录次端点),保留原 defaultBaseUrl 不变。
67
+ const withProfile = (profile) => opts?.skipDefaultUpdate
68
+ ? upsertProfile(config, profile)
69
+ : setDefaultBaseUrl(upsertProfile(config, profile), baseUrl);
70
+ if (await isKeychainAvailable()) {
71
+ await setSecret(sessionAccount(baseUrl), sessionId);
72
+ // On Windows, keytar may cache the write in-process so getPassword appears
73
+ // to succeed immediately, but the OS credential store was never actually
74
+ // written. Keep a plaintext copy in config so loadCredentials can fall
75
+ // back to it when the keychain read returns null later.
76
+ if (process.platform !== 'win32') {
77
+ next.sessionId = undefined;
78
+ await writeConfig(withProfile(next));
79
+ return { via: 'keychain' };
80
+ }
81
+ const verified = await getSecret(sessionAccount(baseUrl));
82
+ if (verified === sessionId) {
83
+ next.sessionId = sessionId;
84
+ await writeConfig(withProfile(next));
85
+ return { via: 'keychain' };
86
+ }
87
+ }
88
+ next.sessionId = sessionId;
89
+ await writeConfig(withProfile(next));
90
+ return { via: 'config' };
91
+ }
92
+ export async function persistBearer(baseUrl, bearer) {
93
+ const config = await readConfig();
94
+ const profile = getProfile(config, baseUrl) ?? { baseUrl };
95
+ if (await isKeychainAvailable()) {
96
+ await setSecret(bearerAccount(baseUrl), bearer);
97
+ if (process.platform !== 'win32') {
98
+ profile.bearer = undefined;
99
+ await writeConfig(upsertProfile(config, profile));
100
+ return { via: 'keychain' };
101
+ }
102
+ const verified = await getSecret(bearerAccount(baseUrl));
103
+ if (verified === bearer) {
104
+ profile.bearer = bearer;
105
+ await writeConfig(upsertProfile(config, profile));
106
+ return { via: 'keychain' };
107
+ }
108
+ }
109
+ profile.bearer = bearer;
110
+ await writeConfig(upsertProfile(config, profile));
111
+ return { via: 'config' };
112
+ }
113
+ /**
114
+ * Persist the user's plain password so `vps-work reauth` can refresh sessions silently.
115
+ *
116
+ * Same fallback strategy as session/bearer: keychain when available; otherwise plain text in
117
+ * `~/.vps-work/config.json` with file mode 0600. On Windows, keytar is known to lie about
118
+ * write success in-process, so the value is also kept in config as a verified backup.
119
+ */
120
+ export async function persistPassword(baseUrl, password) {
121
+ const config = await readConfig();
122
+ const profile = getProfile(config, baseUrl) ?? { baseUrl };
123
+ if (await isKeychainAvailable()) {
124
+ await setSecret(passwordAccount(baseUrl), password);
125
+ if (process.platform !== 'win32') {
126
+ profile.password = undefined;
127
+ await writeConfig(upsertProfile(config, profile));
128
+ return { via: 'keychain' };
129
+ }
130
+ const verified = await getSecret(passwordAccount(baseUrl));
131
+ if (verified === password) {
132
+ profile.password = password;
133
+ await writeConfig(upsertProfile(config, profile));
134
+ return { via: 'keychain' };
135
+ }
136
+ }
137
+ profile.password = password;
138
+ await writeConfig(upsertProfile(config, profile));
139
+ return { via: 'config' };
140
+ }
141
+ export async function loadPassword(baseUrl) {
142
+ if (await isKeychainAvailable()) {
143
+ const fromKey = await getSecret(passwordAccount(baseUrl));
144
+ if (fromKey)
145
+ return fromKey;
146
+ }
147
+ const config = await readConfig();
148
+ return getProfile(config, baseUrl)?.password ?? undefined;
149
+ }
150
+ export async function clearSession(baseUrl) {
151
+ if (await isKeychainAvailable()) {
152
+ await deleteSecret(sessionAccount(baseUrl));
153
+ }
154
+ const config = await readConfig();
155
+ const profile = getProfile(config, baseUrl);
156
+ if (!profile)
157
+ return;
158
+ if (profile.sessionId) {
159
+ profile.sessionId = undefined;
160
+ await writeConfig(upsertProfile(config, profile));
161
+ }
162
+ }
163
+ export async function clearBearer(baseUrl) {
164
+ if (await isKeychainAvailable()) {
165
+ await deleteSecret(bearerAccount(baseUrl));
166
+ }
167
+ const config = await readConfig();
168
+ const profile = getProfile(config, baseUrl);
169
+ if (!profile)
170
+ return;
171
+ if (profile.bearer) {
172
+ profile.bearer = undefined;
173
+ await writeConfig(upsertProfile(config, profile));
174
+ }
175
+ }
176
+ export async function clearPassword(baseUrl) {
177
+ if (await isKeychainAvailable()) {
178
+ await deleteSecret(passwordAccount(baseUrl));
179
+ }
180
+ const config = await readConfig();
181
+ const profile = getProfile(config, baseUrl);
182
+ if (!profile)
183
+ return;
184
+ if (profile.password) {
185
+ profile.password = undefined;
186
+ await writeConfig(upsertProfile(config, profile));
187
+ }
188
+ }
189
+ export async function forgetProfile(baseUrl) {
190
+ await clearSession(baseUrl);
191
+ await clearBearer(baseUrl);
192
+ await clearPassword(baseUrl);
193
+ const config = await readConfig();
194
+ await writeConfig(deleteProfile(config, baseUrl));
195
+ }
196
+ export async function buildClient(opts = {}) {
197
+ const resolved = await loadCredentials(opts);
198
+ const client = new OdooClient({ baseUrl: resolved.baseUrl, auth: resolved.auth });
199
+ return { client, resolved };
200
+ }
201
+ /**
202
+ * Build a client for an explicit alias (`default` or any registered endpoint).
203
+ *
204
+ * Mainly used by skill subcommands and `vps-work reauth`, where the caller already knows the
205
+ * target endpoint and just needs to resolve credentials. For dynamic CLI execution see
206
+ * {@link buildClientForLeaf} which also consumes the leaf's `endpoint_alias` hint.
207
+ */
208
+ export async function buildClientForAlias(alias, opts = {}) {
209
+ const config = await readConfig();
210
+ const baseUrl = getEndpointBaseUrl(config, alias);
211
+ if (!baseUrl) {
212
+ throw new AuthRequiredError(`No base URL configured for endpoint "${alias}". ` +
213
+ 'A human operator may configure this legacy endpoint; Agent/Bot/Hermes must use injected app_key credentials instead.');
214
+ }
215
+ const resolved = await loadCredentials({ baseUrlOverride: baseUrl, requireAuth: opts.requireAuth });
216
+ const client = new OdooClient({ baseUrl: resolved.baseUrl, auth: resolved.auth });
217
+ return { client, resolved, alias };
218
+ }
219
+ /**
220
+ * Resolve the target base URL for callers that mix `--base-url` and `--endpoint <alias>` flags
221
+ * but do not have a leaf hint (e.g. skill subcommands). `--base-url` wins; otherwise `--endpoint`
222
+ * is looked up in `config.endpoints` (or `defaultBaseUrl` for the reserved `default` alias).
223
+ */
224
+ export async function buildClientFromOpts(opts) {
225
+ if (opts.baseUrlOverride && opts.baseUrlOverride.trim()) {
226
+ const resolved = await loadCredentials({
227
+ baseUrlOverride: opts.baseUrlOverride.trim(),
228
+ requireAuth: opts.requireAuth,
229
+ });
230
+ const client = new OdooClient({ baseUrl: resolved.baseUrl, auth: resolved.auth });
231
+ return { client, resolved };
232
+ }
233
+ if (opts.endpointOverride && opts.endpointOverride.trim()) {
234
+ return buildClientForAlias(opts.endpointOverride.trim(), { requireAuth: opts.requireAuth });
235
+ }
236
+ const resolved = await loadCredentials({ requireAuth: opts.requireAuth });
237
+ const client = new OdooClient({ baseUrl: resolved.baseUrl, auth: resolved.auth });
238
+ return { client, resolved };
239
+ }
240
+ export const __internals = { EMPTY_CONFIG, PRIMARY_ALIAS };
@@ -0,0 +1,11 @@
1
+ /** 全局可执行命令名(与 package.json `bin` 一致)。 */
2
+ export const CLI_BIN = 'vps-work';
3
+ /** 新包没有旧命令别名,避免和已安装的 vertu-cli 抢 PATH。 */
4
+ export const CLI_BIN_ALIASES = [];
5
+ /** 请求审计通道。 */
6
+ export const CLI_AUTH_CHANNEL = 'vps-work';
7
+ /** Odoo `/ai/cli/v1/execute` 的 `program`(与 capabilities.programs[].slug 一致)。 */
8
+ export const ODOO_CLI_PROGRAM_SLUG = 'vertu';
9
+ export const CLI_PACKAGE = 'vps-work';
10
+ /** 系统钥匙串 / Credential Manager 服务名,与 vertu-cli 隔离。 */
11
+ export const CLI_KEYCHAIN_SERVICE = 'vps-work';