@xmanrui/dsh-im 4.24.0 → 4.25.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 (53) hide show
  1. package/README.en.md +11 -3
  2. package/README.md +11 -3
  3. package/THIRD_PARTY_NOTICES.md +2 -0
  4. package/lib/client.js +965 -607
  5. package/lib/index.js +296 -287
  6. package/package.json +8 -2
  7. package/plugin-src/client/channel-card-meta.js +3 -9
  8. package/plugin-src/client/channel-logos.js +11 -0
  9. package/plugin-src/client/channels/dingtalk/index.js +9 -8
  10. package/plugin-src/client/channels/feishu/index.js +14 -13
  11. package/plugin-src/client/channels/matrix/api.js +11 -0
  12. package/plugin-src/client/channels/matrix/index.js +151 -0
  13. package/plugin-src/client/channels/matrix/styles.js +36 -0
  14. package/plugin-src/client/channels/qq/index.js +9 -8
  15. package/plugin-src/client/channels/shared/collapsible-account.js +49 -26
  16. package/plugin-src/client/channels/shared/token-channel.js +9 -8
  17. package/plugin-src/client/channels/wecom/index.js +9 -8
  18. package/plugin-src/client/channels/wecom-app/index.js +9 -8
  19. package/plugin-src/client/channels/weixin/index.js +9 -8
  20. package/plugin-src/client/channels/whatsapp/index.js +9 -8
  21. package/plugin-src/client/i18n.js +21 -0
  22. package/plugin-src/client/index.js +20 -0
  23. package/plugin-src/client/session-channel-logos.js +2 -1
  24. package/plugin-src/client/styles.js +15 -9
  25. package/plugin-src/client/update-panel.js +42 -18
  26. package/plugin-src/host/channels/matrix/index.mjs +31 -0
  27. package/plugin-src/host/channels/matrix/production.mjs +227 -0
  28. package/plugin-src/host/channels/matrix/rpc.mjs +228 -0
  29. package/plugin-src/host/channels/shared/access-policy-production.mjs +1 -1
  30. package/plugin-src/host/channels/shared/startup-error.mjs +2 -1
  31. package/plugin-src/host/delivery-adapter.mjs +18 -0
  32. package/plugin-src/host/delivery-suggestions.mjs +13 -0
  33. package/plugin-src/host/index.mjs +3 -0
  34. package/plugin-src/host/update-service.mjs +19 -14
  35. package/scripts/verify-lan-management.mjs +1 -1
  36. package/scripts/verify-package.mjs +5 -1
  37. package/src/channels/feishu/bridge.mjs +91 -42
  38. package/src/channels/matrix/matrix-api.mjs +696 -0
  39. package/src/channels/matrix/matrix-bridge.mjs +20 -0
  40. package/src/channels/matrix/matrix-config-store.mjs +356 -0
  41. package/src/channels/matrix/matrix-controller.mjs +404 -0
  42. package/src/channels/matrix/matrix-crypto-store.mjs +279 -0
  43. package/src/channels/matrix/matrix-crypto.mjs +1014 -0
  44. package/src/channels/matrix/matrix-harness-client.mjs +11 -0
  45. package/src/channels/matrix/matrix-normalize.mjs +357 -0
  46. package/src/channels/matrix/matrix-rich-text.mjs +313 -0
  47. package/src/channels/matrix/matrix-runtime.mjs +900 -0
  48. package/src/channels/shared/command-catalog.mjs +1 -1
  49. package/src/channels/shared/i18n-en/matrix.mjs +75 -0
  50. package/src/channels/shared/i18n-en.mjs +2 -0
  51. package/src/channels/shared/session-channel-labels.mjs +1 -0
  52. package/src/channels/shared/text-harness-bridge.mjs +3 -0
  53. package/src/channels/shared/workspace-session.mjs +7 -1
@@ -45,6 +45,8 @@ const required = [
45
45
  'plugin-src/host/channels/telegram/index.mjs',
46
46
  'plugin-src/host/channels/discord/index.mjs',
47
47
  'plugin-src/host/channels/whatsapp/index.mjs',
48
+ 'plugin-src/host/channels/matrix/index.mjs',
49
+ 'plugin-src/client/channels/matrix/index.js',
48
50
  'src/channels/feishu/feishu-runtime.mjs',
49
51
  'src/channels/weixin/weixin-runtime.mjs',
50
52
  'src/channels/dingtalk/dingtalk-runtime.mjs',
@@ -56,6 +58,8 @@ const required = [
56
58
  'src/channels/discord/discord-runtime.mjs',
57
59
  'src/channels/whatsapp/whatsapp-runtime.mjs',
58
60
  'src/channels/whatsapp/whatsapp-web-session.mjs',
61
+ 'src/channels/matrix/matrix-runtime.mjs',
62
+ 'src/channels/matrix/matrix-api.mjs',
59
63
  'src/channels/shared/context-enhancement.mjs',
60
64
  ];
61
65
  await Promise.all(required.map((path) => access(resolve(root, path))));
@@ -174,7 +178,7 @@ if (!client.includes('container-type: inline-size')
174
178
  || !client.includes('@container (max-width: 680px)')) {
175
179
  throw new Error('client bundle does not contain the narrow-panel DingTalk QR layout');
176
180
  }
177
- for (const marker of ['/feishu', '/weixin', '/dingtalk', '/wecom', '/qq', '/slack', '/telegram', '/discord', '/whatsapp']) {
181
+ for (const marker of ['/feishu', '/weixin', '/dingtalk', '/wecom', '/qq', '/slack', '/telegram', '/discord', '/whatsapp', '/matrix']) {
178
182
  if (!host.includes(marker)) {
179
183
  throw new Error(`host bundle does not contain the internal ${marker} RPC provider`);
180
184
  }
@@ -124,6 +124,7 @@ const RESOLVED_REPLY_TTL_MS = 30 * 60_000;
124
124
  const ROOT_CANDIDATE_TTL_MS = 30 * 60_000;
125
125
  /** Bound main-feed roots awaiting a topic so silent drops cannot grow them. */
126
126
  const MAX_ROOT_CANDIDATES = 512;
127
+ const MAX_TOPIC_ANCHORS = 4_000;
127
128
 
128
129
  const MENU_COMMAND = /^\/m(?:enu)?$/i;
129
130
  const REPAIR_COMMAND_PREFIX = /^\/repair(?:\s|$)/i;
@@ -624,6 +625,8 @@ export class FeishuHarnessBridge {
624
625
  #anchorTopicReply = new Map();
625
626
  /** Main-feed roots this bot intends to auto-open as topics, awaiting thread_id. */
626
627
  #rootCandidates = new Map();
628
+ /** Last successfully injected topic root per conversation and AI session. */
629
+ #topicAnchors = new Map();
627
630
  #repair;
628
631
  #repairAttempt = null;
629
632
  #repairMonitorVersion = 0;
@@ -4875,6 +4878,72 @@ export class FeishuHarnessBridge {
4875
4878
  };
4876
4879
  }
4877
4880
 
4881
+ async #preparePrompt(event, key, message) {
4882
+ const snapshot = this.#acceptedMessageIds.get(event.message.message_id);
4883
+ const build = async (input) => {
4884
+ const rich = hasInboundImages(input) || hasReplyReference(input);
4885
+ const original = rich
4886
+ ? await promptContentForInboundMessage(input, { signal: this.#signal, deferImages: true })
4887
+ : input.content;
4888
+ const enhanced = enhanceContextContent(original, snapshot, () => ({
4889
+ channel: 'feishu',
4890
+ senderId: senderOpenId(event),
4891
+ chatId: event.message.chat_id,
4892
+ threadId: event.message.thread_id,
4893
+ }));
4894
+ return {
4895
+ content: rich || enhanced !== original ? enhanced : undefined,
4896
+ contextEnhanced: enhanced !== original,
4897
+ sourceGuidance: snapshot?.config?.guidance,
4898
+ };
4899
+ };
4900
+ const rootId = nonEmptyString(event.message.root_id);
4901
+ const topicAnchor = event.message.chat_type === 'group'
4902
+ && nonEmptyString(event.message.thread_id)
4903
+ && rootId && message.replyTo?.messageId === rootId
4904
+ // A reference-only message must not become an empty prompt.
4905
+ && (nonEmptyString(message.content) || hasInboundImages(message) || hasInboundFiles(message));
4906
+ if (!topicAnchor) return build(message);
4907
+
4908
+ const withoutAnchor = { ...message };
4909
+ delete withoutAnchor.replyTo;
4910
+ const base = await build(withoutAnchor);
4911
+ let preparedSessionId;
4912
+ let anchorLoaded = false;
4913
+ return {
4914
+ ...base,
4915
+ prepareContent: async ({ sessionId }) => {
4916
+ preparedSessionId = sessionId;
4917
+ anchorLoaded = false;
4918
+ const previous = this.#topicAnchors.get(key);
4919
+ if (previous?.sessionId === sessionId && previous.rootMessageId === rootId) {
4920
+ return base.content ?? message.content;
4921
+ }
4922
+ const prepared = await build({
4923
+ ...message,
4924
+ replyTo: {
4925
+ ...message.replyTo,
4926
+ load: async (options) => {
4927
+ const reference = await message.replyTo.load(options);
4928
+ anchorLoaded = !reference?.unavailableReason && Boolean(
4929
+ nonEmptyString(reference?.content) || reference?.attachments?.length,
4930
+ );
4931
+ return reference;
4932
+ },
4933
+ },
4934
+ });
4935
+ return prepared.content;
4936
+ },
4937
+ onAskComplete: (sessionId) => {
4938
+ if (!anchorLoaded || sessionId !== preparedSessionId) return;
4939
+ if (!this.#topicAnchors.has(key) && this.#topicAnchors.size >= MAX_TOPIC_ANCHORS) {
4940
+ this.#topicAnchors.delete(this.#topicAnchors.keys().next().value);
4941
+ }
4942
+ this.#topicAnchors.set(key, { sessionId, rootMessageId: rootId });
4943
+ },
4944
+ };
4945
+ }
4946
+
4878
4947
  /**
4879
4948
  * 分步直推:`#answerWithStream` 流式分支在开关开启时的完整替代路径。
4880
4949
  * 过程(工具调用、助手中间说明)与最终答案均以富文本 post 逐条直推
@@ -4886,30 +4955,18 @@ export class FeishuHarnessBridge {
4886
4955
  const messageId = event.message.message_id;
4887
4956
  const text = message.content;
4888
4957
  let askCompleted = false;
4889
- const markAskComplete = () => {
4958
+ const markAskComplete = (sessionId) => {
4890
4959
  if (askCompleted) return;
4891
4960
  askCompleted = true;
4961
+ prompt.onAskComplete?.(sessionId);
4892
4962
  this.#endImTurn(key);
4893
4963
  onAskComplete?.();
4894
4964
  };
4895
4965
  // 与流式分支一致的提示内容构造:图片与回复引用展开为富提示内容,已接受
4896
4966
  // 的上下文增强按原样重放。直推分流发生在 `#answerWithStream` 构造之前,
4897
4967
  // 这里就是本回合唯一一次构造(无重复的 prompt 往返)。
4898
- let content = hasInboundImages(message) || hasReplyReference(message)
4899
- ? await promptContentForInboundMessage(message, { signal: this.#signal, deferImages: true })
4900
- : undefined;
4901
- const snapshot = this.#acceptedMessageIds.get(messageId);
4902
- let contextEnhanced = false;
4903
- if (snapshot) {
4904
- const originalContent = content ?? text;
4905
- content = enhanceContextContent(originalContent, snapshot, () => ({
4906
- channel: 'feishu',
4907
- senderId: senderOpenId(event),
4908
- chatId: event.message.chat_id,
4909
- threadId: event.message.thread_id,
4910
- }));
4911
- contextEnhanced = content !== originalContent;
4912
- }
4968
+ const prompt = await this.#preparePrompt(event, key, message);
4969
+ const { content, contextEnhanced } = prompt;
4913
4970
  // Every turn reopens the per-conversation circuit breaker.
4914
4971
  const priorState = this.#stepPushSendState.get(key);
4915
4972
  if (this.#stepPushSendState.size >= MAX_STEP_PUSH_SEND_STATES && !priorState) {
@@ -5010,7 +5067,8 @@ export class FeishuHarnessBridge {
5010
5067
  text,
5011
5068
  content,
5012
5069
  titleText: event.batchSubmission?.title,
5013
- sourceGuidance: snapshot?.config?.guidance,
5070
+ sourceGuidance: prompt.sourceGuidance,
5071
+ prepareContent: prompt.prepareContent,
5014
5072
  contextEnhanced,
5015
5073
  createOptions: { signal: this.#signal },
5016
5074
  existsOptions: { signal: this.#signal },
@@ -5089,8 +5147,8 @@ export class FeishuHarnessBridge {
5089
5147
  // 回合结束(成功/失败/中断):停看门狗并撤回残留思考中心跳。
5090
5148
  await watchdog?.stop();
5091
5149
  }
5150
+ markAskComplete(completed.sessionId);
5092
5151
  await cot?.finish();
5093
- markAskComplete();
5094
5152
  const finalStepText = pendingStep ? pendingStep.text : null;
5095
5153
  pendingStep = null;
5096
5154
  const finalText = (typeof finalStepText === 'string' && finalStepText.trim())
@@ -5228,9 +5286,10 @@ export class FeishuHarnessBridge {
5228
5286
  const messageId = event.message.message_id;
5229
5287
  const text = message.content;
5230
5288
  let askCompleted = false;
5231
- const markAskComplete = () => {
5289
+ const markAskComplete = (sessionId) => {
5232
5290
  if (askCompleted) return;
5233
5291
  askCompleted = true;
5292
+ prompt.onAskComplete?.(sessionId);
5234
5293
  this.#endImTurn(key);
5235
5294
  onAskComplete?.();
5236
5295
  };
@@ -5241,23 +5300,10 @@ export class FeishuHarnessBridge {
5241
5300
  if (this.#stepPush && this.#channel?.stream) {
5242
5301
  return this.#answerWithStepPush(event, key, message, { onAskComplete });
5243
5302
  }
5244
- let content = hasInboundImages(message) || hasReplyReference(message)
5245
- ? await promptContentForInboundMessage(message, { signal: this.#signal, deferImages: true })
5246
- : undefined;
5247
- const snapshot = this.#acceptedMessageIds.get(messageId);
5248
- let contextEnhanced = false;
5249
- if (snapshot) {
5250
- const originalContent = content ?? text;
5251
- content = enhanceContextContent(originalContent, snapshot, () => ({
5252
- channel: 'feishu',
5253
- senderId: senderOpenId(event),
5254
- chatId: event.message.chat_id,
5255
- threadId: event.message.thread_id,
5256
- }));
5257
- contextEnhanced = content !== originalContent;
5258
- }
5303
+ const prompt = await this.#preparePrompt(event, key, message);
5304
+ const { content, contextEnhanced } = prompt;
5259
5305
  if (!this.#channel?.stream) {
5260
- const { answer, artifacts = [] } = await askInWorkspaceSession({
5306
+ const { sessionId, answer, artifacts = [] } = await askInWorkspaceSession({
5261
5307
  deferredDelivery: () => ({ coordinator: this.#deferred, chatId, replyToMessageId: messageId }),
5262
5308
  harness: this.#harness,
5263
5309
  state: this.#state,
@@ -5265,13 +5311,14 @@ export class FeishuHarnessBridge {
5265
5311
  text,
5266
5312
  content,
5267
5313
  titleText: event.batchSubmission?.title,
5268
- sourceGuidance: snapshot?.config?.guidance,
5314
+ sourceGuidance: prompt.sourceGuidance,
5315
+ prepareContent: prompt.prepareContent,
5269
5316
  contextEnhanced,
5270
5317
  createOptions: { signal: this.#signal },
5271
5318
  existsOptions: { signal: this.#signal },
5272
5319
  askOptions: this.#interactionAskOptions(event, key, message.files, message.images),
5273
5320
  });
5274
- markAskComplete();
5321
+ markAskComplete(sessionId);
5275
5322
  let textReceipt;
5276
5323
  let textSendError = null;
5277
5324
  try {
@@ -5346,13 +5393,14 @@ export class FeishuHarnessBridge {
5346
5393
  text,
5347
5394
  content,
5348
5395
  titleText: event.batchSubmission?.title,
5349
- sourceGuidance: snapshot?.config?.guidance,
5396
+ sourceGuidance: prompt.sourceGuidance,
5397
+ prepareContent: prompt.prepareContent,
5350
5398
  contextEnhanced,
5351
5399
  createOptions: { signal: this.#signal },
5352
5400
  existsOptions: { signal: this.#signal },
5353
5401
  askOptions,
5354
5402
  });
5355
- markAskComplete();
5403
+ markAskComplete(completed.sessionId);
5356
5404
  completedAnswer = completed.answer;
5357
5405
  completedArtifacts = completed.artifacts ?? [];
5358
5406
  await controller.setContent(answerTextForDelivery(completedAnswer, completedArtifacts));
@@ -5410,7 +5458,7 @@ export class FeishuHarnessBridge {
5410
5458
  if (promptStarted) throw error;
5411
5459
 
5412
5460
  this.#logger.warn?.('[dsh-feishu] native stream unavailable; using text fallback:', error.message);
5413
- const { answer, artifacts = [] } = await askInWorkspaceSession({
5461
+ const { sessionId, answer, artifacts = [] } = await askInWorkspaceSession({
5414
5462
  deferredDelivery: () => ({ coordinator: this.#deferred, chatId, replyToMessageId: messageId }),
5415
5463
  harness: this.#harness,
5416
5464
  state: this.#state,
@@ -5418,13 +5466,14 @@ export class FeishuHarnessBridge {
5418
5466
  text,
5419
5467
  content,
5420
5468
  titleText: event.batchSubmission?.title,
5421
- sourceGuidance: snapshot?.config?.guidance,
5469
+ sourceGuidance: prompt.sourceGuidance,
5470
+ prepareContent: prompt.prepareContent,
5422
5471
  contextEnhanced,
5423
5472
  createOptions: { signal: this.#signal },
5424
5473
  existsOptions: { signal: this.#signal },
5425
5474
  askOptions: this.#interactionAskOptions(event, key, message.files, message.images),
5426
5475
  });
5427
- markAskComplete();
5476
+ markAskComplete(sessionId);
5428
5477
  let textReceipt;
5429
5478
  let textSendError = null;
5430
5479
  try {