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,382 @@
1
+ import { request } from 'undici';
2
+ import { VertuError } from './errors.js';
3
+ import { agentKeyAccount, deleteSecret, getSecret, isKeychainAvailable, setSecret, vpsSessionTokenAccount, } from './keychain.js';
4
+ import { DEFAULT_VPS_SERVICE_BASE_URL } from './sales-query-client.js';
5
+ import { readVpsServiceConfig, writeVpsServiceConfig, } from './vps-service-config.js';
6
+ import { CLI_AUTH_CHANNEL } from './cli-bin.js';
7
+ import { VERTU_EXPECTED_USER_ID_ENV } from './vertu-agent-env.js';
8
+ /** 过期时间是否仍有效;空或不可解析按长期有效处理(兼容旧逻辑)。 */
9
+ function notExpired(expiresAt) {
10
+ const trimmed = expiresAt?.trim();
11
+ if (!trimmed) {
12
+ return true;
13
+ }
14
+ const parsed = Date.parse(trimmed);
15
+ if (!Number.isFinite(parsed)) {
16
+ return true;
17
+ }
18
+ return parsed > Date.now();
19
+ }
20
+ /**
21
+ * 综合本地配置与宿主环境变量,解析当前 vps-service 鉴权状态。
22
+ * 优先使用有效的登录 session;否则回落到有效的长效/代理 agent key。
23
+ */
24
+ export function resolveVpsAuthState(cfg, env) {
25
+ const sessionToken = cfg.sessionToken?.trim() || undefined;
26
+ const sessionActive = Boolean(sessionToken) && notExpired(cfg.expiresAt);
27
+ const agentKey = env.appKey?.trim() || cfg.agentAppKey?.trim() || undefined;
28
+ const agentAppId = env.appId?.trim() || cfg.agentAppId?.trim() || null;
29
+ const agentKeyValid = Boolean(agentKey) && agentKey.startsWith('vta_') && notExpired(cfg.agentKeyExpiresAt);
30
+ const login = cfg.login?.trim() || env.login?.trim() || null;
31
+ if (sessionActive) {
32
+ return {
33
+ mode: 'session',
34
+ active: true,
35
+ token: sessionToken ?? null,
36
+ login,
37
+ agentAppId,
38
+ expiresAt: cfg.expiresAt?.trim() || null,
39
+ neverExpires: false,
40
+ };
41
+ }
42
+ if (agentKeyValid) {
43
+ return {
44
+ mode: 'agent',
45
+ active: true,
46
+ token: agentKey ?? null,
47
+ login,
48
+ agentAppId,
49
+ expiresAt: cfg.agentKeyExpiresAt?.trim() || null,
50
+ neverExpires: !cfg.agentKeyExpiresAt?.trim(),
51
+ };
52
+ }
53
+ return {
54
+ mode: 'none',
55
+ active: false,
56
+ token: null,
57
+ login,
58
+ agentAppId,
59
+ expiresAt: null,
60
+ neverExpires: false,
61
+ };
62
+ }
63
+ function stringOrNull(value) {
64
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
65
+ }
66
+ function numberOrNull(value) {
67
+ return typeof value === 'number' && Number.isFinite(value) ? value : null;
68
+ }
69
+ function pickFromUserOrInfo(user, info, camel, snake) {
70
+ return stringOrNull(user[camel]) ?? stringOrNull(info?.[snake]);
71
+ }
72
+ /** 从服务端 user / userinfo 提取企业身份字段。 */
73
+ export function pickVpsServiceUserInfo(user, userinfo) {
74
+ return {
75
+ userId: numberOrNull(user.userId) ?? numberOrNull(userinfo?.user_id),
76
+ tenantId: pickFromUserOrInfo(user, userinfo, 'tenantId', 'tenant_id'),
77
+ tenantName: pickFromUserOrInfo(user, userinfo, 'tenantName', 'tenant_name'),
78
+ tenantType: pickFromUserOrInfo(user, userinfo, 'tenantType', 'tenant_type'),
79
+ membershipId: pickFromUserOrInfo(user, userinfo, 'membershipId', 'membership_id'),
80
+ membershipRole: pickFromUserOrInfo(user, userinfo, 'membershipRole', 'membership_role'),
81
+ productProfile: pickFromUserOrInfo(user, userinfo, 'productProfile', 'product_profile'),
82
+ login: stringOrNull(user.login) ?? stringOrNull(userinfo?.login) ?? '',
83
+ employeeName: stringOrNull(user.employeeName) ?? stringOrNull(userinfo?.employee_name),
84
+ departmentName: stringOrNull(user.departmentName) ?? stringOrNull(userinfo?.department_name),
85
+ departmentPath: stringOrNull(user.departmentPath) ?? stringOrNull(userinfo?.department_path),
86
+ role: stringOrNull(user.role) ?? stringOrNull(userinfo?.role),
87
+ };
88
+ }
89
+ function normalizeServiceRoot(url) {
90
+ let trimmed = url.trim().replace(/\/+$/, '');
91
+ if (!/^https?:\/\//i.test(trimmed)) {
92
+ trimmed = `https://${trimmed}`;
93
+ }
94
+ return trimmed;
95
+ }
96
+ export async function loginVpsServiceWithPassword(serviceBaseUrl, login, password) {
97
+ const base = normalizeServiceRoot(serviceBaseUrl);
98
+ const headers = {
99
+ 'content-type': 'application/json',
100
+ accept: 'application/json',
101
+ 'x-vertu-auth-channel': CLI_AUTH_CHANNEL,
102
+ 'user-agent': CLI_AUTH_CHANNEL,
103
+ };
104
+ const res = await request(`${base}/v1/auth/login`, {
105
+ method: 'POST',
106
+ headers,
107
+ body: JSON.stringify({ login, password }),
108
+ });
109
+ const text = await res.body.text();
110
+ let data;
111
+ try {
112
+ data = JSON.parse(text);
113
+ }
114
+ catch {
115
+ throw new VertuError(`Vertu 登录失败:非 JSON 响应(HTTP ${res.statusCode})`);
116
+ }
117
+ if (res.statusCode >= 400) {
118
+ const errMsg = data && typeof data === 'object' && 'error' in data
119
+ ? String(data.error)
120
+ : `HTTP ${res.statusCode}`;
121
+ throw new VertuError(`Vertu 登录失败:${errMsg}`);
122
+ }
123
+ if (!data || typeof data !== 'object') {
124
+ throw new VertuError('Vertu 登录失败:响应格式无效');
125
+ }
126
+ const payload = data;
127
+ const token = payload.token?.trim();
128
+ const expiresAt = payload.expires_at?.trim();
129
+ const userLogin = payload.user?.login?.trim() ?? login;
130
+ if (!token || !expiresAt) {
131
+ throw new VertuError('Vertu 登录失败:未返回 token(请升级服务端)');
132
+ }
133
+ return {
134
+ login: userLogin,
135
+ token,
136
+ expiresAt,
137
+ user: (payload.user ?? {}),
138
+ };
139
+ }
140
+ /** 查询当前会话用户(GET /v1/auth/me)。 */
141
+ export async function fetchVpsServiceMe(serviceBaseUrl, sessionToken) {
142
+ const base = normalizeServiceRoot(serviceBaseUrl);
143
+ const expectedUserId = process.env[VERTU_EXPECTED_USER_ID_ENV]?.trim();
144
+ const res = await request(`${base}/v1/auth/me`, {
145
+ method: 'GET',
146
+ headers: {
147
+ accept: 'application/json',
148
+ authorization: `Bearer ${sessionToken}`,
149
+ 'x-vertu-auth-channel': CLI_AUTH_CHANNEL,
150
+ 'user-agent': CLI_AUTH_CHANNEL,
151
+ ...(expectedUserId && /^[1-9]\d*$/.test(expectedUserId)
152
+ ? { 'x-vertu-expected-user-id': expectedUserId }
153
+ : {}),
154
+ },
155
+ });
156
+ const text = await res.body.text();
157
+ let data;
158
+ try {
159
+ data = JSON.parse(text);
160
+ }
161
+ catch {
162
+ throw new VertuError(`Vertu 身份查询失败:非 JSON 响应(HTTP ${res.statusCode})`);
163
+ }
164
+ if (res.statusCode >= 400) {
165
+ throw new VertuError(`Vertu 身份查询失败:HTTP ${res.statusCode}`);
166
+ }
167
+ const payload = data;
168
+ if (!payload.user || typeof payload.user !== 'object') {
169
+ throw new VertuError('Vertu 身份查询失败:响应缺少 user');
170
+ }
171
+ return pickVpsServiceUserInfo(payload.user, payload.userinfo);
172
+ }
173
+ export async function bindVpsServiceAgentCredential(serviceBaseUrl, sessionToken, appId) {
174
+ const base = normalizeServiceRoot(serviceBaseUrl);
175
+ const res = await request(`${base}/v1/auth/agent-bind`, {
176
+ method: 'POST',
177
+ headers: {
178
+ 'content-type': 'application/json',
179
+ accept: 'application/json',
180
+ authorization: `Bearer ${sessionToken}`,
181
+ 'x-vertu-auth-channel': CLI_AUTH_CHANNEL,
182
+ 'user-agent': CLI_AUTH_CHANNEL,
183
+ },
184
+ body: JSON.stringify({ app_id: appId }),
185
+ });
186
+ const text = await res.body.text();
187
+ let data;
188
+ try {
189
+ data = JSON.parse(text);
190
+ }
191
+ catch {
192
+ throw new VertuError(`Agent 绑定失败:非 JSON(HTTP ${res.statusCode})`);
193
+ }
194
+ if (res.statusCode >= 400) {
195
+ const errMsg = data && typeof data === 'object' && 'error' in data
196
+ ? String(data.error)
197
+ : `HTTP ${res.statusCode}`;
198
+ throw new VertuError(`Agent 绑定失败:${errMsg}`);
199
+ }
200
+ if (!data || typeof data !== 'object') {
201
+ throw new VertuError('Agent 绑定失败:响应无效');
202
+ }
203
+ const payload = data;
204
+ const boundAppId = payload.app_id?.trim();
205
+ const appKey = payload.app_key?.trim();
206
+ const login = payload.login?.trim();
207
+ // 长效密钥 expires_at 返回 null,属正常情况,不作为缺失校验。
208
+ const expiresAt = payload.expires_at?.trim() || null;
209
+ if (!boundAppId || !appKey || !login) {
210
+ throw new VertuError('Agent 绑定失败:缺少 app_key(请先执行数据库迁移 005)');
211
+ }
212
+ return { appId: boundAppId, appKey, login, expiresAt };
213
+ }
214
+ export async function persistVpsServiceAgentCredential(serviceBaseUrl, bound) {
215
+ const root = normalizeServiceRoot(serviceBaseUrl);
216
+ const existing = await readVpsServiceConfig();
217
+ if (existing.agentAppId && existing.agentAppId !== bound.appId) {
218
+ if (await isKeychainAvailable()) {
219
+ await deleteSecret(agentKeyAccount(root, existing.agentAppId));
220
+ }
221
+ }
222
+ if (await isKeychainAvailable()) {
223
+ await setSecret(agentKeyAccount(root, bound.appId), bound.appKey);
224
+ }
225
+ await writeVpsServiceConfig({
226
+ version: 1,
227
+ baseUrl: root,
228
+ login: bound.login,
229
+ sessionToken: existing.sessionToken,
230
+ expiresAt: existing.expiresAt,
231
+ apiKey: existing.apiKey,
232
+ agentAppId: bound.appId,
233
+ agentAppKey: bound.appKey,
234
+ agentKeyExpiresAt: bound.expiresAt ?? undefined,
235
+ });
236
+ }
237
+ export async function loadStoredAgentKey(serviceBaseUrl, appId) {
238
+ const root = normalizeServiceRoot(serviceBaseUrl);
239
+ const config = await readVpsServiceConfig();
240
+ if (normalizeServiceRoot(config.baseUrl ?? '') === root &&
241
+ config.agentAppId === appId &&
242
+ config.agentAppKey?.trim()) {
243
+ return config.agentAppKey.trim();
244
+ }
245
+ if (!(await isKeychainAvailable())) {
246
+ return undefined;
247
+ }
248
+ const raw = await getSecret(agentKeyAccount(root, appId));
249
+ return raw?.trim() || undefined;
250
+ }
251
+ export async function persistVpsServiceSession(serviceBaseUrl, session) {
252
+ const existing = await readVpsServiceConfig();
253
+ const root = normalizeServiceRoot(serviceBaseUrl);
254
+ if (await isKeychainAvailable()) {
255
+ await setSecret(vpsSessionTokenAccount(root), session.token);
256
+ }
257
+ const next = {
258
+ version: 1,
259
+ baseUrl: root,
260
+ login: session.login,
261
+ sessionToken: session.token,
262
+ expiresAt: session.expiresAt,
263
+ apiKey: existing.apiKey,
264
+ agentAppId: existing.agentAppId,
265
+ agentAppKey: existing.agentAppKey,
266
+ agentKeyExpiresAt: existing.agentKeyExpiresAt,
267
+ };
268
+ await writeVpsServiceConfig(next);
269
+ }
270
+ export async function clearVpsServiceSession(forgetLogin = false) {
271
+ const existing = await readVpsServiceConfig();
272
+ await writeVpsServiceConfig({
273
+ version: 1,
274
+ baseUrl: existing.baseUrl,
275
+ apiKey: existing.apiKey,
276
+ login: forgetLogin ? undefined : existing.login,
277
+ });
278
+ }
279
+ export async function logoutVpsServiceRemote() {
280
+ const existing = await readVpsServiceConfig();
281
+ const token = existing.sessionToken?.trim();
282
+ const base = existing.baseUrl?.trim();
283
+ if (!token || !base) {
284
+ return;
285
+ }
286
+ const root = normalizeServiceRoot(base);
287
+ try {
288
+ await request(`${root}/v1/auth/logout`, {
289
+ method: 'POST',
290
+ headers: {
291
+ authorization: `Bearer ${token}`,
292
+ 'x-vertu-auth-channel': CLI_AUTH_CHANNEL,
293
+ 'user-agent': CLI_AUTH_CHANNEL,
294
+ },
295
+ });
296
+ }
297
+ catch {
298
+ /* 仍清除本地会话 */
299
+ }
300
+ }
301
+ function parseJsonObject(text, statusCode, label) {
302
+ let data;
303
+ try {
304
+ data = JSON.parse(text);
305
+ }
306
+ catch {
307
+ throw new VertuError(`${label}失败:非 JSON(HTTP ${statusCode})`);
308
+ }
309
+ if (statusCode >= 400) {
310
+ const errMsg = data && typeof data === 'object' && 'error' in data
311
+ ? String(data.error)
312
+ : `HTTP ${statusCode}`;
313
+ throw new VertuError(`${label}失败:${errMsg}`);
314
+ }
315
+ if (!data || typeof data !== 'object') {
316
+ throw new VertuError(`${label}失败:响应无效`);
317
+ }
318
+ return data;
319
+ }
320
+ /** 列出当前账号可切换的企业。 */
321
+ export async function fetchVpsServiceTenants(serviceBaseUrl, sessionToken) {
322
+ const base = normalizeServiceRoot(serviceBaseUrl);
323
+ const res = await request(`${base}/v1/bootstrap`, {
324
+ method: 'GET',
325
+ headers: {
326
+ accept: 'application/json',
327
+ authorization: `Bearer ${sessionToken}`,
328
+ 'x-vertu-auth-channel': CLI_AUTH_CHANNEL,
329
+ 'user-agent': CLI_AUTH_CHANNEL,
330
+ },
331
+ });
332
+ const payload = parseJsonObject(await res.body.text(), res.statusCode, '企业列表');
333
+ const tenants = payload.tenants;
334
+ if (!Array.isArray(tenants)) {
335
+ throw new VertuError('企业列表失败:响应缺少 tenants');
336
+ }
337
+ return tenants.map((item) => {
338
+ const row = item;
339
+ return {
340
+ id: stringOrNull(row.id) ?? '',
341
+ key: stringOrNull(row.key) ?? undefined,
342
+ name: stringOrNull(row.name) ?? '',
343
+ type: stringOrNull(row.type) ?? 'internal',
344
+ membership_id: stringOrNull(row.membership_id) ?? '',
345
+ role: stringOrNull(row.role) ?? 'member',
346
+ active: row.active === true,
347
+ };
348
+ });
349
+ }
350
+ /** 切换当前 membership,返回新会话 token。 */
351
+ export async function switchVpsServiceTenant(serviceBaseUrl, sessionToken, tenantId) {
352
+ const base = normalizeServiceRoot(serviceBaseUrl);
353
+ const res = await request(`${base}/v1/auth/tenant/switch`, {
354
+ method: 'POST',
355
+ headers: {
356
+ 'content-type': 'application/json',
357
+ accept: 'application/json',
358
+ authorization: `Bearer ${sessionToken}`,
359
+ 'x-vertu-auth-channel': CLI_AUTH_CHANNEL,
360
+ 'user-agent': CLI_AUTH_CHANNEL,
361
+ },
362
+ body: JSON.stringify({ tenant_id: tenantId }),
363
+ });
364
+ const payload = parseJsonObject(await res.body.text(), res.statusCode, '切换企业');
365
+ const token = stringOrNull(payload.token);
366
+ const expiresAt = stringOrNull(payload.expires_at);
367
+ const user = (payload.user && typeof payload.user === 'object'
368
+ ? payload.user
369
+ : {});
370
+ if (!token || !expiresAt) {
371
+ throw new VertuError('切换企业失败:未返回 token');
372
+ }
373
+ return {
374
+ login: stringOrNull(user.login) ?? '',
375
+ token,
376
+ expiresAt,
377
+ user,
378
+ };
379
+ }
380
+ export function defaultVpsServiceLoginUrl() {
381
+ return DEFAULT_VPS_SERVICE_BASE_URL;
382
+ }
@@ -0,0 +1,83 @@
1
+ import { mkdir, writeFile, chmod } from 'node:fs/promises';
2
+ import { dirname } from 'node:path';
3
+ import { z } from 'zod';
4
+ import { configFilePath, vpsServiceConfigFilePath } from './paths.js';
5
+ import { readUtf8File } from '../utils/read-text.js';
6
+ const VpsServiceConfigSchema = z.object({
7
+ version: z.literal(1).default(1),
8
+ baseUrl: z.string().url().optional(),
9
+ login: z.string().optional(),
10
+ sessionToken: z.string().optional(),
11
+ expiresAt: z.string().optional(),
12
+ agentAppId: z.string().optional(),
13
+ /** 代理密钥明文(文件态);keychain 不可用的服务运行时(如 Hermes)从此处读取。 */
14
+ agentAppKey: z.string().optional(),
15
+ agentKeyExpiresAt: z.string().optional(),
16
+ apiKey: z.string().optional(),
17
+ });
18
+ export const EMPTY_VPS_SERVICE_CONFIG = { version: 1 };
19
+ async function tryRestrictPermissions(path) {
20
+ if (process.platform === 'win32') {
21
+ return;
22
+ }
23
+ try {
24
+ await chmod(path, 0o600);
25
+ }
26
+ catch {
27
+ /* best-effort */
28
+ }
29
+ }
30
+ async function migrateLegacySalesQueryBlock() {
31
+ try {
32
+ const raw = await readUtf8File(configFilePath());
33
+ const parsed = JSON.parse(raw);
34
+ const legacy = parsed.salesQuery;
35
+ if (!legacy?.baseUrl && !legacy?.apiKey) {
36
+ return null;
37
+ }
38
+ const migrated = {
39
+ version: 1,
40
+ baseUrl: legacy.baseUrl,
41
+ apiKey: legacy.apiKey,
42
+ };
43
+ await writeVpsServiceConfig(migrated);
44
+ const rawAgain = await readUtf8File(configFilePath());
45
+ const obj = JSON.parse(rawAgain);
46
+ if ('salesQuery' in obj) {
47
+ delete obj.salesQuery;
48
+ await writeFile(configFilePath(), `${JSON.stringify(obj, null, 2)}\n`, {
49
+ encoding: 'utf8',
50
+ mode: 0o600,
51
+ });
52
+ await tryRestrictPermissions(configFilePath());
53
+ }
54
+ return migrated;
55
+ }
56
+ catch {
57
+ return null;
58
+ }
59
+ }
60
+ export async function readVpsServiceConfig() {
61
+ const path = vpsServiceConfigFilePath();
62
+ try {
63
+ const raw = await readUtf8File(path);
64
+ return VpsServiceConfigSchema.parse(JSON.parse(raw));
65
+ }
66
+ catch (err) {
67
+ if (err.code !== 'ENOENT') {
68
+ throw err;
69
+ }
70
+ const migrated = await migrateLegacySalesQueryBlock();
71
+ if (migrated) {
72
+ return migrated;
73
+ }
74
+ return { ...EMPTY_VPS_SERVICE_CONFIG };
75
+ }
76
+ }
77
+ export async function writeVpsServiceConfig(config) {
78
+ const path = vpsServiceConfigFilePath();
79
+ await mkdir(dirname(path), { recursive: true });
80
+ const payload = VpsServiceConfigSchema.parse(config);
81
+ await writeFile(path, `${JSON.stringify(payload, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
82
+ await tryRestrictPermissions(path);
83
+ }
@@ -0,0 +1,187 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { execFileSync } from 'node:child_process';
3
+ import { CLI_AUTH_CHANNEL, CLI_KEYCHAIN_SERVICE } from './cli-bin.js';
4
+ import { readVertuAgentEnvFromProcess, VERTU_ACCESS_TOKEN_ENV, VERTU_EXPECTED_USER_ID_ENV, } from './vertu-agent-env.js';
5
+ import { vpsServiceConfigFilePath } from './paths.js';
6
+ const KEYCHAIN_SERVICE_NAME = CLI_KEYCHAIN_SERVICE;
7
+ const DEFAULT_VPS_SERVICE_ORIGIN = 'https://vps-service.vertu.cn';
8
+ const HERMES_TENANT_API_HOST = 'tenant-api';
9
+ const HERMES_TENANT_API_PORT = '8080';
10
+ let dotenvCache = null;
11
+ function readDotenv() {
12
+ if (dotenvCache) {
13
+ return dotenvCache;
14
+ }
15
+ const out = {};
16
+ dotenvCache = out;
17
+ return out;
18
+ }
19
+ function envTrim(key) {
20
+ const v = (process.env[key] ?? readDotenv()[key])?.trim();
21
+ return v || undefined;
22
+ }
23
+ function isVpsSessionExpired(expiresAt) {
24
+ if (!expiresAt) {
25
+ return true;
26
+ }
27
+ const parsed = Date.parse(expiresAt);
28
+ return !Number.isFinite(parsed) || parsed <= Date.now() + 30_000;
29
+ }
30
+ function isAgentKeyExpired(expiresAt) {
31
+ if (!expiresAt) {
32
+ return true;
33
+ }
34
+ const parsed = Date.parse(expiresAt);
35
+ return !Number.isFinite(parsed) || parsed <= Date.now() + 30_000;
36
+ }
37
+ function normalizeServiceRoot(url) {
38
+ const raw = url?.trim();
39
+ if (!raw) {
40
+ return undefined;
41
+ }
42
+ let trimmed = raw.replace(/\/+$/, '');
43
+ if (!/^https?:\/\//i.test(trimmed)) {
44
+ trimmed = `https://${trimmed}`;
45
+ }
46
+ return trimmed;
47
+ }
48
+ function parsedCredentialOrigin(raw) {
49
+ const normalized = normalizeServiceRoot(raw);
50
+ if (!normalized)
51
+ return undefined;
52
+ try {
53
+ const url = new URL(normalized);
54
+ const loopback = ['localhost', '127.0.0.1', '[::1]', '::1'].includes(url.hostname.toLowerCase());
55
+ const hermesTenantApi = url.protocol === 'http:'
56
+ && url.hostname.toLowerCase() === HERMES_TENANT_API_HOST
57
+ && url.port === HERMES_TENANT_API_PORT;
58
+ if (url.username || url.password || (url.protocol !== 'https:' && !(url.protocol === 'http:' && (loopback || hermesTenantApi)))) {
59
+ return undefined;
60
+ }
61
+ return url.origin.toLowerCase();
62
+ }
63
+ catch {
64
+ return undefined;
65
+ }
66
+ }
67
+ function readMacKeychainAgentKeySync(serviceBaseUrl, appId) {
68
+ const root = normalizeServiceRoot(serviceBaseUrl);
69
+ const agentAppId = appId?.trim();
70
+ if (process.platform !== 'darwin' || !root || !agentAppId) {
71
+ return undefined;
72
+ }
73
+ try {
74
+ const account = `vps-agent:${root}:${agentAppId}`;
75
+ const raw = execFileSync('security', ['find-generic-password', '-s', KEYCHAIN_SERVICE_NAME, '-a', account, '-w'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
76
+ return raw.trim() || undefined;
77
+ }
78
+ catch {
79
+ return undefined;
80
+ }
81
+ }
82
+ function readVpsServiceConnectionSync() {
83
+ const path = vpsServiceConfigFilePath();
84
+ if (!existsSync(path)) {
85
+ return {};
86
+ }
87
+ try {
88
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
89
+ const baseUrl = parsed.baseUrl?.trim() || undefined;
90
+ const agentAppId = parsed.agentAppId?.trim() || undefined;
91
+ return {
92
+ baseUrl,
93
+ sessionToken: parsed.sessionToken?.trim() || undefined,
94
+ expiresAt: parsed.expiresAt?.trim() || undefined,
95
+ agentAppId,
96
+ agentAppKey: parsed.agentAppKey?.trim() ||
97
+ readMacKeychainAgentKeySync(baseUrl, agentAppId),
98
+ agentKeyExpiresAt: parsed.agentKeyExpiresAt?.trim() || undefined,
99
+ login: parsed.login?.trim() || undefined,
100
+ apiKey: parsed.apiKey?.trim() || undefined,
101
+ };
102
+ }
103
+ catch {
104
+ return {};
105
+ }
106
+ }
107
+ /**
108
+ * 凭证只绑定到官方地址或明确配置的 HTTPS/loopback origin。
109
+ * 临时 `--base-url` 不会自动成为可信目标,避免把 Session/Agent Key 发往任意站点。
110
+ */
111
+ export function isTrustedVpsServiceCredentialTarget(serviceBaseUrl) {
112
+ if (!serviceBaseUrl?.trim())
113
+ return true;
114
+ const targetOrigin = parsedCredentialOrigin(serviceBaseUrl);
115
+ if (!targetOrigin)
116
+ return false;
117
+ const fileCfg = readVpsServiceConnectionSync();
118
+ const agentEnv = readVertuAgentEnvFromProcess();
119
+ const configured = [
120
+ DEFAULT_VPS_SERVICE_ORIGIN,
121
+ fileCfg.baseUrl,
122
+ agentEnv.serviceUrl,
123
+ envTrim('VERTU_SALES_QUERY_URL'),
124
+ ...(envTrim('VERTU_VPS_SERVICE_TRUSTED_ORIGINS') ?? '').split(',').map((item) => item.trim()),
125
+ ];
126
+ const trustedOrigins = new Set(configured.map(parsedCredentialOrigin).filter((item) => Boolean(item)));
127
+ return trustedOrigins.has(targetOrigin);
128
+ }
129
+ /** vps-service 请求鉴权头(session / 智能体 key / API Key)。 */
130
+ export function vpsServiceAuthHeaders(serviceBaseUrl) {
131
+ const headers = {
132
+ 'x-vertu-auth-channel': CLI_AUTH_CHANNEL,
133
+ 'user-agent': CLI_AUTH_CHANNEL,
134
+ };
135
+ if (!isTrustedVpsServiceCredentialTarget(serviceBaseUrl)) {
136
+ return headers;
137
+ }
138
+ const expectedUserId = envTrim(VERTU_EXPECTED_USER_ID_ENV);
139
+ if (expectedUserId && /^[1-9]\d*$/.test(expectedUserId)) {
140
+ headers['x-vertu-expected-user-id'] = expectedUserId;
141
+ }
142
+ const fileCfg = readVpsServiceConnectionSync();
143
+ const agentEnv = readVertuAgentEnvFromProcess();
144
+ const accessTokenEnv = envTrim(VERTU_ACCESS_TOKEN_ENV);
145
+ const persistedSessionToken = fileCfg.sessionToken && !isVpsSessionExpired(fileCfg.expiresAt)
146
+ ? fileCfg.sessionToken
147
+ : undefined;
148
+ // keychain 不可用的服务运行时(如 Hermes)从文件读取明文 agent key 作为兜底。
149
+ const agentAppKey = agentEnv.appKey ?? fileCfg.agentAppKey;
150
+ const agentAppId = agentEnv.appId ?? fileCfg.agentAppId;
151
+ const agentLogin = agentEnv.login ?? fileCfg.login;
152
+ const persistedAgentKeyValid = agentAppKey &&
153
+ agentAppKey.startsWith('vta_') &&
154
+ (!fileCfg.agentKeyExpiresAt || !isAgentKeyExpired(fileCfg.agentKeyExpiresAt));
155
+ const serviceApiKey = envTrim('VERTU_VPS_SERVICE_API_KEY') ?? envTrim('VERTU_SALES_QUERY_API_KEY') ?? fileCfg.apiKey;
156
+ if (accessTokenEnv) {
157
+ headers.authorization = `Bearer ${accessTokenEnv}`;
158
+ }
159
+ else if (agentEnv.appKey) {
160
+ // Agent/Skill hosts must not inherit a different human identity from a
161
+ // workstation's persisted session. Send an explicitly injected key even
162
+ // when malformed so authentication fails closed instead of falling back.
163
+ headers.authorization = `Bearer ${agentEnv.appKey}`;
164
+ if (agentAppId) {
165
+ headers['x-vertu-agent-app-id'] = agentAppId;
166
+ }
167
+ if (agentLogin) {
168
+ headers['x-vertu-user-login'] = encodeURIComponent(agentLogin);
169
+ }
170
+ }
171
+ else if (persistedSessionToken) {
172
+ headers.authorization = `Bearer ${persistedSessionToken}`;
173
+ }
174
+ else if (persistedAgentKeyValid) {
175
+ headers.authorization = `Bearer ${agentAppKey}`;
176
+ if (agentAppId) {
177
+ headers['x-vertu-agent-app-id'] = agentAppId;
178
+ }
179
+ if (agentLogin) {
180
+ headers['x-vertu-user-login'] = encodeURIComponent(agentLogin);
181
+ }
182
+ }
183
+ else if (serviceApiKey) {
184
+ headers.authorization = `Bearer ${serviceApiKey}`;
185
+ }
186
+ return headers;
187
+ }
@@ -0,0 +1,34 @@
1
+ import { VertuError } from '../core/errors.js';
2
+ import { readStdinText, readUtf8File } from './read-text.js';
3
+ /**
4
+ * Resolve the curl-style `@file` / `@-` shorthand into the actual payload string.
5
+ *
6
+ * `@-` → read entire process.stdin (UTF-8, BOM stripped)
7
+ * `@<path>` → read file at <path> (UTF-8, BOM stripped)
8
+ * `@@<rest>` → escape: literal value `@<rest>` (rare, but unambiguous)
9
+ * anything else → returned verbatim
10
+ *
11
+ * Why: Windows PowerShell 5.1 mangles double quotes when forwarding to a native
12
+ * command (Win32 `CreateProcess`), so any `--<flag> '[["a","b"]]'`-style call
13
+ * silently arrives at node with the inner quotes stripped. `@file` / `@-` is
14
+ * the universally reliable shape because the path / pipe contents never travel
15
+ * through the shell's argument quoter.
16
+ */
17
+ export async function expandAtFileToken(token, source) {
18
+ if (!token.startsWith('@'))
19
+ return token;
20
+ if (token.startsWith('@@'))
21
+ return token.slice(1); // literal-`@` escape
22
+ const ref = token.slice(1);
23
+ if (ref.length === 0)
24
+ return token; // bare '@' isn't a reference; let server validate
25
+ try {
26
+ const raw = ref === '-' ? await readStdinText() : await readUtf8File(ref);
27
+ // Strip a single trailing newline (parity with bash `$(cat file)`); preserves
28
+ // the more common editor-saved-with-trailing-newline case without surprising users.
29
+ return raw.replace(/\r?\n$/u, '');
30
+ }
31
+ catch (err) {
32
+ throw new VertuError(`Cannot read ${source} ${ref}: ${err.message}`);
33
+ }
34
+ }