xabot-cli 1.4.2

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 (116) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +126 -0
  3. package/README.zh-CN.md +126 -0
  4. package/dist/bridge/index.d.ts +93 -0
  5. package/dist/bridge/index.d.ts.map +1 -0
  6. package/dist/bridge/index.js +811 -0
  7. package/dist/bridge/index.js.map +1 -0
  8. package/dist/bridge/input-parser.d.ts +16 -0
  9. package/dist/bridge/input-parser.d.ts.map +1 -0
  10. package/dist/bridge/input-parser.js +20 -0
  11. package/dist/bridge/input-parser.js.map +1 -0
  12. package/dist/cli/chat.d.ts +38 -0
  13. package/dist/cli/chat.d.ts.map +1 -0
  14. package/dist/cli/chat.js +147 -0
  15. package/dist/cli/chat.js.map +1 -0
  16. package/dist/cli/feishu.d.ts +3 -0
  17. package/dist/cli/feishu.d.ts.map +1 -0
  18. package/dist/cli/feishu.js +79 -0
  19. package/dist/cli/feishu.js.map +1 -0
  20. package/dist/cli/health.d.ts +15 -0
  21. package/dist/cli/health.d.ts.map +1 -0
  22. package/dist/cli/health.js +26 -0
  23. package/dist/cli/health.js.map +1 -0
  24. package/dist/cli/index.d.ts +13 -0
  25. package/dist/cli/index.d.ts.map +1 -0
  26. package/dist/cli/index.js +43 -0
  27. package/dist/cli/index.js.map +1 -0
  28. package/dist/cli/run.d.ts +20 -0
  29. package/dist/cli/run.d.ts.map +1 -0
  30. package/dist/cli/run.js +43 -0
  31. package/dist/cli/run.js.map +1 -0
  32. package/dist/cli/wechat.d.ts +3 -0
  33. package/dist/cli/wechat.d.ts.map +1 -0
  34. package/dist/cli/wechat.js +86 -0
  35. package/dist/cli/wechat.js.map +1 -0
  36. package/dist/core/client.d.ts +31 -0
  37. package/dist/core/client.d.ts.map +1 -0
  38. package/dist/core/client.js +2 -0
  39. package/dist/core/client.js.map +1 -0
  40. package/dist/core/error.d.ts +11 -0
  41. package/dist/core/error.d.ts.map +1 -0
  42. package/dist/core/error.js +21 -0
  43. package/dist/core/error.js.map +1 -0
  44. package/dist/core/fs-utils.d.ts +17 -0
  45. package/dist/core/fs-utils.d.ts.map +1 -0
  46. package/dist/core/fs-utils.js +84 -0
  47. package/dist/core/fs-utils.js.map +1 -0
  48. package/dist/core/logger.d.ts +10 -0
  49. package/dist/core/logger.d.ts.map +1 -0
  50. package/dist/core/logger.js +44 -0
  51. package/dist/core/logger.js.map +1 -0
  52. package/dist/core/pairing.d.ts +10 -0
  53. package/dist/core/pairing.d.ts.map +1 -0
  54. package/dist/core/pairing.js +22 -0
  55. package/dist/core/pairing.js.map +1 -0
  56. package/dist/core/types.d.ts +50 -0
  57. package/dist/core/types.d.ts.map +1 -0
  58. package/dist/core/types.js +11 -0
  59. package/dist/core/types.js.map +1 -0
  60. package/dist/index.d.ts +13 -0
  61. package/dist/index.d.ts.map +1 -0
  62. package/dist/index.js +9 -0
  63. package/dist/index.js.map +1 -0
  64. package/dist/platforms/feishu/client.d.ts +49 -0
  65. package/dist/platforms/feishu/client.d.ts.map +1 -0
  66. package/dist/platforms/feishu/client.js +296 -0
  67. package/dist/platforms/feishu/client.js.map +1 -0
  68. package/dist/platforms/feishu/message.d.ts +49 -0
  69. package/dist/platforms/feishu/message.d.ts.map +1 -0
  70. package/dist/platforms/feishu/message.js +131 -0
  71. package/dist/platforms/feishu/message.js.map +1 -0
  72. package/dist/platforms/feishu/upload.d.ts +11 -0
  73. package/dist/platforms/feishu/upload.d.ts.map +1 -0
  74. package/dist/platforms/feishu/upload.js +60 -0
  75. package/dist/platforms/feishu/upload.js.map +1 -0
  76. package/dist/platforms/wechat/client.d.ts +53 -0
  77. package/dist/platforms/wechat/client.d.ts.map +1 -0
  78. package/dist/platforms/wechat/client.js +301 -0
  79. package/dist/platforms/wechat/client.js.map +1 -0
  80. package/dist/platforms/wechat/crypto.d.ts +23 -0
  81. package/dist/platforms/wechat/crypto.d.ts.map +1 -0
  82. package/dist/platforms/wechat/crypto.js +46 -0
  83. package/dist/platforms/wechat/crypto.js.map +1 -0
  84. package/dist/platforms/wechat/login.d.ts +19 -0
  85. package/dist/platforms/wechat/login.d.ts.map +1 -0
  86. package/dist/platforms/wechat/login.js +71 -0
  87. package/dist/platforms/wechat/login.js.map +1 -0
  88. package/dist/platforms/wechat/message.d.ts +102 -0
  89. package/dist/platforms/wechat/message.d.ts.map +1 -0
  90. package/dist/platforms/wechat/message.js +212 -0
  91. package/dist/platforms/wechat/message.js.map +1 -0
  92. package/dist/platforms/wechat/upload.d.ts +10 -0
  93. package/dist/platforms/wechat/upload.d.ts.map +1 -0
  94. package/dist/platforms/wechat/upload.js +86 -0
  95. package/dist/platforms/wechat/upload.js.map +1 -0
  96. package/dist/xacpp/establish-handler.d.ts +58 -0
  97. package/dist/xacpp/establish-handler.d.ts.map +1 -0
  98. package/dist/xacpp/establish-handler.js +152 -0
  99. package/dist/xacpp/establish-handler.js.map +1 -0
  100. package/dist/xacpp/index.d.ts +3 -0
  101. package/dist/xacpp/index.d.ts.map +1 -0
  102. package/dist/xacpp/index.js +3 -0
  103. package/dist/xacpp/index.js.map +1 -0
  104. package/dist/xacpp/initiator-session-handler.d.ts +30 -0
  105. package/dist/xacpp/initiator-session-handler.d.ts.map +1 -0
  106. package/dist/xacpp/initiator-session-handler.js +233 -0
  107. package/dist/xacpp/initiator-session-handler.js.map +1 -0
  108. package/dist/xacpp/session-handler.d.ts +17 -0
  109. package/dist/xacpp/session-handler.d.ts.map +1 -0
  110. package/dist/xacpp/session-handler.js +27 -0
  111. package/dist/xacpp/session-handler.js.map +1 -0
  112. package/dist/xacpp/stdin-router.d.ts +23 -0
  113. package/dist/xacpp/stdin-router.d.ts.map +1 -0
  114. package/dist/xacpp/stdin-router.js +46 -0
  115. package/dist/xacpp/stdin-router.js.map +1 -0
  116. package/package.json +44 -0
@@ -0,0 +1,811 @@
1
+ import { StreamCapability } from '../core/types.js';
2
+ import { parseInput } from './input-parser.js';
3
+ import { createLogger } from '../core/logger.js';
4
+ const log = createLogger('Bridge');
5
+ /**
6
+ * Bridge — bidirectional message loop between cloud platform and XACPP agents.
7
+ *
8
+ * Cloud → Agent: reads cloud messages, resolves/creates activity,
9
+ * sends invoke_activity command via session.
10
+ * Agent → Cloud: reads agent events, routes content_delta/complete/notify
11
+ * back to the corresponding chatId.
12
+ */
13
+ export class Bridge {
14
+ cloud = null;
15
+ transport;
16
+ cloudReady;
17
+ cloudReadyResolve = null;
18
+ /** chatId → (senderId → activityId) */
19
+ chatUserToActivity = new Map();
20
+ /** activityId → { chatId, senderId } */
21
+ activityToTarget = new Map();
22
+ /** Obtained after establish, used to proactively send command/event */
23
+ session = null;
24
+ /** chatId bound to the established session (from session.credentials) */
25
+ sessionChatId = null;
26
+ /** targetKey (chatId:senderId) → pending queue state */
27
+ pendingQueues = new Map();
28
+ /** targetKey → buffered media ContentParts awaiting next text invoke */
29
+ pendingMediaByTarget = new Map();
30
+ abortCtrl = new AbortController();
31
+ closed = false;
32
+ runPromise = null;
33
+ /** Whether the establish handshake has completed. */
34
+ established = false;
35
+ /** Reference to the establish handler — Phase 1 feeds discovered challenges to it. */
36
+ establishHandler = null;
37
+ verbose;
38
+ constructor(transport, options) {
39
+ this.transport = transport;
40
+ this.verbose = options?.verbose ?? false;
41
+ if (options?.cloud) {
42
+ this.cloud = options.cloud;
43
+ this.cloudReady = Promise.resolve();
44
+ }
45
+ else {
46
+ this.cloudReady = new Promise((resolve) => {
47
+ this.cloudReadyResolve = resolve;
48
+ });
49
+ }
50
+ }
51
+ /** Replace the cloud PlatformClient (used after Establish login creates a new client). */
52
+ replaceCloud(cloud) {
53
+ this.cloud = cloud;
54
+ if (this.cloudReadyResolve) {
55
+ this.cloudReadyResolve();
56
+ this.cloudReadyResolve = null;
57
+ }
58
+ }
59
+ /** Call after establish succeeds to inject session reference. */
60
+ setSession(session) {
61
+ this.session = session;
62
+ // Extract chatId from credentials — WeChat uses JSON, Feishu uses plain string
63
+ const raw = session.credentials;
64
+ try {
65
+ const parsed = JSON.parse(raw);
66
+ if (typeof parsed === 'object' && parsed !== null && 'chatId' in parsed) {
67
+ this.sessionChatId = parsed.chatId;
68
+ }
69
+ else {
70
+ this.sessionChatId = raw;
71
+ }
72
+ }
73
+ catch {
74
+ this.sessionChatId = raw;
75
+ }
76
+ }
77
+ /** Set the establish handler — Phase 1 feeds discovered challenges to it. */
78
+ setEstablishHandler(handler) {
79
+ this.establishHandler = handler;
80
+ }
81
+ /** Generate targetKey for pending queue lookup. */
82
+ targetKey(chatId, senderId) {
83
+ return `${chatId}:${senderId}`;
84
+ }
85
+ /** Get or create pending queue for a target. */
86
+ ensureQueue(key) {
87
+ let pq = this.pendingQueues.get(key);
88
+ if (!pq) {
89
+ pq = { active: null, queue: [] };
90
+ this.pendingQueues.set(key, pq);
91
+ }
92
+ return pq;
93
+ }
94
+ /** Create a PendingItem from an event. */
95
+ createPendingItem(chatId, senderId, type, eventPayload) {
96
+ return { requestId: eventPayload.requestId, type, chatId, senderId, eventPayload, resolve: () => { } };
97
+ }
98
+ /** Format action_request for cloud display. */
99
+ formatActionMessage(payload) {
100
+ const alertLabel = payload.alert === 'critical' ? '🔴' : payload.alert === 'warn' ? '🟡' : 'ℹ️';
101
+ return [
102
+ `${alertLabel} [Authorization Request] ${payload.intent || payload.description}`,
103
+ `Tool: ${payload.toolName}`,
104
+ payload.arguments ? `Arguments: ${payload.arguments}` : '',
105
+ '───────────────',
106
+ 'a: 始终允许 | y: 单次允许',
107
+ 'n [原因]: 本次拒绝(可附带拒绝原因)',
108
+ 'c: 取消执行',
109
+ ].filter(Boolean).join('\n');
110
+ }
111
+ /** Format question for cloud display. */
112
+ formatQuestionMessage(payload) {
113
+ const lines = [`❓ [Question] ${payload.question}`];
114
+ if (payload.options.length > 0) {
115
+ lines.push('Options:');
116
+ payload.options.forEach((opt, i) => lines.push(` ${i + 1}. ${opt}`));
117
+ lines.push('───────────────');
118
+ lines.push(`回复 1~${payload.options.length} 选择选项,或直接输入文本`);
119
+ lines.push('c: 取消执行');
120
+ }
121
+ else {
122
+ lines.push('───────────────');
123
+ lines.push('直接输入回答');
124
+ lines.push('c: 取消执行');
125
+ }
126
+ return lines.join('\n');
127
+ }
128
+ /** Format sensitive_info_operation for cloud display. */
129
+ formatSensitiveInfoMessage(payload) {
130
+ const lines = [`🔒 [Sensitive Info Operation] ${payload.operation.type}`];
131
+ const items = payload.operation.items;
132
+ lines.push('Items:');
133
+ items.forEach((item, i) => lines.push(` ${i + 1}. ${item.displayText} (${item.siType})`));
134
+ lines.push('───────────────');
135
+ lines.push('逐行回复值(每行对应一项) | t: 取消执行');
136
+ return lines.join('\n');
137
+ }
138
+ /** Parse user text into a pending response, or null if unrecognized. */
139
+ tryParsePendingResponse(item, text) {
140
+ const input = text.trim().toLowerCase();
141
+ switch (item.type) {
142
+ case 'action_request': {
143
+ if (input === 'a')
144
+ return { kind: 'action', requestId: item.requestId, type: 'approve_always' };
145
+ if (input === 'y')
146
+ return { kind: 'action', requestId: item.requestId, type: 'approve' };
147
+ if (input === 'n' || input.startsWith('n ')) {
148
+ const reason = text.trim().slice(1).trim() || '用户拒绝';
149
+ return { kind: 'action', requestId: item.requestId, type: 'reject', reason };
150
+ }
151
+ if (input === 'c')
152
+ return { kind: 'action', requestId: item.requestId, type: 'reject', reason: '用户取消执行' };
153
+ return null;
154
+ }
155
+ case 'question': {
156
+ const payload = item.eventPayload;
157
+ if (input === 'c')
158
+ return { kind: 'question', requestId: item.requestId, type: 'skip', reason: '用户取消执行' };
159
+ if (payload.options.length > 0) {
160
+ const num = parseInt(input, 10);
161
+ if (!isNaN(num) && num >= 1 && num <= payload.options.length) {
162
+ return { kind: 'question', requestId: item.requestId, type: 'answer', content: payload.options[num - 1] };
163
+ }
164
+ }
165
+ return { kind: 'question', requestId: item.requestId, type: 'answer', content: text.trim() };
166
+ }
167
+ case 'sensitive_info_operation': {
168
+ const siPayload = item.eventPayload;
169
+ if (input === 'c') {
170
+ const results = siPayload.operation.items.map((itm) => siPayload.operation.type === 'collect'
171
+ ? { type: 'collect_skipped', key: itm.key, reason: '用户取消执行' }
172
+ : { type: 'delete_rejected', id: itm.id ?? '', reason: '用户取消执行' });
173
+ return { kind: 'sensitive_info_operation', requestId: item.requestId, results };
174
+ }
175
+ const lines = text.trim().split('\n');
176
+ const items = siPayload.operation.items;
177
+ if (siPayload.operation.type === 'collect') {
178
+ const results = items.map((itm, i) => {
179
+ const value = lines[i]?.trim() ?? '';
180
+ return value
181
+ ? { type: 'provided', key: itm.key, value }
182
+ : { type: 'collect_skipped', key: itm.key, reason: '未提供值' };
183
+ });
184
+ return { kind: 'sensitive_info_operation', requestId: item.requestId, results };
185
+ }
186
+ const results = items.map((itm) => ({ type: 'deleted', id: itm.id ?? '' }));
187
+ return { kind: 'sensitive_info_operation', requestId: item.requestId, results };
188
+ }
189
+ }
190
+ }
191
+ /** Get or create senderId→activityId sub-map for a chatId. */
192
+ ensureUserMap(chatId) {
193
+ let map = this.chatUserToActivity.get(chatId);
194
+ if (!map) {
195
+ map = new Map();
196
+ this.chatUserToActivity.set(chatId, map);
197
+ }
198
+ return map;
199
+ }
200
+ /** Look up activityId for (chatId, senderId). */
201
+ getActivityForUser(chatId, senderId) {
202
+ return this.chatUserToActivity.get(chatId)?.get(senderId);
203
+ }
204
+ /** Bind activityId ↔ (chatId, senderId) bidirectionally. */
205
+ bindActivity(chatId, senderId, activityId) {
206
+ this.ensureUserMap(chatId).set(senderId, activityId);
207
+ this.activityToTarget.set(activityId, { chatId, senderId });
208
+ }
209
+ /** Unbind activityId for (chatId, senderId). */
210
+ unbindActivity(chatId, senderId) {
211
+ const userMap = this.chatUserToActivity.get(chatId);
212
+ if (!userMap)
213
+ return;
214
+ const oldActivityId = userMap.get(senderId);
215
+ userMap.delete(senderId);
216
+ if (userMap.size === 0)
217
+ this.chatUserToActivity.delete(chatId);
218
+ if (oldActivityId)
219
+ this.activityToTarget.delete(oldActivityId);
220
+ }
221
+ /** activityId → state (processingMessageId, thinkingStart, toolActiveStart, expressingStart) */
222
+ activityStates = new Map();
223
+ async setTypingIndicatorIfNeeded(chatId, senderId, activityId, messageId) {
224
+ if (!this.cloud)
225
+ return;
226
+ let state = this.activityStates.get(activityId);
227
+ if (!state) {
228
+ state = {};
229
+ this.activityStates.set(activityId, state);
230
+ }
231
+ state.processingMessageId = messageId;
232
+ try {
233
+ await this.cloud.setTypingIndicator(chatId, senderId, messageId);
234
+ }
235
+ catch (err) {
236
+ log.debug('setTypingIndicator failed: %s', err);
237
+ }
238
+ }
239
+ async releaseTypingIndicatorForActivity(chatId, senderId, activityId) {
240
+ if (!this.cloud)
241
+ return;
242
+ const state = this.activityStates.get(activityId);
243
+ const msgId = state?.processingMessageId;
244
+ if (msgId === undefined)
245
+ return;
246
+ delete state.processingMessageId;
247
+ try {
248
+ await this.cloud.releaseTypingIndicator(chatId, senderId, msgId);
249
+ }
250
+ catch (err) {
251
+ log.debug('releaseTypingIndicator failed: %s', err);
252
+ }
253
+ }
254
+ async endThinking(_chatId, activityId) {
255
+ const state = this.activityStates.get(activityId);
256
+ if (!state?.thinkingStart)
257
+ return;
258
+ // Elapsed time available for future use: (Date.now() - state.thinkingStart) / 1000
259
+ delete state.thinkingStart;
260
+ }
261
+ /** Send a single ContentPart to the cloud platform. */
262
+ async _sendContentPart(chatId, senderId, activityId, part) {
263
+ if (!this.cloud)
264
+ return;
265
+ if (part.type === 'text') {
266
+ log.debug('→ cloud send: text (chatId=%s)', chatId);
267
+ await this.cloud.send(chatId, { type: 'text', text: part.text });
268
+ }
269
+ else {
270
+ const src = part.source;
271
+ if (src.localUri || src.remoteUrl) {
272
+ log.info('→ cloud send: %s (chatId=%s)', part.type, chatId);
273
+ await this.cloud.send(chatId, { type: part.type, source: src });
274
+ }
275
+ else {
276
+ log.warn('→ cloud send: %s fallback to text — no source (chatId=%s)', part.type, chatId);
277
+ await this.cloud.send(chatId, { type: 'text', text: `[${part.type}]` });
278
+ }
279
+ }
280
+ }
281
+ /** Handle Command from downstream Agent (forwarded via XabotSessionHandler.onCommand). */
282
+ async handleCommand(command) {
283
+ if (!this.cloud)
284
+ return { kind: 'acknowledge' };
285
+ if (typeof command === 'object' && 'message' in command) {
286
+ const chatId = this.sessionChatId;
287
+ if (!chatId)
288
+ return { kind: 'acknowledge' };
289
+ for (const part of command.message.content) {
290
+ await this._sendContentPart(chatId, '', '', part);
291
+ }
292
+ return { kind: 'acknowledge' };
293
+ }
294
+ return { kind: 'acknowledge' };
295
+ }
296
+ /** Handle Event from downstream Agent (forwarded via XabotSessionHandler.onEvent). */
297
+ async handleEvent(activityId, event) {
298
+ if (!this.cloud)
299
+ return { kind: 'acknowledge' };
300
+ const target = this.activityToTarget.get(activityId);
301
+ if (!target)
302
+ return { kind: 'acknowledge' };
303
+ const { chatId } = target;
304
+ // Non-think events end the thinking phase
305
+ if (event.event.type !== 'think') {
306
+ const state = this.activityStates.get(activityId);
307
+ if (state?.thinkingStart) {
308
+ await this.endThinking(chatId, activityId);
309
+ }
310
+ }
311
+ // Non-content_delta events clear expressing phase
312
+ if (event.event.type !== 'content_delta') {
313
+ const state = this.activityStates.get(activityId);
314
+ if (state?.expressingStart !== undefined) {
315
+ delete state.expressingStart;
316
+ }
317
+ }
318
+ // Handle typing indicator: refresh for non-complete, release for complete
319
+ if (event.event.type === 'complete') {
320
+ const state = this.activityStates.get(activityId);
321
+ if (state?.processingMessageId !== undefined) {
322
+ await this.cloud.releaseTypingIndicator(chatId, target.senderId, state.processingMessageId);
323
+ delete state.processingMessageId;
324
+ }
325
+ }
326
+ else {
327
+ try {
328
+ await this.cloud.refreshTypingIndicator(chatId, target.senderId);
329
+ }
330
+ catch (err) {
331
+ log.debug('refreshTypingIndicator failed: %s', err);
332
+ }
333
+ }
334
+ switch (event.event.type) {
335
+ case 'think': {
336
+ if (!chatId)
337
+ return { kind: 'acknowledge' };
338
+ let state = this.activityStates.get(activityId);
339
+ if (!state) {
340
+ state = {};
341
+ this.activityStates.set(activityId, state);
342
+ }
343
+ if (state.thinkingStart === undefined) {
344
+ state.thinkingStart = Date.now();
345
+ if (this.verbose) {
346
+ try {
347
+ await this.cloud.send(chatId, { type: 'text', text: '💭 正在思考...' });
348
+ }
349
+ catch (err) {
350
+ log.debug('think indicator send failed: %s', err);
351
+ }
352
+ }
353
+ }
354
+ return { kind: 'acknowledge' };
355
+ }
356
+ case 'content_delta': {
357
+ if (!chatId)
358
+ return { kind: 'acknowledge' };
359
+ // content_delta is only emitted in streaming mode — forward only on streaming platforms
360
+ if (this.cloud.streamCapability() === StreamCapability.NonStreaming) {
361
+ return { kind: 'acknowledge' };
362
+ }
363
+ let state = this.activityStates.get(activityId);
364
+ if (!state) {
365
+ state = {};
366
+ this.activityStates.set(activityId, state);
367
+ }
368
+ if (state.expressingStart === undefined) {
369
+ state.expressingStart = Date.now();
370
+ try {
371
+ await this.cloud.send(chatId, { type: 'text', text: '正在组织表达...' });
372
+ }
373
+ catch (err) {
374
+ log.debug('expressing indicator send failed: %s', err);
375
+ }
376
+ }
377
+ const payload = event.event.payload;
378
+ await this._sendContentPart(chatId, target.senderId, activityId, payload);
379
+ return { kind: 'acknowledge' };
380
+ }
381
+ case 'content_part': {
382
+ if (!chatId)
383
+ return { kind: 'acknowledge' };
384
+ // content_part is emitted in non-streaming mode — forward only if verbose
385
+ if (this.verbose) {
386
+ const payload = event.event.payload;
387
+ await this._sendContentPart(chatId, target.senderId, activityId, payload);
388
+ }
389
+ return { kind: 'acknowledge' };
390
+ }
391
+ case 'tool_result': {
392
+ if (!chatId)
393
+ return { kind: 'acknowledge' };
394
+ const payload = event.event;
395
+ if (payload.toolName === 'send_message') {
396
+ for (const part of payload.parts) {
397
+ await this._sendContentPart(chatId, target.senderId, activityId, part);
398
+ }
399
+ }
400
+ return { kind: 'acknowledge' };
401
+ }
402
+ case 'complete': {
403
+ const payload = event.event;
404
+ for (const part of payload.assistantReply) {
405
+ await this._sendContentPart(chatId, target.senderId, activityId, part);
406
+ }
407
+ return { kind: 'acknowledge' };
408
+ }
409
+ case 'notify': {
410
+ if (!chatId)
411
+ return { kind: 'acknowledge' };
412
+ await this.cloud.send(chatId, { type: 'text', text: event.event.message });
413
+ return { kind: 'acknowledge' };
414
+ }
415
+ case 'action_request': {
416
+ if (!chatId)
417
+ return { kind: 'acknowledge' };
418
+ const key = this.targetKey(chatId, target.senderId);
419
+ const pq = this.ensureQueue(key);
420
+ const item = this.createPendingItem(chatId, target.senderId, 'action_request', event.event);
421
+ const pendingPromise = new Promise((resolve) => { item.resolve = resolve; });
422
+ if (!pq.active) {
423
+ pq.active = item;
424
+ try {
425
+ await this.cloud.send(chatId, { type: 'text', text: this.formatActionMessage(event.event) });
426
+ }
427
+ catch {
428
+ pq.active = null;
429
+ item.resolve({ kind: 'error', code: 'send_failed', message: 'failed to forward action_request to cloud' });
430
+ }
431
+ }
432
+ else {
433
+ pq.queue.push(item);
434
+ }
435
+ return pendingPromise;
436
+ }
437
+ case 'question': {
438
+ if (!chatId)
439
+ return { kind: 'acknowledge' };
440
+ const key = this.targetKey(chatId, target.senderId);
441
+ const pq = this.ensureQueue(key);
442
+ const item = this.createPendingItem(chatId, target.senderId, 'question', event.event);
443
+ const pendingPromise = new Promise((resolve) => { item.resolve = resolve; });
444
+ if (!pq.active) {
445
+ pq.active = item;
446
+ try {
447
+ await this.cloud.send(chatId, { type: 'text', text: this.formatQuestionMessage(event.event) });
448
+ }
449
+ catch {
450
+ pq.active = null;
451
+ item.resolve({ kind: 'error', code: 'send_failed', message: 'failed to forward question to cloud' });
452
+ }
453
+ }
454
+ else {
455
+ pq.queue.push(item);
456
+ }
457
+ return pendingPromise;
458
+ }
459
+ case 'sensitive_info_operation': {
460
+ if (!chatId)
461
+ return { kind: 'acknowledge' };
462
+ const key = this.targetKey(chatId, target.senderId);
463
+ const pq = this.ensureQueue(key);
464
+ const item = this.createPendingItem(chatId, target.senderId, 'sensitive_info_operation', event.event);
465
+ const pendingPromise = new Promise((resolve) => { item.resolve = resolve; });
466
+ if (!pq.active) {
467
+ pq.active = item;
468
+ try {
469
+ await this.cloud.send(chatId, { type: 'text', text: this.formatSensitiveInfoMessage(event.event) });
470
+ }
471
+ catch {
472
+ pq.active = null;
473
+ item.resolve({ kind: 'error', code: 'send_failed', message: 'failed to forward sensitive_info_operation to cloud' });
474
+ }
475
+ }
476
+ else {
477
+ pq.queue.push(item);
478
+ }
479
+ return pendingPromise;
480
+ }
481
+ case 'info': {
482
+ if (!chatId)
483
+ return { kind: 'acknowledge' };
484
+ await this.cloud.send(chatId, { type: 'text', text: `ℹ️ ${event.event.content}` });
485
+ return { kind: 'acknowledge' };
486
+ }
487
+ case 'warn': {
488
+ if (!chatId)
489
+ return { kind: 'acknowledge' };
490
+ await this.cloud.send(chatId, { type: 'text', text: `⚠️ ${event.event.content}` });
491
+ return { kind: 'acknowledge' };
492
+ }
493
+ case 'error': {
494
+ if (!chatId)
495
+ return { kind: 'acknowledge' };
496
+ await this.cloud.send(chatId, { type: 'text', text: `❌ ${event.event.content}` });
497
+ return { kind: 'acknowledge' };
498
+ }
499
+ case 'tool_use': {
500
+ if (!chatId)
501
+ return { kind: 'acknowledge' };
502
+ let state = this.activityStates.get(activityId);
503
+ if (!state) {
504
+ state = {};
505
+ this.activityStates.set(activityId, state);
506
+ }
507
+ if (state.toolActiveStart === undefined) {
508
+ state.toolActiveStart = Date.now();
509
+ if (this.verbose) {
510
+ try {
511
+ await this.cloud.send(chatId, { type: 'text', text: '🔧 行动中...' });
512
+ }
513
+ catch (err) {
514
+ log.debug('tool_use indicator send failed: %s', err);
515
+ }
516
+ }
517
+ }
518
+ return { kind: 'acknowledge' };
519
+ }
520
+ case 'pair_complete': {
521
+ if (this.verbose) {
522
+ const state = this.activityStates.get(activityId);
523
+ if (state?.toolActiveStart !== undefined) {
524
+ delete state.toolActiveStart;
525
+ // Elapsed time available for future use: (Date.now() - start) / 1000
526
+ }
527
+ }
528
+ return { kind: 'acknowledge' };
529
+ }
530
+ default: {
531
+ return { kind: 'acknowledge' };
532
+ }
533
+ }
534
+ }
535
+ /** Send the next queued pending item to cloud. */
536
+ async sendNextPending(item) {
537
+ let message;
538
+ switch (item.type) {
539
+ case 'action_request':
540
+ message = this.formatActionMessage(item.eventPayload);
541
+ break;
542
+ case 'question':
543
+ message = this.formatQuestionMessage(item.eventPayload);
544
+ break;
545
+ case 'sensitive_info_operation':
546
+ message = this.formatSensitiveInfoMessage(item.eventPayload);
547
+ break;
548
+ }
549
+ await this.cloud.send(item.chatId, { type: 'text', text: message });
550
+ }
551
+ /** Resolve a pending request and drive the queue forward. */
552
+ resolvePending(requestId, response) {
553
+ for (const [key, pq] of this.pendingQueues) {
554
+ if (pq.active?.requestId === requestId) {
555
+ const resolve = pq.active.resolve;
556
+ pq.active = null;
557
+ resolve(response);
558
+ if (pq.queue.length > 0) {
559
+ const next = pq.queue.shift();
560
+ pq.active = next;
561
+ this.sendNextPending(next).catch(() => {
562
+ pq.active = null;
563
+ next.resolve({ kind: 'error', code: 'send_failed', message: 'failed to forward queued event to cloud' });
564
+ });
565
+ }
566
+ else {
567
+ this.pendingQueues.delete(key);
568
+ }
569
+ return;
570
+ }
571
+ }
572
+ }
573
+ /** Cloud message loop: two-phase consumption (idempotent). */
574
+ async run() {
575
+ if (this.runPromise)
576
+ return this.runPromise;
577
+ if (this.closed) {
578
+ this.runPromise = Promise.resolve();
579
+ return this.runPromise;
580
+ }
581
+ this.runPromise = this._run();
582
+ return this.runPromise;
583
+ }
584
+ async _run() {
585
+ if (this.closed)
586
+ return;
587
+ await this.cloudReady;
588
+ if (this.closed || !this.cloud)
589
+ return;
590
+ const { signal } = this.abortCtrl;
591
+ try {
592
+ for await (const msg of this.cloud.messages()) {
593
+ if (signal.aborted)
594
+ break;
595
+ // Phase 1: pre-establish — scan for challenge messages
596
+ if (!this.established) {
597
+ if (msg.content.type === 'text') {
598
+ log.debug('Phase1: scanning challenge from text (chatId=%s)', msg.chatId);
599
+ this.establishHandler?.submitChallenge(msg.content.text, msg.chatId);
600
+ }
601
+ // Continue consuming — don't route to session yet
602
+ continue;
603
+ }
604
+ // Phase 2: post-establish — normal activity routing
605
+ if (!this.session) {
606
+ continue;
607
+ }
608
+ const { chatId, senderId } = msg;
609
+ // ── Text messages: parse and dispatch by kind ─────────────────────
610
+ if (msg.content.type === 'text' && msg.fallback) {
611
+ const mediaKey = this.targetKey(chatId, senderId);
612
+ let buffered = this.pendingMediaByTarget.get(mediaKey);
613
+ if (!buffered) {
614
+ buffered = [];
615
+ this.pendingMediaByTarget.set(mediaKey, buffered);
616
+ }
617
+ buffered.push({ ...msg.content });
618
+ log.debug('← cloud recv: fallback (chatId=%s, buffered=%d)', chatId, buffered.length);
619
+ continue;
620
+ }
621
+ if (msg.content.type === 'text') {
622
+ const parsed = parseInput(msg.content.text);
623
+ switch (parsed.kind) {
624
+ case 'new': {
625
+ this.unbindActivity(chatId, senderId);
626
+ const createResponse = await this.session.requestCommand({
627
+ new_activity: { title: '' },
628
+ });
629
+ if (createResponse.kind === 'activity_ready') {
630
+ const newActivityId = createResponse.activity;
631
+ this.bindActivity(chatId, senderId, newActivityId);
632
+ if (parsed.prompt) {
633
+ await this.session.requestCommand({
634
+ invoke_activity: {
635
+ activity: newActivityId,
636
+ messages: [{ type: 'text', text: parsed.prompt }],
637
+ },
638
+ });
639
+ await this.setTypingIndicatorIfNeeded(chatId, senderId, newActivityId, msg.id);
640
+ }
641
+ }
642
+ continue;
643
+ }
644
+ case 'compact': {
645
+ let activityId = this.getActivityForUser(chatId, senderId);
646
+ if (!activityId) {
647
+ const lastResponse = await this.session.requestCommand('last_activity');
648
+ if (lastResponse.kind === 'activity_ready') {
649
+ activityId = lastResponse.activity;
650
+ this.bindActivity(chatId, senderId, activityId);
651
+ }
652
+ else {
653
+ await this.cloud.send(chatId, { type: 'text', text: '当前暂无活动中的对话' });
654
+ continue;
655
+ }
656
+ }
657
+ await this.session.requestCommand({
658
+ compact_activity: { activity: activityId },
659
+ });
660
+ continue;
661
+ }
662
+ case 'cancel': {
663
+ let activityId = this.getActivityForUser(chatId, senderId);
664
+ if (!activityId) {
665
+ const lastResponse = await this.session.requestCommand('last_activity');
666
+ if (lastResponse.kind === 'activity_ready') {
667
+ activityId = lastResponse.activity;
668
+ this.bindActivity(chatId, senderId, activityId);
669
+ }
670
+ else {
671
+ await this.cloud.send(chatId, { type: 'text', text: '当前暂无活动中的对话' });
672
+ continue;
673
+ }
674
+ }
675
+ await this.session.requestCommand({
676
+ cancel_activity: { activity: activityId },
677
+ });
678
+ continue;
679
+ }
680
+ case 'unknown_command': {
681
+ await this.cloud.send(chatId, {
682
+ type: 'text',
683
+ text: `未知命令: ${parsed.command},支持的命令: /new, /compact, /cancel`,
684
+ });
685
+ continue;
686
+ }
687
+ case 'invoke': {
688
+ // ── Pending response routing (highest priority) ──
689
+ const key = this.targetKey(chatId, senderId);
690
+ const pq = this.pendingQueues.get(key);
691
+ if (pq?.active) {
692
+ const response = this.tryParsePendingResponse(pq.active, parsed.text);
693
+ if (response) {
694
+ this.resolvePending(pq.active.requestId, response);
695
+ // c: cancel execution — cancel activity and notify cloud
696
+ if (parsed.text.trim().toLowerCase() === 'c') {
697
+ const actId = this.getActivityForUser(chatId, senderId);
698
+ if (actId) {
699
+ await this.session.requestCommand({ cancel_activity: { activity: actId } });
700
+ await this.cloud.send(chatId, { type: 'text', text: '✅ 已终止当前任务,可下达新的指令' });
701
+ }
702
+ }
703
+ }
704
+ else {
705
+ await this.cloud.send(chatId, { type: 'text', text: '⚠️ 无法识别的指令,请按照提示回复' });
706
+ }
707
+ continue;
708
+ }
709
+ // ── Original invoke logic ──
710
+ let activityId = this.getActivityForUser(chatId, senderId);
711
+ if (!activityId) {
712
+ const lastResponse = await this.session.requestCommand('last_activity');
713
+ if (lastResponse.kind === 'activity_ready') {
714
+ activityId = lastResponse.activity;
715
+ }
716
+ else {
717
+ const createResponse = await this.session.requestCommand({
718
+ new_activity: { title: '' },
719
+ });
720
+ if (createResponse.kind === 'activity_ready') {
721
+ activityId = createResponse.activity;
722
+ }
723
+ else {
724
+ continue;
725
+ }
726
+ }
727
+ this.bindActivity(chatId, senderId, activityId);
728
+ }
729
+ const invokeKey = this.targetKey(chatId, senderId);
730
+ const bufferedMedia = this.pendingMediaByTarget.get(invokeKey) ?? [];
731
+ if (bufferedMedia.length > 0) {
732
+ this.pendingMediaByTarget.delete(invokeKey);
733
+ }
734
+ await this.session.requestCommand({
735
+ invoke_activity: {
736
+ activity: activityId,
737
+ messages: [...bufferedMedia, { type: 'text', text: parsed.text }],
738
+ },
739
+ });
740
+ await this.setTypingIndicatorIfNeeded(chatId, senderId, activityId, msg.id);
741
+ continue;
742
+ }
743
+ }
744
+ }
745
+ // ── Non-text messages: PlatformClient already resolved localUri ─────
746
+ const part = { ...msg.content };
747
+ const mediaKey = this.targetKey(chatId, senderId);
748
+ let buffered = this.pendingMediaByTarget.get(mediaKey);
749
+ if (!buffered) {
750
+ buffered = [];
751
+ this.pendingMediaByTarget.set(mediaKey, buffered);
752
+ }
753
+ buffered.push(part);
754
+ log.debug('← cloud recv: %s (chatId=%s, buffered=%d)', msg.content.type, chatId, buffered.length);
755
+ }
756
+ }
757
+ catch (err) {
758
+ log.error('cloud message loop error: %s', err);
759
+ this.abortCtrl.abort();
760
+ }
761
+ }
762
+ /** Mark the establish handshake as completed — switches to Phase 2 routing. */
763
+ markEstablished() {
764
+ this.established = true;
765
+ if (this.cloud && this.sessionChatId) {
766
+ this.cloud.send(this.sessionChatId, { type: 'text', text: '✅ 连接已建立' })
767
+ .catch((err) => log.debug('established notification send failed: %s', err));
768
+ }
769
+ }
770
+ async close() {
771
+ if (this.closed)
772
+ return;
773
+ this.closed = true;
774
+ if (this.established && this.cloud && this.sessionChatId) {
775
+ await this.cloud.send(this.sessionChatId, { type: 'text', text: '👋 连接已关闭' })
776
+ .catch((err) => log.debug('disconnect notification send failed: %s', err));
777
+ }
778
+ this.abortCtrl.abort();
779
+ this.runPromise = null;
780
+ for (const [, pq] of this.pendingQueues) {
781
+ if (pq.active) {
782
+ pq.active.resolve({ kind: 'error', code: 'cancelled', message: 'Bridge closing' });
783
+ }
784
+ for (const item of pq.queue) {
785
+ item.resolve({ kind: 'error', code: 'cancelled', message: 'Bridge closing' });
786
+ }
787
+ }
788
+ this.pendingQueues.clear();
789
+ this.pendingMediaByTarget.clear();
790
+ for (const [activityId, state] of this.activityStates) {
791
+ if (state.processingMessageId !== undefined) {
792
+ const target = this.activityToTarget.get(activityId);
793
+ if (target) {
794
+ await this.cloud?.releaseTypingIndicator(target.chatId, target.senderId, state.processingMessageId).catch(() => { });
795
+ }
796
+ }
797
+ }
798
+ this.activityStates.clear();
799
+ this.chatUserToActivity.clear();
800
+ this.activityToTarget.clear();
801
+ if (this.cloudReadyResolve) {
802
+ this.cloudReadyResolve();
803
+ this.cloudReadyResolve = null;
804
+ }
805
+ await Promise.all([
806
+ this.cloud?.close(),
807
+ this.transport.disconnect(),
808
+ ]);
809
+ }
810
+ }
811
+ //# sourceMappingURL=index.js.map