vps-work 2.5.1 → 2.7.0

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 +14 -4
  2. package/README.md +1 -0
  3. package/USAGE.md +22 -0
  4. package/dist/vps-work.cjs +824 -546
  5. package/package.json +1 -1
  6. package/skills/vps-documents/references/api.md +4 -0
  7. package/skills/vps-okr/SKILL.md +12 -2
  8. package/skills/vps-okr-management-assistant/SKILL.md +2 -0
  9. package/skills/vps-store-management/references/store-reception.md +14 -2
  10. package/dist/packages/vps-work/src/bin/vps-work.js +0 -271
  11. package/dist/packages/vps-work/src/commands/agent/index.js +0 -264
  12. package/dist/packages/vps-work/src/commands/approval/index.js +0 -7
  13. package/dist/packages/vps-work/src/commands/approval/shortcuts.js +0 -259
  14. package/dist/packages/vps-work/src/commands/attendance/applications.js +0 -21
  15. package/dist/packages/vps-work/src/commands/attendance/index.js +0 -9
  16. package/dist/packages/vps-work/src/commands/attendance/scheduling.js +0 -183
  17. package/dist/packages/vps-work/src/commands/attendance/shortcuts.js +0 -131
  18. package/dist/packages/vps-work/src/commands/auth/check.js +0 -34
  19. package/dist/packages/vps-work/src/commands/auth/index.js +0 -21
  20. package/dist/packages/vps-work/src/commands/auth/list.js +0 -50
  21. package/dist/packages/vps-work/src/commands/auth/scopes.js +0 -17
  22. package/dist/packages/vps-work/src/commands/auth/status.js +0 -127
  23. package/dist/packages/vps-work/src/commands/auth/tenants.js +0 -35
  24. package/dist/packages/vps-work/src/commands/auth/use.js +0 -40
  25. package/dist/packages/vps-work/src/commands/calendar/index.js +0 -7
  26. package/dist/packages/vps-work/src/commands/calendar/shortcuts.js +0 -598
  27. package/dist/packages/vps-work/src/commands/catalog.js +0 -29
  28. package/dist/packages/vps-work/src/commands/cli-install.js +0 -70
  29. package/dist/packages/vps-work/src/commands/collab-invoke.js +0 -19
  30. package/dist/packages/vps-work/src/commands/corporate/index.js +0 -204
  31. package/dist/packages/vps-work/src/commands/creative/index.js +0 -369
  32. package/dist/packages/vps-work/src/commands/documents/index.js +0 -991
  33. package/dist/packages/vps-work/src/commands/drive/index.js +0 -435
  34. package/dist/packages/vps-work/src/commands/exec.js +0 -46
  35. package/dist/packages/vps-work/src/commands/hr/business.js +0 -22
  36. package/dist/packages/vps-work/src/commands/hr/data-view-shortcuts.js +0 -519
  37. package/dist/packages/vps-work/src/commands/hr/index.js +0 -15
  38. package/dist/packages/vps-work/src/commands/hr/job-responsibilities.js +0 -266
  39. package/dist/packages/vps-work/src/commands/hr/payslip.js +0 -45
  40. package/dist/packages/vps-work/src/commands/hr/shortcuts.js +0 -146
  41. package/dist/packages/vps-work/src/commands/im/im-message-payload.js +0 -150
  42. package/dist/packages/vps-work/src/commands/im/index.js +0 -7
  43. package/dist/packages/vps-work/src/commands/im/shortcuts.js +0 -856
  44. package/dist/packages/vps-work/src/commands/knowledge/index.js +0 -36
  45. package/dist/packages/vps-work/src/commands/login.js +0 -52
  46. package/dist/packages/vps-work/src/commands/logout.js +0 -12
  47. package/dist/packages/vps-work/src/commands/mail/email.js +0 -132
  48. package/dist/packages/vps-work/src/commands/mail/index.js +0 -9
  49. package/dist/packages/vps-work/src/commands/mail/shortcuts.js +0 -43
  50. package/dist/packages/vps-work/src/commands/meeting/index.js +0 -7
  51. package/dist/packages/vps-work/src/commands/meeting/shortcuts.js +0 -120
  52. package/dist/packages/vps-work/src/commands/odoo-invoke.js +0 -21
  53. package/dist/packages/vps-work/src/commands/odoo-leaf-registry.js +0 -13
  54. package/dist/packages/vps-work/src/commands/office/index.js +0 -59
  55. package/dist/packages/vps-work/src/commands/okr/index.js +0 -7
  56. package/dist/packages/vps-work/src/commands/okr/shortcuts.js +0 -75
  57. package/dist/packages/vps-work/src/commands/org-structure.js +0 -347
  58. package/dist/packages/vps-work/src/commands/popup/index.js +0 -147
  59. package/dist/packages/vps-work/src/commands/product/abnormal-statistics.js +0 -250
  60. package/dist/packages/vps-work/src/commands/product/data-view-shortcuts.js +0 -253
  61. package/dist/packages/vps-work/src/commands/product/index.js +0 -11
  62. package/dist/packages/vps-work/src/commands/purchase/index.js +0 -45
  63. package/dist/packages/vps-work/src/commands/reauth.js +0 -127
  64. package/dist/packages/vps-work/src/commands/report/index.js +0 -7
  65. package/dist/packages/vps-work/src/commands/report/missing-daily-reports.js +0 -9
  66. package/dist/packages/vps-work/src/commands/report/shortcuts.js +0 -251
  67. package/dist/packages/vps-work/src/commands/sales/index.js +0 -11
  68. package/dist/packages/vps-work/src/commands/sales/period-params.js +0 -75
  69. package/dist/packages/vps-work/src/commands/sales/shortcuts-personnel.js +0 -124
  70. package/dist/packages/vps-work/src/commands/sales/shortcuts.js +0 -800
  71. package/dist/packages/vps-work/src/commands/shortcut-common.js +0 -88
  72. package/dist/packages/vps-work/src/commands/shortcut-params.js +0 -22
  73. package/dist/packages/vps-work/src/commands/skill/index.js +0 -136
  74. package/dist/packages/vps-work/src/commands/skill/personal.js +0 -167
  75. package/dist/packages/vps-work/src/commands/store/index.js +0 -82
  76. package/dist/packages/vps-work/src/commands/task/index.js +0 -7
  77. package/dist/packages/vps-work/src/commands/task/task-center.js +0 -595
  78. package/dist/packages/vps-work/src/commands/tools/index.js +0 -127
  79. package/dist/packages/vps-work/src/commands/vemory/index.js +0 -7
  80. package/dist/packages/vps-work/src/commands/vemory/shortcuts.js +0 -53
  81. package/dist/packages/vps-work/src/commands/vertu-aisales.js +0 -30
  82. package/dist/packages/vps-work/src/commands/vertu-erp.js +0 -30
  83. package/dist/packages/vps-work/src/core/aisales-mcp-client.js +0 -35
  84. package/dist/packages/vps-work/src/core/auth.js +0 -240
  85. package/dist/packages/vps-work/src/core/cli-bin.js +0 -11
  86. package/dist/packages/vps-work/src/core/cli-root-help.js +0 -190
  87. package/dist/packages/vps-work/src/core/client.js +0 -158
  88. package/dist/packages/vps-work/src/core/config.js +0 -183
  89. package/dist/packages/vps-work/src/core/corporate-client.js +0 -52
  90. package/dist/packages/vps-work/src/core/creative-attachment-download.js +0 -48
  91. package/dist/packages/vps-work/src/core/documents-client.js +0 -57
  92. package/dist/packages/vps-work/src/core/endpoints.js +0 -46
  93. package/dist/packages/vps-work/src/core/erp-mcp-client.js +0 -11
  94. package/dist/packages/vps-work/src/core/errors.js +0 -107
  95. package/dist/packages/vps-work/src/core/execute-catalog-client.js +0 -42
  96. package/dist/packages/vps-work/src/core/hermes-client.js +0 -70
  97. package/dist/packages/vps-work/src/core/im-attachment-download.js +0 -109
  98. package/dist/packages/vps-work/src/core/im-client.js +0 -64
  99. package/dist/packages/vps-work/src/core/keychain.js +0 -58
  100. package/dist/packages/vps-work/src/core/mail-client.js +0 -82
  101. package/dist/packages/vps-work/src/core/mcp-client.js +0 -129
  102. package/dist/packages/vps-work/src/core/office-client.js +0 -25
  103. package/dist/packages/vps-work/src/core/office-files.js +0 -82
  104. package/dist/packages/vps-work/src/core/paths.js +0 -70
  105. package/dist/packages/vps-work/src/core/public-result.js +0 -43
  106. package/dist/packages/vps-work/src/core/sales-invoice-attachments-client.js +0 -75
  107. package/dist/packages/vps-work/src/core/sales-query-client.js +0 -452
  108. package/dist/packages/vps-work/src/core/skill-catalog.js +0 -158
  109. package/dist/packages/vps-work/src/core/skill-key.js +0 -19
  110. package/dist/packages/vps-work/src/core/v-drive-store.js +0 -392
  111. package/dist/packages/vps-work/src/core/vertu-agent-env.js +0 -32
  112. package/dist/packages/vps-work/src/core/vps-auth-scopes.js +0 -73
  113. package/dist/packages/vps-work/src/core/vps-service-auth.js +0 -382
  114. package/dist/packages/vps-work/src/core/vps-service-config.js +0 -83
  115. package/dist/packages/vps-work/src/core/vps-service-http.js +0 -187
  116. package/dist/packages/vps-work/src/utils/at-file.js +0 -34
  117. package/dist/packages/vps-work/src/utils/cli-option-helpers.js +0 -52
  118. package/dist/packages/vps-work/src/utils/frontmatter.js +0 -26
  119. package/dist/packages/vps-work/src/utils/normalize-argv.js +0 -51
  120. package/dist/packages/vps-work/src/utils/parse-json-arg.js +0 -85
  121. package/dist/packages/vps-work/src/utils/prompt.js +0 -46
  122. package/dist/packages/vps-work/src/utils/read-text.js +0 -44
  123. package/dist/packages/vps-work/src/utils/schema-to-options.js +0 -307
  124. package/dist/packages/vps-work/src/utils/unwrap-envelope.js +0 -31
  125. package/dist/shared/im-org-tree/index.js +0 -200
@@ -1,129 +0,0 @@
1
- import { VertuError } from './errors.js';
2
- const PROTOCOL = '2025-03-26';
3
- const MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
4
- const record = (value) => value !== null && typeof value === 'object' && !Array.isArray(value) ? value : {};
5
- function rpcResult(value, id) {
6
- const rpc = record(value);
7
- if (rpc.id !== id)
8
- return undefined;
9
- if (rpc.error)
10
- throw new VertuError('MCP 请求失败,请检查工具参数和服务端授权。');
11
- if (!('result' in rpc))
12
- throw new VertuError('MCP 返回了无效响应。');
13
- return record(rpc.result);
14
- }
15
- async function readResult(response, id) {
16
- const reader = response.body?.getReader();
17
- if (!reader)
18
- throw new VertuError('MCP 返回了空响应。');
19
- const sse = response.headers.get('content-type')?.includes('text/event-stream');
20
- const decoder = new TextDecoder();
21
- let buffer = '';
22
- let size = 0;
23
- try {
24
- while (true) {
25
- const chunk = await reader.read();
26
- if (chunk.done) {
27
- buffer += decoder.decode();
28
- break;
29
- }
30
- size += chunk.value.byteLength;
31
- if (size > MAX_RESPONSE_BYTES)
32
- throw new VertuError('MCP 结果过大,请缩小查询范围。');
33
- buffer += decoder.decode(chunk.value, { stream: true });
34
- if (sse) {
35
- let boundary;
36
- while ((boundary = /\r?\n\r?\n/.exec(buffer))) {
37
- const frame = buffer.slice(0, boundary.index);
38
- buffer = buffer.slice(boundary.index + boundary[0].length);
39
- const data = frame.split(/\r?\n/).filter(line => line.startsWith('data:'))
40
- .map(line => line.slice(5).replace(/^ /, '')).join('\n');
41
- if (!data)
42
- continue;
43
- const result = rpcResult(JSON.parse(data), id);
44
- if (result)
45
- return result;
46
- }
47
- }
48
- }
49
- if (!sse) {
50
- const result = rpcResult(JSON.parse(buffer), id);
51
- if (result)
52
- return result;
53
- }
54
- throw new VertuError('MCP 未返回匹配的请求结果。');
55
- }
56
- finally {
57
- await reader.cancel().catch(() => { });
58
- }
59
- }
60
- export async function runMcp(endpoint, authorizationHeaders, allowedTools, operation) {
61
- let sessionId;
62
- let protocol = PROTOCOL;
63
- let nextId = 0;
64
- const headers = () => ({
65
- ...authorizationHeaders, 'content-type': 'application/json', accept: 'application/json, text/event-stream',
66
- 'mcp-protocol-version': protocol, ...(sessionId ? { 'mcp-session-id': sessionId } : {}),
67
- });
68
- const send = async (method, params, notification = false) => {
69
- const id = ++nextId;
70
- const response = await fetch(endpoint, {
71
- method: 'POST', headers: headers(), redirect: 'error', signal: AbortSignal.timeout(60_000),
72
- body: JSON.stringify({ jsonrpc: '2.0', ...(notification ? {} : { id }), method, params }),
73
- });
74
- if (!response.ok) {
75
- await response.body?.cancel();
76
- throw new VertuError(`MCP 请求被拒绝或服务不可用(HTTP ${response.status})。`, { statusCode: response.status });
77
- }
78
- if (method === 'initialize')
79
- sessionId = response.headers.get('mcp-session-id') || undefined;
80
- if (notification) {
81
- await response.body?.cancel();
82
- return {};
83
- }
84
- return readResult(response, id);
85
- };
86
- try {
87
- const initialized = await send('initialize', {
88
- protocolVersion: PROTOCOL, capabilities: {}, clientInfo: { name: 'vps-work', version: '1' },
89
- });
90
- if (initialized.protocolVersion !== PROTOCOL)
91
- throw new VertuError('MCP 协议版本不兼容。');
92
- protocol = initialized.protocolVersion;
93
- await send('notifications/initialized', undefined, true);
94
- const tools = [];
95
- const cursors = new Set();
96
- let cursor;
97
- do {
98
- const page = await send('tools/list', cursor ? { cursor } : {});
99
- if (!Array.isArray(page.tools))
100
- throw new VertuError('MCP 工具目录无效。');
101
- tools.push(...page.tools.map(record).filter(tool => allowedTools.has(String(tool.name))));
102
- cursor = typeof page.nextCursor === 'string' && page.nextCursor ? page.nextCursor : undefined;
103
- if (cursor && (cursors.has(cursor) || cursors.size >= 100))
104
- throw new VertuError('MCP 工具目录分页异常。');
105
- if (cursor)
106
- cursors.add(cursor);
107
- } while (cursor);
108
- if (!operation)
109
- return { tools };
110
- if (!tools.some(tool => tool.name === operation.tool))
111
- throw new VertuError('当前 MCP 未开放该工具。');
112
- const result = await send('tools/call', { name: operation.tool, arguments: operation.arguments });
113
- if (result.isError === true)
114
- throw new VertuError('MCP 工具执行失败,请检查参数、SQL 和服务端授权。');
115
- return result;
116
- }
117
- catch (error) {
118
- if (error instanceof VertuError)
119
- throw error;
120
- // Transport/parser exceptions must not expose credentials or request bodies.
121
- throw new VertuError('MCP 通信失败,请检查网络和服务状态。');
122
- }
123
- finally {
124
- if (sessionId) {
125
- await fetch(endpoint, { method: 'DELETE', headers: headers(), redirect: 'error', signal: AbortSignal.timeout(5_000) })
126
- .then(response => response.body?.cancel()).catch(() => { });
127
- }
128
- }
129
- }
@@ -1,25 +0,0 @@
1
- import { readFile } from 'node:fs/promises';
2
- import { join } from 'node:path';
3
- import { homeDir } from './paths.js';
4
- /** Credentials stay in a mode-0600 file; never accept remote URLs for local file authority. */
5
- export async function officeRequest(action, body = {}, connectionFile) {
6
- const path = connectionFile ?? process.env.VPS_OFFICE_CONNECTION_FILE ?? join(homeDir(), 'office.json');
7
- let config;
8
- try {
9
- config = JSON.parse(await readFile(path, 'utf8'));
10
- }
11
- catch {
12
- throw new Error('未发现本机 VPS Office。请启动新版 VPS 桌面端,或设置 VPS_OFFICE_CONNECTION_FILE。');
13
- }
14
- const url = new URL(config.url);
15
- if (url.protocol !== 'http:' || url.hostname !== '127.0.0.1' || url.username || url.password || !config.token || config.protocolVersion !== 1)
16
- throw new Error('Office 连接配置无效。');
17
- const response = await fetch(new URL(`/office/${action}`, url), {
18
- method: 'POST', redirect: 'error', signal: AbortSignal.timeout(120000),
19
- headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${config.token}` }, body: JSON.stringify(body),
20
- });
21
- const result = await response.json();
22
- if (!response.ok)
23
- throw new Error(result.error ?? `Office HTTP ${response.status}`);
24
- return result;
25
- }
@@ -1,82 +0,0 @@
1
- import { createHash, randomUUID } from 'node:crypto';
2
- import { open, readFile, realpath, rename, rm, stat, link } from 'node:fs/promises';
3
- import { createRequire } from 'node:module';
4
- import { dirname, extname, join, resolve } from 'node:path';
5
- import { packageRootDir } from './paths.js';
6
- export function officeRuntime() {
7
- const path = join(packageRootDir(), 'dist/office-runtime.cjs');
8
- try {
9
- return createRequire(path)(path);
10
- }
11
- catch (error) {
12
- throw new Error(`Office runtime 无法加载,请重新安装完整 vps-work 包(源码开发先 npm run build)。${String(error)}`);
13
- }
14
- }
15
- export function officeFormat(path) {
16
- const format = extname(path).slice(1).toLowerCase();
17
- if (!['docx', 'xlsx', 'pptx', 'md'].includes(format))
18
- throw new Error('支持 .docx / .xlsx / .pptx / .md。');
19
- return format;
20
- }
21
- export function officeVersion(bytes) {
22
- return createHash('sha256').update(bytes).digest('hex');
23
- }
24
- export async function readOfficeFile(path) {
25
- const file = await realpath(path);
26
- const bytes = await readFile(file);
27
- const format = officeFormat(file);
28
- return { file, format, version: officeVersion(bytes), content: await officeRuntime().readDocument(format, bytes) };
29
- }
30
- /** Lock cooperating writers, compare immediately before rename, never truncate the source. */
31
- async function writeAtomic(path, bytes, expected) {
32
- const file = expected === undefined ? resolve(path) : await realpath(path);
33
- const lock = `${file}.vps-office.lock`;
34
- const handle = await open(lock, 'wx', 0o600).catch(() => { throw new Error(`文件正在修改或存在未清理锁:${lock}`); });
35
- const temp = join(dirname(file), `.${randomUUID()}.office.tmp`);
36
- try {
37
- const mode = expected === undefined ? 0o600 : (await stat(file)).mode;
38
- const output = await open(temp, 'wx', mode);
39
- try {
40
- await output.writeFile(bytes);
41
- await output.sync();
42
- }
43
- finally {
44
- await output.close();
45
- }
46
- if (expected !== undefined) {
47
- if (officeVersion(await readFile(file)) !== expected)
48
- throw new Error('文件版本冲突,请重新读取后修改。');
49
- await rename(temp, file);
50
- }
51
- else {
52
- // Hard link gives atomic create-if-absent, including races with other processes.
53
- await link(temp, file);
54
- }
55
- }
56
- finally {
57
- await rm(temp, { force: true });
58
- await handle.close();
59
- await rm(lock, { force: true });
60
- }
61
- }
62
- export async function createOfficeFile(path, spec) {
63
- const format = officeFormat(path);
64
- const bytes = await officeRuntime().createDocument(format, spec);
65
- await officeRuntime().readDocument(format, bytes);
66
- await writeAtomic(path, bytes);
67
- return { file: resolve(path), format, version: officeVersion(bytes), size: bytes.length };
68
- }
69
- export async function applyOfficeFile(path, changes, expected, output, dryRun = false) {
70
- const file = await realpath(path);
71
- const format = officeFormat(file);
72
- if (output && officeFormat(output) !== format)
73
- throw new Error('输出文件扩展名必须与源文件相同。');
74
- const original = await readFile(file);
75
- if (officeVersion(original) !== expected)
76
- throw new Error('文件版本冲突,请重新读取后修改。');
77
- const bytes = await officeRuntime().applyDocument(format, original, changes);
78
- const content = await officeRuntime().readDocument(format, bytes);
79
- if (!dryRun)
80
- await writeAtomic(output ?? file, bytes, output && resolve(output) !== file ? undefined : expected);
81
- return { file: resolve(output ?? file), format, version: officeVersion(bytes), previousVersion: expected, dryRun, content };
82
- }
@@ -1,70 +0,0 @@
1
- import { createHash } from 'node:crypto';
2
- import { realpathSync } from 'node:fs';
3
- import { homedir } from 'node:os';
4
- import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
5
- /** 本包独立会话目录,不共用 vertu-cli 的 `~/.vertu`。 */
6
- const HOME_DIR_NAME = '.vps-work';
7
- /** 与 larksuite/cli 一致:包根目录下的 skill 源目录名。 */
8
- export const PACKAGE_SKILLS_DIR_NAME = 'skills';
9
- /**
10
- * 从已解析的入口脚本路径推导 npm 包根(`dist/vps-work.cjs` 或 `src/bin/vps-work.ts` 的上一级)。
11
- */
12
- function packageRootFromEntryScript(resolvedEntry) {
13
- const entryBase = basename(resolvedEntry);
14
- const entryDir = dirname(resolvedEntry);
15
- const entryParent = basename(entryDir);
16
- if (entryBase === 'vps-work.cjs' && entryParent === 'dist') {
17
- return join(entryDir, '..');
18
- }
19
- if (entryBase === 'vps-work.ts' && entryParent === 'bin') {
20
- return join(entryDir, '..', '..');
21
- }
22
- return undefined;
23
- }
24
- /**
25
- * 当前运行的 `vps-work` npm 包根目录(`dist/vps-work.cjs` 的上一级)。
26
- * 全局安装时 `argv[1]` 常为 `.../bin/vps-work` 符号链接,须 `realpath` 后再推导。
27
- */
28
- export function packageRootDir() {
29
- const bin = process.argv[1];
30
- if (bin) {
31
- try {
32
- const resolved = realpathSync(resolve(bin));
33
- const fromEntry = packageRootFromEntryScript(resolved);
34
- if (fromEntry) {
35
- return fromEntry;
36
- }
37
- }
38
- catch {
39
- // 入口路径不可读时走下方回退
40
- }
41
- }
42
- if (typeof __dirname !== 'undefined' && basename(__dirname) === 'dist') {
43
- return join(__dirname, '..');
44
- }
45
- return process.cwd();
46
- }
47
- /** 包内 `skills/` 绝对路径(随 npm 包分发,供 Agent 只读引用)。 */
48
- export function packageSkillsDir() {
49
- return join(packageRootDir(), PACKAGE_SKILLS_DIR_NAME);
50
- }
51
- export function homeDir() {
52
- const configured = process.env.VPS_WORK_HOME?.trim();
53
- if (configured) {
54
- if (!isAbsolute(configured))
55
- throw new Error('VPS_WORK_HOME must be an absolute directory');
56
- return configured;
57
- }
58
- return join(homedir(), HOME_DIR_NAME);
59
- }
60
- export function configFilePath() {
61
- return join(homeDir(), 'config.json');
62
- }
63
- /** vps-work → 独立 vps-service 连接配置(与 Odoo login 的 config.json 分离)。 */
64
- export function vpsServiceConfigFilePath() {
65
- return join(homeDir(), 'vps-service.json');
66
- }
67
- export function hashBaseUrl(baseUrl) {
68
- const normalized = baseUrl.replace(/\/+$/, '').toLowerCase();
69
- return createHash('sha1').update(normalized).digest('hex').slice(0, 12);
70
- }
@@ -1,43 +0,0 @@
1
- /** Keep infrastructure provenance out of CLI-visible business responses. */
2
- export function publicBusinessResult(value) {
3
- if (Array.isArray(value))
4
- return value.map(publicBusinessResult);
5
- if (!value || typeof value !== 'object')
6
- return value;
7
- const out = {};
8
- for (const [key, item] of Object.entries(value)) {
9
- if (key === 'odoo_user_id') {
10
- out.user_id ??= publicBusinessResult(item);
11
- continue;
12
- }
13
- if ((key === 'database' || key === 'db') && typeof item === 'string' && /^odoo$/i.test(item))
14
- continue;
15
- if (key === 'source' && typeof item === 'string' && /^odoo(?:\b|[_-])/i.test(item)) {
16
- out.source = 'vps';
17
- continue;
18
- }
19
- if (key === 'source' && item && typeof item === 'object' && !Array.isArray(item)) {
20
- const { db: _db, database: _database, login: _login, base_url: _baseUrl, ...source } = item;
21
- out.source = publicBusinessResult(source);
22
- continue;
23
- }
24
- out[key] = publicBusinessResult(item);
25
- }
26
- return out;
27
- }
28
- export function publicBusinessErrorMessage(value) {
29
- const message = String(value ?? '');
30
- if (!/odoo/i.test(message))
31
- return message;
32
- const status = /HTTP\s+(\d{3})\b/i.exec(message)?.[0];
33
- const detail = status ? `(${status})` : '';
34
- if (/AccessDenied|forbidden|权限|HTTP\s+403\b/i.test(message))
35
- return `无权访问该业务数据${detail}`;
36
- if (/session|not authenticated|unauthorized|登录|会话|HTTP\s+401\b/i.test(message))
37
- return `VPS 登录凭证已失效,请通过宿主刷新${detail}`;
38
- if (/RATE_LIMITED|rate.?limit|限流/i.test(message)) {
39
- const fields = message.match(/(?:limit_type|retry_after_seconds)=[\w]+/g) ?? [];
40
- return `业务查询触发限流${detail}${fields.length ? `:${fields.join(' ')}` : ''}`;
41
- }
42
- return `业务数据服务暂不可用${detail}`;
43
- }
@@ -1,75 +0,0 @@
1
- import { mkdir, writeFile } from 'node:fs/promises';
2
- import { join, resolve } from 'node:path';
3
- import { request } from 'undici';
4
- import { VertuError } from './errors.js';
5
- import { resolveVpsServiceIdentity } from './sales-query-client.js';
6
- import { vpsServiceAuthHeaders } from './vps-service-http.js';
7
- async function errorMessage(status, text) {
8
- try {
9
- const payload = JSON.parse(text);
10
- return new VertuError(payload.error ? String(payload.error) : `HTTP ${status}`);
11
- }
12
- catch {
13
- return new VertuError(`Invoice attachment request failed: HTTP ${status}`);
14
- }
15
- }
16
- function safeFileName(value) {
17
- return value.replace(/[\r\n"\\/:*?<>|]/g, '_').slice(0, 180) || 'invoice-attachment';
18
- }
19
- function serviceHeaders(extra) {
20
- const headers = { ...vpsServiceAuthHeaders(), ...extra };
21
- const identity = resolveVpsServiceIdentity();
22
- if (identity.login && !headers['x-vertu-user-login']) {
23
- headers['x-vps-user-login'] = encodeURIComponent(identity.login);
24
- }
25
- return headers;
26
- }
27
- export async function listSalesInvoiceAttachments(baseUrl, orderNo) {
28
- const query = new URLSearchParams({ order_no: orderNo });
29
- const response = await request(`${baseUrl}/v1/sales/invoice-attachments?${query}`, {
30
- method: 'GET',
31
- headers: serviceHeaders({ accept: 'application/json' }),
32
- });
33
- const text = await response.body.text();
34
- if (response.statusCode >= 400)
35
- throw await errorMessage(response.statusCode, text);
36
- try {
37
- const payload = JSON.parse(text);
38
- if (!payload.result)
39
- throw new Error('missing result');
40
- return payload.result;
41
- }
42
- catch {
43
- throw new VertuError('Invoice attachment list returned invalid JSON.');
44
- }
45
- }
46
- export async function downloadSalesInvoiceAttachments(baseUrl, items, outputDir) {
47
- const directory = resolve(outputDir);
48
- await mkdir(directory, { recursive: true });
49
- const outputs = [];
50
- for (const item of items) {
51
- const fallback = `${item.order_no}_${item.move_id}_${item.index + 1}.${item.file_type || 'bin'}`;
52
- const prefix = `${safeFileName(item.order_no)}_${item.move_id}_${item.index + 1}_`;
53
- const output = join(directory, `${prefix}${safeFileName(item.name || fallback)}`);
54
- const response = await request(`${baseUrl}/v1/sales/invoice-attachments/download`, {
55
- method: 'POST',
56
- headers: serviceHeaders({ 'content-type': 'application/json' }),
57
- body: JSON.stringify({ order_no: item.order_no, move_id: item.move_id, index: item.index }),
58
- });
59
- const bytes = Buffer.from(await response.body.arrayBuffer());
60
- if (response.statusCode >= 400)
61
- throw await errorMessage(response.statusCode, bytes.toString('utf8'));
62
- await writeFile(output, bytes, { flag: 'wx' }).catch((error) => {
63
- if (error.code === 'EEXIST')
64
- throw new VertuError(`Output already exists: ${output}`);
65
- throw error;
66
- });
67
- outputs.push({
68
- output,
69
- bytes: bytes.length,
70
- contentType: typeof response.headers['content-type'] === 'string'
71
- ? response.headers['content-type'] : null,
72
- });
73
- }
74
- return outputs;
75
- }