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,57 @@
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
+ function buildPath(path, query) {
6
+ const search = new URLSearchParams();
7
+ for (const [key, value] of Object.entries(query ?? {})) {
8
+ if (value !== undefined && value !== null && value !== '') {
9
+ search.set(key, String(value));
10
+ }
11
+ }
12
+ const encoded = search.toString();
13
+ return encoded ? `${path}?${encoded}` : path;
14
+ }
15
+ function errorMessage(payload, statusCode) {
16
+ if (typeof payload === 'object' && payload !== null) {
17
+ const record = payload;
18
+ if (typeof record['error'] === 'string') {
19
+ return record['error'];
20
+ }
21
+ if (typeof record['message'] === 'string') {
22
+ return record['message'];
23
+ }
24
+ }
25
+ return `HTTP ${statusCode}`;
26
+ }
27
+ export async function documentsRequest(method, path, opts = {}) {
28
+ const baseUrl = resolveVpsServiceUrl(opts.baseUrl);
29
+ const response = await request(`${baseUrl}${buildPath(path, opts.query)}`, {
30
+ method,
31
+ headers: {
32
+ accept: 'application/json',
33
+ 'content-type': 'application/json',
34
+ ...vpsServiceAuthHeaders(),
35
+ },
36
+ body: opts.body === undefined ? undefined : JSON.stringify(opts.body),
37
+ });
38
+ const text = await response.body.text();
39
+ let payload = {};
40
+ try {
41
+ payload = text.trim() ? JSON.parse(text) : {};
42
+ }
43
+ catch {
44
+ throw new VertuError(`文档服务返回非 JSON(HTTP ${response.statusCode})`);
45
+ }
46
+ if (response.statusCode >= 400) {
47
+ throw new VertuError(`文档服务失败:${errorMessage(payload, response.statusCode)}`);
48
+ }
49
+ return payload;
50
+ }
51
+ export function printDocumentsResult(json, payload) {
52
+ if (json !== false) {
53
+ console.log(JSON.stringify(payload, null, 2));
54
+ return;
55
+ }
56
+ console.log(typeof payload === 'string' ? payload : JSON.stringify(payload));
57
+ }
@@ -0,0 +1,46 @@
1
+ import { PRIMARY_ALIAS, findAliasForBaseUrl, getEndpointBaseUrl, isValidAlias, resolveEffectiveBaseUrl, } from './config.js';
2
+ import { EndpointNotConfiguredError, VertuError } from './errors.js';
3
+ /**
4
+ * Resolve the target endpoint for one CLI invocation.
5
+ *
6
+ * Priority (highest first):
7
+ * 1. `baseUrlOverride` — explicit `--base-url` wins, no alias resolution.
8
+ * 2. `endpointOverride` — explicit `--endpoint <alias>` debug flag.
9
+ * 3. `leafEndpointAlias` — server-side capabilities hint.
10
+ * 4. Primary endpoint (`config.defaultBaseUrl` registered as alias `default`).
11
+ *
12
+ * Throws:
13
+ * - `VertuError` when no base URL can be determined at all.
14
+ * - `EndpointNotConfiguredError` when an alias is requested but not registered.
15
+ */
16
+ export function resolveEndpoint(opts) {
17
+ const { config, baseUrlOverride, endpointOverride, leafEndpointAlias, leafLabel } = opts;
18
+ if (baseUrlOverride && baseUrlOverride.trim()) {
19
+ const baseUrl = baseUrlOverride.trim();
20
+ const alias = findAliasForBaseUrl(config, baseUrl) ?? PRIMARY_ALIAS;
21
+ return { alias, baseUrl, reason: 'override-base-url' };
22
+ }
23
+ if (endpointOverride && endpointOverride.trim()) {
24
+ const alias = endpointOverride.trim();
25
+ if (!isValidAlias(alias)) {
26
+ throw new VertuError(`Invalid --endpoint alias "${alias}"; expected ^[a-z][a-z0-9-]*$ (e.g. "im", "default").`);
27
+ }
28
+ const baseUrl = getEndpointBaseUrl(config, alias);
29
+ if (!baseUrl)
30
+ throw new EndpointNotConfiguredError(alias, { leafLabel });
31
+ return { alias, baseUrl, reason: 'override-endpoint' };
32
+ }
33
+ const hint = (leafEndpointAlias ?? '').trim();
34
+ if (hint) {
35
+ if (!isValidAlias(hint)) {
36
+ throw new VertuError(`Capabilities hint endpoint_alias="${hint}" does not match ^[a-z][a-z0-9-]*$. ` +
37
+ `Fix the server-side vt.cli.command record.`);
38
+ }
39
+ const baseUrl = getEndpointBaseUrl(config, hint);
40
+ if (!baseUrl)
41
+ throw new EndpointNotConfiguredError(hint, { leafLabel });
42
+ return { alias: hint, baseUrl, reason: 'leaf-hint' };
43
+ }
44
+ const primary = getEndpointBaseUrl(config, PRIMARY_ALIAS) ?? resolveEffectiveBaseUrl(config);
45
+ return { alias: PRIMARY_ALIAS, baseUrl: primary, reason: 'primary' };
46
+ }
@@ -0,0 +1,11 @@
1
+ import { VertuError } from './errors.js';
2
+ import { hermesRequest } from './hermes-client.js';
3
+ const tools = new Set(['erp-query', 'erp-list-tables', 'erp-table-columns']);
4
+ export async function runErpMcp(operation, baseUrl) {
5
+ if (operation && !tools.has(operation.tool))
6
+ throw new VertuError('仅支持 ERP 只读工具,请先运行 vps-work vertu-erp tools。');
7
+ const reply = await hermesRequest('POST', '/hermes-console/api/platform-tools/vertu-erp', { baseUrl, body: operation ? { action: 'call', tool: operation.tool, arguments: operation.arguments } : { action: 'tools' } });
8
+ if (!reply.result || typeof reply.result !== 'object')
9
+ throw new VertuError('ERP 服务响应无效。');
10
+ return reply.result;
11
+ }
@@ -0,0 +1,107 @@
1
+ import { publicBusinessErrorMessage } from './public-result.js';
2
+ export class VertuError extends Error {
3
+ exitCode;
4
+ hint;
5
+ /** 由服务端 HTTP 响应触发时的状态码;本地校验类错误为 undefined。 */
6
+ statusCode;
7
+ constructor(message, options = {}) {
8
+ super(message);
9
+ this.name = 'VertuError';
10
+ this.exitCode = options.exitCode ?? 1;
11
+ this.hint = options.hint;
12
+ this.statusCode = options.statusCode;
13
+ if (options.cause !== undefined) {
14
+ this.cause = options.cause;
15
+ }
16
+ }
17
+ }
18
+ export class AuthRequiredError extends VertuError {
19
+ constructor(message = 'Not authenticated.') {
20
+ super(message, {
21
+ exitCode: 2,
22
+ hint: 'Agent/Bot/Hermes: verify injected app_key credentials (Hermes app_id must be vertu-hermes-agent). Human operator: run `vps-work auth login` interactively.',
23
+ });
24
+ this.name = 'AuthRequiredError';
25
+ }
26
+ }
27
+ export class SessionExpiredError extends VertuError {
28
+ constructor(message = 'Session expired or invalid.') {
29
+ super(message, {
30
+ exitCode: 2,
31
+ hint: 'Agent/Bot/Hermes: refresh the injected app_key through the host. Human operator: log in interactively again.',
32
+ });
33
+ this.name = 'SessionExpiredError';
34
+ }
35
+ }
36
+ export class EndpointNotConfiguredError extends VertuError {
37
+ alias;
38
+ constructor(alias, opts = {}) {
39
+ const leafBit = opts.leafLabel ? `Leaf "${opts.leafLabel}" requires` : 'Command requires';
40
+ const msg = `${leafBit} endpoint "${alias}" but it is not configured locally. ` +
41
+ `This is a human-only legacy service endpoint. ` +
42
+ `Agent/Bot/Hermes must not log in with a password or configure legacy endpoints.`;
43
+ super(msg, {
44
+ exitCode: 2,
45
+ hint: `Configured endpoints: \`vps-work auth list\` or \`vps-work auth status\`.`,
46
+ });
47
+ this.name = 'EndpointNotConfiguredError';
48
+ this.alias = alias;
49
+ }
50
+ }
51
+ export class RemoteError extends VertuError {
52
+ status;
53
+ code;
54
+ data;
55
+ constructor(message, options = {}) {
56
+ super(message, { hint: options.hint, exitCode: options.exitCode });
57
+ this.name = 'RemoteError';
58
+ this.status = options.status;
59
+ this.code = options.code;
60
+ this.data = options.data;
61
+ }
62
+ }
63
+ /**
64
+ * 服务端限流(AiRateLimitError)。exitCode=3 便于外层 agent / 脚本区分
65
+ * 「被限流」与普通远端错误,从而做退避(backoff)而非直接失败。
66
+ */
67
+ export class RateLimitError extends RemoteError {
68
+ retryAfterSeconds;
69
+ limitType;
70
+ constructor(message, options = {}) {
71
+ super(message, { ...options, exitCode: 3 });
72
+ this.name = 'RateLimitError';
73
+ this.retryAfterSeconds = options.retryAfterSeconds;
74
+ this.limitType = options.limitType;
75
+ }
76
+ }
77
+ /** 从服务端限流消息体中解析机读字段(RATE_LIMITED limit_type=... retry_after_seconds=...)。 */
78
+ export function parseRateLimitInfo(message) {
79
+ const out = {};
80
+ const ra = /retry_after_seconds=(\d+)/.exec(message);
81
+ if (ra)
82
+ out.retryAfterSeconds = Number(ra[1]);
83
+ const lt = /limit_type=([a-z_]+)/i.exec(message);
84
+ if (lt)
85
+ out.limitType = lt[1];
86
+ return out;
87
+ }
88
+ export function formatErrorForCli(err) {
89
+ if (err instanceof RateLimitError) {
90
+ const lines = [publicBusinessErrorMessage(err.message)];
91
+ if (err.limitType)
92
+ lines.push(` limit_type: ${err.limitType}`);
93
+ if (err.retryAfterSeconds != null) {
94
+ lines.push(` retry_after_seconds: ${err.retryAfterSeconds}`);
95
+ }
96
+ lines.push(' hint: 命中服务端限流。请降低调用频率、合并查询或减少并发,并在 retry_after_seconds 之后再重试。');
97
+ return lines.join('\n');
98
+ }
99
+ if (err instanceof VertuError) {
100
+ const message = publicBusinessErrorMessage(err.message);
101
+ return err.hint ? `${message}\n hint: ${publicBusinessErrorMessage(err.hint)}` : message;
102
+ }
103
+ if (err instanceof Error) {
104
+ return /odoo/i.test(err.message) ? publicBusinessErrorMessage(err.message) : err.stack ?? err.message;
105
+ }
106
+ return publicBusinessErrorMessage(err);
107
+ }
@@ -0,0 +1,42 @@
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
+ /** 拉取当前身份可见的命令清单。 */
6
+ export async function fetchExecuteCatalog(serviceBaseUrl, domain) {
7
+ const base = resolveVpsServiceUrl(serviceBaseUrl);
8
+ const query = domain?.trim() ? `?domain=${encodeURIComponent(domain.trim())}` : '';
9
+ const res = await request(`${base}/v1/execute/catalog${query}`, {
10
+ method: 'GET',
11
+ headers: {
12
+ accept: 'application/json',
13
+ ...vpsServiceAuthHeaders(base),
14
+ },
15
+ });
16
+ const text = await res.body.text();
17
+ let data;
18
+ try {
19
+ data = JSON.parse(text);
20
+ }
21
+ catch {
22
+ throw new VertuError(`读取命令目录失败:非 JSON(HTTP ${res.statusCode})`);
23
+ }
24
+ if (res.statusCode >= 400) {
25
+ const errMsg = data && typeof data === 'object' && 'error' in data
26
+ ? String(data.error)
27
+ : `HTTP ${res.statusCode}`;
28
+ throw new VertuError(`读取命令目录失败:${errMsg}`);
29
+ }
30
+ if (!data || typeof data !== 'object' || !('shortcuts' in data)) {
31
+ throw new VertuError('读取命令目录失败:响应无效');
32
+ }
33
+ const raw = data;
34
+ const { disabled_third_party_apps: _internalDisabledApps, ...publicCatalog } = raw;
35
+ return {
36
+ ...publicCatalog,
37
+ shortcuts: raw.shortcuts.map((shortcut) => {
38
+ const { provider: _internalProvider, ...item } = shortcut;
39
+ return item;
40
+ }),
41
+ };
42
+ }
@@ -0,0 +1,70 @@
1
+ import { request } from 'undici';
2
+ import { VertuError } from './errors.js';
3
+ import { vpsServiceAuthHeaders } from './vps-service-http.js';
4
+ const DEFAULT_HERMES_AGENT_ADMIN_BASE_URL = 'https://vps-console.vertu.cn';
5
+ function normalizeServiceRoot(url) {
6
+ const raw = url?.trim() || DEFAULT_HERMES_AGENT_ADMIN_BASE_URL;
7
+ let trimmed = raw.replace(/\/+$/, '');
8
+ if (!/^https?:\/\//i.test(trimmed)) {
9
+ trimmed = `https://${trimmed}`;
10
+ }
11
+ return trimmed;
12
+ }
13
+ function firstEnv(...keys) {
14
+ for (const key of keys) {
15
+ const value = process.env[key]?.trim();
16
+ if (value) {
17
+ return value;
18
+ }
19
+ }
20
+ return undefined;
21
+ }
22
+ export function resolveHermesAgentAdminUrl(override) {
23
+ return normalizeServiceRoot(override ||
24
+ firstEnv('VERTU_HERMES_BASE_URL', 'HERMES_AGENT_ADMIN_BASE_URL', 'HERMES_CONSOLE_BASE_URL', 'HERMES_BASE_URL'));
25
+ }
26
+ function headers() {
27
+ const authHeaders = vpsServiceAuthHeaders();
28
+ const authorization = authHeaders.authorization;
29
+ return {
30
+ accept: 'application/json',
31
+ 'content-type': 'application/json',
32
+ ...authHeaders,
33
+ ...(authorization ? { cookie: `vps_session=${authorization.replace(/^Bearer\s+/i, '')}` } : {}),
34
+ };
35
+ }
36
+ async function parseJson(res) {
37
+ const text = await res.body.text();
38
+ try {
39
+ return text.trim() ? JSON.parse(text) : {};
40
+ }
41
+ catch {
42
+ throw new VertuError(`Hermes 服务返回非 JSON(HTTP ${res.statusCode})`);
43
+ }
44
+ }
45
+ export async function hermesRequest(method, path, opts = {}) {
46
+ const base = resolveHermesAgentAdminUrl(opts.baseUrl);
47
+ const target = new URL(base);
48
+ const trusted = new URL(resolveHermesAgentAdminUrl());
49
+ if (target.origin !== trusted.origin || target.username || target.password
50
+ || target.search || target.hash || target.pathname !== '/') {
51
+ throw new VertuError('Hermes 请求目标不受信任;请由宿主配置服务地址,不能通过临时参数转发凭据。');
52
+ }
53
+ const res = await request(`${base}${path}`, {
54
+ method,
55
+ headers: headers(),
56
+ body: opts.body === undefined ? undefined : JSON.stringify(opts.body),
57
+ });
58
+ const data = await parseJson(res);
59
+ if (res.statusCode >= 400) {
60
+ const errMsg = data && typeof data === 'object' && 'error' in data
61
+ ? String(data.error)
62
+ : `HTTP ${res.statusCode}`;
63
+ throw new VertuError(`Hermes 服务失败:${errMsg}`);
64
+ }
65
+ if (data && typeof data === 'object' && 'ok' in data && data.ok === false) {
66
+ const errMsg = 'error' in data ? String(data.error) : 'unknown error';
67
+ throw new VertuError(`Hermes 服务失败:${errMsg}`);
68
+ }
69
+ return data;
70
+ }
@@ -0,0 +1,109 @@
1
+ import { createWriteStream } from 'node:fs';
2
+ import { mkdir, rm, stat } from 'node:fs/promises';
3
+ import { dirname, resolve } from 'node:path';
4
+ import { pipeline } from 'node:stream/promises';
5
+ import { request } from 'undici';
6
+ import { VertuError } from './errors.js';
7
+ import { resolveVpsServiceUrl } from './sales-query-client.js';
8
+ import { vpsServiceAuthHeaders } from './vps-service-http.js';
9
+ const IM_FILE_PREFIX = '/v1/im/files/';
10
+ const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
11
+ const MAX_REDIRECTS = 3;
12
+ function encodedStorageKey(storageKey) {
13
+ const segments = storageKey.trim().split('/').filter(Boolean);
14
+ if (segments.length === 0) {
15
+ throw new VertuError('--storage-key 不能为空。');
16
+ }
17
+ return segments.map(encodeURIComponent).join('/');
18
+ }
19
+ export function resolveImAttachmentDownloadPath(serviceBaseUrl, input) {
20
+ const storageKey = input.storageKey?.trim() ?? '';
21
+ const rawUrl = input.url?.trim() ?? '';
22
+ if (Boolean(storageKey) === Boolean(rawUrl)) {
23
+ throw new VertuError('--url 与 --storage-key 必须且只能提供一个。');
24
+ }
25
+ if (storageKey) {
26
+ return `${IM_FILE_PREFIX}${encodedStorageKey(storageKey)}`;
27
+ }
28
+ let parsed;
29
+ try {
30
+ parsed = new URL(rawUrl, `${serviceBaseUrl.replace(/\/+$/, '')}/`);
31
+ }
32
+ catch {
33
+ throw new VertuError('--url 不是有效的附件 URL。');
34
+ }
35
+ const service = new URL(serviceBaseUrl);
36
+ if (parsed.origin !== service.origin || !parsed.pathname.startsWith(IM_FILE_PREFIX)) {
37
+ throw new VertuError('--url 必须是当前 VPS 服务的 /v1/im/files/... 地址。');
38
+ }
39
+ if (parsed.search || parsed.hash || parsed.pathname.length === IM_FILE_PREFIX.length) {
40
+ throw new VertuError('--url 不是有效的聊天附件地址。');
41
+ }
42
+ return parsed.pathname;
43
+ }
44
+ async function responseError(status, text) {
45
+ try {
46
+ const payload = JSON.parse(text);
47
+ if (payload.error)
48
+ return new VertuError(`聊天附件下载失败:${String(payload.error)}`);
49
+ }
50
+ catch {
51
+ // 非 JSON 错误响应使用 HTTP 状态兜底。
52
+ }
53
+ return new VertuError(`聊天附件下载失败:HTTP ${status}`);
54
+ }
55
+ export async function downloadImAttachment(input) {
56
+ const baseUrl = resolveVpsServiceUrl(input.baseUrl).replace(/\/+$/, '');
57
+ const sourcePath = resolveImAttachmentDownloadPath(baseUrl, input);
58
+ const sourceUrl = `${baseUrl}${sourcePath}`;
59
+ let currentUrl = sourceUrl;
60
+ let response = await request(currentUrl, {
61
+ method: 'GET',
62
+ headers: { accept: '*/*', ...vpsServiceAuthHeaders() },
63
+ });
64
+ for (let redirects = 0; REDIRECT_STATUSES.has(response.statusCode); redirects += 1) {
65
+ if (redirects >= MAX_REDIRECTS) {
66
+ await response.body.dump();
67
+ throw new VertuError(`聊天附件下载失败:重定向超过 ${MAX_REDIRECTS} 次。`);
68
+ }
69
+ const location = response.headers.location;
70
+ await response.body.dump();
71
+ if (typeof location !== 'string' || !location.trim()) {
72
+ throw new VertuError('聊天附件下载失败:服务端重定向缺少 Location。');
73
+ }
74
+ currentUrl = new URL(location, currentUrl).toString();
75
+ response = await request(currentUrl, {
76
+ method: 'GET',
77
+ headers: { accept: '*/*' },
78
+ });
79
+ }
80
+ if (response.statusCode >= 400) {
81
+ const text = await response.body.text();
82
+ throw await responseError(response.statusCode, text);
83
+ }
84
+ if (response.statusCode < 200 || response.statusCode >= 300) {
85
+ await response.body.dump();
86
+ throw new VertuError(`聊天附件下载失败:HTTP ${response.statusCode}`);
87
+ }
88
+ const output = resolve(input.output);
89
+ await mkdir(dirname(output), { recursive: true });
90
+ try {
91
+ await pipeline(response.body, createWriteStream(output, { flags: 'wx' }));
92
+ }
93
+ catch (error) {
94
+ if (error.code === 'EEXIST') {
95
+ throw new VertuError(`输出文件已存在:${output}`);
96
+ }
97
+ await rm(output, { force: true }).catch(() => undefined);
98
+ throw error;
99
+ }
100
+ const file = await stat(output);
101
+ return {
102
+ bytes: file.size,
103
+ content_type: typeof response.headers['content-type'] === 'string'
104
+ ? response.headers['content-type']
105
+ : null,
106
+ output,
107
+ source_url: sourcePath,
108
+ };
109
+ }
@@ -0,0 +1,64 @@
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
+ import { publicBusinessResult } from './public-result.js';
6
+ function headers(baseUrl) {
7
+ return {
8
+ accept: 'application/json',
9
+ 'content-type': 'application/json',
10
+ ...vpsServiceAuthHeaders(baseUrl),
11
+ };
12
+ }
13
+ async function parseJson(res) {
14
+ const text = await res.body.text();
15
+ try {
16
+ return text.trim() ? JSON.parse(text) : {};
17
+ }
18
+ catch {
19
+ throw new VertuError(`Vertu 服务返回非 JSON(HTTP ${res.statusCode})`);
20
+ }
21
+ }
22
+ function buildPath(path, query) {
23
+ const search = new URLSearchParams();
24
+ for (const [key, value] of Object.entries(query ?? {})) {
25
+ if (value === undefined || value === null || value === '') {
26
+ continue;
27
+ }
28
+ search.set(key, String(value));
29
+ }
30
+ const q = search.toString();
31
+ return q ? `${path}?${q}` : path;
32
+ }
33
+ export async function imRequest(method, path, opts = {}) {
34
+ const base = resolveVpsServiceUrl(opts.baseUrl);
35
+ const res = await request(`${base}${buildPath(path, opts.query)}`, {
36
+ method,
37
+ headers: headers(base),
38
+ body: opts.body === undefined ? undefined : JSON.stringify(opts.body),
39
+ });
40
+ const data = await parseJson(res);
41
+ if (res.statusCode >= 400) {
42
+ const errMsg = data && typeof data === 'object' && 'error' in data
43
+ ? String(data.error)
44
+ : `HTTP ${res.statusCode}`;
45
+ throw new VertuError(`Vertu 服务失败:${errMsg}`);
46
+ }
47
+ if (data && typeof data === 'object' && 'ok' in data) {
48
+ const payload = data;
49
+ if (payload.ok === false) {
50
+ const errMsg = 'error' in payload ? String(payload.error) : 'unknown error';
51
+ throw new VertuError(`Vertu 服务失败:${errMsg}`);
52
+ }
53
+ }
54
+ return data;
55
+ }
56
+ export function printImResult(json, out) {
57
+ const publicOut = publicBusinessResult(out);
58
+ if (json !== false) {
59
+ console.log(JSON.stringify(publicOut, null, 2));
60
+ }
61
+ else {
62
+ console.log(typeof publicOut === 'string' ? publicOut : JSON.stringify(publicOut));
63
+ }
64
+ }
@@ -0,0 +1,58 @@
1
+ import { CLI_KEYCHAIN_SERVICE } from './cli-bin.js';
2
+ const SERVICE_NAME = CLI_KEYCHAIN_SERVICE;
3
+ let cachedModulePromise = null;
4
+ async function loadKeytar() {
5
+ if (process.env.VERTU_DISABLE_KEYTAR === '1')
6
+ return null;
7
+ if (cachedModulePromise)
8
+ return cachedModulePromise;
9
+ cachedModulePromise = (async () => {
10
+ try {
11
+ const mod = (await import('keytar'));
12
+ const keytar = mod.default ?? mod;
13
+ if (!keytar || typeof keytar.setPassword !== 'function')
14
+ return null;
15
+ return keytar;
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ })();
21
+ return cachedModulePromise;
22
+ }
23
+ export async function isKeychainAvailable() {
24
+ return (await loadKeytar()) !== null;
25
+ }
26
+ export async function setSecret(account, secret) {
27
+ const keytar = await loadKeytar();
28
+ if (!keytar)
29
+ throw new Error('Keychain not available');
30
+ await keytar.setPassword(SERVICE_NAME, account, secret);
31
+ }
32
+ export async function getSecret(account) {
33
+ const keytar = await loadKeytar();
34
+ if (!keytar)
35
+ return null;
36
+ return keytar.getPassword(SERVICE_NAME, account);
37
+ }
38
+ export async function deleteSecret(account) {
39
+ const keytar = await loadKeytar();
40
+ if (!keytar)
41
+ return false;
42
+ return keytar.deletePassword(SERVICE_NAME, account);
43
+ }
44
+ export function sessionAccount(baseUrl) {
45
+ return `session:${baseUrl}`;
46
+ }
47
+ export function bearerAccount(baseUrl) {
48
+ return `bearer:${baseUrl}`;
49
+ }
50
+ export function passwordAccount(baseUrl) {
51
+ return `password:${baseUrl}`;
52
+ }
53
+ export function agentKeyAccount(serviceBaseUrl, appId) {
54
+ return `vps-agent:${serviceBaseUrl}:${appId}`;
55
+ }
56
+ export function vpsSessionTokenAccount(serviceBaseUrl) {
57
+ return `vps-session:${serviceBaseUrl}`;
58
+ }
@@ -0,0 +1,82 @@
1
+ import { openAsBlob } from 'node:fs';
2
+ import { open, stat, unlink } from 'node:fs/promises';
3
+ import { basename } from 'node:path';
4
+ import { pipeline } from 'node:stream/promises';
5
+ import { FormData, request } from 'undici';
6
+ import { VertuError } from './errors.js';
7
+ import { resolveVpsServiceUrl } from './sales-query-client.js';
8
+ import { vpsServiceAuthHeaders } from './vps-service-http.js';
9
+ const timeout = 30 * 60_000;
10
+ export function mailUrl(path, scope, query = {}) {
11
+ const url = new URL(`${resolveVpsServiceUrl(scope.baseUrl).replace(/\/$/, '')}/v1/mail${path}`);
12
+ for (const [key, value] of Object.entries(query)) {
13
+ if (value !== undefined && value !== '')
14
+ url.searchParams.set(key, String(value));
15
+ }
16
+ if (scope.mailboxId)
17
+ url.searchParams.set('mailbox_id', scope.mailboxId);
18
+ return url.toString();
19
+ }
20
+ async function jsonResponse(res) {
21
+ const text = await res.body.text();
22
+ let data;
23
+ try {
24
+ data = text ? JSON.parse(text) : {};
25
+ }
26
+ catch {
27
+ throw new VertuError(`邮箱服务返回非 JSON(HTTP ${res.statusCode})`);
28
+ }
29
+ if (res.statusCode >= 300) {
30
+ const value = data;
31
+ const reason = typeof value?.error === 'string' ? value.error : value?.error?.message;
32
+ throw new VertuError(`邮箱服务失败(HTTP ${res.statusCode}):${value?.message || reason || '请求失败'}`);
33
+ }
34
+ return data;
35
+ }
36
+ export async function mailRequest(method, path, scope, body, query) {
37
+ const url = mailUrl(path, scope, query);
38
+ return jsonResponse(await request(url, {
39
+ method, headersTimeout: timeout, bodyTimeout: timeout,
40
+ headers: { ...vpsServiceAuthHeaders(resolveVpsServiceUrl(scope.baseUrl)), accept: 'application/json', 'content-type': 'application/json' },
41
+ body: body === undefined ? undefined : JSON.stringify(body),
42
+ }));
43
+ }
44
+ export async function inspectMailFile(file) {
45
+ const info = await stat(file);
46
+ if (!info.isFile() || info.size > 1073741824)
47
+ throw new VertuError('附件必须是普通文件且不超过 1 GiB。');
48
+ return { name: basename(file), size: info.size };
49
+ }
50
+ export async function uploadMailFile(file, scope) {
51
+ const info = await inspectMailFile(file);
52
+ const form = new FormData();
53
+ form.set('file', await openAsBlob(file), info.name);
54
+ return jsonResponse(await request(mailUrl('/attachments', scope), {
55
+ method: 'POST', headersTimeout: timeout, bodyTimeout: timeout,
56
+ headers: vpsServiceAuthHeaders(resolveVpsServiceUrl(scope.baseUrl)), body: form,
57
+ }));
58
+ }
59
+ export async function downloadMailFile(blobId, output, scope) {
60
+ // Exclusive creation preserves existing files. A failed stream removes only this new file.
61
+ const file = await open(output, 'wx', 0o600);
62
+ try {
63
+ const res = await request(mailUrl(`/attachments/${encodeURIComponent(blobId)}`, scope), {
64
+ method: 'GET', headersTimeout: timeout, bodyTimeout: timeout,
65
+ headers: vpsServiceAuthHeaders(resolveVpsServiceUrl(scope.baseUrl)),
66
+ });
67
+ if (res.statusCode !== 200) {
68
+ await jsonResponse(res);
69
+ throw new VertuError('附件下载未返回完整文件。');
70
+ }
71
+ await pipeline(res.body, file.createWriteStream());
72
+ return { output, size: (await stat(output)).size };
73
+ }
74
+ catch (error) {
75
+ await file.close();
76
+ await unlink(output);
77
+ throw error;
78
+ }
79
+ finally {
80
+ await file.close();
81
+ }
82
+ }