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,190 @@
1
+ import { CLI_BIN } from './cli-bin.js';
2
+ /** Lark-cli-style sections appended to `vps-work --help`. */
3
+ export function buildCliRootHelpAfter() {
4
+ const b = CLI_BIN;
5
+ return [
6
+ '',
7
+ 'USAGE:',
8
+ ` ${b} catalog [--domain sales]`,
9
+ ` ${b} exec --skill <key> --script <key> [--param k=v]`,
10
+ ` ${b} vertu-aisales tools | vertu-aisales call --tool <name> --args @params.json`,
11
+ ` ${b} vertu-erp tools | vertu-erp call --tool <name> --args @params.json`,
12
+ ` ${b} <domain> +<shortcut> [options]`,
13
+ ` ${b} auth <subcommand> [options]`,
14
+ '',
15
+ 'EXAMPLES:',
16
+ ' # Human interactive sign-in (Agent/Bot/Hermes must use host-injected app_key)',
17
+ ` ${b} auth login`,
18
+ ` ${b} auth tenants`,
19
+ ` ${b} auth use --tenant <uuid>`,
20
+ ` ${b} catalog --domain sales`,
21
+ ` ${b} exec --skill vps-dealer-ops --script sales --param period=this_month`,
22
+ '',
23
+ ' # Session and API scopes',
24
+ ` ${b} auth status`,
25
+ ` ${b} auth scopes`,
26
+ '',
27
+ ' # Email (legacy mail +send is IM direct messaging)',
28
+ ` ${b} mail +messages --unread-only`,
29
+ ` ${b} mail +mailboxes`,
30
+ ` ${b} mail +send-email --help`,
31
+ '',
32
+ ' # Sales KPI',
33
+ ` ${b} sales +headline-kpi`,
34
+ ` ${b} sales +headline-kpi --period this_month --dry-run`,
35
+ ` ${b} sales +orders --period this_month --logistics-no SF123`,
36
+ ` ${b} sales +order-logistics --logistics-no SF123`,
37
+ ` ${b} sales +dept-breakdown --period this_month --group-by salesperson`,
38
+ '',
39
+ ` # Desktop Office: discover sessions and native editor tools`,
40
+ ` ${b} office +status`,
41
+ ` ${b} office +sessions`,
42
+ ` ${b} office +tools --session-id <id>`,
43
+ '',
44
+ ' # Purchasing (explicit read-only grants)',
45
+ ` ${b} purchase +requests --start-date 2026-10-01`,
46
+ ` ${b} purchase +order-lines --id <ERP-order-id>`,
47
+ '',
48
+ ' # Store management',
49
+ ` ${b} store +reception --period this_month`,
50
+ ` ${b} store +walk-in --period this_month --group-by department,date`,
51
+ ` ${b} store +inventory --group-by store,product`,
52
+ ` ${b} store +reception --start-date 2026-08-01 --end-date 2026-08-18 --region 东区`,
53
+ '',
54
+ ' # HR identity, organization, personnel, and contracts',
55
+ ` ${b} hr +me`,
56
+ ` ${b} hr +org-tree --max-depth 2`,
57
+ ` ${b} hr +headcount`,
58
+ ` ${b} hr +profile`,
59
+ ` ${b} hr +subordinates # all levels by default; add --direct for direct reports`,
60
+ ` ${b} hr +manager`,
61
+ ` ${b} hr +job-details --department-l1 "线下事业部" --job "销售"`,
62
+ ` ${b} hr +job-responsibilities --department "AI业务" --job "私域销售" --all`,
63
+ ` ${b} hr +job-responsibilities --name "谭启林" --doc-type all`,
64
+ ` ${b} hr +contracts`,
65
+ ` ${b} hr +payslip --name "张三" --salary-month 2026-04`,
66
+ '',
67
+ ' # Corporate records, seals, business workflows, and regulations',
68
+ ` ${b} corporate +partner-list --search "深圳"`,
69
+ ` ${b} corporate +seal-list --kind contract --status finished`,
70
+ ` ${b} corporate +business-list --kind business-policy`,
71
+ ` ${b} corporate +regulation-list`,
72
+ ` ${b} corporate +attachments --record-type regulation --record-id 53`,
73
+ '',
74
+ ' # Product & supply chain (data-view)',
75
+ ` ${b} product +inventory --product-name "AGENT Q"`,
76
+ ` ${b} product +inventory --product-subcategory "VERTU ALPHAFOLD" --product-material 小牛皮 --product-color 黑色 --group-by store_detail`,
77
+ ` ${b} product +price --product-subcategory "VERTU ALPHAFOLD" --product-material 鳄鱼皮 --price-region overseas`,
78
+ ` ${b} product +delivery --department 线上`,
79
+ ` ${b} product +kit-materials --product-name IRONFLIP`,
80
+ ` ${b} product +phone-head-inventory --product-name QUANTUM`,
81
+ ` ${b} product +abnormal-location --default-selected`,
82
+ ` ${b} product +abnormal-details --imeis 860000000000001,860000000000002`,
83
+ '',
84
+ ' # OKR',
85
+ ` ${b} okr +dept-okr --okr-period 2026-05`,
86
+ '',
87
+ ' # Daily report',
88
+ ` ${b} report +checkin-missing --date 2026-06-11`,
89
+ ` ${b} report +mine --user-id 123 --start-time 2026-06-01 --end-time 2026-06-11`,
90
+ ` ${b} report +tasks --limit 50`,
91
+ ` ${b} report +submit --task-id 2454 --progress 80 --spent-hours 6 --content '今日进展' --tomorrow '明日计划'`,
92
+ ` ${b} report +sync`,
93
+ '',
94
+ ' # Attendance daily reports',
95
+ ` ${b} attendance +daily --from-date 2026-08-01 --to-date 2026-08-25`,
96
+ ` ${b} attendance +daily --from-date 2026-08-01 --to-date 2026-08-25 --managed-departments`,
97
+ ` ${b} attendance +daily --from-date 2026-08-01 --to-date 2026-08-25 --employee-id 123`,
98
+ ` ${b} attendance +daily --from-date 2026-08-01 --to-date 2026-08-25 --department-path "总部 / 研发"`,
99
+ ` ${b} attendance +daily --from-date 2026-08-01 --to-date 2026-08-25 --all`,
100
+ ` ${b} attendance +schedule-rules`,
101
+ ` ${b} attendance +schedule-shifts --rule-id <id> --availability all`,
102
+ ` ${b} attendance +schedule-members --rule-id <id> --from-date 2026-08-26 --to-date 2026-08-31`,
103
+ ` ${b} attendance +schedule-preview --rule-id <id> --employee-ids 101,102 --from-date 2026-08-26 --to-date 2026-08-31 --shift-template-id <id>`,
104
+ ` ${b} attendance +schedule-apply --rule-id <id> --items-file plan.json --preview-token <token> --client-request-id <uuid>`,
105
+ '',
106
+ ' # Calendar',
107
+ ` ${b} calendar +mine --start-date 2026-07-20 --end-date 2026-07-26`,
108
+ ` ${b} calendar +person --name "张三" --start-date 2026-07-23`,
109
+ ` ${b} calendar +create --title "项目评审" --starts-at "2026-07-24 09:00" --ends-at "2026-07-24 10:00" --reminder-minutes 15m,1h`,
110
+ ` ${b} calendar +update --event-id <uuid> --location "深圳办公室"`,
111
+ ` ${b} calendar +respond --event-id <uuid> --status accepted`,
112
+ ` ${b} calendar +calendars`,
113
+ ` ${b} calendar +availability --user-ids 12708,12556 --start-date 2026-07-23`,
114
+ '',
115
+ ' # Cloud documents',
116
+ ` ${b} docs +list`,
117
+ ` ${b} docs +get --doc-id <uuid>`,
118
+ ` ${b} docs +block-schema`,
119
+ ` ${b} docs +get-blocks --doc-id <uuid>`,
120
+ ` ${b} docs +update-block --doc-id <uuid> --block-id <id> --expected-version <version> --type heading --level 2 --align center`,
121
+ ` ${b} docs +create --title "项目方案" --content-file ./方案.md`,
122
+ ` ${b} docs +create --title "项目台账" --type sheet`,
123
+ ` ${b} docs +share --doc-id <uuid> --mode login-view`,
124
+ ` ${b} docs +send --doc-id <uuid> --permission view`,
125
+ '',
126
+ ' # Content creation (AI image / video / audio)',
127
+ ` ${b} creative +models --type image`,
128
+ ` ${b} creative +image --model openai/gpt-image-2.5-sunburst --prompt "一只金丝猴在雪山上" --aspect-ratio 16:9`,
129
+ ` ${b} creative +video --model <id> --prompt "..." --ratio 16:9 --duration 5 --wait`,
130
+ ` ${b} creative +video-status --model <id> --task-id <task_id>`,
131
+ ` ${b} creative +tts --model <id> --text "你好,欢迎使用" --voice-id <voice_id>`,
132
+ ` ${b} creative +music --model <id> --prompt "轻松愉快的爵士乐"`,
133
+ '',
134
+ ' # Local V Drive files',
135
+ ` ${b} drive +list --recursive`,
136
+ ` ${b} drive +mkdir --name "项目资料"`,
137
+ ` ${b} drive +upload --parent-id <uuid> --source ./方案.pdf`,
138
+ ` ${b} drive +read --item-id <uuid>`,
139
+ ` ${b} drive +download --item-id <uuid> --output ./downloads/`,
140
+ '',
141
+ ' # Platform common tools',
142
+ ` ${b} tools +oss-upload --file ./report.pdf`,
143
+ ` ${b} tools +oss-upload --file ./logo.png --visibility public`,
144
+ '',
145
+ ' # Agent mobile popup',
146
+ ` ${b} popup +send --title "今日提醒" --content "还有 2 个待办即将到期" --gradient-start "#FCEBE4" --gradient-end "#FFFBFA"`,
147
+ ` ${b} popup +send --user-id 12708 --title "管理员提醒" --content "请及时查看新的工作安排"`,
148
+ '',
149
+ ' # R&D tasks / todos (task-center)',
150
+ ` ${b} task +tc-tasks`,
151
+ ` ${b} task +tc-todos`,
152
+ ` ${b} task +tc-assisted-users`,
153
+ ` ${b} task +tc-projects`,
154
+ ` ${b} task +tc-todo-create --title '跟进事项' --project-id <uuid>`,
155
+ ` ${b} task +tc-todo-update --todo-id <uuid> --status done`,
156
+ ` ${b} task +my-okr --type mine`,
157
+ '',
158
+ ' # IM (Vertu native)',
159
+ ` ${b} im +chat --user-id 12708`,
160
+ ` ${b} im +history --channel-id <id> --date-from 2026-06-01 --date-to 2026-06-11`,
161
+ ` ${b} im +send-user --user-id 12708 --body '你好'`,
162
+ ` ${b} im +send --channel-id <id> --body '群公告' --attach ./slide.pdf`,
163
+ ` ${b} im +broadcast --user-ids '12708,12556' --body '通知'`,
164
+ '',
165
+ 'FLAGS (shortcuts):',
166
+ ' --params <json> merge JSON into defaults; also @path or @- (stdin)',
167
+ ' --params-file <path> read --params from file (mutually exclusive)',
168
+ ' --validate-only preview request payload without executing / querying',
169
+ ' --dry-run alias for --validate-only; no network call for execute shortcuts',
170
+ ' --no-json plain text instead of formatted JSON',
171
+ ' --base-url <url> override Vertu service URL (shortcuts / auth)',
172
+ '',
173
+ 'AI AGENT SKILLS:',
174
+ ' Public skills ship inside the package and install with no login:',
175
+ ` ${b} skill list`,
176
+ ` ${b} skill install --all --agent all # cursor / claude-code / hermes / ...`,
177
+ ` ${b} skill update # re-sync after upgrade`,
178
+ '',
179
+ ` \`${b} install\` also lays them into detected agent dirs automatically.`,
180
+ ' Manage personal skills with the current host-injected identity:',
181
+ ` ${b} skill +create-personal --slug <slug> --title <title> --instructions @file`,
182
+ ` ${b} skill +update-personal --slug <slug> --skill-md @SKILL.md`,
183
+ ` ${b} skill +change-category --slug <slug> --category <key>`,
184
+ ` ${b} skill +request-publication --slug <slug>`,
185
+ ` ${b} skill +delete-personal --slug <slug>`,
186
+ ].join('\n');
187
+ }
188
+ export function buildCliRootHelpBefore() {
189
+ return `${CLI_BIN} — VPS tenant-aware business CLI.\n`;
190
+ }
@@ -0,0 +1,158 @@
1
+ import { request as undiciRequest } from 'undici';
2
+ import { AuthRequiredError, parseRateLimitInfo, RateLimitError, RemoteError, SessionExpiredError, } from './errors.js';
3
+ const DEFAULT_TIMEOUT_MS = 60_000;
4
+ function joinUrl(baseUrl, path, query) {
5
+ const trimmedBase = baseUrl.replace(/\/+$/, '');
6
+ const trimmedPath = path.startsWith('/') ? path : `/${path}`;
7
+ const url = new URL(`${trimmedBase}${trimmedPath}`);
8
+ if (query) {
9
+ for (const [key, value] of Object.entries(query)) {
10
+ if (value !== undefined && value !== null)
11
+ url.searchParams.set(key, value);
12
+ }
13
+ }
14
+ return url.toString();
15
+ }
16
+ function buildHeaders(opts, internal) {
17
+ const headers = {
18
+ accept: 'application/json',
19
+ ...(opts.headers ?? {}),
20
+ };
21
+ if (internal.body !== undefined && internal.contentType) {
22
+ headers['content-type'] = internal.contentType;
23
+ }
24
+ if (opts.auth?.bearer && !internal.noBearer) {
25
+ headers.authorization = `Bearer ${opts.auth.bearer}`;
26
+ }
27
+ if (opts.auth?.sessionId) {
28
+ const existing = headers.cookie ? `${headers.cookie}; ` : '';
29
+ headers.cookie = `${existing}session_id=${opts.auth.sessionId}`;
30
+ }
31
+ return headers;
32
+ }
33
+ async function readBody(stream) {
34
+ const chunks = [];
35
+ for await (const chunk of stream)
36
+ chunks.push(chunk);
37
+ return Buffer.concat(chunks);
38
+ }
39
+ async function performRequest(opts, path, internal) {
40
+ const url = joinUrl(opts.baseUrl, path, internal.query);
41
+ const method = internal.method ?? (internal.body !== undefined ? 'POST' : 'GET');
42
+ const controller = new AbortController();
43
+ const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
44
+ const headers = buildHeaders(opts, internal);
45
+ if (process.env.VERTU_DEBUG === '1') {
46
+ process.stderr.write(`[vps-work] ${method} ${url}\n`);
47
+ }
48
+ try {
49
+ const response = await undiciRequest(url, {
50
+ method,
51
+ headers,
52
+ body: internal.body,
53
+ signal: controller.signal,
54
+ });
55
+ if (process.env.VERTU_DEBUG === '1') {
56
+ process.stderr.write(`[vps-work] <- ${response.statusCode}\n`);
57
+ }
58
+ const buf = await readBody(response.body);
59
+ return { status: response.statusCode, body: buf, headers: response.headers };
60
+ }
61
+ catch (err) {
62
+ if (err.name === 'AbortError') {
63
+ throw new RemoteError(`Request to ${url} timed out`, { hint: 'Try increasing --timeout or check network.' });
64
+ }
65
+ throw new RemoteError(`Network error contacting ${url}: ${err.message}`, { hint: 'Check base-url and connectivity.' });
66
+ }
67
+ finally {
68
+ clearTimeout(timer);
69
+ }
70
+ }
71
+ function parseSetCookie(headers) {
72
+ const raw = headers['set-cookie'];
73
+ if (!raw)
74
+ return [];
75
+ return Array.isArray(raw) ? raw : [raw];
76
+ }
77
+ export class OdooClient {
78
+ opts;
79
+ constructor(opts) {
80
+ if (!opts.baseUrl)
81
+ throw new Error('OdooClient requires a baseUrl');
82
+ this.opts = { ...opts };
83
+ }
84
+ get baseUrl() {
85
+ return this.opts.baseUrl;
86
+ }
87
+ setAuth(auth) {
88
+ this.opts.auth = auth;
89
+ }
90
+ getAuth() {
91
+ return this.opts.auth;
92
+ }
93
+ /** JSON-RPC POST helper for any Odoo `type="json"` route. */
94
+ async jsonRpc(path, params = {}, extra) {
95
+ const body = Buffer.from(JSON.stringify({ jsonrpc: '2.0', method: 'call', params, id: Date.now() }), 'utf8');
96
+ const res = await performRequest(this.opts, path, {
97
+ method: 'POST',
98
+ body,
99
+ contentType: 'application/json',
100
+ raw: true,
101
+ noBearer: extra?.noBearer ?? false,
102
+ });
103
+ if (res.status === 401 || res.status === 403) {
104
+ throw new SessionExpiredError(`Odoo rejected the request (HTTP ${res.status}) on ${path}`);
105
+ }
106
+ let parsed;
107
+ try {
108
+ parsed = JSON.parse(res.body.toString('utf8'));
109
+ }
110
+ catch (err) {
111
+ throw new RemoteError(`Failed to parse JSON-RPC response from ${path} (HTTP ${res.status}): ${err.message}`, { status: res.status });
112
+ }
113
+ if (parsed.error) {
114
+ const data = parsed.error.data;
115
+ const message = data?.message ?? parsed.error.message ?? 'Odoo returned an error';
116
+ const name = data?.name ?? '';
117
+ if (name.includes('AccessDenied') || name.includes('AccessError') || /not\s+authenticated/i.test(message)) {
118
+ throw new AuthRequiredError(message);
119
+ }
120
+ if (name.includes('AiRateLimitError') || /\bRATE_LIMITED\b/.test(message)) {
121
+ const info = parseRateLimitInfo(message);
122
+ throw new RateLimitError(message, {
123
+ status: res.status,
124
+ code: name || undefined,
125
+ data: parsed.error.data,
126
+ retryAfterSeconds: info.retryAfterSeconds,
127
+ limitType: info.limitType,
128
+ });
129
+ }
130
+ throw new RemoteError(message, {
131
+ status: res.status,
132
+ code: name || undefined,
133
+ data: parsed.error.data,
134
+ });
135
+ }
136
+ if (!('result' in parsed)) {
137
+ throw new RemoteError(`Unexpected JSON-RPC payload from ${path}: missing "result"`, { status: res.status });
138
+ }
139
+ return { result: parsed.result, setCookie: parseSetCookie(res.headers) };
140
+ }
141
+ /** Plain HTTP GET (e.g. for /ai/skills/v1/skill_file). */
142
+ async httpGet(path, query) {
143
+ const res = await performRequest(this.opts, path, { method: 'GET', query, raw: true });
144
+ if (res.status === 401 || res.status === 403) {
145
+ throw new SessionExpiredError(`Odoo rejected GET ${path} (HTTP ${res.status})`);
146
+ }
147
+ return res;
148
+ }
149
+ }
150
+ /** Extract Odoo session_id from a Set-Cookie header array, if present. */
151
+ export function extractSessionIdFromSetCookie(setCookie) {
152
+ for (const raw of setCookie) {
153
+ const match = /(?:^|;\s*)session_id=([^;]+)/i.exec(raw);
154
+ if (match && match[1])
155
+ return match[1];
156
+ }
157
+ return undefined;
158
+ }
@@ -0,0 +1,183 @@
1
+ import { mkdir, writeFile, chmod } from 'node:fs/promises';
2
+ import { dirname } from 'node:path';
3
+ import { z } from 'zod';
4
+ import { readUtf8File } from '../utils/read-text.js';
5
+ import { configFilePath, homeDir } from './paths.js';
6
+ const ProfileSchema = z.object({
7
+ baseUrl: z.string().url(),
8
+ db: z.string().optional(),
9
+ login: z.string().optional(),
10
+ /** Plain-text fallback when keytar is unavailable. */
11
+ sessionId: z.string().optional(),
12
+ /** Plain-text fallback when keytar is unavailable. */
13
+ bearer: z.string().optional(),
14
+ /** Plain-text fallback when keytar is unavailable; only present when the user opted in to "remember password". */
15
+ password: z.string().optional(),
16
+ updatedAt: z.string().optional(),
17
+ });
18
+ const EndpointSchema = z.object({
19
+ baseUrl: z.string().url(),
20
+ });
21
+ const ConfigSchema = z.object({
22
+ version: z.literal(1).default(1),
23
+ defaultBaseUrl: z.string().url().optional(),
24
+ profiles: z.record(z.string(), ProfileSchema).default({}),
25
+ /**
26
+ * Map of alias → endpoint metadata. The primary endpoint is implied by `defaultBaseUrl`
27
+ * (and is registered as alias `default` on first load). Other entries (e.g. `im`) are
28
+ * synced automatically from the server-side `endpoint_aliases` capabilities payload.
29
+ */
30
+ endpoints: z.record(z.string(), EndpointSchema).default({}),
31
+ });
32
+ export const EMPTY_CONFIG = { version: 1, profiles: {}, endpoints: {} };
33
+ /** Reserved alias used to represent the primary endpoint (i.e. `defaultBaseUrl`). */
34
+ export const PRIMARY_ALIAS = 'default';
35
+ /**
36
+ * 默认服务环境(Vertu VPS Service 正式环境)。
37
+ *
38
+ * 仅供人工兼容 legacy Odoo CLI 时解析旧 endpoint。Agent/Bot/Hermes 不使用该配置,
39
+ * 必须由宿主注入 vps-service app_key(Hermes 固定 app_id=vertu-hermes-agent)。
40
+ */
41
+ export const DEFAULT_BASE_URL = 'https://vps-service.vertu.cn';
42
+ /**
43
+ * 人工 legacy Odoo CLI 的有效 base URL:CLI 全局 `--base-url` > `VERTU_BASE_URL` > config > {@link DEFAULT_BASE_URL}。
44
+ */
45
+ export function resolveEffectiveBaseUrl(config, opts = {}) {
46
+ const fromOverride = opts.override?.trim();
47
+ if (fromOverride)
48
+ return fromOverride;
49
+ const fromEnv = opts.envBaseUrl?.trim();
50
+ if (fromEnv)
51
+ return fromEnv;
52
+ if (config.defaultBaseUrl)
53
+ return config.defaultBaseUrl;
54
+ return DEFAULT_BASE_URL;
55
+ }
56
+ const ALIAS_RE = /^[a-z][a-z0-9-]*$/;
57
+ export function isValidAlias(alias) {
58
+ return ALIAS_RE.test(alias);
59
+ }
60
+ async function ensureHomeDir() {
61
+ await mkdir(homeDir(), { recursive: true });
62
+ }
63
+ async function tryRestrictPermissions(path) {
64
+ if (process.platform === 'win32')
65
+ return;
66
+ try {
67
+ await chmod(path, 0o600);
68
+ }
69
+ catch {
70
+ /* best-effort */
71
+ }
72
+ }
73
+ export async function readConfig() {
74
+ try {
75
+ const raw = await readUtf8File(configFilePath());
76
+ const parsed = JSON.parse(raw);
77
+ return ConfigSchema.parse(parsed);
78
+ }
79
+ catch (err) {
80
+ if (err.code === 'ENOENT')
81
+ return { ...EMPTY_CONFIG };
82
+ throw err;
83
+ }
84
+ }
85
+ export async function writeConfig(config) {
86
+ await ensureHomeDir();
87
+ const path = configFilePath();
88
+ await mkdir(dirname(path), { recursive: true });
89
+ await writeFile(path, `${JSON.stringify(config, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
90
+ await tryRestrictPermissions(path);
91
+ }
92
+ export function getProfile(config, baseUrl) {
93
+ return config.profiles[baseUrl];
94
+ }
95
+ export function upsertProfile(config, profile) {
96
+ return {
97
+ ...config,
98
+ defaultBaseUrl: config.defaultBaseUrl ?? profile.baseUrl,
99
+ profiles: {
100
+ ...config.profiles,
101
+ [profile.baseUrl]: { ...profile, updatedAt: new Date().toISOString() },
102
+ },
103
+ };
104
+ }
105
+ export function deleteProfile(config, baseUrl) {
106
+ if (!(baseUrl in config.profiles))
107
+ return config;
108
+ const next = { ...config.profiles };
109
+ delete next[baseUrl];
110
+ return {
111
+ ...config,
112
+ defaultBaseUrl: config.defaultBaseUrl === baseUrl ? undefined : config.defaultBaseUrl,
113
+ profiles: next,
114
+ };
115
+ }
116
+ export function setDefaultBaseUrl(config, baseUrl) {
117
+ return { ...config, defaultBaseUrl: baseUrl };
118
+ }
119
+ export function upsertEndpoint(config, alias, baseUrl) {
120
+ return {
121
+ ...config,
122
+ endpoints: {
123
+ ...config.endpoints,
124
+ [alias]: { baseUrl },
125
+ },
126
+ };
127
+ }
128
+ export function deleteEndpoint(config, alias) {
129
+ if (!(alias in config.endpoints))
130
+ return config;
131
+ const next = { ...config.endpoints };
132
+ delete next[alias];
133
+ return { ...config, endpoints: next };
134
+ }
135
+ /**
136
+ * Resolve an endpoint alias to its base URL.
137
+ *
138
+ * - The reserved alias `default` always resolves to `defaultBaseUrl`, regardless of whether
139
+ * it was explicitly registered in the `endpoints` map.
140
+ * - Other aliases must exist in `endpoints`.
141
+ * - Returns `undefined` when the alias is not registered (callers decide whether to throw).
142
+ */
143
+ export function getEndpointBaseUrl(config, alias) {
144
+ if (alias === PRIMARY_ALIAS)
145
+ return config.defaultBaseUrl ?? DEFAULT_BASE_URL;
146
+ return config.endpoints[alias]?.baseUrl;
147
+ }
148
+ /**
149
+ * Return the alias registered for the given base URL (case-insensitive trailing-slash agnostic),
150
+ * preferring `default` when the URL matches `defaultBaseUrl`.
151
+ */
152
+ export function findAliasForBaseUrl(config, baseUrl) {
153
+ const normalize = (url) => url.replace(/\/+$/, '').toLowerCase();
154
+ const target = normalize(baseUrl);
155
+ if (normalize(config.defaultBaseUrl ?? DEFAULT_BASE_URL) === target)
156
+ return PRIMARY_ALIAS;
157
+ for (const [alias, entry] of Object.entries(config.endpoints)) {
158
+ if (normalize(entry.baseUrl) === target)
159
+ return alias;
160
+ }
161
+ return undefined;
162
+ }
163
+ /**
164
+ * List every known endpoint as `(alias, baseUrl)` pairs. The primary alias (`default`) comes
165
+ * first when `defaultBaseUrl` is set, then registered endpoints are appended in alphabetical
166
+ * order. Aliases registered both as primary and explicitly are deduplicated.
167
+ */
168
+ export function listEndpoints(config) {
169
+ const out = [];
170
+ const seenAliases = new Set();
171
+ if (config.defaultBaseUrl) {
172
+ out.push({ alias: PRIMARY_ALIAS, baseUrl: config.defaultBaseUrl });
173
+ seenAliases.add(PRIMARY_ALIAS);
174
+ }
175
+ const sorted = Object.entries(config.endpoints).sort(([a], [b]) => a.localeCompare(b));
176
+ for (const [alias, entry] of sorted) {
177
+ if (seenAliases.has(alias))
178
+ continue;
179
+ out.push({ alias, baseUrl: entry.baseUrl });
180
+ seenAliases.add(alias);
181
+ }
182
+ return out;
183
+ }
@@ -0,0 +1,52 @@
1
+ import { writeFile } from 'node:fs/promises';
2
+ import { request } from 'undici';
3
+ import { VertuError } from './errors.js';
4
+ import { vpsServiceAuthHeaders } from './vps-service-http.js';
5
+ async function errorMessage(status, text) {
6
+ try {
7
+ const payload = JSON.parse(text);
8
+ return new VertuError(payload.error ? String(payload.error) : `HTTP ${status}`);
9
+ }
10
+ catch {
11
+ return new VertuError(`Corporate request failed: HTTP ${status}`);
12
+ }
13
+ }
14
+ export async function runCorporateQuery(baseUrl, payload) {
15
+ const response = await request(`${baseUrl}/v1/corporate/query`, {
16
+ method: 'POST',
17
+ headers: {
18
+ 'content-type': 'application/json', accept: 'application/json', ...vpsServiceAuthHeaders(),
19
+ },
20
+ body: JSON.stringify(payload),
21
+ });
22
+ const text = await response.body.text();
23
+ if (response.statusCode >= 400)
24
+ throw await errorMessage(response.statusCode, text);
25
+ try {
26
+ return JSON.parse(text);
27
+ }
28
+ catch {
29
+ throw new VertuError('Corporate query returned invalid JSON.');
30
+ }
31
+ }
32
+ export async function downloadCorporateAttachment(baseUrl, selector, output) {
33
+ const response = await request(`${baseUrl}/v1/corporate/attachment`, {
34
+ method: 'POST',
35
+ headers: { 'content-type': 'application/json', ...vpsServiceAuthHeaders() },
36
+ body: JSON.stringify(selector),
37
+ });
38
+ const bytes = Buffer.from(await response.body.arrayBuffer());
39
+ if (response.statusCode >= 400)
40
+ throw await errorMessage(response.statusCode, bytes.toString('utf8'));
41
+ await writeFile(output, bytes, { flag: 'wx' }).catch((error) => {
42
+ if (error.code === 'EEXIST')
43
+ throw new VertuError(`Output already exists: ${output}`);
44
+ throw error;
45
+ });
46
+ return {
47
+ output,
48
+ bytes: bytes.length,
49
+ contentType: typeof response.headers['content-type'] === 'string'
50
+ ? response.headers['content-type'] : null,
51
+ };
52
+ }
@@ -0,0 +1,48 @@
1
+ import { createWriteStream } from 'node:fs';
2
+ import { mkdir, rm } 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 { vpsServiceAuthHeaders } from './vps-service-http.js';
8
+ export function creativeDownloadPath(baseUrl, input) {
9
+ if (Boolean(input.storageKey?.trim()) === Boolean(input.url?.trim()))
10
+ throw new VertuError('--url 与 --storage-key 必须且只能提供一个。');
11
+ let key = input.storageKey?.trim();
12
+ if (!key) {
13
+ const base = new URL(baseUrl), url = new URL(input.url, base);
14
+ const publicAlias = base.hostname === 'tenant-api' && url.origin === 'https://vps-service.vertu.cn';
15
+ if ((!publicAlias && url.origin !== base.origin) || url.username || url.password || url.hash
16
+ || url.pathname !== '/v1/creative/oss' || [...url.searchParams.keys()].some(k => k !== 'key')
17
+ || url.searchParams.getAll('key').length !== 1)
18
+ throw new VertuError('仅支持当前 VPS 服务的创意素材地址。');
19
+ key = url.searchParams.get('key') || '';
20
+ }
21
+ if (!key || /[\x00-\x1f\\]/.test(key) || key.split('/').some(p => p === '.' || p === '..'))
22
+ throw new VertuError('素材 storage key 无效。');
23
+ return '/v1/creative/oss?key=' + encodeURIComponent(key);
24
+ }
25
+ export async function downloadCreativeAttachment(baseUrl, input) {
26
+ const source = creativeDownloadPath(baseUrl, input);
27
+ // Always use the configured account gateway; never fetch the public URL or follow redirects with its credential.
28
+ const response = await request(new URL(source, baseUrl), { headers: { accept: '*/*', ...vpsServiceAuthHeaders() } });
29
+ if (response.statusCode !== 200) {
30
+ await response.body.dump();
31
+ throw new VertuError(`创意素材下载失败:HTTP ${response.statusCode}`);
32
+ }
33
+ const output = resolve(input.output);
34
+ await mkdir(dirname(output), { recursive: true });
35
+ const file = createWriteStream(output, { flags: 'wx' });
36
+ let opened = false, bytes = 0;
37
+ file.once('open', () => { opened = true; });
38
+ response.body.on('data', (chunk) => { bytes += chunk.length; });
39
+ try {
40
+ await pipeline(response.body, file);
41
+ }
42
+ catch (error) {
43
+ if (opened)
44
+ await rm(output, { force: true });
45
+ throw error;
46
+ }
47
+ return { output, bytes, content_type: response.headers['content-type'] || null, source_url: source };
48
+ }