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,856 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { setTimeout as sleep } from 'node:timers/promises';
3
+ import { CLI_AUTH_CHANNEL } from '../../core/cli-bin.js';
4
+ import { VertuError } from '../../core/errors.js';
5
+ import { hermesRequest, resolveHermesAgentAdminUrl } from '../../core/hermes-client.js';
6
+ import { downloadImAttachment } from '../../core/im-attachment-download.js';
7
+ import { imRequest, printImResult } from '../../core/im-client.js';
8
+ import { parseJsonArg } from '../../utils/parse-json-arg.js';
9
+ import { attachOrgStructureShortcut } from '../org-structure.js';
10
+ import { attachSharedShortcutOptions, mergeShortcutParams, parseOptionalInt, } from '../shortcut-common.js';
11
+ import { buildImMessagePayload, parseUserIdList } from './im-message-payload.js';
12
+ function baseUrl(opts, cmd) {
13
+ const merged = cmd.optsWithGlobals();
14
+ return opts.baseUrl ?? merged.baseUrl;
15
+ }
16
+ function print(opts, out) {
17
+ printImResult(opts.json, out);
18
+ }
19
+ function requiredDate(value, flag) {
20
+ const trimmed = value?.trim() ?? '';
21
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(trimmed);
22
+ if (!match) {
23
+ throw new VertuError(`${flag} 需为 YYYY-MM-DD。`);
24
+ }
25
+ const year = Number(match[1]);
26
+ const month = Number(match[2]);
27
+ const day = Number(match[3]);
28
+ const parsed = new Date(Date.UTC(year, month - 1, day));
29
+ if (parsed.getUTCFullYear() !== year ||
30
+ parsed.getUTCMonth() !== month - 1 ||
31
+ parsed.getUTCDate() !== day) {
32
+ throw new VertuError(`${flag} 不是有效日期。`);
33
+ }
34
+ return trimmed;
35
+ }
36
+ function requiredPositiveInteger(value, flag, max) {
37
+ const trimmed = value?.trim() ?? '';
38
+ if (!/^\d+$/.test(trimmed)) {
39
+ throw new VertuError(`${flag} 需为正整数。`);
40
+ }
41
+ const parsed = Number(trimmed);
42
+ if (!Number.isSafeInteger(parsed) || parsed < 1 || (max !== undefined && parsed > max)) {
43
+ throw new VertuError(max === undefined ? `${flag} 需为正整数。` : `${flag} 需为 1-${max}。`);
44
+ }
45
+ return parsed;
46
+ }
47
+ function requiredBoundedInteger(value, flag, min, max) {
48
+ const trimmed = value?.trim() ?? '';
49
+ if (!/^\d+$/.test(trimmed)) {
50
+ throw new VertuError(`${flag} 需为 ${min}-${max} 的整数。`);
51
+ }
52
+ const parsed = Number(trimmed);
53
+ if (!Number.isSafeInteger(parsed) || parsed < min || parsed > max) {
54
+ throw new VertuError(`${flag} 需为 ${min}-${max} 的整数。`);
55
+ }
56
+ return parsed;
57
+ }
58
+ function collectAttach(value, previous) {
59
+ return [...previous, value];
60
+ }
61
+ function requiredInt(value, flag) {
62
+ const n = parseOptionalInt(value, flag);
63
+ if (n === undefined) {
64
+ throw new VertuError(`Invalid ${flag}.`);
65
+ }
66
+ return n;
67
+ }
68
+ function channelId(opts) {
69
+ const id = opts.channelId?.trim();
70
+ if (!id) {
71
+ throw new VertuError('--channel-id is required.');
72
+ }
73
+ return id;
74
+ }
75
+ function messageId(opts) {
76
+ const id = opts.messageId?.trim();
77
+ if (!id) {
78
+ throw new VertuError('--message-id is required.');
79
+ }
80
+ return id;
81
+ }
82
+ function callId(opts) {
83
+ const id = opts.callId?.trim();
84
+ if (!id) {
85
+ throw new VertuError('--call-id is required.');
86
+ }
87
+ return id;
88
+ }
89
+ function firstEnv(...keys) {
90
+ for (const key of keys) {
91
+ const value = process.env[key]?.trim();
92
+ if (value) {
93
+ return value;
94
+ }
95
+ }
96
+ return '';
97
+ }
98
+ function jsonObject(raw, flag) {
99
+ if (!raw?.trim()) {
100
+ return undefined;
101
+ }
102
+ const parsed = parseJsonArg(raw.trim(), flag);
103
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
104
+ throw new VertuError(`${flag} 必须为 JSON 对象。`);
105
+ }
106
+ return parsed;
107
+ }
108
+ function jsonArray(raw, flag) {
109
+ if (!raw?.trim()) {
110
+ return undefined;
111
+ }
112
+ const parsed = parseJsonArg(raw.trim(), flag);
113
+ if (!Array.isArray(parsed)) {
114
+ throw new VertuError(`${flag} 必须为 JSON 数组。`);
115
+ }
116
+ return parsed;
117
+ }
118
+ function extractChannelId(out) {
119
+ if (typeof out === 'object' && out !== null && 'channel' in out) {
120
+ const channel = out.channel;
121
+ if (typeof channel === 'object' && channel !== null && 'id' in channel) {
122
+ const id = channel.id;
123
+ if (typeof id === 'string') {
124
+ return id;
125
+ }
126
+ }
127
+ }
128
+ throw new VertuError('Vertu IM 未返回 channel.id。');
129
+ }
130
+ function attachmentDocument(out) {
131
+ if (typeof out !== 'object' || out === null || !('document' in out)) {
132
+ throw new VertuError('Vertu IM 未返回 document。');
133
+ }
134
+ const document = out.document;
135
+ if (typeof document !== 'object' || document === null) {
136
+ throw new VertuError('Vertu IM 返回的 document 无效。');
137
+ }
138
+ const row = document;
139
+ const id = typeof row.id === 'string' ? row.id : '';
140
+ const status = typeof row.status === 'string' ? row.status : '';
141
+ if (!id || !status) {
142
+ throw new VertuError('Vertu IM 返回的 document 缺少 id/status。');
143
+ }
144
+ return {
145
+ id,
146
+ status,
147
+ errorMessage: typeof row.error_message === 'string' ? row.error_message : '',
148
+ };
149
+ }
150
+ async function buildMessageBody(opts, serviceBaseUrl) {
151
+ const payload = await buildImMessagePayload({ ...opts, baseUrl: serviceBaseUrl ?? opts.baseUrl });
152
+ const body = { body: payload.body };
153
+ if (payload.attachments?.length) {
154
+ body.attachments = payload.attachments;
155
+ }
156
+ if (opts.messageType?.trim()) {
157
+ body.message_type = opts.messageType.trim();
158
+ }
159
+ if (opts.parentMessageId?.trim()) {
160
+ body.parent_message_id = opts.parentMessageId.trim();
161
+ }
162
+ if (opts.quoteMessageId?.trim()) {
163
+ body.quote_message_id = opts.quoteMessageId.trim();
164
+ }
165
+ body.client_message_id = opts.clientMessageId?.trim() || randomUUID();
166
+ body.idempotency_key = opts.idempotencyKey?.trim() || body.client_message_id;
167
+ const metadata = jsonObject(opts.metadataJson, '--metadata-json');
168
+ if (metadata) {
169
+ body.metadata = metadata;
170
+ }
171
+ return body;
172
+ }
173
+ async function buildAgentNotifyBody(opts, serviceBaseUrl) {
174
+ const agentSlug = opts.agentSlug?.trim() || firstEnv('VERTU_AGENT_SLUG', 'HERMES_AGENT_SLUG');
175
+ if (!agentSlug) {
176
+ throw new VertuError('--agent-slug is required,或设置 VERTU_AGENT_SLUG。');
177
+ }
178
+ const channelId = opts.channelId?.trim() || firstEnv('VERTU_AGENT_CHANNEL_ID');
179
+ const rawUserId = opts.userId?.trim() || firstEnv('VERTU_AGENT_USER_ID', 'VERTU_AGENT_ODOO_USER_ID');
180
+ const userId = rawUserId ? requiredInt(rawUserId, '--user-id') : undefined;
181
+ const agentName = opts.agentName?.trim() || firstEnv('VERTU_AGENT_NAME', 'VERTU_AGENT_PROFILE_NAME');
182
+ const sessionId = opts.sessionId?.trim() || firstEnv('VERTU_AGENT_SESSION_ID', 'HERMES_SESSION_ID');
183
+ const eventId = opts.eventId?.trim() || `agent:${agentSlug}:${userId ?? 'current'}:${randomUUID()}`;
184
+ const message = await buildMessageBody({
185
+ ...opts,
186
+ idempotencyKey: opts.idempotencyKey?.trim() || eventId,
187
+ }, serviceBaseUrl);
188
+ const metadata = {
189
+ ...(typeof message.metadata === 'object' && message.metadata !== null && !Array.isArray(message.metadata)
190
+ ? message.metadata
191
+ : {}),
192
+ source: CLI_AUTH_CHANNEL,
193
+ agent_slug: agentSlug,
194
+ agent_name: agentName || null,
195
+ session_id: sessionId || null,
196
+ };
197
+ return {
198
+ ...message,
199
+ agent_slug: agentSlug,
200
+ ...(channelId ? { channel_id: channelId } : {}),
201
+ ...(userId !== undefined ? { user_id: userId } : {}),
202
+ ...(agentName ? { agent_name: agentName } : {}),
203
+ ...(opts.botKey?.trim() ? { bot_key: opts.botKey.trim() } : {}),
204
+ ...(opts.botName?.trim() ? { bot_name: opts.botName.trim() } : {}),
205
+ ...(sessionId ? { session_id: sessionId } : {}),
206
+ event_id: eventId,
207
+ metadata,
208
+ };
209
+ }
210
+ async function buildAgentSessionNotifyBody(opts, serviceBaseUrl) {
211
+ const message = await buildMessageBody({
212
+ ...opts,
213
+ idempotencyKey: opts.idempotencyKey?.trim() || opts.eventId?.trim(),
214
+ }, serviceBaseUrl);
215
+ const agentName = opts.agentName?.trim() || firstEnv('VERTU_AGENT_NAME', 'VERTU_AGENT_PROFILE_NAME');
216
+ const eventId = opts.eventId?.trim() || String(message.idempotency_key || message.client_message_id || randomUUID());
217
+ const metadata = {
218
+ ...(typeof message.metadata === 'object' && message.metadata !== null && !Array.isArray(message.metadata)
219
+ ? message.metadata
220
+ : {}),
221
+ source: CLI_AUTH_CHANNEL,
222
+ agent_name: agentName || null,
223
+ event_id: eventId,
224
+ };
225
+ return {
226
+ ...message,
227
+ ...(agentName ? { agent_name: agentName } : {}),
228
+ event_id: eventId,
229
+ metadata,
230
+ };
231
+ }
232
+ function attachMessageOptions(cmd) {
233
+ return cmd
234
+ .option('--body <text>', '消息正文')
235
+ .option('--body-file <path>', '从文件读取正文;- 表示 stdin')
236
+ .option('--attach <path>', '附件路径,可重复', collectAttach, [])
237
+ .option('--attachments-json <json>', '附件 JSON 数组')
238
+ .option('--message-type <type>', 'text/rich_text/image/file/audio/video/link/emoji')
239
+ .option('--parent-message-id <id>', '回复的消息 id')
240
+ .option('--quote-message-id <id>', '引用的消息 id')
241
+ .option('--client-message-id <id>', '客户端消息 id,用于并发幂等')
242
+ .option('--idempotency-key <key>', '幂等键')
243
+ .option('--metadata-json <json>', '消息 metadata JSON 对象');
244
+ }
245
+ export function attachImShortcuts(parent) {
246
+ attachSharedShortcutOptions(parent.command('+me').description('当前 IM 身份')).action(async (opts, cmd) => {
247
+ print(opts, await imRequest('GET', '/v1/im/me', { baseUrl: baseUrl(opts, cmd) }));
248
+ });
249
+ attachSharedShortcutOptions(parent
250
+ .command('+users')
251
+ .description('按姓名/工号/登录名/部门搜索组织成员')
252
+ .option('--query <text>', '搜索关键字')
253
+ .option('--department-path <path>', '部门路径')
254
+ .option('--include-children', '部门路径包含子部门', true)
255
+ .option('--limit <n>', '数量', '30')).action(async (opts, cmd) => {
256
+ print(opts, await imRequest('GET', '/v1/im/org/users', {
257
+ baseUrl: baseUrl(opts, cmd),
258
+ query: {
259
+ query: opts.query,
260
+ department_path: opts.departmentPath,
261
+ include_children: opts.includeChildren,
262
+ limit: opts.limit,
263
+ },
264
+ }));
265
+ });
266
+ attachOrgStructureShortcut(parent, '+departments', '组织部门树(默认一级/二级;用 --max-depth 0 查看全量)');
267
+ attachSharedShortcutOptions(parent.command('+channels').description('我的 IM 会话列表').option('--limit <n>', '数量', '50')).action(async (opts, cmd) => {
268
+ print(opts, await imRequest('GET', '/v1/im/channels', {
269
+ baseUrl: baseUrl(opts, cmd),
270
+ query: { limit: opts.limit },
271
+ }));
272
+ });
273
+ attachSharedShortcutOptions(parent.command('+chat').description('获取或创建私聊').requiredOption('--user-id <id>', '对方 user_id').option('--membership-id <id>', '对方当前企业 membership_id')).action(async (opts, cmd) => {
274
+ const body = await mergeShortcutParams({ channel_type: 'direct', user_id: requiredInt(opts.userId, '--user-id'), membership_id: opts.membershipId?.trim() || undefined }, opts);
275
+ print(opts, await imRequest('POST', '/v1/im/channels', { baseUrl: baseUrl(opts, cmd), body }));
276
+ });
277
+ attachSharedShortcutOptions(parent
278
+ .command('+create-group')
279
+ .description('创建群聊')
280
+ .requiredOption('--name <text>', '群名称')
281
+ .requiredOption('--user-ids <list>', '成员 user_id,逗号分隔或 JSON 数组')
282
+ .option('--description <text>', '群说明')).action(async (opts, cmd) => {
283
+ const body = await mergeShortcutParams({
284
+ channel_type: 'group',
285
+ name: opts.name?.trim(),
286
+ description: opts.description?.trim(),
287
+ user_ids: parseUserIdList(opts.userIds, '--user-ids'),
288
+ }, opts);
289
+ print(opts, await imRequest('POST', '/v1/im/channels', { baseUrl: baseUrl(opts, cmd), body }));
290
+ });
291
+ attachSharedShortcutOptions(parent
292
+ .command('+create-org-group')
293
+ .description('按组织架构创建群聊')
294
+ .requiredOption('--department-path <path>', '部门完整路径')
295
+ .option('--name <text>', '群名称')
296
+ .option('--include-children', '包含子部门', true)).action(async (opts, cmd) => {
297
+ const body = await mergeShortcutParams({
298
+ channel_type: 'org',
299
+ name: opts.name?.trim(),
300
+ department_path: opts.departmentPath?.trim(),
301
+ include_children: opts.includeChildren,
302
+ }, opts);
303
+ print(opts, await imRequest('POST', '/v1/im/channels', { baseUrl: baseUrl(opts, cmd), body }));
304
+ });
305
+ attachSharedShortcutOptions(parent
306
+ .command('+history')
307
+ .description('查看会话消息历史')
308
+ .requiredOption('--channel-id <id>', '会话 id')
309
+ .option('--date-from <iso>', '起始时间')
310
+ .option('--date-to <iso>', '结束时间')
311
+ .option('--keyword <text>', '正文关键字')
312
+ .option('--limit <n>', '数量', '50')
313
+ .option('--offset <n>', '分页偏移')).action(async (opts, cmd) => {
314
+ print(opts, await imRequest('GET', `/v1/im/channels/${encodeURIComponent(channelId(opts))}/messages`, {
315
+ baseUrl: baseUrl(opts, cmd),
316
+ query: {
317
+ date_from: opts.dateFrom,
318
+ date_to: opts.dateTo,
319
+ keyword: opts.keyword,
320
+ limit: opts.limit,
321
+ offset: opts.offset,
322
+ },
323
+ }));
324
+ });
325
+ attachSharedShortcutOptions(parent
326
+ .command('+all-chat-records')
327
+ .description('凭独立权限分页查询全域聊天记录')
328
+ .requiredOption('--start-date <ymd>', '起始日期 YYYY-MM-DD(中国时区)')
329
+ .requiredOption('--end-date <ymd>', '结束日期 YYYY-MM-DD(中国时区,最多 31 天)')
330
+ .requiredOption('--page <n>', '页码,从 1 开始')
331
+ .requiredOption('--page-size <n>', '每页条数,1-100')).action(async (opts, cmd) => {
332
+ const startDate = requiredDate(opts.startDate, '--start-date');
333
+ const endDate = requiredDate(opts.endDate, '--end-date');
334
+ const page = requiredPositiveInteger(opts.page, '--page');
335
+ const pageSize = requiredPositiveInteger(opts.pageSize, '--page-size', 100);
336
+ print(opts, await imRequest('GET', '/v1/im/audit/messages', {
337
+ baseUrl: baseUrl(opts, cmd),
338
+ query: {
339
+ start_date: startDate,
340
+ end_date: endDate,
341
+ page,
342
+ page_size: pageSize,
343
+ },
344
+ }));
345
+ });
346
+ attachSharedShortcutOptions(parent
347
+ .command('+attachments')
348
+ .description('凭独立附件权限按发送人精确查询最近聊天附件')
349
+ .requiredOption('--sender-name <name>', '发送人姓名,精确匹配')
350
+ .requiredOption('--access-reason <text>', '本次查询附件的业务事由')
351
+ .option('--limit <n>', '返回数量,1-100', '20')).action(async (opts, cmd) => {
352
+ const senderName = opts.senderName?.trim() ?? '';
353
+ const accessReason = opts.accessReason?.trim() ?? '';
354
+ if (!senderName) {
355
+ throw new VertuError('--sender-name is required.');
356
+ }
357
+ if (!accessReason) {
358
+ throw new VertuError('--access-reason is required.');
359
+ }
360
+ const limit = requiredBoundedInteger(opts.limit, '--limit', 1, 100);
361
+ print(opts, await imRequest('GET', '/v1/im/audit/attachments', {
362
+ baseUrl: baseUrl(opts, cmd),
363
+ query: {
364
+ sender_name: senderName,
365
+ access_reason: accessReason,
366
+ limit,
367
+ },
368
+ }));
369
+ });
370
+ attachSharedShortcutOptions(parent
371
+ .command('+attachment-content')
372
+ .description('凭精确审计权限把聊天附件解析为 Markdown')
373
+ .requiredOption('--attachment-id <id>', '聊天附件 id')
374
+ .requiredOption('--access-reason <text>', '本次读取附件内容的业务事由')
375
+ .option('--wait-seconds <n>', '等待解析完成的秒数,0 表示不等待', '120')
376
+ .option('--poll-interval-ms <n>', '解析状态轮询间隔', '2000')).action(async (opts, cmd) => {
377
+ const attachmentId = opts.attachmentId?.trim() ?? '';
378
+ const accessReason = opts.accessReason?.trim() ?? '';
379
+ if (!attachmentId) {
380
+ throw new VertuError('--attachment-id is required.');
381
+ }
382
+ if (!accessReason) {
383
+ throw new VertuError('--access-reason is required.');
384
+ }
385
+ const waitSeconds = requiredBoundedInteger(opts.waitSeconds, '--wait-seconds', 0, 900);
386
+ const pollIntervalMs = requiredBoundedInteger(opts.pollIntervalMs, '--poll-interval-ms', 500, 10_000);
387
+ const path = `/v1/im/audit/attachments/${encodeURIComponent(attachmentId)}/content`;
388
+ let out = await imRequest('POST', path, {
389
+ baseUrl: baseUrl(opts, cmd),
390
+ body: { access_reason: accessReason },
391
+ });
392
+ let document = attachmentDocument(out);
393
+ const deadline = Date.now() + waitSeconds * 1000;
394
+ while ((document.status === 'pending' || document.status === 'processing') &&
395
+ Date.now() < deadline) {
396
+ await sleep(Math.min(pollIntervalMs, Math.max(1, deadline - Date.now())));
397
+ out = await imRequest('GET', path, {
398
+ baseUrl: baseUrl(opts, cmd),
399
+ query: {
400
+ access_reason: accessReason,
401
+ document_id: document.id,
402
+ },
403
+ });
404
+ document = attachmentDocument(out);
405
+ }
406
+ if (document.status === 'failed') {
407
+ throw new VertuError(`附件解析失败:${document.errorMessage || '未知错误'}`);
408
+ }
409
+ print(opts, out);
410
+ });
411
+ attachSharedShortcutOptions(parent
412
+ .command('+attachment-download')
413
+ .description('下载当前账号有权访问的聊天图片或附件原文件')
414
+ .option('--url <path>', '+history 返回的 attachments[].url')
415
+ .option('--storage-key <key>', '+history 返回的 attachments[].storage_key')
416
+ .requiredOption('--output <path>', '保存到本地的完整文件路径;不会覆盖已有文件')).action(async (opts, cmd) => {
417
+ const output = opts.output?.trim() ?? '';
418
+ if (!output)
419
+ throw new VertuError('--output is required.');
420
+ print(opts, await downloadImAttachment({
421
+ baseUrl: baseUrl(opts, cmd),
422
+ output,
423
+ storageKey: opts.storageKey,
424
+ url: opts.url,
425
+ }));
426
+ });
427
+ attachSharedShortcutOptions(parent
428
+ .command('+channel-update')
429
+ .description('更新群名称、头像、公告')
430
+ .requiredOption('--channel-id <id>', '会话 id')
431
+ .option('--name <text>', '群名称')
432
+ .option('--description <text>', '群说明')
433
+ .option('--avatar-url <url>', '群头像 URL')
434
+ .option('--announcement <text>', '群公告')
435
+ .option('--group-nature <value>', '群性质:daily_report/日报群;传空字符串取消日报群;已设置其他性质后不可修改')
436
+ .option('--expected-version <n>', '并发版本号')).action(async (opts, cmd) => {
437
+ const expectedVersion = parseOptionalInt(opts.expectedVersion, '--expected-version');
438
+ const body = await mergeShortcutParams({
439
+ name: opts.name?.trim(),
440
+ description: opts.description?.trim(),
441
+ avatar_url: opts.avatarUrl?.trim(),
442
+ announcement: opts.announcement?.trim(),
443
+ group_nature: opts.groupNature?.trim(),
444
+ expected_version: expectedVersion,
445
+ }, opts);
446
+ print(opts, await imRequest('PATCH', `/v1/im/channels/${encodeURIComponent(channelId(opts))}`, {
447
+ baseUrl: baseUrl(opts, cmd),
448
+ body,
449
+ }));
450
+ });
451
+ attachSharedShortcutOptions(attachMessageOptions(parent.command('+send').description('向会话发送消息').requiredOption('--channel-id <id>', '会话 id'))).action(async (opts, cmd) => {
452
+ const serviceBaseUrl = baseUrl(opts, cmd);
453
+ const body = await mergeShortcutParams(await buildMessageBody(opts, serviceBaseUrl), opts);
454
+ print(opts, await imRequest('POST', `/v1/im/channels/${encodeURIComponent(channelId(opts))}/messages`, {
455
+ baseUrl: serviceBaseUrl,
456
+ body,
457
+ }));
458
+ });
459
+ attachSharedShortcutOptions(attachMessageOptions(parent
460
+ .command('+send-approval-card')
461
+ .description('发送审批类互动卡片')
462
+ .requiredOption('--channel-id <id>', '会话 id')
463
+ .requiredOption('--title <text>', '审批标题')
464
+ .option('--summary <text>', '审批摘要')
465
+ .option('--approval-id <id>', '外部审批单 id')
466
+ .option('--actions-json <json>', '按钮动作 JSON 数组'))).action(async (opts, cmd) => {
467
+ const title = opts.title?.trim();
468
+ if (!title) {
469
+ throw new VertuError('--title is required.');
470
+ }
471
+ const metadata = {
472
+ ...(jsonObject(opts.metadataJson, '--metadata-json') ?? {}),
473
+ card: {
474
+ kind: 'approval',
475
+ title,
476
+ summary: opts.summary?.trim() ?? '',
477
+ approval_id: opts.approvalId?.trim() ?? null,
478
+ actions: jsonArray(opts.actionsJson, '--actions-json') ?? [
479
+ { key: 'approve', label: '同意' },
480
+ { key: 'reject', label: '拒绝' },
481
+ ],
482
+ },
483
+ };
484
+ const body = await mergeShortcutParams({
485
+ ...(await buildMessageBody({
486
+ ...opts,
487
+ body: opts.body?.trim() || title,
488
+ messageType: 'approval_card',
489
+ metadataJson: JSON.stringify(metadata),
490
+ }, baseUrl(opts, cmd))),
491
+ }, opts);
492
+ print(opts, await imRequest('POST', `/v1/im/channels/${encodeURIComponent(channelId(opts))}/messages`, {
493
+ baseUrl: baseUrl(opts, cmd),
494
+ body,
495
+ }));
496
+ });
497
+ attachSharedShortcutOptions(parent
498
+ .command('+revoke')
499
+ .description('撤回消息')
500
+ .requiredOption('--channel-id <id>', '会话 id')
501
+ .requiredOption('--message-id <id>', '消息 id')).action(async (opts, cmd) => {
502
+ const body = await mergeShortcutParams({ action: 'revoke', message_id: messageId(opts) }, opts);
503
+ print(opts, await imRequest('POST', `/v1/im/channels/${encodeURIComponent(channelId(opts))}/messages`, {
504
+ baseUrl: baseUrl(opts, cmd),
505
+ body,
506
+ }));
507
+ });
508
+ attachSharedShortcutOptions(parent
509
+ .command('+interact')
510
+ .description('对消息执行互动动作:表情、审批按钮、卡片按钮、确认')
511
+ .requiredOption('--channel-id <id>', '会话 id')
512
+ .requiredOption('--message-id <id>', '消息 id')
513
+ .requiredOption('--interaction-type <reaction|approval_action|card_action|ack>', '互动类型')
514
+ .requiredOption('--value <text>', '互动值,例如 approve/reject/👍')
515
+ .option('--metadata-json <json>', '互动 metadata JSON 对象')).action(async (opts, cmd) => {
516
+ const body = await mergeShortcutParams({
517
+ action: 'interact',
518
+ message_id: messageId(opts),
519
+ interaction_type: opts.interactionType?.trim(),
520
+ value: opts.value?.trim(),
521
+ metadata: jsonObject(opts.metadataJson, '--metadata-json'),
522
+ }, opts);
523
+ print(opts, await imRequest('POST', `/v1/im/channels/${encodeURIComponent(channelId(opts))}/messages`, {
524
+ baseUrl: baseUrl(opts, cmd),
525
+ body,
526
+ }));
527
+ });
528
+ attachSharedShortcutOptions(attachMessageOptions(parent
529
+ .command('+send-user')
530
+ .description('给某人发消息(自动创建/复用私聊)')
531
+ .requiredOption('--user-id <id>', '对方 user_id')
532
+ .option('--membership-id <id>', '对方当前企业 membership_id'))).action(async (opts, cmd) => {
533
+ const serviceBaseUrl = baseUrl(opts, cmd);
534
+ const created = await imRequest('POST', '/v1/im/channels', {
535
+ baseUrl: serviceBaseUrl,
536
+ body: { channel_type: 'direct', user_id: requiredInt(opts.userId, '--user-id'), membership_id: opts.membershipId?.trim() || undefined },
537
+ });
538
+ const id = extractChannelId(created);
539
+ const body = await mergeShortcutParams(await buildMessageBody(opts, serviceBaseUrl), opts);
540
+ print(opts, await imRequest('POST', `/v1/im/channels/${encodeURIComponent(id)}/messages`, {
541
+ baseUrl: serviceBaseUrl,
542
+ body,
543
+ }));
544
+ });
545
+ attachSharedShortcutOptions(attachMessageOptions(parent
546
+ .command('+agent-notify')
547
+ .description('专家智能体主动通知;默认有 channel_id 写入 IM,否则回退专家会话')
548
+ .option('--agent-slug <slug>', '专家 agent_slug;默认读 VERTU_AGENT_SLUG')
549
+ .option('--agent-name <text>', '专家名称;默认读 VERTU_AGENT_NAME / VERTU_AGENT_PROFILE_NAME')
550
+ .option('--user-id <id>', '目标 user_id;默认读 VERTU_AGENT_USER_ID 或服务端当前用户')
551
+ .option('--event-id <id>', '业务事件 id;默认自动生成并作为幂等键')
552
+ .option('--session-id <id>', 'Hermes 会话 id;默认读 VERTU_AGENT_SESSION_ID')
553
+ .option('--target <target>', '投递目标:auto / im / hermes-session;默认读 VERTU_AGENT_NOTIFY_TARGET,缺省为 auto')
554
+ .option('--hermes-base-url <url>', 'Hermes agent admin URL;默认读 VERTU_HERMES_BASE_URL / HERMES_AGENT_ADMIN_BASE_URL')
555
+ .option('--channel-id <id>', '目标 IM 会话 id;默认读 VERTU_AGENT_CHANNEL_ID,提供后不会自动创建专家会话')
556
+ .option('--bot-key <key>', '覆盖 bot_key')
557
+ .option('--bot-name <text>', '覆盖 bot_name'))).action(async (opts, cmd) => {
558
+ const rawTarget = (opts.target?.trim() || firstEnv('VERTU_AGENT_NOTIFY_TARGET') || 'auto').toLowerCase();
559
+ const agentSlugForEvent = opts.agentSlug?.trim() || firstEnv('VERTU_AGENT_SLUG', 'HERMES_AGENT_SLUG');
560
+ const rawUserIdForEvent = opts.userId?.trim() || firstEnv('VERTU_AGENT_USER_ID', 'VERTU_AGENT_ODOO_USER_ID');
561
+ if (!opts.eventId && agentSlugForEvent) {
562
+ opts.eventId = `agent:${agentSlugForEvent}:${rawUserIdForEvent || 'current'}:${randomUUID()}`;
563
+ }
564
+ if (rawTarget === 'auto') {
565
+ const body = await mergeShortcutParams(await buildAgentNotifyBody(opts, baseUrl(opts, cmd)), opts);
566
+ const agentSlug = String(body.agent_slug || '').trim();
567
+ if (!agentSlug) {
568
+ throw new VertuError('--agent-slug is required,或设置 VERTU_AGENT_SLUG。');
569
+ }
570
+ print(opts, await hermesRequest('POST', `/hermes-console/api/agents/${encodeURIComponent(agentSlug)}/notify`, { baseUrl: resolveHermesAgentAdminUrl(opts.hermesBaseUrl), body }));
571
+ return;
572
+ }
573
+ const target = rawTarget;
574
+ if (target === 'hermes-session' || target === 'session' || target === 'agent-session') {
575
+ const agentSlug = opts.agentSlug?.trim() || firstEnv('VERTU_AGENT_SLUG', 'HERMES_AGENT_SLUG');
576
+ const sessionId = opts.sessionId?.trim() || firstEnv('VERTU_AGENT_SESSION_ID', 'HERMES_SESSION_ID');
577
+ const rawUserId = opts.userId?.trim() || firstEnv('VERTU_AGENT_USER_ID', 'VERTU_AGENT_ODOO_USER_ID');
578
+ if (!agentSlug) {
579
+ throw new VertuError('--agent-slug is required,或设置 VERTU_AGENT_SLUG。');
580
+ }
581
+ if (!sessionId) {
582
+ throw new VertuError('--session-id is required for --target hermes-session,或设置 VERTU_AGENT_SESSION_ID。');
583
+ }
584
+ const body = await mergeShortcutParams({
585
+ ...(await buildAgentSessionNotifyBody(opts, baseUrl(opts, cmd))),
586
+ odoo_user_id: rawUserId ? requiredInt(rawUserId, '--user-id') : undefined,
587
+ }, opts);
588
+ print(opts, await hermesRequest('POST', `/hermes-console/api/agents/${encodeURIComponent(agentSlug)}/sessions/${encodeURIComponent(sessionId)}/messages`, { baseUrl: resolveHermesAgentAdminUrl(opts.hermesBaseUrl), body }));
589
+ return;
590
+ }
591
+ if (target !== 'im') {
592
+ throw new VertuError('--target 仅支持 auto、hermes-session 或 im。');
593
+ }
594
+ const body = await mergeShortcutParams(await buildAgentNotifyBody(opts, baseUrl(opts, cmd)), opts);
595
+ print(opts, await imRequest('POST', '/v1/im/agents/hermes/notify', { baseUrl: baseUrl(opts, cmd), body }));
596
+ });
597
+ attachSharedShortcutOptions(parent
598
+ .command('+add-members')
599
+ .description('添加群成员')
600
+ .requiredOption('--channel-id <id>', '会话 id')
601
+ .requiredOption('--user-ids <list>', '成员 user_id')).action(async (opts, cmd) => {
602
+ const body = await mergeShortcutParams({ user_ids: parseUserIdList(opts.userIds, '--user-ids') }, opts);
603
+ print(opts, await imRequest('POST', `/v1/im/channels/${encodeURIComponent(channelId(opts))}/members`, {
604
+ baseUrl: baseUrl(opts, cmd),
605
+ body,
606
+ }));
607
+ });
608
+ attachSharedShortcutOptions(parent
609
+ .command('+set-role')
610
+ .description('调整群成员角色')
611
+ .requiredOption('--channel-id <id>', '会话 id')
612
+ .requiredOption('--user-id <id>', '成员 user_id')
613
+ .requiredOption('--role <owner|admin|member>', '角色')).action(async (opts, cmd) => {
614
+ const body = await mergeShortcutParams({ user_id: requiredInt(opts.userId, '--user-id'), role: opts.role }, opts);
615
+ print(opts, await imRequest('PATCH', `/v1/im/channels/${encodeURIComponent(channelId(opts))}/members`, {
616
+ baseUrl: baseUrl(opts, cmd),
617
+ body,
618
+ }));
619
+ });
620
+ attachSharedShortcutOptions(parent
621
+ .command('+remove-member')
622
+ .description('移除群成员')
623
+ .requiredOption('--channel-id <id>', '会话 id')
624
+ .requiredOption('--user-id <id>', '成员 user_id')).action(async (opts, cmd) => {
625
+ print(opts, await imRequest('DELETE', `/v1/im/channels/${encodeURIComponent(channelId(opts))}/members`, {
626
+ baseUrl: baseUrl(opts, cmd),
627
+ query: { user_id: requiredInt(opts.userId, '--user-id') },
628
+ }));
629
+ });
630
+ attachSharedShortcutOptions(parent
631
+ .command('+sync')
632
+ .description('按游标同步 IM 增量事件')
633
+ .option('--cursor <n>', '事件游标', '0')
634
+ .option('--limit <n>', '数量', '100')).action(async (opts, cmd) => {
635
+ print(opts, await imRequest('GET', '/v1/im/sync', {
636
+ baseUrl: baseUrl(opts, cmd),
637
+ query: { cursor: opts.cursor, limit: opts.limit },
638
+ }));
639
+ });
640
+ attachSharedShortcutOptions(parent
641
+ .command('+call-start')
642
+ .description('发起语音/视频通话,支持群通话')
643
+ .requiredOption('--channel-id <id>', '会话 id')
644
+ .option('--call-type <audio|video>', '通话类型', 'audio')
645
+ .option('--user-ids <list>', '邀请成员 user_id;默认邀请会话内所有成员')
646
+ .option('--title <text>', '通话标题')
647
+ .option('--device-id <id>', '当前设备 id')
648
+ .option('--audio-enabled', '开启麦克风', true)
649
+ .option('--video-enabled', '开启摄像头', false)
650
+ .option('--metadata-json <json>', '通话 metadata JSON 对象')).action(async (opts, cmd) => {
651
+ const body = await mergeShortcutParams({
652
+ call_type: opts.callType?.trim() || 'audio',
653
+ user_ids: opts.userIds ? parseUserIdList(opts.userIds, '--user-ids') : undefined,
654
+ title: opts.title?.trim(),
655
+ device_id: opts.deviceId?.trim(),
656
+ audio_enabled: opts.audioEnabled,
657
+ video_enabled: opts.videoEnabled,
658
+ metadata: jsonObject(opts.metadataJson, '--metadata-json'),
659
+ }, opts);
660
+ print(opts, await imRequest('POST', `/v1/im/channels/${encodeURIComponent(channelId(opts))}/calls`, {
661
+ baseUrl: baseUrl(opts, cmd),
662
+ body,
663
+ }));
664
+ });
665
+ attachSharedShortcutOptions(parent
666
+ .command('+call-join')
667
+ .description('加入语音/视频通话')
668
+ .requiredOption('--call-id <id>', '通话 id')
669
+ .option('--device-id <id>', '当前设备 id')
670
+ .option('--audio-enabled', '开启麦克风', true)
671
+ .option('--video-enabled', '开启摄像头', false)).action(async (opts, cmd) => {
672
+ const body = await mergeShortcutParams({
673
+ action: 'join',
674
+ device_id: opts.deviceId?.trim(),
675
+ audio_enabled: opts.audioEnabled,
676
+ video_enabled: opts.videoEnabled,
677
+ }, opts);
678
+ print(opts, await imRequest('POST', `/v1/im/calls/${encodeURIComponent(callId(opts))}`, {
679
+ baseUrl: baseUrl(opts, cmd),
680
+ body,
681
+ }));
682
+ });
683
+ attachSharedShortcutOptions(parent
684
+ .command('+call-leave')
685
+ .description('离开或拒绝通话')
686
+ .requiredOption('--call-id <id>', '通话 id')
687
+ .option('--status <left|declined|failed>', '离开状态', 'left')).action(async (opts, cmd) => {
688
+ const body = await mergeShortcutParams({ action: 'leave', status: opts.status?.trim() || 'left' }, opts);
689
+ print(opts, await imRequest('POST', `/v1/im/calls/${encodeURIComponent(callId(opts))}`, {
690
+ baseUrl: baseUrl(opts, cmd),
691
+ body,
692
+ }));
693
+ });
694
+ attachSharedShortcutOptions(parent
695
+ .command('+call-end')
696
+ .description('结束通话')
697
+ .requiredOption('--call-id <id>', '通话 id')
698
+ .option('--status <ended|cancelled|missed|failed>', '结束状态', 'ended')
699
+ .option('--reason <text>', '结束原因')).action(async (opts, cmd) => {
700
+ const body = await mergeShortcutParams({
701
+ action: 'end',
702
+ status: opts.status?.trim() || 'ended',
703
+ reason: opts.reason?.trim(),
704
+ }, opts);
705
+ print(opts, await imRequest('POST', `/v1/im/calls/${encodeURIComponent(callId(opts))}`, {
706
+ baseUrl: baseUrl(opts, cmd),
707
+ body,
708
+ }));
709
+ });
710
+ attachSharedShortcutOptions(parent
711
+ .command('+call-signal')
712
+ .description('发送 WebRTC 通话信令')
713
+ .requiredOption('--call-id <id>', '通话 id')
714
+ .requiredOption('--signal-type <type>', 'offer/answer/ice_candidate/heartbeat 等')
715
+ .option('--recipient-user-id <id>', '目标 user_id;省略为广播给通话内成员')
716
+ .option('--payload-json <json>', '信令 payload JSON 对象')
717
+ .option('--client-signal-id <id>', '客户端信令 id,用于幂等')).action(async (opts, cmd) => {
718
+ const body = await mergeShortcutParams({
719
+ signal_type: opts.signalType?.trim(),
720
+ recipient_user_id: parseOptionalInt(opts.recipientUserId, '--recipient-user-id'),
721
+ payload: jsonObject(opts.payloadJson, '--payload-json') ?? {},
722
+ client_signal_id: opts.clientSignalId?.trim() || randomUUID(),
723
+ }, opts);
724
+ print(opts, await imRequest('POST', `/v1/im/calls/${encodeURIComponent(callId(opts))}/signals`, {
725
+ baseUrl: baseUrl(opts, cmd),
726
+ body,
727
+ }));
728
+ });
729
+ attachSharedShortcutOptions(parent
730
+ .command('+call-signals')
731
+ .description('拉取通话信令')
732
+ .requiredOption('--call-id <id>', '通话 id')
733
+ .option('--cursor <iso>', 'created_at 游标')
734
+ .option('--limit <n>', '数量', '100')).action(async (opts, cmd) => {
735
+ print(opts, await imRequest('GET', `/v1/im/calls/${encodeURIComponent(callId(opts))}/signals`, {
736
+ baseUrl: baseUrl(opts, cmd),
737
+ query: { cursor: opts.cursor, limit: opts.limit },
738
+ }));
739
+ });
740
+ attachSharedShortcutOptions(parent
741
+ .command('+watch')
742
+ .description('轮询 IM 增量事件,用于命令行及时推送')
743
+ .option('--cursor <n>', '事件游标', '0')
744
+ .option('--limit <n>', '每轮数量', '100')
745
+ .option('--interval-ms <n>', '轮询间隔毫秒', '3000')
746
+ .option('--once', '只拉取一次', false)).action(async (opts, cmd) => {
747
+ let cursor = parseOptionalInt(opts.cursor, '--cursor') ?? 0;
748
+ const intervalMs = parseOptionalInt(opts.intervalMs, '--interval-ms') ?? 3000;
749
+ do {
750
+ const out = await imRequest('GET', '/v1/im/sync', {
751
+ baseUrl: baseUrl(opts, cmd),
752
+ query: { cursor, limit: opts.limit },
753
+ });
754
+ print(opts, out);
755
+ if (typeof out === 'object' && out !== null && 'next_cursor' in out) {
756
+ const nextCursor = Number(out.next_cursor);
757
+ if (Number.isInteger(nextCursor) && nextCursor >= cursor) {
758
+ cursor = nextCursor;
759
+ }
760
+ }
761
+ if (!opts.once) {
762
+ await sleep(Math.max(intervalMs, 1000));
763
+ }
764
+ } while (!opts.once);
765
+ });
766
+ attachSharedShortcutOptions(attachMessageOptions(parent
767
+ .command('+broadcast')
768
+ .description('群发消息')
769
+ .option('--user-ids <list>', '收件人 user_id')
770
+ .option('--department-path <path>', '按部门路径群发')
771
+ .option('--include-children', '部门包含子部门', true)
772
+ .option('--name <text>', '群发会话名称')
773
+ .option('--limit <n>', '部门群发人数上限', '1000'))).action(async (opts, cmd) => {
774
+ const payload = await buildMessageBody(opts, baseUrl(opts, cmd));
775
+ const body = await mergeShortcutParams({
776
+ ...payload,
777
+ user_ids: opts.userIds ? parseUserIdList(opts.userIds, '--user-ids') : undefined,
778
+ department_path: opts.departmentPath?.trim(),
779
+ include_children: opts.includeChildren,
780
+ name: opts.name?.trim(),
781
+ limit: opts.limit,
782
+ }, opts);
783
+ print(opts, await imRequest('POST', '/v1/im/broadcast', { baseUrl: baseUrl(opts, cmd), body }));
784
+ });
785
+ attachSharedShortcutOptions(parent.command('+bots').description('我的机器人列表').option('--include-public', '同时列出其他人的公开机器人')).action(async (opts, cmd) => {
786
+ const path = opts.includePublic ? '/v1/im/user-robots?include_public=true' : '/v1/im/user-robots';
787
+ print(opts, await imRequest('GET', path, { baseUrl: baseUrl(opts, cmd) }));
788
+ });
789
+ attachSharedShortcutOptions(parent.command('+bot-channels').description('查询我的全部机器人已加入的群聊')).action(async (opts, cmd) => {
790
+ print(opts, await imRequest('GET', '/v1/im/user-robots/channels', { baseUrl: baseUrl(opts, cmd) }));
791
+ });
792
+ attachSharedShortcutOptions(attachMessageOptions(parent
793
+ .command('+bot-send-user')
794
+ .description('以我的指定机器人身份给员工发起私聊')
795
+ .requiredOption('--app-id <id>', '机器人 App ID')
796
+ .requiredOption('--user-id <id>', '对方 user_id'))).action(async (opts, cmd) => {
797
+ const serviceBaseUrl = baseUrl(opts, cmd);
798
+ const appID = opts.appId?.trim() ?? '';
799
+ if (!appID)
800
+ throw new VertuError('--app-id is required.');
801
+ const body = await mergeShortcutParams({
802
+ ...(await buildMessageBody(opts, serviceBaseUrl)),
803
+ user_id: requiredInt(opts.userId, '--user-id'),
804
+ }, opts);
805
+ print(opts, await imRequest('POST', `/v1/im/user-robots/${encodeURIComponent(appID)}/message-user`, { baseUrl: serviceBaseUrl, body }));
806
+ });
807
+ attachSharedShortcutOptions(parent
808
+ .command('+bot-create')
809
+ .description('创建 IM 机器人')
810
+ .requiredOption('--name <text>', '机器人名称')
811
+ .option('--bot-key <key>', '机器人唯一 key')
812
+ .option('--description <text>', '说明')
813
+ .option('--webhook-url <url>', '机器人 webhook')
814
+ .option('--public', '设为公开,允许其他群管理员添加')).action(async (opts, cmd) => {
815
+ const body = await mergeShortcutParams({
816
+ name: opts.name?.trim(),
817
+ bot_key: opts.botKey?.trim(),
818
+ description: opts.description?.trim(),
819
+ webhook_url: opts.webhookUrl?.trim(),
820
+ is_public: opts.public === true,
821
+ }, opts);
822
+ print(opts, await imRequest('POST', '/v1/im/user-robots', { baseUrl: baseUrl(opts, cmd), body }));
823
+ });
824
+ attachSharedShortcutOptions(parent
825
+ .command('+bot-update')
826
+ .description('更新机器人的公开范围')
827
+ .requiredOption('--app-id <id>', '机器人 App ID')
828
+ .option('--public', '设为公开')
829
+ .option('--private', '设为仅自己可见')).action(async (opts, cmd) => {
830
+ if (opts.public === opts.private) {
831
+ throw new VertuError('--public 和 --private 必须且只能指定一个。');
832
+ }
833
+ const appID = opts.appId?.trim() ?? '';
834
+ const body = await mergeShortcutParams({ is_public: opts.public === true }, opts);
835
+ print(opts, await imRequest('PATCH', `/v1/im/user-robots/${encodeURIComponent(appID)}`, {
836
+ baseUrl: baseUrl(opts, cmd),
837
+ body,
838
+ }));
839
+ });
840
+ attachSharedShortcutOptions(parent
841
+ .command('+add-bot')
842
+ .description('添加机器人到会话')
843
+ .requiredOption('--channel-id <id>', '会话 id')
844
+ .option('--app-id <id>', '机器人 App ID')
845
+ .option('--bot-id <id>', '机器人 App ID(兼容旧参数名)')).action(async (opts, cmd) => {
846
+ const appID = opts.appId?.trim() || opts.botId?.trim();
847
+ if (!appID) {
848
+ throw new VertuError('--app-id is required.');
849
+ }
850
+ const body = await mergeShortcutParams({ app_id: appID }, opts);
851
+ print(opts, await imRequest('POST', `/v1/im/channels/${encodeURIComponent(channelId(opts))}/bots`, {
852
+ baseUrl: baseUrl(opts, cmd),
853
+ body,
854
+ }));
855
+ });
856
+ }