@xmanrui/dsh-im 0.8.0 → 0.10.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 (48) hide show
  1. package/README.en.md +8 -4
  2. package/README.md +8 -4
  3. package/lib/client.js +16 -14
  4. package/lib/index.js +123 -121
  5. package/package.json +1 -1
  6. package/plugin-src/client/styles.js +15 -14
  7. package/plugin-src/host/channels/dingtalk/index.mjs +1 -1
  8. package/plugin-src/host/channels/dingtalk/production.mjs +3 -0
  9. package/plugin-src/host/channels/discord/index.mjs +1 -1
  10. package/plugin-src/host/channels/feishu/index.mjs +1 -1
  11. package/plugin-src/host/channels/feishu/production.mjs +3 -0
  12. package/plugin-src/host/channels/qq/index.mjs +1 -1
  13. package/plugin-src/host/channels/qq/production.mjs +3 -0
  14. package/plugin-src/host/channels/shared/production.mjs +3 -0
  15. package/plugin-src/host/channels/slack/index.mjs +1 -1
  16. package/plugin-src/host/channels/slack/production.mjs +3 -0
  17. package/plugin-src/host/channels/telegram/index.mjs +1 -1
  18. package/plugin-src/host/channels/wecom/index.mjs +1 -1
  19. package/plugin-src/host/channels/wecom/production.mjs +3 -0
  20. package/plugin-src/host/channels/weixin/index.mjs +1 -1
  21. package/plugin-src/host/channels/weixin/production.mjs +3 -0
  22. package/plugin-src/host/channels/whatsapp/index.mjs +1 -1
  23. package/plugin-src/host/channels/whatsapp/production.mjs +3 -0
  24. package/plugin-src/host/harness-command-executor.mjs +21 -0
  25. package/plugin-src/host/index.mjs +1 -1
  26. package/src/channels/dingtalk/dingtalk-api.mjs +82 -1
  27. package/src/channels/dingtalk/dingtalk-bridge.mjs +162 -13
  28. package/src/channels/discord/discord-api.mjs +1 -1
  29. package/src/channels/discord/discord-runtime.mjs +44 -1
  30. package/src/channels/feishu/bridge.mjs +45 -11
  31. package/src/channels/feishu/message-utils.mjs +142 -8
  32. package/src/channels/feishu/plugin-controller.mjs +1 -0
  33. package/src/channels/qq/qq-bridge.mjs +107 -13
  34. package/src/channels/shared/bot-workspace-store.mjs +13 -0
  35. package/src/channels/shared/compact-command.mjs +95 -0
  36. package/src/channels/shared/harness-client.mjs +32 -2
  37. package/src/channels/shared/image-prompt.mjs +268 -0
  38. package/src/channels/shared/text-harness-bridge.mjs +49 -9
  39. package/src/channels/shared/workspace-session.mjs +2 -1
  40. package/src/channels/slack/manifest.mjs +1 -0
  41. package/src/channels/slack/slack-api.mjs +95 -0
  42. package/src/channels/slack/slack-runtime.mjs +24 -3
  43. package/src/channels/telegram/telegram-api.mjs +37 -0
  44. package/src/channels/telegram/telegram-runtime.mjs +71 -9
  45. package/src/channels/wecom/wecom-bridge.mjs +163 -16
  46. package/src/channels/weixin/weixin-api.mjs +98 -2
  47. package/src/channels/weixin/weixin-bridge.mjs +55 -12
  48. package/src/channels/whatsapp/whatsapp-runtime.mjs +144 -1
@@ -1,4 +1,5 @@
1
1
  import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
2
+ import { runCompactCommand } from '../shared/compact-command.mjs';
2
3
  import {
3
4
  harnessAnswerForQuestion,
4
5
  harnessQuestionText,
@@ -6,14 +7,32 @@ import {
6
7
  } from '../shared/harness-question.mjs';
7
8
  import { HarnessApprovalQueue } from '../shared/harness-approval.mjs';
8
9
  import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
10
+ import {
11
+ fetchImageBuffer,
12
+ hasInboundImages,
13
+ imagePromptUserMessage,
14
+ promptContentForMessage,
15
+ } from '../shared/image-prompt.mjs';
9
16
 
10
17
  const INTERACTION_RESOLVED_TEXT = '这个问题已在其他客户端处理,无需再次回答。';
11
18
 
19
+ export const QQ_IMAGE_HOSTS = Object.freeze([
20
+ '.myqcloud.com',
21
+ '.qpic.cn',
22
+ '.qq.com',
23
+ '.qq.com.cn',
24
+ '.tencentcos.com',
25
+ '.ugcimg.cn',
26
+ ]);
27
+
28
+ const QQ_IMAGE_FILENAME = /\.(?:gif|jpe?g|png|webp)$/i;
29
+
12
30
  const HELP_TEXT = [
13
31
  'QQ 机器人已连接 DeepSeek Harness。',
14
32
  '',
15
- '直接发送文字即可继续当前会话。',
33
+ '直接发送文字或图片即可继续当前会话。',
16
34
  '/new 开启一个全新会话',
35
+ '/compact 压缩当前会话的较早上下文',
17
36
  '/workspace 工作区绝对路径 切换工作区',
18
37
  '/workspacelist 列出工作区绝对路径',
19
38
  '/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
@@ -30,6 +49,51 @@ function safeText(message) {
30
49
  return typeof message?.content === 'string' ? message.content.trim() : '';
31
50
  }
32
51
 
52
+ function attachmentMediaType(attachment) {
53
+ const value = nonEmptyString(attachment?.content_type ?? attachment?.contentType);
54
+ if (!value) return null;
55
+ return value.split(';', 1)[0].trim().toLowerCase();
56
+ }
57
+
58
+ function isQqImageAttachment(attachment) {
59
+ const mediaType = attachmentMediaType(attachment);
60
+ return mediaType?.startsWith('image/') === true
61
+ || QQ_IMAGE_FILENAME.test(nonEmptyString(attachment?.filename) ?? '');
62
+ }
63
+
64
+ function hasQqImageAttachments(message) {
65
+ return Array.isArray(message?.attachments)
66
+ && message.attachments.some(isQqImageAttachment);
67
+ }
68
+
69
+ /** Convert QQ's attachment metadata into lazily downloaded image references. */
70
+ export function qqInboundMessage(message, { fetchImpl = fetch } = {}) {
71
+ if (typeof fetchImpl !== 'function') throw new TypeError('fetchImpl must be a function');
72
+ const images = [];
73
+ for (const attachment of message?.attachments ?? []) {
74
+ if (!isQqImageAttachment(attachment)) continue;
75
+ const url = nonEmptyString(attachment?.url);
76
+ const name = nonEmptyString(attachment?.filename) ?? undefined;
77
+ const mediaType = attachmentMediaType(attachment);
78
+ const declaredSize = Number(attachment?.size);
79
+ images.push({
80
+ ...(name ? { name } : {}),
81
+ ...(mediaType?.startsWith('image/') ? { mediaType } : {}),
82
+ ...(Number.isFinite(declaredSize) && declaredSize >= 0 ? { size: declaredSize } : {}),
83
+ load: ({ signal, maxBytes }) => {
84
+ if (!url) throw new Error('QQ image attachment has no download URL');
85
+ return fetchImageBuffer(url, {
86
+ fetchImpl,
87
+ signal,
88
+ maxBytes,
89
+ allowedHosts: QQ_IMAGE_HOSTS,
90
+ });
91
+ },
92
+ });
93
+ }
94
+ return { content: safeText(message), images };
95
+ }
96
+
33
97
  function nonEmptyString(value) {
34
98
  return typeof value === 'string' && value.trim() ? value.trim() : null;
35
99
  }
@@ -38,6 +102,7 @@ function canClaimInteractionReply(message, pending) {
38
102
  return pending.questions[pending.index]
39
103
  && nonEmptyString(message?.senderId) === pending.actor
40
104
  && (message.kind !== 'group' || message.rawEventType === 'GROUP_AT_MESSAGE_CREATE')
105
+ && !hasQqImageAttachments(message)
41
106
  && nonEmptyString(safeText(message));
42
107
  }
43
108
 
@@ -62,6 +127,7 @@ export class QqHarnessBridge {
62
127
  #logger;
63
128
  #replyTimeoutMs;
64
129
  #signal;
130
+ #fetchImpl;
65
131
  #queues = new Map();
66
132
  #pendingInteractions = new Map();
67
133
  #interactionKeys = new Map();
@@ -78,10 +144,12 @@ export class QqHarnessBridge {
78
144
  logger = console,
79
145
  replyTimeoutMs = 600_000,
80
146
  signal,
147
+ fetchImpl = fetch,
81
148
  }) {
82
149
  if (!bot || typeof bot.sendText !== 'function') throw new TypeError('QQ bot client is required');
83
150
  if (!ownerUserOpenid) throw new TypeError('QQ scanner identity is required');
84
151
  if (!harness || !state) throw new TypeError('Harness client and state store are required');
152
+ if (typeof fetchImpl !== 'function') throw new TypeError('fetchImpl must be a function');
85
153
  this.#bot = bot;
86
154
  this.#ownerUserOpenid = ownerUserOpenid;
87
155
  this.#harness = harness;
@@ -90,6 +158,7 @@ export class QqHarnessBridge {
90
158
  this.#logger = logger;
91
159
  this.#replyTimeoutMs = replyTimeoutMs;
92
160
  this.#signal = signal;
161
+ this.#fetchImpl = fetchImpl;
93
162
  this.#approvals = new HarnessApprovalQueue({ label: 'qq', logger });
94
163
  }
95
164
 
@@ -112,7 +181,7 @@ export class QqHarnessBridge {
112
181
  key,
113
182
  actor: sender,
114
183
  messageId,
115
- text: safeText(message),
184
+ text: hasQqImageAttachments(message) ? '' : safeText(message),
116
185
  addressed: message.kind !== 'group' || message.rawEventType === 'GROUP_AT_MESSAGE_CREATE',
117
186
  hasPendingQuestion: Boolean(pending),
118
187
  questionCompletion: pending?.submitting || pending?.claimedReplyMessageId
@@ -208,32 +277,37 @@ export class QqHarnessBridge {
208
277
  if (message.kind === 'group' && message.rawEventType !== 'GROUP_AT_MESSAGE_CREATE') return;
209
278
 
210
279
  const target = message.replyTarget;
211
- const text = safeText(message);
280
+ const promptMessage = qqInboundMessage(message, { fetchImpl: this.#fetchImpl });
281
+ const text = promptMessage.content;
282
+ const hasImages = hasInboundImages(promptMessage);
283
+ let stream = null;
212
284
  try {
213
- if (!text) {
214
- await this.#bot.sendText(target, '目前仅支持文字消息。');
285
+ if (!text && !hasImages) {
286
+ await this.#bot.sendText(target, '目前支持文字和图片消息。');
215
287
  await this.#state.markSeen(messageId);
216
288
  return;
217
289
  }
218
290
  const command = text.toLowerCase();
219
- if (command === '/help') {
291
+ if (!hasImages && command === '/help') {
220
292
  await this.#bot.sendText(target, HELP_TEXT);
221
293
  await this.#state.markSeen(messageId);
222
294
  return;
223
295
  }
224
- if (command === '/status') {
296
+ if (!hasImages && command === '/status') {
225
297
  await this.#harness.ensureRunning({ signal: this.#signal });
226
298
  await this.#bot.sendText(target, 'QQ 机器人与 DeepSeek Harness 连接正常。');
227
299
  await this.#state.markSeen(messageId);
228
300
  return;
229
301
  }
230
- if (command === '/new') {
302
+ if (!hasImages && command === '/new') {
231
303
  await this.#state.clearSession(key);
232
304
  await this.#bot.sendText(target, '已开启新会话。请发送你的问题。');
233
305
  await this.#state.markSeen(messageId);
234
306
  return;
235
307
  }
236
- const workspaceCommand = await runWorkspaceCommand(text, this.#harness, key);
308
+ const workspaceCommand = hasImages
309
+ ? null
310
+ : await runWorkspaceCommand(text, this.#harness, key);
237
311
  if (workspaceCommand) {
238
312
  for (const reply of workspaceCommand.messages ?? [workspaceCommand.message]) {
239
313
  await this.#bot.sendText(target, reply);
@@ -241,8 +315,24 @@ export class QqHarnessBridge {
241
315
  await this.#state.markSeen(messageId);
242
316
  return;
243
317
  }
318
+ const compactCommand = hasImages
319
+ ? null
320
+ : await runCompactCommand(
321
+ text,
322
+ this.#harness,
323
+ this.#state,
324
+ key,
325
+ { signal: this.#signal },
326
+ );
327
+ if (compactCommand) {
328
+ await this.#bot.sendText(target, compactCommand.message);
329
+ await this.#state.markSeen(messageId);
330
+ return;
331
+ }
244
332
 
245
- let stream = null;
333
+ const content = hasImages
334
+ ? await promptContentForMessage(promptMessage, { signal: this.#signal })
335
+ : undefined;
246
336
  let streamFinished = false;
247
337
  if (message.kind === 'c2c' && target?.msgId && typeof this.#bot.openStream === 'function') {
248
338
  try {
@@ -257,7 +347,7 @@ export class QqHarnessBridge {
257
347
  harness: this.#harness,
258
348
  state: this.#state,
259
349
  key,
260
- text,
350
+ ...(hasImages ? { content } : { text }),
261
351
  createOptions: { signal: this.#signal },
262
352
  existsOptions: { signal: this.#signal },
263
353
  askOptions: {
@@ -302,11 +392,15 @@ export class QqHarnessBridge {
302
392
  this.#status.lastReplyAt = new Date().toISOString();
303
393
  this.#status.lastError = null;
304
394
  } catch (error) {
395
+ stream?.cancel?.();
305
396
  if (this.#signal?.aborted) return;
306
397
  this.#status.lastError = error?.message ?? String(error);
307
398
  this.#logger.error?.('[dsh-im:qq] failed to process an inbound message:', error);
308
399
  try {
309
- await this.#bot.sendText(target, '消息处理失败,请稍后重试。');
400
+ await this.#bot.sendText(
401
+ target,
402
+ imagePromptUserMessage(error) ?? '消息处理失败,请稍后重试。',
403
+ );
310
404
  await this.#state.markSeen(messageId);
311
405
  } catch (sendError) {
312
406
  this.#logger.error?.('[dsh-im:qq] failed to send the safe error reply:', sendError);
@@ -331,7 +425,7 @@ export class QqHarnessBridge {
331
425
 
332
426
  if (message.kind === 'group' && message.rawEventType !== 'GROUP_AT_MESSAGE_CREATE') return;
333
427
  const text = nonEmptyString(safeText(message));
334
- if (!text) {
428
+ if (!text || hasQqImageAttachments(message)) {
335
429
  await this.#bot.sendText(message.replyTarget, '请用文字回答当前问题。');
336
430
  return;
337
431
  }
@@ -668,6 +668,19 @@ export function createBotWorkspaceScope(harness, { botId, workspaces, state }) {
668
668
  return target.ask(sessionId, ...args);
669
669
  };
670
670
  }
671
+ if (property === 'executeCommand' && typeof target.executeCommand === 'function') {
672
+ return (sessionId, ...args) => {
673
+ const generation = sessionGenerations.get(sessionId);
674
+ sessionGenerations.delete(sessionId);
675
+ if (!isCurrentScope()
676
+ || (generation !== undefined && generation !== workspaces.generationFor(botId))) {
677
+ const error = new Error('The bot workspace changed before this command started.');
678
+ error.code = WORKSPACE_SESSION_STALE;
679
+ throw error;
680
+ }
681
+ return target.executeCommand(sessionId, ...args);
682
+ };
683
+ }
671
684
  const value = Reflect.get(target, property, target);
672
685
  return typeof value === 'function' ? value.bind(target) : value;
673
686
  },
@@ -0,0 +1,95 @@
1
+ import { WORKSPACE_SESSION_STALE } from './workspace-session.mjs';
2
+
3
+ const COMPACT_COMMAND = /^\/compact(?=$|\s)([\s\S]*)$/i;
4
+ const COMPACT_USAGE = '用法:/compact(不带参数)';
5
+
6
+ const COMPACT_RESULT_TEXT = new Map([
7
+ ['No compactable history yet.', '暂无可压缩的历史记录。'],
8
+ [
9
+ 'Compaction is unavailable because this process has an active compaction, or the agent is not idle.',
10
+ '当前会话正在生成回复或执行压缩,请稍后重试。',
11
+ ],
12
+ ['Compaction cancelled.', '上下文压缩已取消。'],
13
+ [
14
+ 'The history selected for compaction changed before it could be replaced. The conversation is unchanged; the attempt is recorded in the session log.',
15
+ '压缩期间会话历史发生变化,本次未修改会话,请重试。',
16
+ ],
17
+ [
18
+ 'Compaction could not produce a useful summary. The conversation is unchanged; the attempt is recorded in the session log.',
19
+ '未能生成有效的压缩摘要,本次未修改会话。',
20
+ ],
21
+ [
22
+ 'Compaction did not finish cleanly; some session history may have changed. Inspect the current session state before retrying.',
23
+ '上下文压缩未正常完成,部分会话历史可能已变化,请检查会话后再重试。',
24
+ ],
25
+ [
26
+ 'Compaction finished, but the session could not be saved.',
27
+ '上下文已压缩,但会话保存失败。',
28
+ ],
29
+ ]);
30
+
31
+ function commandResult(message) {
32
+ return { handled: true, message, messages: [message] };
33
+ }
34
+
35
+ function compactResultText(result) {
36
+ if (!result || typeof result !== 'object'
37
+ || !['success', 'error'].includes(result.kind)
38
+ || (result.text !== undefined && typeof result.text !== 'string')) {
39
+ throw new TypeError('Harness returned an invalid /compact result');
40
+ }
41
+ const text = result.text?.trim() ?? '';
42
+ const compacted = /^Compacted (\d+) history items \(~(\d+) tokens\)\.$/u.exec(text);
43
+ if (compacted) {
44
+ return `已压缩 ${compacted[1]} 条历史记录(约 ${compacted[2]} 个 token)。`;
45
+ }
46
+ if (COMPACT_RESULT_TEXT.has(text)) return COMPACT_RESULT_TEXT.get(text);
47
+ if (text) return text;
48
+ return result.kind === 'success' ? '上下文压缩完成。' : '上下文压缩失败。';
49
+ }
50
+
51
+ function compactErrorMessage(error) {
52
+ const code = error?.code ?? error?.failure?.code;
53
+ if (code === 'session-not-found') {
54
+ return '当前聊天绑定的会话已不存在,请发送新消息开启会话。';
55
+ }
56
+ if (code === 'agent-busy') return '当前会话正在生成回复,请稍后重试。';
57
+ if (code === 'cancelled' || error?.name === 'AbortError') return '上下文压缩已取消。';
58
+ if (code === WORKSPACE_SESSION_STALE || code === 'workspace-bot-not-found') {
59
+ return '工作区或机器人状态已发生变化,请重试。';
60
+ }
61
+ if (code === 'commands-unavailable') {
62
+ return '当前 Harness 暂不支持从机器人执行上下文压缩。';
63
+ }
64
+ return '上下文压缩失败,请稍后重试。';
65
+ }
66
+
67
+ /**
68
+ * Execute the explicit Harness compaction command for an existing IM conversation Session.
69
+ * Unknown input returns null so the caller may continue ordinary message routing.
70
+ */
71
+ export async function runCompactCommand(text, harness, state, conversationKey, options = {}) {
72
+ if (typeof text !== 'string') return null;
73
+ const match = COMPACT_COMMAND.exec(text.trim());
74
+ if (!match) return null;
75
+ if (match[1].trim()) return commandResult(COMPACT_USAGE);
76
+ if (typeof state?.sessionFor !== 'function') {
77
+ return commandResult('当前机器人没有可用的会话状态。');
78
+ }
79
+ const sessionId = state.sessionFor(conversationKey);
80
+ if (typeof sessionId !== 'string' || !sessionId) {
81
+ return commandResult('当前聊天还没有可压缩的会话,请先发送一条消息。');
82
+ }
83
+ if (typeof harness?.executeCommand !== 'function') {
84
+ return commandResult('当前机器人暂不支持上下文压缩。');
85
+ }
86
+ try {
87
+ const execution = await harness.executeCommand(sessionId, '/compact', options);
88
+ if (execution === undefined) {
89
+ return commandResult('当前 Harness 未注册 /compact 命令,请确认上下文压缩组件已启用。');
90
+ }
91
+ return commandResult(compactResultText(execution?.result));
92
+ } catch (error) {
93
+ return commandResult(compactErrorMessage(error));
94
+ }
95
+ }
@@ -318,6 +318,7 @@ export class HarnessClient {
318
318
  #interactionReconnectDelayMs;
319
319
  #rpcIdPrefix;
320
320
  #logPrefix;
321
+ #commandExecutor;
321
322
  #managedProcess = null;
322
323
  #interactionRegistry;
323
324
  #interactionOwnerships;
@@ -334,6 +335,7 @@ export class HarnessClient {
334
335
  interactionReconnectDelayMs = 500,
335
336
  rpcIdPrefix = 'im',
336
337
  logPrefix = 'dsh-im',
338
+ commandExecutor,
337
339
  }) {
338
340
  if (typeof createWebSocket !== 'function') {
339
341
  throw new TypeError('createWebSocket must be a function');
@@ -347,6 +349,9 @@ export class HarnessClient {
347
349
  if (typeof logPrefix !== 'string' || !logPrefix.trim()) {
348
350
  throw new TypeError('logPrefix must be a non-empty string');
349
351
  }
352
+ if (commandExecutor !== undefined && typeof commandExecutor !== 'function') {
353
+ throw new TypeError('commandExecutor must be a function');
354
+ }
350
355
  this.#baseUrl = new URL(baseUrl);
351
356
  this.#workspace = workspace;
352
357
  this.#agentPreset = agentPreset;
@@ -357,6 +362,7 @@ export class HarnessClient {
357
362
  this.#interactionReconnectDelayMs = interactionReconnectDelayMs;
358
363
  this.#rpcIdPrefix = rpcIdPrefix.trim();
359
364
  this.#logPrefix = logPrefix.trim();
365
+ this.#commandExecutor = commandExecutor;
360
366
  this.#interactionRegistry = interactionRegistry(this.#baseUrl.origin);
361
367
  this.#interactionOwnerships = this.#interactionRegistry.ownerships;
362
368
  this.#interactionClaims = this.#interactionRegistry.claims;
@@ -459,6 +465,24 @@ export class HarnessClient {
459
465
  return created.sessionId;
460
466
  }
461
467
 
468
+ async executeCommand(sessionId, line, options = {}) {
469
+ if (typeof sessionId !== 'string' || !sessionId) throw new TypeError('sessionId is required');
470
+ if (typeof line !== 'string' || !line) throw new TypeError('command line is required');
471
+ if (!this.#commandExecutor) {
472
+ const error = new Error('Harness command execution is unavailable');
473
+ error.code = 'commands-unavailable';
474
+ throw error;
475
+ }
476
+ try {
477
+ return await this.#commandExecutor(sessionId, line, options);
478
+ } catch (error) {
479
+ if (error?.failure && typeof error.failure === 'object') {
480
+ throw new HarnessRpcError('commands.execute', error.failure);
481
+ }
482
+ throw error;
483
+ }
484
+ }
485
+
462
486
  async sessionExists(sessionId, options = {}) {
463
487
  try {
464
488
  await this.rpc('session.history', { sessionId, maxMessages: 1 }, 30_000, options);
@@ -599,7 +623,7 @@ export class HarnessClient {
599
623
  return ownership ? { ownership, recovered: true } : null;
600
624
  }
601
625
 
602
- async ask(sessionId, text, options = {}) {
626
+ async ask(sessionId, prompt, options = {}) {
603
627
  if (typeof options === 'number') options = { timeoutMs: options };
604
628
  const timeoutMs = options.timeoutMs ?? 600_000;
605
629
  const signal = options.signal;
@@ -669,10 +693,16 @@ export class HarnessClient {
669
693
  ]);
670
694
  }
671
695
 
696
+ const content = typeof prompt === 'string'
697
+ ? [{ type: 'text', text: prompt }]
698
+ : prompt;
699
+ if (!Array.isArray(content) || content.length === 0) {
700
+ throw new TypeError('Harness prompt content is required');
701
+ }
672
702
  await this.rpc('session.prompt', {
673
703
  sessionId,
674
704
  mode: 'queue',
675
- content: [{ type: 'text', text }],
705
+ content,
676
706
  clientTimeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
677
707
  }, 30_000, { rpcId: promptRpcId, signal });
678
708