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,452 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { request } from 'undici';
4
+ import { VertuError } from './errors.js';
5
+ import { configFilePath, vpsServiceConfigFilePath } from './paths.js';
6
+ import { resolveServerSkillKey } from './skill-key.js';
7
+ import { readVertuAgentEnvFromProcess, } from './vertu-agent-env.js';
8
+ import { vpsServiceAuthHeaders } from './vps-service-http.js';
9
+ import { publicBusinessErrorMessage, publicBusinessResult } from './public-result.js';
10
+ const DAILY_SALES_LOCAL = 'vps-daily-sales-report';
11
+ const DAILY_SALES_SERVER = 'odoo-daily-sales-report';
12
+ const STORE_MANAGEMENT = 'vps-store-management';
13
+ const PERSONNEL_LOCAL = 'vps-sale-personnel-efficiency';
14
+ const PERSONNEL_SERVER = 'odoo-sale-personnel-efficiency';
15
+ const HR_COMPENSATION_LOCAL = 'vps-hr-compensation-documents';
16
+ const HR_COMPENSATION_SERVER = 'odoo-hr-compensation-documents';
17
+ const OKR_MANAGEMENT_LOCAL = 'vps-okr-management-assistant';
18
+ const OKR_MANAGEMENT_SERVER = 'odoo-okr-management-assistant';
19
+ const OKR_LOCAL = 'vps-okr';
20
+ const OKR_SERVER = 'odoo-okr';
21
+ const DAILY_REPORT_LOCAL = 'vps-daily-report-assistant';
22
+ const DAILY_REPORT_SERVER = 'odoo-daily-report-assistant';
23
+ const DEV_MANAGEMENT_LOCAL = 'vps-task-management-assistant';
24
+ const DEV_MANAGEMENT_SERVER = 'odoo-dev-management-assistant';
25
+ const DEALER_OPS = 'vps-dealer-ops';
26
+ /** 未配置时的 vps-service 生产根地址(可被 `--base-url` / 环境变量 / config 覆盖)。 */
27
+ export const DEFAULT_VPS_SERVICE_BASE_URL = 'https://vps-service.vertu.cn';
28
+ let dotenvCache = null;
29
+ function readVpsServiceConnectionSync() {
30
+ const path = vpsServiceConfigFilePath();
31
+ if (existsSync(path)) {
32
+ try {
33
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
34
+ return {
35
+ baseUrl: parsed.baseUrl?.trim() || undefined,
36
+ apiKey: parsed.apiKey?.trim() || undefined,
37
+ sessionToken: parsed.sessionToken?.trim() || undefined,
38
+ expiresAt: parsed.expiresAt?.trim() || undefined,
39
+ login: parsed.login?.trim() || undefined,
40
+ agentAppId: parsed.agentAppId?.trim() || undefined,
41
+ agentKeyExpiresAt: parsed.agentKeyExpiresAt?.trim() || undefined,
42
+ };
43
+ }
44
+ catch {
45
+ return {};
46
+ }
47
+ }
48
+ const legacy = readLocalCliConfig()?.salesQuery;
49
+ if (!legacy) {
50
+ return {};
51
+ }
52
+ return {
53
+ baseUrl: legacy.baseUrl?.trim() || undefined,
54
+ apiKey: legacy.apiKey?.trim() || undefined,
55
+ };
56
+ }
57
+ function readDotenv() {
58
+ if (dotenvCache) {
59
+ return dotenvCache;
60
+ }
61
+ const out = {};
62
+ const filePath = resolve(process.cwd(), '.env');
63
+ if (!existsSync(filePath)) {
64
+ dotenvCache = out;
65
+ return out;
66
+ }
67
+ const raw = readFileSync(filePath, 'utf8');
68
+ for (const line of raw.split(/\r?\n/)) {
69
+ const trimmed = line.trim();
70
+ if (!trimmed || trimmed.startsWith('#')) {
71
+ continue;
72
+ }
73
+ const eq = trimmed.indexOf('=');
74
+ if (eq <= 0) {
75
+ continue;
76
+ }
77
+ const key = trimmed.slice(0, eq).trim();
78
+ const value = trimmed.slice(eq + 1).trim().replace(/^['"]|['"]$/g, '');
79
+ if (key) {
80
+ out[key] = value;
81
+ }
82
+ }
83
+ dotenvCache = out;
84
+ return out;
85
+ }
86
+ function envTrim(key) {
87
+ const v = (process.env[key] ?? readDotenv()[key])?.trim();
88
+ return v || undefined;
89
+ }
90
+ function readLocalCliConfig() {
91
+ const filePath = configFilePath();
92
+ if (!existsSync(filePath)) {
93
+ return undefined;
94
+ }
95
+ try {
96
+ return JSON.parse(readFileSync(filePath, 'utf8'));
97
+ }
98
+ catch {
99
+ return undefined;
100
+ }
101
+ }
102
+ function readCurrentCliLogin() {
103
+ const config = readLocalCliConfig();
104
+ if (!config) {
105
+ return undefined;
106
+ }
107
+ try {
108
+ const baseUrl = envTrim('VERTU_BASE_URL') ?? config.defaultBaseUrl;
109
+ if (baseUrl && config.profiles?.[baseUrl]?.login) {
110
+ return config.profiles[baseUrl].login;
111
+ }
112
+ return Object.values(config.profiles ?? {}).find((profile) => profile.login)?.login;
113
+ }
114
+ catch {
115
+ return undefined;
116
+ }
117
+ }
118
+ /** 当前 vps-service 请求可携带的用户登录身份。 */
119
+ export function resolveVpsServiceIdentity() {
120
+ const fileCfg = readVpsServiceConnectionSync();
121
+ const agentEnv = readVertuAgentEnvFromProcess();
122
+ return { login: agentEnv.login ?? fileCfg.login ?? readCurrentCliLogin() };
123
+ }
124
+ function normalizeServiceRoot(url) {
125
+ let trimmed = url.trim().replace(/\/+$/, '');
126
+ if (!trimmed) {
127
+ return DEFAULT_VPS_SERVICE_BASE_URL;
128
+ }
129
+ if (!/^https?:\/\//i.test(trimmed)) {
130
+ trimmed = `https://${trimmed}`;
131
+ }
132
+ return trimmed;
133
+ }
134
+ /**
135
+ * vps-service 根 URL:`--base-url` > `VERTU_VPS_SERVICE_URL` > `~/.vps-work/vps-service.json` >
136
+ * (兼容)`VERTU_SALES_QUERY_URL` / 旧 config.json `salesQuery` > 默认生产地址。
137
+ */
138
+ export function resolveVpsServiceUrl(override) {
139
+ const fileCfg = readVpsServiceConnectionSync();
140
+ const url = override?.trim() ||
141
+ envTrim('VERTU_VPS_SERVICE_URL') ||
142
+ envTrim('VERTU_SALES_QUERY_URL') ||
143
+ fileCfg.baseUrl ||
144
+ DEFAULT_VPS_SERVICE_BASE_URL;
145
+ return normalizeServiceRoot(url);
146
+ }
147
+ /** @deprecated 使用 {@link resolveVpsServiceUrl} */
148
+ export function resolveSalesQueryServiceUrl(override) {
149
+ return resolveVpsServiceUrl(override);
150
+ }
151
+ function serviceHeaders(serviceBaseUrl, extra) {
152
+ const headers = {
153
+ 'content-type': 'application/json',
154
+ accept: 'application/json',
155
+ ...vpsServiceAuthHeaders(serviceBaseUrl),
156
+ };
157
+ const fileCfg = readVpsServiceConnectionSync();
158
+ const agentEnv = readVertuAgentEnvFromProcess();
159
+ const agentLogin = agentEnv.login ?? fileCfg.login ?? readCurrentCliLogin();
160
+ if (headers.authorization && agentLogin && !headers['x-vertu-user-login']) {
161
+ headers['x-vps-user-login'] = encodeURIComponent(agentLogin);
162
+ }
163
+ for (const [key, value] of Object.entries(extra ?? {})) {
164
+ const clean = value?.trim();
165
+ if (clean) {
166
+ headers[key] = clean;
167
+ }
168
+ }
169
+ return headers;
170
+ }
171
+ function isVpsServiceSkill(skillKey) {
172
+ const server = resolveServerSkillKey(skillKey);
173
+ return (skillKey === DAILY_SALES_LOCAL ||
174
+ server === DAILY_SALES_SERVER ||
175
+ skillKey === STORE_MANAGEMENT ||
176
+ skillKey === PERSONNEL_LOCAL ||
177
+ server === PERSONNEL_SERVER ||
178
+ skillKey === HR_COMPENSATION_LOCAL ||
179
+ server === HR_COMPENSATION_SERVER ||
180
+ skillKey === OKR_MANAGEMENT_LOCAL ||
181
+ server === OKR_MANAGEMENT_SERVER ||
182
+ skillKey === OKR_LOCAL ||
183
+ server === OKR_SERVER ||
184
+ skillKey === DAILY_REPORT_LOCAL ||
185
+ server === DAILY_REPORT_SERVER ||
186
+ skillKey === DEV_MANAGEMENT_LOCAL ||
187
+ server === DEV_MANAGEMENT_SERVER ||
188
+ skillKey === DEALER_OPS);
189
+ }
190
+ function isDailyReportServiceSkill(skillKey) {
191
+ const server = resolveServerSkillKey(skillKey);
192
+ return skillKey === DAILY_REPORT_LOCAL || server === DAILY_REPORT_SERVER;
193
+ }
194
+ function isOkrServiceSkill(skillKey) {
195
+ const server = resolveServerSkillKey(skillKey);
196
+ return (skillKey === OKR_MANAGEMENT_LOCAL ||
197
+ server === OKR_MANAGEMENT_SERVER ||
198
+ skillKey === OKR_LOCAL ||
199
+ server === OKR_SERVER);
200
+ }
201
+ function isDevManagementServiceSkill(skillKey) {
202
+ const server = resolveServerSkillKey(skillKey);
203
+ return skillKey === DEV_MANAGEMENT_LOCAL || server === DEV_MANAGEMENT_SERVER;
204
+ }
205
+ function vpsServiceRequestLabel(skillKey, scriptKey) {
206
+ if (isDailyReportServiceSkill(skillKey)) {
207
+ return scriptKey ? `Vertu 日报(${scriptKey})` : 'Vertu 日报查询';
208
+ }
209
+ if (isDevManagementServiceSkill(skillKey)) {
210
+ return scriptKey ? `Vertu 研发任务(${scriptKey})` : 'Vertu 研发任务查询';
211
+ }
212
+ if (isOkrServiceSkill(skillKey)) {
213
+ return scriptKey ? `Vertu OKR(${scriptKey})` : 'Vertu OKR 查询';
214
+ }
215
+ if (skillKey === HR_COMPENSATION_LOCAL ||
216
+ resolveServerSkillKey(skillKey) === HR_COMPENSATION_SERVER) {
217
+ return 'Vertu 薪酬查询';
218
+ }
219
+ if (skillKey === PERSONNEL_LOCAL ||
220
+ resolveServerSkillKey(skillKey) === PERSONNEL_SERVER) {
221
+ return 'Vertu 销售人效查询';
222
+ }
223
+ return 'Vertu 数据查询';
224
+ }
225
+ async function parseJsonBody(res, requestLabel) {
226
+ const text = await res.body.text();
227
+ try {
228
+ return JSON.parse(text);
229
+ }
230
+ catch {
231
+ const preview = text.replace(/\s+/g, ' ').trim().slice(0, 240);
232
+ const detail = preview ? `:${preview}` : '';
233
+ throw new VertuError(`${requestLabel} 返回非 JSON(HTTP ${res.statusCode})${detail}`);
234
+ }
235
+ }
236
+ export async function vpsServiceHealth(serviceBaseUrl) {
237
+ const base = resolveVpsServiceUrl(serviceBaseUrl);
238
+ const label = 'Vertu 服务';
239
+ const res = await request(`${base}/health`, {
240
+ method: 'GET',
241
+ headers: serviceHeaders(base, { 'x-vertu-request-kind': 'health' }),
242
+ });
243
+ const data = await parseJsonBody(res, label);
244
+ if (res.statusCode >= 400) {
245
+ throw new VertuError(`${label} /health 失败:HTTP ${res.statusCode}`);
246
+ }
247
+ return data;
248
+ }
249
+ /** @deprecated 使用 {@link vpsServiceHealth} */
250
+ export async function salesQueryHealth(serviceBaseUrl) {
251
+ return vpsServiceHealth(serviceBaseUrl);
252
+ }
253
+ /**
254
+ * 服务端支持该脚本时返回 result;否则 null(可走 Odoo execute)。
255
+ */
256
+ export async function tryVpsServiceExecute(skillKey, scriptKey, params, serviceBaseUrl) {
257
+ const base = resolveVpsServiceUrl(serviceBaseUrl);
258
+ if (!base || !isVpsServiceSkill(skillKey)) {
259
+ return null;
260
+ }
261
+ const requestLabel = vpsServiceRequestLabel(skillKey, scriptKey);
262
+ const serverSkillKey = resolveServerSkillKey(skillKey);
263
+ const res = await request(`${base}/v1/execute`, {
264
+ method: 'POST',
265
+ headers: serviceHeaders(base, {
266
+ 'x-vertu-request-kind': 'execute',
267
+ 'x-vertu-skill-key': serverSkillKey,
268
+ 'x-vertu-script-key': scriptKey,
269
+ }),
270
+ body: JSON.stringify({
271
+ skill_key: serverSkillKey,
272
+ script_key: scriptKey,
273
+ params,
274
+ }),
275
+ });
276
+ const data = await parseJsonBody(res, requestLabel);
277
+ if (res.statusCode === 404) {
278
+ return null;
279
+ }
280
+ if (res.statusCode >= 400) {
281
+ const errMsg = data && typeof data === 'object' && 'error' in data
282
+ ? publicBusinessErrorMessage(data.error)
283
+ : `HTTP ${res.statusCode}`;
284
+ throw new VertuError(`${requestLabel} 失败:${errMsg}${errMsg === 'Unauthorized'
285
+ ? `(Agent/Bot/Hermes 请检查宿主注入的 app_key;仅人工用户可交互执行 \`vps-work auth login\`)`
286
+ : ''}`);
287
+ }
288
+ if (data && typeof data === 'object' && 'result' in data) {
289
+ return publicBusinessResult(data.result);
290
+ }
291
+ return publicBusinessResult(data);
292
+ }
293
+ /** 通用执行:按 catalog 给出的 skill_key/script_key 直打 /v1/execute,不改写键名。 */
294
+ export async function executeVpsServiceScript(skillKey, scriptKey, params, serviceBaseUrl) {
295
+ const base = resolveVpsServiceUrl(serviceBaseUrl);
296
+ const requestLabel = `execute ${skillKey}/${scriptKey}`;
297
+ const res = await request(`${base}/v1/execute`, {
298
+ method: 'POST',
299
+ headers: serviceHeaders(base, {
300
+ 'x-vertu-request-kind': 'execute',
301
+ 'x-vertu-skill-key': skillKey,
302
+ 'x-vertu-script-key': scriptKey,
303
+ }),
304
+ body: JSON.stringify({
305
+ skill_key: skillKey,
306
+ script_key: scriptKey,
307
+ params,
308
+ }),
309
+ });
310
+ const data = await parseJsonBody(res, requestLabel);
311
+ if (res.statusCode >= 400) {
312
+ const errMsg = data && typeof data === 'object' && 'error' in data
313
+ ? publicBusinessErrorMessage(data.error)
314
+ : `HTTP ${res.statusCode}`;
315
+ throw new VertuError(`${requestLabel} 失败:${errMsg}`);
316
+ }
317
+ if (data && typeof data === 'object' && 'result' in data) {
318
+ return publicBusinessResult(data.result);
319
+ }
320
+ return publicBusinessResult(data);
321
+ }
322
+ /** @deprecated 使用 {@link tryVpsServiceExecute} */
323
+ export async function trySalesQueryExecute(skillKey, scriptKey, params, serviceBaseUrl) {
324
+ return tryVpsServiceExecute(skillKey, scriptKey, params, serviceBaseUrl);
325
+ }
326
+ /**
327
+ * 直连 vps-service 的 REST GET 路由(区别于 `/v1/execute` 脚本通道)。
328
+ * 用于 meeting-center 等已由服务直连数据库实现的接口。
329
+ */
330
+ export async function vpsServiceGet(path, query, serviceBaseUrl, requestLabel = 'Vertu 服务') {
331
+ const base = resolveVpsServiceUrl(serviceBaseUrl);
332
+ const qs = new URLSearchParams();
333
+ for (const [key, value] of Object.entries(query)) {
334
+ if (value !== undefined && `${value}`.trim() !== '') {
335
+ qs.set(key, `${value}`);
336
+ }
337
+ }
338
+ const suffix = qs.toString() ? `?${qs.toString()}` : '';
339
+ const res = await request(`${base}${path}${suffix}`, {
340
+ method: 'GET',
341
+ headers: serviceHeaders(base, {
342
+ 'x-vertu-request-kind': 'rest',
343
+ 'x-vertu-rest-path': path,
344
+ }),
345
+ });
346
+ const data = await parseJsonBody(res, requestLabel);
347
+ if (res.statusCode === 401) {
348
+ throw new VertuError(`${requestLabel} 失败:Unauthorized(Agent/Bot/Hermes 请检查宿主注入的 app_key;仅人工用户可交互登录)`);
349
+ }
350
+ if (res.statusCode >= 400) {
351
+ const errMsg = data && typeof data === 'object' && 'error' in data
352
+ ? publicBusinessErrorMessage(data.error)
353
+ : `HTTP ${res.statusCode}`;
354
+ throw new VertuError(`${requestLabel} 失败:${errMsg}`);
355
+ }
356
+ return data;
357
+ }
358
+ /**
359
+ * 直连 vps-service 的 REST POST 路由(区别于 `/v1/execute` 脚本通道与 GET)。
360
+ * 用于 creative(创意工坊生图/生视频/生音频)等由服务直连实现的写接口。
361
+ */
362
+ export async function vpsServicePost(path, body, serviceBaseUrl, requestLabel = 'Vertu 服务') {
363
+ const base = resolveVpsServiceUrl(serviceBaseUrl);
364
+ const res = await request(`${base}${path}`, {
365
+ method: 'POST',
366
+ headers: serviceHeaders(base, {
367
+ 'x-vertu-request-kind': 'rest',
368
+ 'x-vertu-rest-path': path,
369
+ }),
370
+ body: JSON.stringify(body),
371
+ });
372
+ const data = await parseJsonBody(res, requestLabel);
373
+ if (res.statusCode === 401) {
374
+ throw new VertuError(`${requestLabel} 失败:Unauthorized(Agent/Bot/Hermes 请检查宿主注入的 app_key;仅人工用户可交互登录)`);
375
+ }
376
+ if (res.statusCode >= 400) {
377
+ throw new VertuError(`${requestLabel} 失败:${extractRemoteErrorMessage(data, res.statusCode)}`);
378
+ }
379
+ return data;
380
+ }
381
+ /** 直连 vps-service 的 multipart/form-data POST,供上传类公共工具复用。 */
382
+ export async function vpsServiceMultipartPost(path, body, serviceBaseUrl, requestLabel = 'Vertu 服务') {
383
+ const base = resolveVpsServiceUrl(serviceBaseUrl);
384
+ const res = await request(`${base}${path}`, {
385
+ method: 'POST',
386
+ headers: vpsServiceAuthHeaders(base),
387
+ body,
388
+ });
389
+ const data = await parseJsonBody(res, requestLabel);
390
+ if (res.statusCode === 401) {
391
+ throw new VertuError(`${requestLabel} 失败:Unauthorized(Agent/Bot/Hermes 请检查宿主注入的 app_key;仅人工用户可交互登录)`);
392
+ }
393
+ if (res.statusCode >= 400) {
394
+ throw new VertuError(`${requestLabel} 失败:${extractRemoteErrorMessage(data, res.statusCode)}`);
395
+ }
396
+ return data;
397
+ }
398
+ /** 兼容创意工坊等接口两种错误形态:`{error:"..."}` 与 `{error:{message:"..."}}`。 */
399
+ function extractRemoteErrorMessage(data, statusCode) {
400
+ if (data && typeof data === 'object' && 'error' in data) {
401
+ const error = data.error;
402
+ if (typeof error === 'string' && error.trim()) {
403
+ return error;
404
+ }
405
+ if (error && typeof error === 'object' && 'message' in error) {
406
+ const message = error.message;
407
+ if (typeof message === 'string' && message.trim()) {
408
+ return message;
409
+ }
410
+ }
411
+ if (error !== null && error !== undefined) {
412
+ return String(error);
413
+ }
414
+ }
415
+ return `HTTP ${statusCode}`;
416
+ }
417
+ export async function runVpsServiceDataView(viewId, filters, optionsOrLimit, serviceBaseUrl) {
418
+ const base = resolveVpsServiceUrl(serviceBaseUrl);
419
+ const options = typeof optionsOrLimit === 'number'
420
+ ? { limit: optionsOrLimit }
421
+ : (optionsOrLimit ?? {});
422
+ const res = await request(`${base}/v1/data-view/query`, {
423
+ method: 'POST',
424
+ headers: serviceHeaders(base, {
425
+ 'x-vertu-request-kind': 'data-view',
426
+ 'x-vertu-data-view-id': viewId,
427
+ }),
428
+ body: JSON.stringify({
429
+ view_id: viewId,
430
+ filters,
431
+ ...(options.limit !== undefined ? { limit: options.limit } : {}),
432
+ ...(options.page !== undefined ? { page: options.page } : {}),
433
+ ...(options.pageSize !== undefined ? { page_size: options.pageSize } : {}),
434
+ ...(options.groupBy ? { group_by: options.groupBy } : {}),
435
+ }),
436
+ });
437
+ const data = await parseJsonBody(res, 'Vertu 数据视图');
438
+ if (res.statusCode >= 400) {
439
+ const errMsg = data && typeof data === 'object' && 'error' in data
440
+ ? publicBusinessErrorMessage(data.error)
441
+ : `HTTP ${res.statusCode}`;
442
+ throw new VertuError(`Vertu 数据视图查询失败:${errMsg}`, { statusCode: res.statusCode });
443
+ }
444
+ if (data && typeof data === 'object' && 'result' in data) {
445
+ return publicBusinessResult(data.result);
446
+ }
447
+ return publicBusinessResult(data);
448
+ }
449
+ /** @deprecated 使用 {@link runVpsServiceDataView} */
450
+ export async function runSalesQueryDataView(viewId, filters, limit, serviceBaseUrl) {
451
+ return runVpsServiceDataView(viewId, filters, limit, serviceBaseUrl);
452
+ }
@@ -0,0 +1,158 @@
1
+ import { cp, readFile, readdir, rm, stat } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { VertuError } from './errors.js';
5
+ import { packageRootDir, packageSkillsDir } from './paths.js';
6
+ export const AGENT_TARGETS = [
7
+ { id: 'cursor', rootName: '.cursor', aliases: [] },
8
+ { id: 'claude-code', rootName: '.claude', aliases: ['claude'] },
9
+ { id: 'trae', rootName: '.trae', aliases: [] },
10
+ { id: 'trae-cn', rootName: '.trae-cn', aliases: [] },
11
+ { id: 'openclaw', rootName: '.openclaw', aliases: ['open-claw', 'claw'] },
12
+ { id: 'hermes', rootName: '.hermes', aliases: ['hermes-agent', 'nous'] },
13
+ ];
14
+ /** `skills/<key>/SKILL.md` 内 frontmatter 的 description(取首个 description 行/块)。 */
15
+ function parseFrontmatterDescription(md) {
16
+ const match = /^---\s*\n([\s\S]*?)\n---/.exec(md);
17
+ if (!match) {
18
+ return '';
19
+ }
20
+ const body = match[1] ?? '';
21
+ const line = /^description:\s*(.+)$/m.exec(body);
22
+ if (!line) {
23
+ return '';
24
+ }
25
+ const raw = (line[1] ?? '').trim();
26
+ if (raw === '|' || raw === '>') {
27
+ const block = new RegExp('^description:\\s*[|>]\\s*\\n([\\s\\S]*?)(?=\\n\\S|$)', 'm').exec(body);
28
+ return (block?.[1] ?? '')
29
+ .split('\n')
30
+ .map((l) => l.trim())
31
+ .filter(Boolean)
32
+ .join(' ');
33
+ }
34
+ return raw.replace(/^["']|["']$/g, '').trim();
35
+ }
36
+ /**
37
+ * 扫描包内 `skills/` 下所有含 `SKILL.md` 的目录,组成公开 skill 目录。
38
+ * 不依赖任何远端服务或登录态。
39
+ */
40
+ export async function loadPublicSkills() {
41
+ const root = packageSkillsDir();
42
+ let entries;
43
+ try {
44
+ entries = await readdir(root);
45
+ }
46
+ catch (err) {
47
+ if (err.code === 'ENOENT') {
48
+ throw new VertuError(`未找到包内 skills 目录:${root}(包安装可能不完整)。`);
49
+ }
50
+ throw err;
51
+ }
52
+ const skills = [];
53
+ for (const name of entries.sort()) {
54
+ const sourceDir = join(root, name);
55
+ const skillFile = join(sourceDir, 'SKILL.md');
56
+ try {
57
+ const info = await stat(skillFile);
58
+ if (!info.isFile()) {
59
+ continue;
60
+ }
61
+ }
62
+ catch {
63
+ continue;
64
+ }
65
+ const md = await readFile(skillFile, 'utf8');
66
+ skills.push({ key: name, description: parseFrontmatterDescription(md), sourceDir });
67
+ }
68
+ return skills;
69
+ }
70
+ /** 把 `--agent` 输入(id/别名,逗号或空格分隔;`all` 表示全部)解析为目标列表。 */
71
+ export function resolveAgentTargets(input) {
72
+ if (!input?.trim()) {
73
+ return [];
74
+ }
75
+ const tokens = input
76
+ .split(/[,\s]+/)
77
+ .map((t) => t.trim().toLowerCase())
78
+ .filter(Boolean);
79
+ if (tokens.includes('all')) {
80
+ return [...AGENT_TARGETS];
81
+ }
82
+ const resolved = [];
83
+ for (const token of tokens) {
84
+ const hit = AGENT_TARGETS.find((t) => t.id === token || t.aliases.includes(token));
85
+ if (!hit) {
86
+ throw new VertuError(`未知 --agent "${token}"。可用:${AGENT_TARGETS.map((t) => t.id).join(', ')}, all。`);
87
+ }
88
+ if (!resolved.some((t) => t.id === hit.id)) {
89
+ resolved.push(hit);
90
+ }
91
+ }
92
+ return resolved;
93
+ }
94
+ function agentSkillsRoot(target) {
95
+ return join(homedir(), target.rootName, 'skills');
96
+ }
97
+ /** HOME 下已存在根目录(`~/.cursor` 等)的 Agent —— 视为"用户在用"。 */
98
+ export async function detectInstalledAgents() {
99
+ const found = [];
100
+ for (const target of AGENT_TARGETS) {
101
+ try {
102
+ const info = await stat(join(homedir(), target.rootName));
103
+ if (info.isDirectory()) {
104
+ found.push(target);
105
+ }
106
+ }
107
+ catch {
108
+ // 根目录不存在 → 用户未使用该 Agent,跳过
109
+ }
110
+ }
111
+ return found;
112
+ }
113
+ export function resolveDestinations(targets, dirs) {
114
+ const dests = [];
115
+ const seen = new Set();
116
+ const push = (label, skillsRoot) => {
117
+ if (seen.has(skillsRoot)) {
118
+ return;
119
+ }
120
+ seen.add(skillsRoot);
121
+ dests.push({ label, skillsRoot });
122
+ };
123
+ for (const target of targets) {
124
+ push(target.id, agentSkillsRoot(target));
125
+ }
126
+ for (const raw of dirs) {
127
+ const expanded = raw.trim().startsWith('~')
128
+ ? join(homedir(), raw.trim().slice(1).replace(/^[\\/]/, ''))
129
+ : raw.trim();
130
+ if (expanded) {
131
+ push(`dir:${expanded}`, expanded);
132
+ }
133
+ }
134
+ return dests;
135
+ }
136
+ /** 把单个公开 skill 递归拷贝到目的地 `<skillsRoot>/<key>/`(覆盖式同步)。 */
137
+ export async function copySkillTo(skill, skillsRoot) {
138
+ const dest = join(skillsRoot, skill.key);
139
+ await rm(dest, { recursive: true, force: true });
140
+ await cp(skill.sourceDir, dest, { recursive: true });
141
+ return dest;
142
+ }
143
+ /** 从目的地移除某个 skill 目录;返回是否真的删除过。 */
144
+ export async function removeSkillFrom(key, skillsRoot) {
145
+ const dest = join(skillsRoot, key);
146
+ try {
147
+ await stat(dest);
148
+ }
149
+ catch {
150
+ return false;
151
+ }
152
+ await rm(dest, { recursive: true, force: true });
153
+ return true;
154
+ }
155
+ /** 包根目录(供 install 提示用户公开 skill 源位置)。 */
156
+ export function publicSkillsSourceRoot() {
157
+ return join(packageRootDir(), 'skills');
158
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * 仓库与文档使用 vps-*;遗留 Odoo execute 仍多为 odoo-*,在此统一映射。
3
+ */
4
+ const LOCAL_TO_SERVER = {
5
+ 'vps-domain-skill-creator': 'odoo-vps-domain-skill-creator',
6
+ 'vps-store-management': 'vps-store-management',
7
+ 'vps-dealer-ops': 'vps-dealer-ops',
8
+ };
9
+ /** 传给 /ai/skills/v1/execute 等 API 的 skill_key */
10
+ export function resolveServerSkillKey(localKey) {
11
+ const aliased = LOCAL_TO_SERVER[localKey];
12
+ if (aliased) {
13
+ return aliased;
14
+ }
15
+ if (localKey.startsWith('vps-')) {
16
+ return `odoo-${localKey.slice(4)}`;
17
+ }
18
+ return localKey;
19
+ }