@xmanrui/dsh-im 4.22.0 → 4.24.0

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 (148) hide show
  1. package/README.en.md +5 -4
  2. package/README.md +5 -4
  3. package/lib/client.js +2584 -1153
  4. package/lib/index.js +290 -298
  5. package/package.json +14 -2
  6. package/plugin-src/client/channel-card-meta.js +2 -0
  7. package/plugin-src/client/channel-logos.js +11 -0
  8. package/plugin-src/client/channels/dingtalk/api.js +5 -1
  9. package/plugin-src/client/channels/dingtalk/index.js +15 -13
  10. package/plugin-src/client/channels/email/api.js +52 -0
  11. package/plugin-src/client/channels/email/index.js +728 -0
  12. package/plugin-src/client/channels/email/styles.js +51 -0
  13. package/plugin-src/client/channels/feishu/api.js +4 -2
  14. package/plugin-src/client/channels/feishu/index.js +23 -7
  15. package/plugin-src/client/channels/imessage/index.js +2 -1
  16. package/plugin-src/client/channels/office/api.js +2 -0
  17. package/plugin-src/client/channels/office/index.js +6 -5
  18. package/plugin-src/client/channels/qq/api.js +7 -0
  19. package/plugin-src/client/channels/qq/index.js +16 -3
  20. package/plugin-src/client/channels/shared/token-api.js +8 -1
  21. package/plugin-src/client/channels/shared/token-channel.js +28 -3
  22. package/plugin-src/client/channels/slack/index.js +2 -1
  23. package/plugin-src/client/channels/telegram/index.js +3 -0
  24. package/plugin-src/client/channels/telegram/styles.js +12 -0
  25. package/plugin-src/client/channels/telegram/thinking-traces.js +25 -0
  26. package/plugin-src/client/channels/wecom/api.js +7 -1
  27. package/plugin-src/client/channels/wecom/index.js +16 -3
  28. package/plugin-src/client/channels/wecom-app/api.js +6 -1
  29. package/plugin-src/client/channels/wecom-app/index.js +16 -3
  30. package/plugin-src/client/channels/weixin/api.js +2 -1
  31. package/plugin-src/client/channels/weixin/connection-error.js +1 -71
  32. package/plugin-src/client/channels/whatsapp/api.js +5 -0
  33. package/plugin-src/client/channels/whatsapp/index.js +16 -3
  34. package/plugin-src/client/connection-error.js +87 -0
  35. package/plugin-src/client/global-settings.js +81 -1
  36. package/plugin-src/client/i18n.js +69 -1
  37. package/plugin-src/client/index.js +40 -4
  38. package/plugin-src/client/last-message-error.js +2 -1
  39. package/plugin-src/client/styles.js +8 -2
  40. package/plugin-src/host/build.mjs +4 -0
  41. package/plugin-src/host/channels/dingtalk/production.mjs +2 -0
  42. package/plugin-src/host/channels/dingtalk/rpc.mjs +7 -4
  43. package/plugin-src/host/channels/email/availability.mjs +60 -0
  44. package/plugin-src/host/channels/email/index.mjs +36 -0
  45. package/plugin-src/host/channels/email/production.mjs +58 -0
  46. package/plugin-src/host/channels/email/rpc.mjs +163 -0
  47. package/plugin-src/host/channels/feishu/production.mjs +3 -0
  48. package/plugin-src/host/channels/feishu/rpc.mjs +7 -4
  49. package/plugin-src/host/channels/imessage/rpc.mjs +13 -6
  50. package/plugin-src/host/channels/office/rpc.mjs +3 -1
  51. package/plugin-src/host/channels/qq/production.mjs +2 -0
  52. package/plugin-src/host/channels/qq/rpc.mjs +5 -3
  53. package/plugin-src/host/channels/shared/production.mjs +52 -5
  54. package/plugin-src/host/channels/shared/rpc.mjs +14 -3
  55. package/plugin-src/host/channels/shared/startup-error.mjs +4 -3
  56. package/plugin-src/host/channels/shared/startup.mjs +11 -6
  57. package/plugin-src/host/channels/shared/thinking-traces-rpc.mjs +11 -0
  58. package/plugin-src/host/channels/slack/production.mjs +2 -0
  59. package/plugin-src/host/channels/slack/rpc.mjs +5 -3
  60. package/plugin-src/host/channels/wecom/production.mjs +2 -0
  61. package/plugin-src/host/channels/wecom/rpc.mjs +5 -3
  62. package/plugin-src/host/channels/wecom-app/production.mjs +2 -0
  63. package/plugin-src/host/channels/wecom-app/rpc.mjs +5 -3
  64. package/plugin-src/host/channels/weixin/production.mjs +2 -0
  65. package/plugin-src/host/channels/whatsapp/production.mjs +2 -0
  66. package/plugin-src/host/channels/whatsapp/rpc.mjs +5 -3
  67. package/plugin-src/host/delivery-adapter.mjs +11 -0
  68. package/plugin-src/host/image-input-rpc.mjs +24 -0
  69. package/plugin-src/host/inbound-ttl-rpc.mjs +6 -1
  70. package/plugin-src/host/index.mjs +3 -0
  71. package/plugin-src/host/modern-harness-api.mjs +7 -2
  72. package/plugin-src/management-rpc.mjs +12 -2
  73. package/scripts/verify-package.mjs +13 -5
  74. package/src/channels/dingtalk/connection-error.mjs +5 -12
  75. package/src/channels/dingtalk/device-auth.mjs +4 -1
  76. package/src/channels/dingtalk/dingtalk-bridge.mjs +6 -4
  77. package/src/channels/dingtalk/dingtalk-controller.mjs +60 -45
  78. package/src/channels/dingtalk/dingtalk-runtime.mjs +20 -4
  79. package/src/channels/discord/discord-api.mjs +3 -3
  80. package/src/channels/discord/discord-runtime.mjs +18 -12
  81. package/src/channels/email/config-store.mjs +194 -0
  82. package/src/channels/email/email-api.mjs +20 -0
  83. package/src/channels/email/email-bridge.mjs +17 -0
  84. package/src/channels/email/email-controller.mjs +824 -0
  85. package/src/channels/email/email-runtime.mjs +699 -0
  86. package/src/channels/email/harness-client.mjs +7 -0
  87. package/src/channels/email/mail-format.mjs +91 -0
  88. package/src/channels/email/state-store.mjs +176 -0
  89. package/src/channels/email/transport.mjs +64 -0
  90. package/src/channels/email/transports/agent-mail.mjs +604 -0
  91. package/src/channels/email/transports/agently-cli.mjs +288 -0
  92. package/src/channels/email/transports/imap-smtp.mjs +165 -0
  93. package/src/channels/feishu/bridge.mjs +39 -11
  94. package/src/channels/feishu/feishu-channel.mjs +35 -0
  95. package/src/channels/feishu/feishu-runtime.mjs +15 -8
  96. package/src/channels/feishu/group-message-permission-manager.mjs +2 -1
  97. package/src/channels/feishu/live-cot.mjs +260 -0
  98. package/src/channels/feishu/message-utils.mjs +3 -2
  99. package/src/channels/feishu/multi-bot-controller.mjs +56 -47
  100. package/src/channels/feishu/registration-manager.mjs +8 -4
  101. package/src/channels/feishu/repair-manager.mjs +2 -1
  102. package/src/channels/feishu/slash-command-registry.mjs +17 -0
  103. package/src/channels/feishu/step-push-mode.mjs +10 -4
  104. package/src/channels/imessage/runtime.mjs +9 -5
  105. package/src/channels/office/office-controller.mjs +23 -12
  106. package/src/channels/office/office-runtime.mjs +6 -3
  107. package/src/channels/office/office-transport.mjs +1 -0
  108. package/src/channels/qq/qq-bridge.mjs +7 -5
  109. package/src/channels/qq/qq-controller.mjs +56 -42
  110. package/src/channels/qq/qq-runtime.mjs +16 -11
  111. package/src/channels/shared/bot-workspace-store.mjs +29 -10
  112. package/src/channels/shared/connection-error.mjs +191 -0
  113. package/src/channels/shared/connection-test.mjs +3 -1
  114. package/src/channels/shared/conversation-state-store.mjs +46 -1
  115. package/src/channels/shared/diagnostic-details.mjs +95 -0
  116. package/src/channels/shared/harness-client.mjs +287 -57
  117. package/src/channels/shared/i18n-en/diagnostics.mjs +55 -0
  118. package/src/channels/shared/i18n-en/email.mjs +25 -0
  119. package/src/channels/shared/i18n-en/image-input.mjs +17 -0
  120. package/src/channels/shared/i18n-en/shared-a.mjs +1 -0
  121. package/src/channels/shared/i18n-en/shared-c.mjs +1 -1
  122. package/src/channels/shared/i18n-en.mjs +6 -0
  123. package/src/channels/shared/image-input-policy.mjs +35 -0
  124. package/src/channels/shared/image-input-settings-store.mjs +60 -0
  125. package/src/channels/shared/image-input.mjs +124 -0
  126. package/src/channels/shared/image-prompt.mjs +15 -7
  127. package/src/channels/shared/message-failure.mjs +12 -0
  128. package/src/channels/shared/semantic/reply-reference.mjs +3 -3
  129. package/src/channels/shared/text-harness-bridge.mjs +150 -25
  130. package/src/channels/shared/token-bot-controller.mjs +52 -47
  131. package/src/channels/slack/slack-api.mjs +3 -3
  132. package/src/channels/slack/slack-controller.mjs +59 -51
  133. package/src/channels/slack/slack-runtime.mjs +18 -12
  134. package/src/channels/telegram/config-store.mjs +4 -1
  135. package/src/channels/telegram/telegram-api.mjs +2 -2
  136. package/src/channels/telegram/telegram-controller.mjs +13 -1
  137. package/src/channels/telegram/telegram-runtime.mjs +215 -14
  138. package/src/channels/wecom/wecom-bridge.mjs +15 -6
  139. package/src/channels/wecom/wecom-controller.mjs +51 -37
  140. package/src/channels/wecom/wecom-runtime.mjs +10 -6
  141. package/src/channels/wecom-app/wecom-app-bridge.mjs +7 -5
  142. package/src/channels/wecom-app/wecom-app-controller.mjs +50 -37
  143. package/src/channels/wecom-app/wecom-app-runtime.mjs +2 -1
  144. package/src/channels/weixin/connection-error.mjs +26 -92
  145. package/src/channels/weixin/diagnostic-details.mjs +1 -63
  146. package/src/channels/weixin/weixin-bridge.mjs +7 -5
  147. package/src/channels/whatsapp/whatsapp-controller.mjs +34 -17
  148. package/src/channels/whatsapp/whatsapp-runtime.mjs +35 -8
@@ -1,3 +1,4 @@
1
+ import { createConnectionDiagnostics, atConnectionStage } from '../shared/connection-error.mjs';
1
2
  import { randomUUID } from 'node:crypto';
2
3
  import { FeishuHarnessBridge } from './bridge.mjs';
3
4
  import { cardActionProbeCard } from './feishu-cards.mjs';
@@ -125,6 +126,7 @@ export class FeishuRuntime {
125
126
  #requestTimeoutMs;
126
127
  #wsAgent;
127
128
  #logger;
129
+ #diagnostics;
128
130
  #repair;
129
131
  #client = null;
130
132
  #bridge = null;
@@ -200,7 +202,7 @@ export class FeishuRuntime {
200
202
  this.#requestTimeoutMs = requestTimeoutMs;
201
203
  this.#slashCommands = Boolean(slashCommands);
202
204
  this.#wsAgent = wsAgent;
203
- this.#logger = logger;
205
+ this.#logger = logger; this.#diagnostics = createConnectionDiagnostics({ channel: 'feishu', logger });
204
206
  this.#status = createBridgeStatus({ allowedSenderCount: normalizedOwners.length });
205
207
  }
206
208
 
@@ -277,10 +279,10 @@ export class FeishuRuntime {
277
279
  };
278
280
  this.#status.startedAt = new Date().toISOString();
279
281
  this.#status.feishuLongConnectionState = 'connecting';
280
- this.#status.lastError = null;
282
+ this.#status.lastError = null; this.#status.error = null; this.#diagnostics.clear();
281
283
 
282
284
  try {
283
- await this.#harness.ensureRunning({ signal });
285
+ await atConnectionStage('harness.check', () => this.#harness.ensureRunning({ signal }));
284
286
  assertCurrentStart();
285
287
  this.#status.harnessReachable = true;
286
288
 
@@ -391,15 +393,16 @@ export class FeishuRuntime {
391
393
  if (!isCurrentStart()) return;
392
394
  this.#status.feishuLongConnectionState = 'connected';
393
395
  this.#status.ready = true;
394
- this.#status.lastError = null;
396
+ this.#status.lastError = null; this.#status.error = null; this.#diagnostics.clear();
395
397
  settleReady();
396
398
  },
397
399
  onError: (error) => {
398
400
  if (!isCurrentStart()) return;
399
401
  this.#status.feishuLongConnectionState = 'failed';
400
402
  this.#status.ready = false;
401
- this.#status.lastError = error?.message ?? String(error);
402
- this.#logger.error('[dsh-feishu] Feishu long connection failed:', this.#status.lastError);
403
+ this.#status.error = this.#diagnostics.report(error, { operation: 'connection.monitor', botId: this.#botId, automatic: true }).publicError;
404
+ this.#status.lastError = this.#status.error.message;
405
+
403
406
  settleError(error);
404
407
  },
405
408
  onReconnecting: () => {
@@ -411,7 +414,7 @@ export class FeishuRuntime {
411
414
  if (!isCurrentStart()) return;
412
415
  this.#status.feishuLongConnectionState = 'connected';
413
416
  this.#status.ready = true;
414
- this.#status.lastError = null;
417
+ this.#status.lastError = null; this.#status.error = null; this.#diagnostics.clear();
415
418
  },
416
419
  });
417
420
  this.#wsClient = wsClient;
@@ -436,7 +439,8 @@ export class FeishuRuntime {
436
439
  if (signal.aborted) throw error;
437
440
  this.#status.ready = false;
438
441
  this.#status.feishuLongConnectionState = 'failed';
439
- this.#status.lastError = error?.message ?? String(error);
442
+ this.#status.error = this.#diagnostics.report(error, { operation: 'connection.restore', reuse: true, botId: this.#botId, automatic: true }).publicError;
443
+ this.#status.lastError = this.#status.error.message;
440
444
  await this.#cleanup({ preserveError: true, abortController });
441
445
  throw error;
442
446
  }
@@ -720,6 +724,7 @@ export class FeishuRuntime {
720
724
 
721
725
  async #cleanup({ preserveError = false, abortController } = {}) {
722
726
  const error = preserveError ? this.#status.lastError : null;
727
+ const diagnostic = preserveError ? this.#status.error : null;
723
728
  if (this.#abortController === abortController) this.#abortController = null;
724
729
  abortController?.abort(new DOMException('Feishu runtime stopped', 'AbortError'));
725
730
  for (const probe of this.#pendingCardActionProbes.values()) {
@@ -742,7 +747,9 @@ export class FeishuRuntime {
742
747
  this.#client = null;
743
748
  this.#status.feishuLongConnectionState = preserveError ? 'failed' : 'idle';
744
749
  this.#status.slashCommandRegistration = 'idle';
750
+ this.#status.error = diagnostic;
745
751
  this.#status.lastError = error;
752
+ if (!preserveError) this.#diagnostics.clear();
746
753
  return this.status;
747
754
  }
748
755
  }
@@ -27,7 +27,7 @@ export class GroupMessagePermissionManager {
27
27
  #appId;
28
28
  #domain;
29
29
 
30
- constructor({ registerApp, onCredentials, appId, domain = 'feishu' } = {}) {
30
+ constructor({ registerApp, onCredentials, appId, domain = 'feishu', diagnostics } = {}) {
31
31
  if (typeof registerApp !== 'function') throw new TypeError('registerApp is required');
32
32
  if (typeof onCredentials !== 'function') throw new TypeError('onCredentials is required');
33
33
  if (typeof appId !== 'string' || !appId.trim()) throw new TypeError('appId is required');
@@ -36,6 +36,7 @@ export class GroupMessagePermissionManager {
36
36
  this.#appId = appId.trim();
37
37
  this.#domain = domain;
38
38
  this.#manager = new RegistrationManager({
39
+ diagnostics,
39
40
  registerApp: (options) => registerApp({
40
41
  ...options,
41
42
  onQRCodeReady: (info) => {
@@ -0,0 +1,260 @@
1
+ const MAX_EVENTS_PER_WRITE = 50;
2
+ const MAX_EVENT_CONTENT_CHARS = 4096;
3
+ const MAX_TOOL_RESULT_CHARS = 1500;
4
+
5
+ const TOOL_ICONS = Object.freeze({
6
+ read: 'read',
7
+ edit: 'write',
8
+ delete: 'write',
9
+ move: 'write',
10
+ search: 'search',
11
+ fetch: 'search',
12
+ execute: 'bash',
13
+ });
14
+
15
+ let lastTimestamp = 0;
16
+
17
+ function boundedEventContent(content) {
18
+ const encoded = JSON.stringify(content);
19
+ if (encoded.length <= MAX_EVENT_CONTENT_CHARS) return encoded;
20
+ const field = typeof content?.delta === 'string'
21
+ ? 'delta'
22
+ : typeof content?.message === 'string' ? 'message' : null;
23
+ if (!field) return JSON.stringify({ ...content, truncated: true });
24
+
25
+ const truncated = { ...content, truncated: true, [field]: content[field] };
26
+ while (truncated[field].length > 0
27
+ && JSON.stringify(truncated).length > MAX_EVENT_CONTENT_CHARS) {
28
+ truncated[field] = truncated[field].slice(0, Math.max(0, truncated[field].length - 256));
29
+ }
30
+ if (truncated[field].length === 0
31
+ && JSON.stringify(truncated).length > MAX_EVENT_CONTENT_CHARS) {
32
+ return JSON.stringify({ truncated: true });
33
+ }
34
+ return JSON.stringify(truncated);
35
+ }
36
+
37
+ function cotEvent(eventType, content) {
38
+ lastTimestamp = Math.max(Date.now(), lastTimestamp + 1);
39
+ return {
40
+ event_type: eventType,
41
+ content: boundedEventContent(content),
42
+ timestamp: String(lastTimestamp),
43
+ };
44
+ }
45
+
46
+ function boundedResult(value) {
47
+ const text = String(value ?? '');
48
+ return text.length <= MAX_TOOL_RESULT_CHARS
49
+ ? text
50
+ : `${text.slice(0, MAX_TOOL_RESULT_CHARS - 1)}…`;
51
+ }
52
+
53
+ function toolKind(name) {
54
+ const value = String(name ?? '').toLowerCase();
55
+ if (/read|view|list|get/.test(value)) return 'read';
56
+ if (/edit|write|create|patch|update/.test(value)) return 'edit';
57
+ if (/delete|remove/.test(value)) return 'delete';
58
+ if (/move|rename/.test(value)) return 'move';
59
+ if (/search|grep|glob|find/.test(value)) return 'search';
60
+ if (/fetch|web|http|download/.test(value)) return 'fetch';
61
+ if (/shell|bash|command|exec|terminal/.test(value)) return 'execute';
62
+ return '';
63
+ }
64
+
65
+ function turnFailure(reason) {
66
+ const kind = typeof reason === 'string' ? reason : reason?.kind;
67
+ if (reason === null || reason === undefined || kind === 'completed') return null;
68
+ if (kind !== 'error') {
69
+ return kind ? `Turn ended: ${kind}` : 'Turn failed';
70
+ }
71
+ const error = reason.error ?? reason.failure;
72
+ if (!error) return 'Turn failed';
73
+ return [error.code, error.message].filter(Boolean).join(': ') || 'Turn failed';
74
+ }
75
+
76
+ /**
77
+ * Maps Harness progress updates to Feishu's native thinking-process events.
78
+ * Process failures are contained here so they can never suppress the answer.
79
+ */
80
+ export class FeishuLiveCot {
81
+ #channel;
82
+ #chatId;
83
+ #replyTo;
84
+ #hidden;
85
+ #onFailure;
86
+ #turn = null;
87
+ #opening = null;
88
+ #chain = Promise.resolve();
89
+ #pending = [];
90
+ #reasoningOpen = false;
91
+ #heldAssistant = null;
92
+ #finished = false;
93
+ #broken = false;
94
+ #messageIndex = 0;
95
+
96
+ constructor(channel, chatId, {
97
+ replyTo,
98
+ hidden = false,
99
+ onFailure = () => {},
100
+ } = {}) {
101
+ this.#channel = channel;
102
+ this.#chatId = chatId;
103
+ this.#replyTo = replyTo;
104
+ this.#hidden = hidden === true;
105
+ this.#onFailure = onFailure;
106
+ }
107
+
108
+ async #start(turn) {
109
+ if (this.#turn !== null || this.#broken) return;
110
+ this.#turn = Number.isSafeInteger(turn) ? turn : 0;
111
+ this.#opening = this.#channel.createCot(this.#chatId, {
112
+ ...(this.#replyTo ? { replyTo: this.#replyTo } : {}),
113
+ hidden: this.#hidden,
114
+ }).catch((error) => {
115
+ this.#broken = true;
116
+ this.#onFailure(error);
117
+ return null;
118
+ });
119
+ await this.#write([
120
+ cotEvent('RUN_STARTED', {
121
+ threadId: this.#chatId,
122
+ runId: `turn-${this.#turn}`,
123
+ }),
124
+ ]);
125
+ }
126
+
127
+ #write(events) {
128
+ if (this.#broken || events.length === 0) return;
129
+ this.#pending.push(...events);
130
+ this.#chain = this.#chain.then(async () => {
131
+ const handle = await this.#opening;
132
+ if (!handle || this.#broken) {
133
+ this.#pending.length = 0;
134
+ return;
135
+ }
136
+ while (this.#pending.length > 0) {
137
+ await this.#channel.writeCotEvents(
138
+ handle,
139
+ this.#pending.splice(0, MAX_EVENTS_PER_WRITE),
140
+ );
141
+ }
142
+ }).catch((error) => {
143
+ this.#broken = true;
144
+ this.#pending.length = 0;
145
+ this.#onFailure(error);
146
+ });
147
+ }
148
+
149
+ async #closeReasoning() {
150
+ if (!this.#reasoningOpen) return;
151
+ this.#reasoningOpen = false;
152
+ await this.#write([
153
+ cotEvent('REASONING_MESSAGE_END', {
154
+ messageId: `reasoning-${this.#turn}`,
155
+ }),
156
+ ]);
157
+ }
158
+
159
+ async #flushHeldAssistant() {
160
+ const held = this.#heldAssistant;
161
+ this.#heldAssistant = null;
162
+ if (!held?.text) return;
163
+ const messageId = `text-${this.#turn}-${this.#messageIndex++}`;
164
+ await this.#write([
165
+ cotEvent('TEXT_MESSAGE_START', { messageId, role: 'assistant' }),
166
+ cotEvent('TEXT_MESSAGE_CONTENT', { messageId, delta: held.text }),
167
+ cotEvent('TEXT_MESSAGE_END', { messageId }),
168
+ ]);
169
+ }
170
+
171
+ async handle(update) {
172
+ if (!update || this.#finished || this.#broken) return;
173
+ if (update.type === 'turn-start') {
174
+ await this.#start(update.turn);
175
+ return;
176
+ }
177
+ await this.#start(update.turn);
178
+ if (update.type === 'reasoning') {
179
+ if (!update.text) return;
180
+ const messageId = `reasoning-${this.#turn}`;
181
+ if (!this.#reasoningOpen) {
182
+ this.#reasoningOpen = true;
183
+ await this.#write([
184
+ cotEvent('REASONING_MESSAGE_START', { messageId, role: 'reasoning' }),
185
+ ]);
186
+ }
187
+ await this.#write([
188
+ cotEvent('REASONING_MESSAGE_CONTENT', { messageId, delta: update.text }),
189
+ ]);
190
+ return;
191
+ }
192
+ if (update.type === 'assistant-message') {
193
+ if (this.#heldAssistant) await this.#flushHeldAssistant();
194
+ this.#heldAssistant = update;
195
+ return;
196
+ }
197
+ if (update.type === 'tool') {
198
+ await this.#flushHeldAssistant();
199
+ await this.#closeReasoning();
200
+ const toolCallId = update.callId ?? `tool-${this.#turn}-${this.#messageIndex++}`;
201
+ const kind = toolKind(update.name);
202
+ await this.#write([
203
+ cotEvent('TOOL_CALL_START', {
204
+ toolCallId,
205
+ icon: TOOL_ICONS[kind] ?? 'default',
206
+ title: update.name || 'Tool',
207
+ toolCallName: update.name || 'tool',
208
+ }),
209
+ cotEvent('TOOL_CALL_ARGS', {
210
+ toolCallId,
211
+ delta: update.arguments ?? '',
212
+ }),
213
+ cotEvent('TOOL_CALL_END', { toolCallId }),
214
+ ]);
215
+ return;
216
+ }
217
+ if (update.type === 'tool-result' && update.callId) {
218
+ await this.#write([
219
+ cotEvent('TOOL_CALL_RESULT', {
220
+ messageId: `result-${update.callId}`,
221
+ toolCallId: update.callId,
222
+ role: 'tool',
223
+ content: { type: 'code', code: boundedResult(update.text) },
224
+ ...(update.errorCode ? { error: update.errorCode } : {}),
225
+ }),
226
+ ]);
227
+ return;
228
+ }
229
+ if (update.type === 'turn-end') {
230
+ await this.finish(update.reason);
231
+ }
232
+ }
233
+
234
+ async finish(reason) {
235
+ if (this.#finished) {
236
+ await this.#chain;
237
+ return;
238
+ }
239
+ this.#finished = true;
240
+ this.#heldAssistant = null;
241
+ if (this.#turn === null || this.#broken) {
242
+ await this.#chain;
243
+ return;
244
+ }
245
+ await this.#closeReasoning();
246
+ const failure = reason instanceof Error
247
+ ? reason.message
248
+ : turnFailure(reason);
249
+ await this.#write([
250
+ failure
251
+ ? cotEvent('RUN_ERROR', { message: failure, code: 'TURN_FAILED' })
252
+ : cotEvent('RUN_FINISHED', {
253
+ threadId: this.#chatId,
254
+ runId: `turn-${this.#turn}`,
255
+ status: 'done',
256
+ }),
257
+ ]);
258
+ await this.#chain;
259
+ }
260
+ }
@@ -1,3 +1,4 @@
1
+ import { imageDownloadLimitMessage } from '../shared/image-prompt.mjs';
1
2
  import { ImagePromptError } from '../shared/image-prompt.mjs';
2
3
  import { t } from '../shared/i18n.mjs';
3
4
 
@@ -256,7 +257,7 @@ async function readBoundedStream(stream, { signal, maxBytes }) {
256
257
  throw new ImagePromptError(
257
258
  'image-too-large',
258
259
  `Feishu image exceeds ${maxBytes} bytes`,
259
- t('图片超过 5 MB,请压缩后重试。'),
260
+ imageDownloadLimitMessage(maxBytes),
260
261
  );
261
262
  }
262
263
  chunks.push(data);
@@ -392,7 +393,7 @@ function feishuImageSource(event, client, key) {
392
393
  throw new ImagePromptError(
393
394
  'image-too-large',
394
395
  `Feishu image declares ${size} bytes; the limit is ${maxBytes}`,
395
- t('图片超过 5 MB,请压缩后重试。'),
396
+ imageDownloadLimitMessage(maxBytes),
396
397
  );
397
398
  }
398
399
  return readBoundedStream(resource?.getReadableStream?.(), { signal, maxBytes });