botmux 2.105.1 → 2.106.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 (62) hide show
  1. package/dist/core/command-handler.d.ts.map +1 -1
  2. package/dist/core/command-handler.js +2 -1
  3. package/dist/core/command-handler.js.map +1 -1
  4. package/dist/core/passthrough-commands.d.ts.map +1 -1
  5. package/dist/core/passthrough-commands.js +1 -1
  6. package/dist/core/passthrough-commands.js.map +1 -1
  7. package/dist/daemon.d.ts +31 -8
  8. package/dist/daemon.d.ts.map +1 -1
  9. package/dist/daemon.js +750 -67
  10. package/dist/daemon.js.map +1 -1
  11. package/dist/dashboard/settings-write-applier.d.ts +5 -0
  12. package/dist/dashboard/settings-write-applier.d.ts.map +1 -1
  13. package/dist/dashboard/settings-write-applier.js +2 -2
  14. package/dist/dashboard/settings-write-applier.js.map +1 -1
  15. package/dist/dashboard/web/settings-page.d.ts.map +1 -1
  16. package/dist/dashboard/web/settings-page.js +13 -4
  17. package/dist/dashboard/web/settings-page.js.map +1 -1
  18. package/dist/dashboard-web/app.js +1 -1
  19. package/dist/dashboard-web/chunks/settings-page-R23I3IZG.js +2 -0
  20. package/dist/dashboard-web/style.css +11 -0
  21. package/dist/dashboard.js +239 -62
  22. package/dist/dashboard.js.map +1 -1
  23. package/dist/i18n/en.d.ts.map +1 -1
  24. package/dist/i18n/en.js +1 -0
  25. package/dist/i18n/en.js.map +1 -1
  26. package/dist/i18n/zh.d.ts.map +1 -1
  27. package/dist/i18n/zh.js +1 -0
  28. package/dist/i18n/zh.js.map +1 -1
  29. package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
  30. package/dist/im/lark/event-dispatcher.js +11 -0
  31. package/dist/im/lark/event-dispatcher.js.map +1 -1
  32. package/dist/im/lark/message-parser.d.ts +6 -0
  33. package/dist/im/lark/message-parser.d.ts.map +1 -1
  34. package/dist/im/lark/message-parser.js +14 -0
  35. package/dist/im/lark/message-parser.js.map +1 -1
  36. package/dist/services/vc-meeting-runtime-store.d.ts +4 -0
  37. package/dist/services/vc-meeting-runtime-store.d.ts.map +1 -1
  38. package/dist/services/vc-meeting-runtime-store.js +26 -0
  39. package/dist/services/vc-meeting-runtime-store.js.map +1 -1
  40. package/dist/setup/open-platform-automation.d.ts +20 -0
  41. package/dist/setup/open-platform-automation.d.ts.map +1 -1
  42. package/dist/setup/open-platform-automation.js +78 -1
  43. package/dist/setup/open-platform-automation.js.map +1 -1
  44. package/dist/types.d.ts +1 -0
  45. package/dist/types.d.ts.map +1 -1
  46. package/dist/vc-agent/cards.d.ts +2 -0
  47. package/dist/vc-agent/cards.d.ts.map +1 -1
  48. package/dist/vc-agent/cards.js +18 -2
  49. package/dist/vc-agent/cards.js.map +1 -1
  50. package/dist/vc-agent/normalizer.d.ts.map +1 -1
  51. package/dist/vc-agent/normalizer.js +59 -0
  52. package/dist/vc-agent/normalizer.js.map +1 -1
  53. package/dist/vc-agent/polling-source.d.ts +28 -0
  54. package/dist/vc-agent/polling-source.d.ts.map +1 -1
  55. package/dist/vc-agent/polling-source.js +57 -5
  56. package/dist/vc-agent/polling-source.js.map +1 -1
  57. package/dist/vc-agent/types.d.ts +1 -0
  58. package/dist/vc-agent/types.d.ts.map +1 -1
  59. package/dist/worker.js +43 -6
  60. package/dist/worker.js.map +1 -1
  61. package/package.json +1 -1
  62. package/dist/dashboard-web/chunks/settings-page-CRDIHRNE.js +0 -2
package/dist/daemon.js CHANGED
@@ -153,6 +153,7 @@ const DEFAULT_VC_MEETING_CONSUMER_MIN_BATCH_ITEMS = 1;
153
153
  const DEFAULT_VC_MEETING_CONSUMER_MAX_INJECT_INTERVAL_MS = 180_000;
154
154
  const DEFAULT_VC_MEETING_TEXT_REVIEW_TIMEOUT_MS = 5 * 60_000;
155
155
  const DEFAULT_VC_MEETING_VOICE_REVIEW_TIMEOUT_MS = 3 * 60_000;
156
+ const VC_MEETING_RESTORE_IMMEDIATE_TICK_DELAY_MS = 6_000;
156
157
  const VC_MEETING_END_MESSAGE_RETRY_DELAYS_MS = [1_000, 3_000, 8_000];
157
158
  const DEFAULT_VC_REALTIME_TEST_SPEAK_CLOSE_GRACE_MS = 3_000;
158
159
  const VC_MEETING_LISTENER_MESSAGE_MAX_CHARS = 3_200;
@@ -2424,6 +2425,8 @@ function persistVcMeetingRuntimeSession(session, cfg) {
2424
2425
  ...(session.syncIntervalMs !== undefined ? { syncIntervalMs: session.syncIntervalMs } : {}),
2425
2426
  ...(session.consumerSelectionExpiresAt !== undefined ? { consumerSelectionExpiresAt: session.consumerSelectionExpiresAt } : {}),
2426
2427
  ...(session.consumerCardMessageId ? { consumerCardMessageId: session.consumerCardMessageId } : {}),
2428
+ temporaryInstructionOpenIds: Object.keys(session.temporaryInstructionOpenIds),
2429
+ temporaryInstructionUnionIds: Object.keys(session.temporaryInstructionUnionIds),
2427
2430
  });
2428
2431
  }
2429
2432
  function restoreVcMeetingRuntimeSessionsForBot(larkAppId, cfg) {
@@ -2461,15 +2464,19 @@ function restoreVcMeetingRuntimeSessionsForBot(larkAppId, cfg) {
2461
2464
  session.selectedAgentLabel = record.selectedAgentLabel;
2462
2465
  session.consumerPaused = record.consumerPaused;
2463
2466
  if (session.consumerMode === 'agent')
2464
- session.consumerLastInjectedAtMs = Date.now();
2467
+ session.consumerLastInjectedAtMs = undefined;
2465
2468
  session.textOutputPolicy = record.textOutputPolicy ?? defaultVcMeetingTextOutputPolicy();
2466
2469
  session.voiceOutputPolicy = record.voiceOutputPolicy ?? defaultVcMeetingVoiceOutputPolicy(cfg);
2467
2470
  session.syncIntervalMs = record.syncIntervalMs;
2468
2471
  session.consumerSelectionExpiresAt = record.consumerSelectionExpiresAt;
2469
2472
  session.consumerCardMessageId = record.consumerCardMessageId;
2473
+ session.temporaryInstructionOpenIds = Object.fromEntries((record.temporaryInstructionOpenIds ?? []).map(openId => [openId, true]));
2474
+ session.temporaryInstructionUnionIds = Object.fromEntries((record.temporaryInstructionUnionIds ?? []).map(unionId => [unionId, true]));
2470
2475
  scheduleVcMeetingListenerFlush(key, cfg);
2471
- if (session.consumerMode === 'agent')
2476
+ scheduleVcMeetingRestoreImmediateTick(key, cfg);
2477
+ if (session.consumerMode === 'agent') {
2472
2478
  scheduleVcMeetingConsumerInjection(key, cfg);
2479
+ }
2473
2480
  if (session.consumerMode === 'pending') {
2474
2481
  const remaining = Math.max(0, (session.consumerSelectionExpiresAt ?? Date.now()) - Date.now());
2475
2482
  armVcMeetingConsumerSelectionTimer(key, cfg, remaining);
@@ -2586,6 +2593,12 @@ function getOrCreateVcMeetingDaemonSession(larkAppId, meeting, cfg, opts = {}) {
2586
2593
  pendingOutputRequests: {},
2587
2594
  consumerPendingItems: [],
2588
2595
  consumerTranscriptRevisions: {},
2596
+ actorNamesByOpenId: {},
2597
+ actorNamesByUnionId: {},
2598
+ actorUnionIdsByOpenId: {},
2599
+ actorOpenIdsByUnionId: {},
2600
+ temporaryInstructionOpenIds: {},
2601
+ temporaryInstructionUnionIds: {},
2589
2602
  flushing: false,
2590
2603
  };
2591
2604
  vcMeetingSessions.set(key, session);
@@ -2604,6 +2617,12 @@ function getOrCreateVcMeetingDaemonSession(larkAppId, meeting, cfg, opts = {}) {
2604
2617
  session.textOutputPolicy ??= defaultVcMeetingTextOutputPolicy();
2605
2618
  session.voiceOutputPolicy ??= defaultVcMeetingVoiceOutputPolicy(cfg);
2606
2619
  session.pendingOutputRequests ??= {};
2620
+ session.actorNamesByOpenId ??= {};
2621
+ session.actorNamesByUnionId ??= {};
2622
+ session.actorUnionIdsByOpenId ??= {};
2623
+ session.actorOpenIdsByUnionId ??= {};
2624
+ session.temporaryInstructionOpenIds ??= {};
2625
+ session.temporaryInstructionUnionIds ??= {};
2607
2626
  }
2608
2627
  return session;
2609
2628
  }
@@ -2612,6 +2631,7 @@ function cleanupVcMeetingDaemonSession(session, reason) {
2612
2631
  clearInterval(session.flushTimer);
2613
2632
  session.flushTimer = undefined;
2614
2633
  }
2634
+ clearVcMeetingRestoreTickTimer(session);
2615
2635
  clearVcMeetingConsumerSelectionTimer(session);
2616
2636
  clearVcMeetingConsumerInjectTimer(session);
2617
2637
  clearVcMeetingOutputRequests(session);
@@ -2655,8 +2675,57 @@ function discardUnjoinedVcMeetingSession(key) {
2655
2675
  vcMeetingSessions.delete(key);
2656
2676
  logger.info(`[vc-agent] unjoined session discarded ${key}`);
2657
2677
  }
2658
- function vcMeetingActorLabel(actor) {
2659
- return actor?.name?.trim() || actor?.openId || '未知成员';
2678
+ function vcMeetingKnownBotActorName(openId) {
2679
+ if (!openId)
2680
+ return undefined;
2681
+ for (const bot of getAllBots()) {
2682
+ if (bot.botOpenId !== openId)
2683
+ continue;
2684
+ const name = effectiveBotDisplayName(bot)?.trim();
2685
+ if (name)
2686
+ return name;
2687
+ }
2688
+ return undefined;
2689
+ }
2690
+ function vcMeetingActorLabel(actor, actorNamesByOpenId, actorNamesByUnionId) {
2691
+ const openId = actor?.openId;
2692
+ const unionId = actor?.unionId;
2693
+ return actor?.name?.trim()
2694
+ || (openId ? actorNamesByOpenId?.[openId]?.trim() : undefined)
2695
+ || (unionId ? actorNamesByUnionId?.[unionId]?.trim() : undefined)
2696
+ || vcMeetingKnownBotActorName(openId)
2697
+ || openId
2698
+ || unionId
2699
+ || '未知成员';
2700
+ }
2701
+ function rememberVcMeetingActorIdentity(session, actor) {
2702
+ const openId = actor?.openId?.trim();
2703
+ const unionId = actor?.unionId?.trim();
2704
+ const name = actor?.name?.trim();
2705
+ session.actorNamesByOpenId ??= {};
2706
+ session.actorNamesByUnionId ??= {};
2707
+ session.actorUnionIdsByOpenId ??= {};
2708
+ session.actorOpenIdsByUnionId ??= {};
2709
+ if (openId && name)
2710
+ session.actorNamesByOpenId[openId] = name;
2711
+ if (unionId && name)
2712
+ session.actorNamesByUnionId[unionId] = name;
2713
+ if (openId && unionId) {
2714
+ session.actorUnionIdsByOpenId[openId] = unionId;
2715
+ session.actorOpenIdsByUnionId[unionId] = openId;
2716
+ }
2717
+ }
2718
+ function rememberVcMeetingActorNames(session, items) {
2719
+ for (const item of items) {
2720
+ if (item.type === 'transcript_received')
2721
+ rememberVcMeetingActorIdentity(session, item.speaker);
2722
+ else if (item.type === 'chat_received')
2723
+ rememberVcMeetingActorIdentity(session, item.sender);
2724
+ else if (item.type === 'participant_joined' || item.type === 'participant_left')
2725
+ rememberVcMeetingActorIdentity(session, item.participant);
2726
+ else if (item.type === 'magic_share_started' || item.type === 'magic_share_ended')
2727
+ rememberVcMeetingActorIdentity(session, item.operator);
2728
+ }
2660
2729
  }
2661
2730
  function vcMeetingTimeLabel(ms, timeZone) {
2662
2731
  if (ms === undefined || !Number.isFinite(ms))
@@ -2730,19 +2799,19 @@ async function sendVcMeetingEndMessageWithRetry(session, listenerChatId, text) {
2730
2799
  }
2731
2800
  throw lastErr instanceof Error ? lastErr : new Error(String(lastErr ?? 'unknown send failure'));
2732
2801
  }
2733
- function formatVcMeetingTranscriptLine(item, timeZone) {
2802
+ function formatVcMeetingTranscriptLine(item, timeZone, actorNamesByOpenId, actorNamesByUnionId) {
2734
2803
  const text = compactVcMeetingText(item.text);
2735
2804
  if (!text)
2736
2805
  return undefined;
2737
2806
  const time = vcMeetingTimeLabel(item.endTimeMs ?? item.startTimeMs, timeZone);
2738
2807
  const prefix = time ? `[字幕 ${time}]` : '[字幕]';
2739
- return `${prefix} ${vcMeetingActorLabel(item.speaker)}:${text}`;
2808
+ return `${prefix} ${vcMeetingActorLabel(item.speaker, actorNamesByOpenId, actorNamesByUnionId)}:${text}`;
2740
2809
  }
2741
- function vcMeetingTranscriptEntry(item, index) {
2810
+ function vcMeetingTranscriptEntry(item, index, actorNamesByOpenId, actorNamesByUnionId) {
2742
2811
  const text = compactVcMeetingText(item.text);
2743
2812
  if (!text)
2744
2813
  return undefined;
2745
- const actorLabel = vcMeetingActorLabel(item.speaker);
2814
+ const actorLabel = vcMeetingActorLabel(item.speaker, actorNamesByOpenId, actorNamesByUnionId);
2746
2815
  return {
2747
2816
  kind: 'transcript',
2748
2817
  sortTime: item.endTimeMs ?? item.startTimeMs,
@@ -2750,13 +2819,13 @@ function vcMeetingTranscriptEntry(item, index) {
2750
2819
  ...(item.startTimeMs !== undefined ? { startTimeMs: item.startTimeMs } : {}),
2751
2820
  ...(item.endTimeMs !== undefined ? { endTimeMs: item.endTimeMs } : {}),
2752
2821
  actorLabel,
2753
- actorKey: item.speaker.openId || actorLabel,
2822
+ actorKey: item.speaker.openId || item.speaker.unionId || actorLabel,
2754
2823
  text,
2755
2824
  };
2756
2825
  }
2757
- function formatVcMeetingItemLine(item, timeZone) {
2826
+ function formatVcMeetingItemLine(item, timeZone, actorNamesByOpenId, actorNamesByUnionId) {
2758
2827
  if (item.type === 'transcript_received')
2759
- return formatVcMeetingTranscriptLine(item, timeZone);
2828
+ return formatVcMeetingTranscriptLine(item, timeZone, actorNamesByOpenId, actorNamesByUnionId);
2760
2829
  const time = vcMeetingTimeLabel(item.occurredAtMs, timeZone);
2761
2830
  const prefix = (label) => time ? `[${label} ${time}]` : `[${label}]`;
2762
2831
  if (item.type === 'chat_received') {
@@ -2764,13 +2833,13 @@ function formatVcMeetingItemLine(item, timeZone) {
2764
2833
  const text = compactVcMeetingText(chat.text);
2765
2834
  if (!text)
2766
2835
  return undefined;
2767
- return `${prefix('聊天')} ${vcMeetingActorLabel(chat.sender)}:${text}`;
2836
+ return `${prefix('聊天')} ${vcMeetingActorLabel(chat.sender, actorNamesByOpenId, actorNamesByUnionId)}:${text}`;
2768
2837
  }
2769
2838
  if (item.type === 'participant_joined') {
2770
- return `${prefix('入会')} ${vcMeetingActorLabel(item.participant)}`;
2839
+ return `${prefix('入会')} ${vcMeetingActorLabel(item.participant, actorNamesByOpenId, actorNamesByUnionId)}`;
2771
2840
  }
2772
2841
  if (item.type === 'participant_left') {
2773
- return `${prefix('离会')} ${vcMeetingActorLabel(item.participant)}`;
2842
+ return `${prefix('离会')} ${vcMeetingActorLabel(item.participant, actorNamesByOpenId, actorNamesByUnionId)}`;
2774
2843
  }
2775
2844
  if (item.type === 'magic_share_started') {
2776
2845
  const title = compactVcMeetingText(item.title) || '共享内容';
@@ -2782,10 +2851,10 @@ function formatVcMeetingItemLine(item, timeZone) {
2782
2851
  }
2783
2852
  return undefined;
2784
2853
  }
2785
- function vcMeetingEventEntry(item, index, timeZone) {
2854
+ function vcMeetingEventEntry(item, index, timeZone, actorNamesByOpenId, actorNamesByUnionId) {
2786
2855
  if (item.type === 'transcript_received')
2787
- return vcMeetingTranscriptEntry(item, index);
2788
- const line = formatVcMeetingItemLine(item, timeZone);
2856
+ return vcMeetingTranscriptEntry(item, index, actorNamesByOpenId, actorNamesByUnionId);
2857
+ const line = formatVcMeetingItemLine(item, timeZone, actorNamesByOpenId, actorNamesByUnionId);
2789
2858
  if (!line)
2790
2859
  return undefined;
2791
2860
  return {
@@ -2818,7 +2887,7 @@ function vcMeetingListenerHeaderLine(meeting, entries, timeZone, opts) {
2818
2887
  function buildVcMeetingListenerLines(meeting, items, cfg, opts = {}) {
2819
2888
  const timeZone = vcMeetingDisplayTimeZone(cfg);
2820
2889
  const entries = items
2821
- .map((item, index) => vcMeetingEventEntry(item, index, timeZone))
2890
+ .map((item, index) => vcMeetingEventEntry(item, index, timeZone, opts.actorNamesByOpenId, opts.actorNamesByUnionId))
2822
2891
  .filter((entry) => !!entry)
2823
2892
  .sort((a, b) => {
2824
2893
  const at = a.sortTime ?? Number.MAX_SAFE_INTEGER;
@@ -2965,6 +3034,12 @@ function clearVcMeetingConsumerInjectTimer(session) {
2965
3034
  session.consumerInjectTimer = undefined;
2966
3035
  }
2967
3036
  }
3037
+ function clearVcMeetingRestoreTickTimer(session) {
3038
+ if (session.restoreTickTimer) {
3039
+ clearTimeout(session.restoreTickTimer);
3040
+ session.restoreTickTimer = undefined;
3041
+ }
3042
+ }
2968
3043
  function clearVcMeetingListenerFlushTimer(session) {
2969
3044
  if (session.flushTimer) {
2970
3045
  clearInterval(session.flushTimer);
@@ -3005,7 +3080,7 @@ async function patchVcMeetingConsumerCard(session, cfg, status, opts = {}) {
3005
3080
  return;
3006
3081
  await updateMessage(session.larkAppId, messageId, JSON.stringify(vcMeetingConsumerCardForSession(status, session, cfg, opts)));
3007
3082
  }
3008
- function vcMeetingOutputAllowedOpenIds(session, cfg) {
3083
+ function vcMeetingApproverOpenIds(session, cfg) {
3009
3084
  const ids = new Set();
3010
3085
  if (session.state.attentionTargetOpenId)
3011
3086
  ids.add(session.state.attentionTargetOpenId);
@@ -3023,12 +3098,427 @@ function vcMeetingOutputAllowedOpenIds(session, cfg) {
3023
3098
  }
3024
3099
  return ids;
3025
3100
  }
3101
+ function vcMeetingInstructionSourceOpenIds(session, cfg) {
3102
+ const ids = vcMeetingApproverOpenIds(session, cfg);
3103
+ for (const openId of Object.keys(session.temporaryInstructionOpenIds ?? {}))
3104
+ ids.add(openId);
3105
+ return ids;
3106
+ }
3107
+ function vcMeetingApproverUnionIds(session, cfg) {
3108
+ const ids = new Set();
3109
+ for (const openId of vcMeetingApproverOpenIds(session, cfg)) {
3110
+ const unionId = session.actorUnionIdsByOpenId?.[openId]?.trim();
3111
+ if (unionId)
3112
+ ids.add(unionId);
3113
+ }
3114
+ return ids;
3115
+ }
3116
+ function vcMeetingInstructionSourceUnionIds(session, cfg) {
3117
+ const ids = vcMeetingApproverUnionIds(session, cfg);
3118
+ for (const unionId of Object.keys(session.temporaryInstructionUnionIds ?? {}))
3119
+ ids.add(unionId);
3120
+ return ids;
3121
+ }
3122
+ function trimmedVcMeetingIdSet(ids) {
3123
+ const result = new Set();
3124
+ for (const id of ids) {
3125
+ const trimmed = id.trim();
3126
+ if (trimmed)
3127
+ result.add(trimmed);
3128
+ }
3129
+ return result;
3130
+ }
3131
+ function createVcMeetingInstructionSourceMatcher(session, cfg) {
3132
+ const sourceOpenIds = trimmedVcMeetingIdSet(vcMeetingInstructionSourceOpenIds(session, cfg));
3133
+ const sourceUnionIds = trimmedVcMeetingIdSet(vcMeetingInstructionSourceUnionIds(session, cfg));
3134
+ const matchingOpenIds = new Set(sourceOpenIds);
3135
+ const matchingUnionIds = new Set(sourceUnionIds);
3136
+ for (const openId of sourceOpenIds) {
3137
+ const unionId = session.actorUnionIdsByOpenId?.[openId]?.trim();
3138
+ if (unionId)
3139
+ matchingUnionIds.add(unionId);
3140
+ }
3141
+ for (const unionId of sourceUnionIds) {
3142
+ const openId = session.actorOpenIdsByUnionId?.[unionId]?.trim();
3143
+ if (openId)
3144
+ matchingOpenIds.add(openId);
3145
+ }
3146
+ for (const [rawOpenId, rawUnionId] of Object.entries(session.actorUnionIdsByOpenId ?? {})) {
3147
+ const openId = rawOpenId.trim();
3148
+ const unionId = rawUnionId.trim();
3149
+ if (!openId || !unionId)
3150
+ continue;
3151
+ if (sourceOpenIds.has(openId))
3152
+ matchingUnionIds.add(unionId);
3153
+ if (sourceUnionIds.has(unionId))
3154
+ matchingOpenIds.add(openId);
3155
+ }
3156
+ for (const [rawUnionId, rawOpenId] of Object.entries(session.actorOpenIdsByUnionId ?? {})) {
3157
+ const unionId = rawUnionId.trim();
3158
+ const openId = rawOpenId.trim();
3159
+ if (!openId || !unionId)
3160
+ continue;
3161
+ if (sourceUnionIds.has(unionId))
3162
+ matchingOpenIds.add(openId);
3163
+ if (sourceOpenIds.has(openId))
3164
+ matchingUnionIds.add(unionId);
3165
+ }
3166
+ return (actor) => {
3167
+ if (!actor)
3168
+ return false;
3169
+ const openId = actor.openId?.trim();
3170
+ const unionId = actor.unionId?.trim();
3171
+ return !!((openId && matchingOpenIds.has(openId)) || (unionId && matchingUnionIds.has(unionId)));
3172
+ };
3173
+ }
3026
3174
  function isVcMeetingOutputAllowedOperator(session, cfg, openId) {
3027
- return typeof openId === 'string' && vcMeetingOutputAllowedOpenIds(session, cfg).has(openId);
3175
+ return typeof openId === 'string' && vcMeetingApproverOpenIds(session, cfg).has(openId);
3176
+ }
3177
+ function isVcMeetingTemporaryAuthAllowedOperator(session, cfg, operator) {
3178
+ if (typeof operator.openId === 'string' && vcMeetingApproverOpenIds(session, cfg).has(operator.openId)) {
3179
+ return true;
3180
+ }
3181
+ if (typeof operator.unionId === 'string' && vcMeetingApproverUnionIds(session, cfg).has(operator.unionId)) {
3182
+ return true;
3183
+ }
3184
+ return false;
3028
3185
  }
3029
3186
  function isVcMeetingConsumerSelectionAllowedOperator(session, cfg, openId) {
3030
3187
  return isVcMeetingOutputAllowedOperator(session, cfg, openId);
3031
3188
  }
3189
+ function findActiveVcMeetingSessionByListenerChat(larkAppId, listenerChatId) {
3190
+ if (!listenerChatId)
3191
+ return undefined;
3192
+ const cfg = effectiveVcMeetingAgentConfig(larkAppId);
3193
+ if (!cfg)
3194
+ return undefined;
3195
+ let best;
3196
+ for (const [key, session] of vcMeetingSessions.entries()) {
3197
+ if (session.larkAppId !== larkAppId || session.ended)
3198
+ continue;
3199
+ if (session.listenerChatId !== listenerChatId)
3200
+ continue;
3201
+ if (!best || session.lastActivityAt > best.session.lastActivityAt) {
3202
+ best = { key, session, cfg };
3203
+ }
3204
+ }
3205
+ return best;
3206
+ }
3207
+ function stripVcMeetingAuthMentionNames(text, mentions) {
3208
+ let out = text;
3209
+ for (const mention of mentions ?? []) {
3210
+ if (!mention.name)
3211
+ continue;
3212
+ out = out.split(`@${mention.name}`).join(' ');
3213
+ }
3214
+ return out.replace(/\s+/g, ' ').trim();
3215
+ }
3216
+ function parseVcMeetingTemporaryAuthCommand(commandContent, mentions, botOpenId) {
3217
+ const stripped = stripVcMeetingAuthMentionNames(commandContent, mentions);
3218
+ const match = /^\/vc-auth(?:\s+([\s\S]*))?$/i.exec(stripped);
3219
+ if (!match)
3220
+ return undefined;
3221
+ const words = (match[1] ?? '').trim().split(/\s+/).filter(Boolean);
3222
+ const verb = words[0]?.toLowerCase();
3223
+ let action = 'grant';
3224
+ if (verb === 'help' || verb === 'h' || verb === '?' || verb === '-h' || verb === '--help')
3225
+ action = 'help';
3226
+ else if (verb === 'list')
3227
+ action = 'list';
3228
+ else if (verb === 'revoke' || verb === 'rm' || verb === 'remove')
3229
+ action = 'revoke';
3230
+ else if (verb === 'grant' || verb === 'add')
3231
+ action = 'grant';
3232
+ else if (verb && !/^o[un]_[A-Za-z0-9_-]+$/.test(verb))
3233
+ action = 'invalid';
3234
+ if (action === 'help' || action === 'list' || action === 'invalid')
3235
+ return { action, targets: [] };
3236
+ const targets = new Map();
3237
+ for (const mention of mentions ?? []) {
3238
+ const openId = mention.openId?.trim();
3239
+ const unionId = mention.unionId?.trim();
3240
+ if ((!openId && !unionId) || openId === botOpenId)
3241
+ continue;
3242
+ const key = unionId ? `union:${unionId}` : `open:${openId}`;
3243
+ if (targets.has(key))
3244
+ continue;
3245
+ targets.set(key, {
3246
+ ...(openId ? { openId } : {}),
3247
+ ...(unionId ? { unionId } : {}),
3248
+ ...(mention.name?.trim() ? { name: mention.name.trim() } : {}),
3249
+ source: 'mention',
3250
+ });
3251
+ }
3252
+ for (const openId of stripped.match(/\bou_[A-Za-z0-9_-]+\b/g) ?? []) {
3253
+ if (!openId || openId === botOpenId)
3254
+ continue;
3255
+ const key = `open:${openId}`;
3256
+ if (targets.has(key))
3257
+ continue;
3258
+ targets.set(key, { openId, source: 'literal-open-id' });
3259
+ }
3260
+ for (const unionId of stripped.match(/\bon_[A-Za-z0-9_-]+\b/g) ?? []) {
3261
+ if (!unionId)
3262
+ continue;
3263
+ const key = `union:${unionId}`;
3264
+ if (targets.has(key))
3265
+ continue;
3266
+ targets.set(key, { unionId, source: 'literal-union-id' });
3267
+ }
3268
+ return { action, targets: [...targets.values()] };
3269
+ }
3270
+ function vcMeetingTemporaryAuthTargetLabel(session, target) {
3271
+ const openId = target.openId?.trim();
3272
+ const unionId = target.unionId?.trim();
3273
+ const mappedOpenId = unionId ? session.actorOpenIdsByUnionId?.[unionId] : undefined;
3274
+ return target.name?.trim()
3275
+ || (openId ? session.actorNamesByOpenId?.[openId]?.trim() : undefined)
3276
+ || (unionId ? session.actorNamesByUnionId?.[unionId]?.trim() : undefined)
3277
+ || (mappedOpenId ? session.actorNamesByOpenId?.[mappedOpenId]?.trim() : undefined)
3278
+ || openId
3279
+ || unionId
3280
+ || '未知成员';
3281
+ }
3282
+ function vcMeetingTemporaryAuthUsage() {
3283
+ return [
3284
+ '入口:在监听群里直接 @会议监听 bot 发送,发给执行 agent 不会生效。',
3285
+ '用法:`/vc-auth @成员` 或 `/vc-auth ou_xxx` 临时授权本场会议指令源。',
3286
+ '撤销:`/vc-auth revoke @成员`;查看:`/vc-auth list`;帮助:`/vc-auth help`。',
3287
+ '临时授权只影响会中语音/聊天的指令源打标,不具备输出审批、再授权或配置会议 agent 的权限,会议结束自动失效。',
3288
+ ].join('\n');
3289
+ }
3290
+ function vcMeetingTemporaryAuthTargetOpenIdUsable(target, session, openIdNamespaceLarkAppId) {
3291
+ if (!target.openId)
3292
+ return false;
3293
+ if (target.source !== 'mention')
3294
+ return true;
3295
+ // Mention open_ids are scoped to the bot app that received the message.
3296
+ // Cross-bot /vc-auth relies on union_id, which is stable only for apps under
3297
+ // the same Lark developer account; cross-developer bot pairs fail closed.
3298
+ return openIdNamespaceLarkAppId === session.larkAppId;
3299
+ }
3300
+ function vcMeetingTemporaryAuthTargetIsApprover(session, cfg, target, openIdNamespaceLarkAppId) {
3301
+ if (vcMeetingTemporaryAuthTargetOpenIdUsable(target, session, openIdNamespaceLarkAppId)
3302
+ && target.openId
3303
+ && vcMeetingApproverOpenIds(session, cfg).has(target.openId)) {
3304
+ return true;
3305
+ }
3306
+ return !!target.unionId && vcMeetingApproverUnionIds(session, cfg).has(target.unionId);
3307
+ }
3308
+ function vcMeetingTemporaryAuthTargetIsTemporary(session, target, openIdNamespaceLarkAppId) {
3309
+ if (vcMeetingTemporaryAuthTargetOpenIdUsable(target, session, openIdNamespaceLarkAppId)
3310
+ && target.openId
3311
+ && session.temporaryInstructionOpenIds?.[target.openId]) {
3312
+ return true;
3313
+ }
3314
+ return !!target.unionId && !!session.temporaryInstructionUnionIds?.[target.unionId];
3315
+ }
3316
+ function rememberVcMeetingTemporaryAuthTarget(session, target, openIdNamespaceLarkAppId) {
3317
+ const openId = vcMeetingTemporaryAuthTargetOpenIdUsable(target, session, openIdNamespaceLarkAppId)
3318
+ ? target.openId?.trim()
3319
+ : undefined;
3320
+ const unionId = target.unionId?.trim();
3321
+ const name = target.name?.trim();
3322
+ rememberVcMeetingActorIdentity(session, {
3323
+ ...(openId ? { openId } : {}),
3324
+ ...(unionId ? { unionId } : {}),
3325
+ ...(name ? { name } : {}),
3326
+ });
3327
+ }
3328
+ function vcMeetingTemporaryAuthListBody(session) {
3329
+ const lines = [];
3330
+ const seen = new Set();
3331
+ for (const openId of Object.keys(session.temporaryInstructionOpenIds ?? {})) {
3332
+ const unionId = session.actorUnionIdsByOpenId?.[openId];
3333
+ if (unionId)
3334
+ seen.add(`union:${unionId}`);
3335
+ seen.add(`open:${openId}`);
3336
+ lines.push(`- ${vcMeetingTemporaryAuthTargetLabel(session, { openId, unionId })}(${openId})`);
3337
+ }
3338
+ for (const unionId of Object.keys(session.temporaryInstructionUnionIds ?? {})) {
3339
+ const key = `union:${unionId}`;
3340
+ if (seen.has(key))
3341
+ continue;
3342
+ const openId = session.actorOpenIdsByUnionId?.[unionId];
3343
+ lines.push(`- ${vcMeetingTemporaryAuthTargetLabel(session, { openId, unionId })}(${unionId})`);
3344
+ }
3345
+ return lines.length > 0 ? lines.join('\n') : '本场暂无临时授权用户。';
3346
+ }
3347
+ async function applyVcMeetingTemporaryAuthCommand(active, parsed, input) {
3348
+ const { session, cfg } = active;
3349
+ const listenerChatId = session.listenerChatId;
3350
+ if (!listenerChatId)
3351
+ return true;
3352
+ const openIdNamespaceLarkAppId = session.larkAppId;
3353
+ if (!isVcMeetingTemporaryAuthAllowedOperator(session, cfg, {
3354
+ openId: input.senderOpenId,
3355
+ unionId: input.senderUnionId,
3356
+ })) {
3357
+ await sendMessage(session.larkAppId, listenerChatId, '只有本场会议原授权人可以设置临时指令源。', 'text');
3358
+ return true;
3359
+ }
3360
+ if (parsed.action === 'list') {
3361
+ await sendMessage(session.larkAppId, listenerChatId, `本场临时授权:\n${vcMeetingTemporaryAuthListBody(session)}`, 'text');
3362
+ return true;
3363
+ }
3364
+ if (parsed.targets.length === 0) {
3365
+ await sendMessage(session.larkAppId, listenerChatId, vcMeetingTemporaryAuthUsage(), 'text');
3366
+ return true;
3367
+ }
3368
+ const changed = [];
3369
+ const unchanged = [];
3370
+ for (const target of parsed.targets) {
3371
+ const openIdUsable = vcMeetingTemporaryAuthTargetOpenIdUsable(target, session, openIdNamespaceLarkAppId);
3372
+ if (!openIdUsable && !target.unionId) {
3373
+ unchanged.push(`${vcMeetingTemporaryAuthTargetLabel(session, target)} 缺少可跨 bot 对齐的 union_id`);
3374
+ continue;
3375
+ }
3376
+ rememberVcMeetingTemporaryAuthTarget(session, target, openIdNamespaceLarkAppId);
3377
+ const label = vcMeetingTemporaryAuthTargetLabel(session, target);
3378
+ if (parsed.action === 'grant' && vcMeetingTemporaryAuthTargetIsApprover(session, cfg, target, openIdNamespaceLarkAppId)) {
3379
+ unchanged.push(`${label} 已是原授权人`);
3380
+ continue;
3381
+ }
3382
+ if (parsed.action === 'grant') {
3383
+ if (vcMeetingTemporaryAuthTargetIsTemporary(session, target, openIdNamespaceLarkAppId)) {
3384
+ unchanged.push(`${label} 已在临时授权列表`);
3385
+ continue;
3386
+ }
3387
+ if (openIdUsable && target.openId)
3388
+ session.temporaryInstructionOpenIds[target.openId] = true;
3389
+ if (target.unionId)
3390
+ session.temporaryInstructionUnionIds[target.unionId] = true;
3391
+ changed.push(label);
3392
+ continue;
3393
+ }
3394
+ if (!vcMeetingTemporaryAuthTargetIsTemporary(session, target, openIdNamespaceLarkAppId)) {
3395
+ unchanged.push(`${label} 不在临时授权列表`);
3396
+ continue;
3397
+ }
3398
+ if (openIdUsable && target.openId)
3399
+ delete session.temporaryInstructionOpenIds[target.openId];
3400
+ if (target.unionId)
3401
+ delete session.temporaryInstructionUnionIds[target.unionId];
3402
+ changed.push(label);
3403
+ }
3404
+ persistVcMeetingRuntimeSession(session, cfg);
3405
+ logger.info(`[vc-agent] temporary instruction auth ${parsed.action} meeting=${session.state.meeting.id} ` +
3406
+ `operator=${input.senderOpenId ?? input.senderUnionId ?? '?'} changed=${changed.join(',') || '-'} unchanged=${unchanged.join(',') || '-'}`);
3407
+ const lines = changed.length > 0
3408
+ ? [
3409
+ parsed.action === 'grant'
3410
+ ? `已临时授权 ${changed.join('、')} 为本场会议指令源。`
3411
+ : `已撤销 ${changed.join('、')} 的本场临时指令源授权。`,
3412
+ '临时授权仅本场有效,不具备输出审批、再授权或会议 agent 配置权限。',
3413
+ ...(unchanged.length > 0 ? [`未变更:${unchanged.join(';')}`] : []),
3414
+ ]
3415
+ : [`未变更:${unchanged.join(';') || '没有可变更目标'}`];
3416
+ await sendMessage(session.larkAppId, listenerChatId, lines.join('\n'), 'text');
3417
+ return true;
3418
+ }
3419
+ function resolveVcMeetingListenerBotLabel(larkAppId) {
3420
+ try {
3421
+ const bot = getBot(larkAppId);
3422
+ if (bot.botName?.trim())
3423
+ return { label: bot.botName.trim(), canMentionByName: true };
3424
+ if (bot.config.displayName?.trim())
3425
+ return { label: bot.config.displayName.trim(), canMentionByName: false };
3426
+ if (bot.config.name?.trim())
3427
+ return { label: bot.config.name.trim(), canMentionByName: false };
3428
+ }
3429
+ catch { /* The listener bot may belong to a different daemon process. */ }
3430
+ try {
3431
+ const infoPath = join(config.session.dataDir, 'bots-info.json');
3432
+ if (existsSync(infoPath)) {
3433
+ const entries = JSON.parse(readFileSync(infoPath, 'utf-8'));
3434
+ const hit = entries.find(entry => entry.larkAppId === larkAppId);
3435
+ if (typeof hit?.botName === 'string' && hit.botName.trim()) {
3436
+ return { label: hit.botName.trim(), canMentionByName: true };
3437
+ }
3438
+ }
3439
+ }
3440
+ catch { /* fall back to static config */ }
3441
+ try {
3442
+ const cfg = loadBotConfigs().find(bot => bot.larkAppId === larkAppId);
3443
+ if (cfg?.displayName?.trim())
3444
+ return { label: cfg.displayName.trim(), canMentionByName: false };
3445
+ if (cfg?.name?.trim())
3446
+ return { label: cfg.name.trim(), canMentionByName: false };
3447
+ }
3448
+ catch { /* fall back to app id */ }
3449
+ return { label: larkAppId, canMentionByName: false };
3450
+ }
3451
+ function findAnyVcMeetingRuntimeSessionByListenerChat(listenerChatId) {
3452
+ const chatId = listenerChatId?.trim();
3453
+ if (!chatId)
3454
+ return undefined;
3455
+ const appIds = new Set();
3456
+ for (const bot of getAllBots())
3457
+ appIds.add(bot.config.larkAppId);
3458
+ for (const daemon of listOnlineDaemons())
3459
+ appIds.add(daemon.larkAppId);
3460
+ try {
3461
+ for (const bot of loadBotConfigs())
3462
+ appIds.add(bot.larkAppId);
3463
+ }
3464
+ catch { /* best effort */ }
3465
+ let best;
3466
+ for (const appId of appIds) {
3467
+ for (const record of listVcMeetingRuntimeSessions(config.session.dataDir, appId)) {
3468
+ if (record.listenerChatId !== chatId)
3469
+ continue;
3470
+ if (!best || record.updatedAt > best.updatedAt)
3471
+ best = record;
3472
+ }
3473
+ }
3474
+ return best;
3475
+ }
3476
+ async function replyVcMeetingTemporaryAuthListenerHint(input) {
3477
+ if (!input.chatId)
3478
+ return false;
3479
+ const record = findVcMeetingRuntimeSessionByListenerAndAgent(config.session.dataDir, {
3480
+ listenerChatId: input.chatId,
3481
+ selectedAgentAppId: input.larkAppId,
3482
+ });
3483
+ if (!record)
3484
+ return false;
3485
+ const listenerBot = resolveVcMeetingListenerBotLabel(record.larkAppId);
3486
+ await sessionReply(input.anchor, [
3487
+ `临时授权只能由本场会议监听 bot(${listenerBot.label})处理。`,
3488
+ listenerBot.canMentionByName
3489
+ ? `请在监听群里直接 @${listenerBot.label} 发送:\`/vc-auth @成员\`。`
3490
+ : '请在监听群里直接 @会议监听 bot 发送:`/vc-auth @成员`。',
3491
+ '这条命令发给执行 agent 不会生效,也不会代转授权。',
3492
+ ].join('\n'), 'text', input.larkAppId);
3493
+ return true;
3494
+ }
3495
+ async function handleVcMeetingTemporaryAuthCommand(input) {
3496
+ const parsed = parseVcMeetingTemporaryAuthCommand(input.commandContent, input.mentions, getBot(input.larkAppId).botOpenId);
3497
+ if (!parsed)
3498
+ return false;
3499
+ if (parsed.action === 'help' || parsed.action === 'invalid') {
3500
+ await sessionReply(input.anchor, vcMeetingTemporaryAuthUsage(), 'text', input.larkAppId);
3501
+ return true;
3502
+ }
3503
+ const active = findActiveVcMeetingSessionByListenerChat(input.larkAppId, input.chatId);
3504
+ if (!active) {
3505
+ const replied = await replyVcMeetingTemporaryAuthListenerHint(input);
3506
+ if (replied)
3507
+ return true;
3508
+ const activeInThisChat = findAnyVcMeetingRuntimeSessionByListenerChat(input.chatId);
3509
+ const message = activeInThisChat
3510
+ ? '本群有正在运行的会议监听,但这个 bot 不是本场会议监听 bot,也不是当前选择的执行 agent。请直接 @会议监听 bot 发送:`/vc-auth @成员`。'
3511
+ : '当前群没有正在运行的会议监听,或这个 bot 不是本场的监听/处理 agent,无法设置本场临时授权。';
3512
+ await sessionReply(input.anchor, message, 'text', input.larkAppId);
3513
+ return true;
3514
+ }
3515
+ return applyVcMeetingTemporaryAuthCommand(active, parsed, {
3516
+ commandContent: input.commandContent,
3517
+ mentions: input.mentions,
3518
+ senderOpenId: input.senderOpenId,
3519
+ senderUnionId: input.senderUnionId,
3520
+ });
3521
+ }
3032
3522
  function clearVcMeetingOutputRequestTimer(req) {
3033
3523
  if (req?.timer) {
3034
3524
  clearTimeout(req.timer);
@@ -3040,6 +3530,18 @@ function clearVcMeetingOutputRequests(session) {
3040
3530
  clearVcMeetingOutputRequestTimer(req);
3041
3531
  session.pendingOutputRequests = {};
3042
3532
  }
3533
+ function armVcMeetingOutputRequestTimer(key, req) {
3534
+ clearVcMeetingOutputRequestTimer(req);
3535
+ req.timer = setTimeout(() => {
3536
+ const current = vcMeetingSessions.get(key);
3537
+ const pending = current?.pendingOutputRequests[req.channel];
3538
+ if (!current || !pending || pending.id !== req.id || pending.applying)
3539
+ return;
3540
+ void rejectVcMeetingOutputRequest(current, pending, 'expired', `你的 ${req.channel === 'voice' ? '语音' : '会中弹幕'} 输出请求已超时并被自动拒绝。`);
3541
+ }, vcMeetingOutputReviewTimeoutMs(req.channel));
3542
+ if (typeof req.timer.unref === 'function')
3543
+ req.timer.unref();
3544
+ }
3043
3545
  async function expireVcMeetingOutputRequestsOnClose(session) {
3044
3546
  const pending = Object.values(session.pendingOutputRequests);
3045
3547
  for (const req of pending) {
@@ -3052,6 +3554,12 @@ async function expireVcMeetingOutputRequestsOnClose(session) {
3052
3554
  }
3053
3555
  session.pendingOutputRequests = {};
3054
3556
  }
3557
+ async function waitVcMeetingOutputSubmits(session) {
3558
+ const pending = Object.values(session.outputSubmitPromises ?? {}).filter((promise) => !!promise);
3559
+ if (pending.length === 0)
3560
+ return;
3561
+ await Promise.allSettled(pending);
3562
+ }
3055
3563
  function vcMeetingPendingOutputById(session, requestId) {
3056
3564
  for (const channel of ['text', 'voice']) {
3057
3565
  const req = session.pendingOutputRequests[channel];
@@ -3060,6 +3568,38 @@ function vcMeetingPendingOutputById(session, requestId) {
3060
3568
  }
3061
3569
  return undefined;
3062
3570
  }
3571
+ function vcMeetingOutputContentParts(req) {
3572
+ return req.contentParts?.length ? [...req.contentParts] : [req.content];
3573
+ }
3574
+ function vcMeetingOutputReasonParts(req) {
3575
+ return req.reasonParts?.length ? [...req.reasonParts] : (req.reason ? [req.reason] : []);
3576
+ }
3577
+ function vcMeetingOutputFallbackParts(req) {
3578
+ if (req.channel !== 'voice')
3579
+ return undefined;
3580
+ if (req.fallbackTextParts?.length)
3581
+ return [...req.fallbackTextParts];
3582
+ if (req.fallbackText)
3583
+ return [req.fallbackText];
3584
+ return vcMeetingOutputContentParts(req);
3585
+ }
3586
+ function vcMeetingOutputFallbackPartForInput(input) {
3587
+ if (input.channel !== 'voice')
3588
+ return undefined;
3589
+ return input.fallbackText?.trim() || input.content;
3590
+ }
3591
+ function normalizeVcMeetingVoicePart(part) {
3592
+ const trimmed = part.trim();
3593
+ if (!trimmed)
3594
+ return '';
3595
+ return /[。.!!??]$/u.test(trimmed) ? trimmed : `${trimmed}。`;
3596
+ }
3597
+ function joinVcMeetingOutputParts(channel, parts) {
3598
+ if (channel === 'voice') {
3599
+ return parts.map(normalizeVcMeetingVoicePart).filter(Boolean).join('');
3600
+ }
3601
+ return parts.join('\n');
3602
+ }
3063
3603
  function vcMeetingOutputReviewCardForRequest(session, req, status, opts = {}) {
3064
3604
  return JSON.parse(buildVcMeetingOutputReviewCard({
3065
3605
  status,
@@ -3069,8 +3609,10 @@ function vcMeetingOutputReviewCardForRequest(session, req, status, opts = {}) {
3069
3609
  nonce: req.nonce,
3070
3610
  agentLabel: session.selectedAgentLabel ?? req.agentAppId,
3071
3611
  content: req.content,
3612
+ ...(req.contentParts?.length && req.contentParts.length > 1 ? { contentItems: req.contentParts } : {}),
3072
3613
  ...(req.reason ? { reason: req.reason } : {}),
3073
3614
  ...(req.fallbackText ? { fallbackText: req.fallbackText } : {}),
3615
+ ...(req.fallbackTextParts?.length && req.fallbackTextParts.length > 1 ? { fallbackTextItems: req.fallbackTextParts } : {}),
3074
3616
  textOutputAvailable: vcMeetingTextOutputAvailable(),
3075
3617
  ...(opts.error ? { error: opts.error } : {}),
3076
3618
  }));
@@ -3186,6 +3728,24 @@ async function rejectVcMeetingOutputRequest(session, req, status, notifyMessage)
3186
3728
  }
3187
3729
  }
3188
3730
  async function submitVcMeetingOutputRequest(input) {
3731
+ const session = vcMeetingSessions.get(vcMeetingSessionKey(input.larkAppId, input.meetingId));
3732
+ if (!session)
3733
+ return submitVcMeetingOutputRequestImpl(input);
3734
+ session.outputSubmitPromises ??= {};
3735
+ const prior = session.outputSubmitPromises[input.channel] ?? Promise.resolve();
3736
+ const run = prior
3737
+ .catch(() => undefined)
3738
+ .then(() => submitVcMeetingOutputRequestImpl(input));
3739
+ const tracked = run.catch(() => undefined);
3740
+ session.outputSubmitPromises[input.channel] = tracked;
3741
+ tracked.finally(() => {
3742
+ if (session.outputSubmitPromises?.[input.channel] === tracked) {
3743
+ delete session.outputSubmitPromises[input.channel];
3744
+ }
3745
+ });
3746
+ return run;
3747
+ }
3748
+ async function submitVcMeetingOutputRequestImpl(input) {
3189
3749
  const cfg = effectiveVcMeetingAgentConfig(input.larkAppId);
3190
3750
  const key = vcMeetingSessionKey(input.larkAppId, input.meetingId);
3191
3751
  const session = vcMeetingSessions.get(key);
@@ -3210,8 +3770,11 @@ async function submitVcMeetingOutputRequest(input) {
3210
3770
  nonce: randomVcMeetingNonce(),
3211
3771
  agentAppId: session.selectedAgentAppId,
3212
3772
  content: input.content,
3773
+ contentParts: [input.content],
3213
3774
  ...(input.reason ? { reason: input.reason } : {}),
3775
+ ...(input.reason ? { reasonParts: [input.reason] } : {}),
3214
3776
  ...(input.fallbackText ? { fallbackText: input.fallbackText } : {}),
3777
+ ...(input.fallbackText ? { fallbackTextParts: [input.fallbackText] } : {}),
3215
3778
  createdAt: now,
3216
3779
  expiresAt: now + vcMeetingOutputReviewTimeoutMs(input.channel),
3217
3780
  };
@@ -3232,38 +3795,95 @@ async function submitVcMeetingOutputRequest(input) {
3232
3795
  return { ok: false, error: `上一条${input.channel === 'voice' ? '语音' : '会中弹幕'}输出请求正在执行,稍后重试` };
3233
3796
  }
3234
3797
  if (prior) {
3235
- await rejectVcMeetingOutputRequest(session, prior, 'superseded').catch((err) => {
3236
- logger.warn(`[vc-agent] supersede output request failed meeting=${input.meetingId}: ${err instanceof Error ? err.message : String(err)}`);
3237
- });
3798
+ const mergedContentParts = [...vcMeetingOutputContentParts(prior), input.content];
3799
+ const mergedContent = joinVcMeetingOutputParts(input.channel, mergedContentParts);
3800
+ const priorFallbackParts = vcMeetingOutputFallbackParts(prior);
3801
+ const nextFallbackPart = vcMeetingOutputFallbackPartForInput(input);
3802
+ const mergedFallbackParts = priorFallbackParts || nextFallbackPart
3803
+ ? [...(priorFallbackParts ?? []), ...(nextFallbackPart ? [nextFallbackPart] : [])]
3804
+ : undefined;
3805
+ const mergedFallbackText = mergedFallbackParts?.length
3806
+ ? joinVcMeetingOutputParts('text', mergedFallbackParts)
3807
+ : undefined;
3808
+ if (mergedContent.length > VC_MEETING_OUTPUT_MAX_CONTENT_CHARS || (mergedFallbackText?.length ?? 0) > VC_MEETING_OUTPUT_MAX_CONTENT_CHARS) {
3809
+ logger.info(`[vc-agent] output review merge overflow meeting=${input.meetingId} channel=${input.channel}; falling back to supersede`);
3810
+ await rejectVcMeetingOutputRequest(session, prior, 'superseded').catch((rejectErr) => {
3811
+ logger.warn(`[vc-agent] supersede output request after merge overflow failed meeting=${input.meetingId}: ${rejectErr instanceof Error ? rejectErr.message : String(rejectErr)}`);
3812
+ });
3813
+ }
3814
+ else {
3815
+ const mergedReasonParts = [...vcMeetingOutputReasonParts(prior), ...(input.reason ? [input.reason] : [])];
3816
+ const nextNonce = randomVcMeetingNonce();
3817
+ const mergedReq = {
3818
+ ...prior,
3819
+ nonce: nextNonce,
3820
+ content: mergedContent,
3821
+ contentParts: mergedContentParts,
3822
+ expiresAt: now + vcMeetingOutputReviewTimeoutMs(input.channel),
3823
+ timer: undefined,
3824
+ };
3825
+ if (mergedReasonParts.length > 0) {
3826
+ mergedReq.reason = mergedReasonParts.join(';');
3827
+ mergedReq.reasonParts = mergedReasonParts;
3828
+ }
3829
+ else {
3830
+ delete mergedReq.reason;
3831
+ delete mergedReq.reasonParts;
3832
+ }
3833
+ if (mergedFallbackText) {
3834
+ mergedReq.fallbackText = mergedFallbackText;
3835
+ mergedReq.fallbackTextParts = mergedFallbackParts;
3836
+ }
3837
+ else {
3838
+ delete mergedReq.fallbackText;
3839
+ delete mergedReq.fallbackTextParts;
3840
+ }
3841
+ prior.nonce = nextNonce;
3842
+ clearVcMeetingOutputRequestTimer(prior);
3843
+ try {
3844
+ await patchVcMeetingOutputReviewCard(session, mergedReq, 'pending');
3845
+ if (session.ended || vcMeetingSessions.get(key) !== session) {
3846
+ await patchVcMeetingOutputReviewCard(session, mergedReq, 'expired').catch((patchErr) => {
3847
+ logger.warn(`[vc-agent] output review card close re-patch failed meeting=${input.meetingId}: ${patchErr instanceof Error ? patchErr.message : String(patchErr)}`);
3848
+ });
3849
+ if (session.pendingOutputRequests[input.channel]?.id === prior.id) {
3850
+ delete session.pendingOutputRequests[input.channel];
3851
+ }
3852
+ return { ok: false, error: 'meeting session not found or ended' };
3853
+ }
3854
+ const { applying: _applying, timer: _timer, ...mergedReqState } = mergedReq;
3855
+ Object.assign(prior, mergedReqState);
3856
+ session.pendingOutputRequests[input.channel] = prior;
3857
+ armVcMeetingOutputRequestTimer(key, prior);
3858
+ logger.info(`[vc-agent] output review merged meeting=${input.meetingId} channel=${input.channel} request=${prior.id} parts=${mergedContentParts.length}`);
3859
+ return { ok: true, status: 'pending', requestId: prior.id, merged: true };
3860
+ }
3861
+ catch (err) {
3862
+ logger.warn(`[vc-agent] merge output review card patch failed meeting=${input.meetingId}; falling back to supersede: ${err instanceof Error ? err.message : String(err)}`);
3863
+ await rejectVcMeetingOutputRequest(session, prior, 'superseded').catch((rejectErr) => {
3864
+ logger.warn(`[vc-agent] supersede output request failed meeting=${input.meetingId}: ${rejectErr instanceof Error ? rejectErr.message : String(rejectErr)}`);
3865
+ });
3866
+ }
3867
+ }
3238
3868
  }
3239
- const cardJson = buildVcMeetingOutputReviewCard({
3240
- status: 'pending',
3241
- meeting: session.state.meeting,
3242
- channel: req.channel,
3243
- requestId: req.id,
3244
- nonce: req.nonce,
3245
- agentLabel: session.selectedAgentLabel ?? session.selectedAgentAppId,
3246
- content: req.content,
3247
- ...(req.reason ? { reason: req.reason } : {}),
3248
- ...(req.fallbackText ? { fallbackText: req.fallbackText } : {}),
3249
- textOutputAvailable: vcMeetingTextOutputAvailable(),
3250
- });
3869
+ if (session.ended || vcMeetingSessions.get(key) !== session) {
3870
+ return { ok: false, error: 'meeting session not found or ended' };
3871
+ }
3872
+ const cardJson = JSON.stringify(vcMeetingOutputReviewCardForRequest(session, req, 'pending'));
3251
3873
  try {
3252
3874
  req.cardMessageId = await sendMessage(session.larkAppId, listenerChatId, cardJson, 'interactive', `vc_${session.state.meeting.id.slice(-12)}_${req.id}`);
3253
3875
  }
3254
3876
  catch (err) {
3255
3877
  return { ok: false, error: err instanceof Error ? err.message : String(err) };
3256
3878
  }
3879
+ if (session.ended || vcMeetingSessions.get(key) !== session) {
3880
+ await patchVcMeetingOutputReviewCard(session, req, 'expired').catch((patchErr) => {
3881
+ logger.warn(`[vc-agent] output review card close patch failed meeting=${input.meetingId}: ${patchErr instanceof Error ? patchErr.message : String(patchErr)}`);
3882
+ });
3883
+ return { ok: false, error: 'meeting session not found or ended' };
3884
+ }
3257
3885
  session.pendingOutputRequests[input.channel] = req;
3258
- req.timer = setTimeout(() => {
3259
- const current = vcMeetingSessions.get(key);
3260
- const pending = current?.pendingOutputRequests[input.channel];
3261
- if (!current || !pending || pending.id !== req.id || pending.applying)
3262
- return;
3263
- void rejectVcMeetingOutputRequest(current, pending, 'expired', `你的 ${input.channel === 'voice' ? '语音' : '会中弹幕'} 输出请求已超时并被自动拒绝。`);
3264
- }, vcMeetingOutputReviewTimeoutMs(input.channel));
3265
- if (typeof req.timer.unref === 'function')
3266
- req.timer.unref();
3886
+ armVcMeetingOutputRequestTimer(key, req);
3267
3887
  logger.info(`[vc-agent] output review requested meeting=${input.meetingId} channel=${input.channel} request=${req.id}`);
3268
3888
  return { ok: true, status: 'pending', requestId: req.id };
3269
3889
  }
@@ -3278,15 +3898,7 @@ async function reviewVcMeetingOutputRequest(input) {
3278
3898
  }
3279
3899
  const found = vcMeetingPendingOutputById(session, input.requestId);
3280
3900
  if (!found || found.req.nonce !== input.nonce) {
3281
- return vcMeetingOutputReviewCardForRequest(session, {
3282
- id: input.requestId,
3283
- channel: 'text',
3284
- nonce: '',
3285
- agentAppId: session.selectedAgentAppId ?? '',
3286
- content: '请求已过期',
3287
- createdAt: Date.now(),
3288
- expiresAt: Date.now(),
3289
- }, 'expired');
3901
+ return { toast: { type: 'warning', content: '这张输出审批卡已失效,请以最新卡片为准' } };
3290
3902
  }
3291
3903
  const { channel, req } = found;
3292
3904
  if (req.applying)
@@ -3420,7 +4032,7 @@ function vcMeetingConsumerBatchTextChars(items) {
3420
4032
  return items.reduce((sum, item) => sum + vcMeetingConsumerItemText(item).length, 0);
3421
4033
  }
3422
4034
  function vcMeetingConsumerHasFastSignal(session, cfg, items) {
3423
- const allowedOpenIds = vcMeetingOutputAllowedOpenIds(session, cfg);
4035
+ const isInstructionSource = createVcMeetingInstructionSourceMatcher(session, cfg);
3424
4036
  for (const item of items) {
3425
4037
  const text = vcMeetingConsumerItemText(item);
3426
4038
  if (!text)
@@ -3432,7 +4044,7 @@ function vcMeetingConsumerHasFastSignal(session, cfg, items) {
3432
4044
  : item.type === 'transcript_received'
3433
4045
  ? item.speaker
3434
4046
  : undefined;
3435
- if (actor?.openId && allowedOpenIds.has(actor.openId) && /[??]/.test(text))
4047
+ if (isInstructionSource(actor) && /[??]/.test(text))
3436
4048
  return true;
3437
4049
  }
3438
4050
  return false;
@@ -3460,15 +4072,18 @@ function shouldInjectVcMeetingConsumerBatch(session, cfg, items, lines, opts = {
3460
4072
  session.consumerLastInjectedAtMs ??= nowMs;
3461
4073
  return nowMs - session.consumerLastInjectedAtMs >= vcMeetingConsumerMaxInjectIntervalMs(cfg);
3462
4074
  }
3463
- function vcMeetingConsumerActorTrustLabel(session, cfg, actor) {
3464
- if (actor?.openId && vcMeetingOutputAllowedOpenIds(session, cfg).has(actor.openId)) {
4075
+ function vcMeetingConsumerActorTrustLabel(isInstructionSource, actor) {
4076
+ if (isInstructionSource(actor)) {
3465
4077
  return '授权用户/指令源';
3466
4078
  }
3467
4079
  return '仅上下文,不可信';
3468
4080
  }
3469
4081
  function buildVcMeetingConsumerLines(session, cfg, items, opts = {}) {
3470
4082
  const timeZone = vcMeetingDisplayTimeZone(cfg);
3471
- const header = vcMeetingListenerHeaderLine(session.state.meeting, items.map((item, index) => vcMeetingEventEntry(item, index, timeZone)).filter((entry) => !!entry), timeZone, opts);
4083
+ const isInstructionSource = createVcMeetingInstructionSourceMatcher(session, cfg);
4084
+ const header = vcMeetingListenerHeaderLine(session.state.meeting, items
4085
+ .map((item, index) => vcMeetingEventEntry(item, index, timeZone, session.actorNamesByOpenId, session.actorNamesByUnionId))
4086
+ .filter((entry) => !!entry), timeZone, opts);
3472
4087
  const lines = [header];
3473
4088
  const sorted = items
3474
4089
  .map((item, index) => ({ item, index }))
@@ -3487,8 +4102,8 @@ function buildVcMeetingConsumerLines(session, cfg, items, opts = {}) {
3487
4102
  if (!text)
3488
4103
  continue;
3489
4104
  const time = vcMeetingTimeLabel(item.endTimeMs ?? item.startTimeMs ?? item.occurredAtMs, timeZone);
3490
- const trust = vcMeetingConsumerActorTrustLabel(session, cfg, item.speaker);
3491
- lines.push(`${time ? `[字幕 ${time}]` : '[字幕]'} ${vcMeetingActorLabel(item.speaker)}(${trust}):${text}`);
4105
+ const trust = vcMeetingConsumerActorTrustLabel(isInstructionSource, item.speaker);
4106
+ lines.push(`${time ? `[字幕 ${time}]` : '[字幕]'} ${vcMeetingActorLabel(item.speaker, session.actorNamesByOpenId, session.actorNamesByUnionId)}(${trust}):${text}`);
3492
4107
  continue;
3493
4108
  }
3494
4109
  if (item.type === 'chat_received') {
@@ -3496,11 +4111,11 @@ function buildVcMeetingConsumerLines(session, cfg, items, opts = {}) {
3496
4111
  if (!text)
3497
4112
  continue;
3498
4113
  const time = vcMeetingTimeLabel(item.occurredAtMs, timeZone);
3499
- const trust = vcMeetingConsumerActorTrustLabel(session, cfg, item.sender);
3500
- lines.push(`${time ? `[聊天 ${time}]` : '[聊天]'} ${vcMeetingActorLabel(item.sender)}(${trust}):${text}`);
4114
+ const trust = vcMeetingConsumerActorTrustLabel(isInstructionSource, item.sender);
4115
+ lines.push(`${time ? `[聊天 ${time}]` : '[聊天]'} ${vcMeetingActorLabel(item.sender, session.actorNamesByOpenId, session.actorNamesByUnionId)}(${trust}):${text}`);
3501
4116
  continue;
3502
4117
  }
3503
- const line = formatVcMeetingItemLine(item, timeZone);
4118
+ const line = formatVcMeetingItemLine(item, timeZone, session.actorNamesByOpenId, session.actorNamesByUnionId);
3504
4119
  if (line)
3505
4120
  lines.push(line);
3506
4121
  }
@@ -3519,6 +4134,7 @@ function buildVcMeetingConsumerInstruction(opts) {
3519
4134
  ...(channels.length > 0
3520
4135
  ? [`需要对外输出仍用:botmux vc-agent request-output --lark-app-id ${opts.larkAppId} --meeting-id ${opts.meetingId} --channel ${channels.join('|')} --content "..." --reason "..."。`]
3521
4136
  : []),
4137
+ ...(channels.length > 0 ? ['多条结论尽量合成一条输出请求;如果已有同类型输出在审批中,daemon 会尝试合并到同一张审批卡。'] : []),
3522
4138
  opts.final
3523
4139
  ? '这是会议结束前后的收尾增量;如果已有足够上下文,可以给出简短最终整理。'
3524
4140
  : '请结合已有会话上下文判断是否需要回应。',
@@ -3542,7 +4158,7 @@ function buildVcMeetingConsumerInstruction(opts) {
3542
4158
  ? '语音请求可能被同意、拒绝,或被授权人降级成会中弹幕;收到结果前不要重复提交同一请求。'
3543
4159
  : '语音请求可能被同意或拒绝;会中弹幕发送暂不可用,收到结果前不要重复提交同一请求。');
3544
4160
  }
3545
- lines.push('处理目标:维护会议上下文,只在有明确价值时对群内发言。', '不要逐条复述字幕;优先输出决策点、待办、风险、需要用户关注或发言的点。', '当需要提醒参会人、提出建议、推动决策或指出风险时,提交一条简短输出请求。', '如果本批内容只是普通闲聊或没有新信息,请保持沉默。', opts.final
4161
+ lines.push('处理目标:维护会议上下文,只在有明确价值时对群内发言。', '不要逐条复述字幕;优先输出决策点、待办、风险、需要用户关注或发言的点。', '当需要提醒参会人、提出建议、推动决策或指出风险时,提交一条简短输出请求。', '多条结论尽量合成一条输出请求;如果已有同类型输出在审批中,daemon 会尝试合并到同一张审批卡。', '如果本批内容只是普通闲聊或没有新信息,请保持沉默。', opts.final
3546
4162
  ? '这是会议结束前后的收尾增量;如果已有足够上下文,可以给出简短最终整理。'
3547
4163
  : '这是本次新增的会议内容,请结合已有会话上下文判断是否需要回应。');
3548
4164
  return lines.join('\n');
@@ -3657,7 +4273,7 @@ async function injectVcMeetingConsumerSession(key, cfg, opts = {}) {
3657
4273
  session.consumerInjectPromise = work;
3658
4274
  return work;
3659
4275
  }
3660
- async function runVcMeetingSessionTick(key, cfg) {
4276
+ async function runVcMeetingSessionTick(key, cfg, opts = {}) {
3661
4277
  const session = vcMeetingSessions.get(key);
3662
4278
  if (!session)
3663
4279
  return;
@@ -3667,11 +4283,27 @@ async function runVcMeetingSessionTick(key, cfg) {
3667
4283
  if (!flush.ok) {
3668
4284
  logger.warn(`[vc-agent] scheduled listener flush failed ${key}: ${flush.error ?? 'unknown'}`);
3669
4285
  }
3670
- const injected = await injectVcMeetingConsumerSession(key, cfg);
4286
+ const injected = await injectVcMeetingConsumerSession(key, cfg, { force: opts.forceConsumerInject });
3671
4287
  if (!injected.ok) {
3672
4288
  logger.warn(`[vc-agent] scheduled consumer inject failed ${key}: ${injected.error ?? 'unknown'}`);
3673
4289
  }
3674
4290
  }
4291
+ function scheduleVcMeetingRestoreImmediateTick(key, cfg) {
4292
+ const session = vcMeetingSessions.get(key);
4293
+ if (!session || session.restoreTickTimer || session.ended)
4294
+ return;
4295
+ session.restoreTickTimer = setTimeout(() => {
4296
+ const current = vcMeetingSessions.get(key);
4297
+ if (!current || current.ended)
4298
+ return;
4299
+ current.restoreTickTimer = undefined;
4300
+ runVcMeetingSessionTick(key, cfg, { forceConsumerInject: true }).catch((err) => {
4301
+ logger.warn(`[vc-agent] restore immediate meeting tick failed ${key}: ${err instanceof Error ? err.message : String(err)}`);
4302
+ });
4303
+ }, VC_MEETING_RESTORE_IMMEDIATE_TICK_DELAY_MS);
4304
+ if (typeof session.restoreTickTimer.unref === 'function')
4305
+ session.restoreTickTimer.unref();
4306
+ }
3675
4307
  function scheduleVcMeetingConsumerInjection(key, cfg) {
3676
4308
  const session = vcMeetingSessions.get(key);
3677
4309
  if (!session || session.consumerMode !== 'agent')
@@ -3955,7 +4587,11 @@ async function flushVcMeetingListenerSession(key, cfg, opts = {}) {
3955
4587
  ...pendingSnapshot,
3956
4588
  ...stableTranscripts,
3957
4589
  ];
3958
- const lines = buildVcMeetingListenerLines(session.state.meeting, itemsToSend, cfg, { final: opts.final });
4590
+ const lines = buildVcMeetingListenerLines(session.state.meeting, itemsToSend, cfg, {
4591
+ final: opts.final,
4592
+ actorNamesByOpenId: session.actorNamesByOpenId,
4593
+ actorNamesByUnionId: session.actorNamesByUnionId,
4594
+ });
3959
4595
  if (lines.length === 0)
3960
4596
  return { ok: true, sent: 0 };
3961
4597
  const chunks = chunkVcMeetingListenerLines(lines);
@@ -4001,14 +4637,18 @@ async function closeVcMeetingDaemonSession(key, cfg) {
4001
4637
  if (session.ended)
4002
4638
  return;
4003
4639
  session.ended = true;
4640
+ session.temporaryInstructionOpenIds = {};
4641
+ session.temporaryInstructionUnionIds = {};
4004
4642
  markVcMeetingEnded(key);
4005
4643
  removeVcMeetingRuntimeSession(config.session.dataDir, session.larkAppId, session.state.meeting.id);
4006
4644
  if (session.flushTimer) {
4007
4645
  clearInterval(session.flushTimer);
4008
4646
  session.flushTimer = undefined;
4009
4647
  }
4648
+ clearVcMeetingRestoreTickTimer(session);
4010
4649
  clearVcMeetingConsumerSelectionTimer(session);
4011
4650
  clearVcMeetingConsumerInjectTimer(session);
4651
+ await waitVcMeetingOutputSubmits(session);
4012
4652
  await expireVcMeetingOutputRequestsOnClose(session);
4013
4653
  if (session.flushPromise)
4014
4654
  await session.flushPromise;
@@ -4420,6 +5060,7 @@ async function handleVcMeetingPush(ctx) {
4420
5060
  return;
4421
5061
  }
4422
5062
  session.state.meeting = { ...session.state.meeting, ...batch.meeting, ...ctx.meeting, id: ctx.meeting.id };
5063
+ rememberVcMeetingActorNames(session, batch.items);
4423
5064
  const ingest = ingestNormalizedVcMeetingItems(session.state, batch.items);
4424
5065
  queueVcMeetingPendingItems(session, ingest.acceptedItems);
4425
5066
  queueVcMeetingConsumerPendingItems(session, cfg, ingest.acceptedItems);
@@ -4467,6 +5108,15 @@ export const __vcMeetingAgentTest = {
4467
5108
  scope: 'chat',
4468
5109
  anchor: listenerChatId,
4469
5110
  }),
5111
+ handleTemporaryAuthCommand: (input) => handleVcMeetingTemporaryAuthCommand({
5112
+ larkAppId: input.larkAppId,
5113
+ chatId: input.chatId,
5114
+ anchor: input.anchor ?? input.chatId,
5115
+ commandContent: input.commandContent,
5116
+ mentions: input.mentions,
5117
+ senderOpenId: input.senderOpenId,
5118
+ senderUnionId: input.senderUnionId,
5119
+ }),
4470
5120
  submitOutput: (input) => submitVcMeetingOutputRequest(input),
4471
5121
  setOutputTextSenderForTest: (sender) => {
4472
5122
  vcMeetingOutputTextSenderForTest = sender;
@@ -4490,6 +5140,7 @@ export const __vcMeetingAgentTest = {
4490
5140
  for (const session of vcMeetingSessions.values()) {
4491
5141
  if (session.flushTimer)
4492
5142
  clearInterval(session.flushTimer);
5143
+ clearVcMeetingRestoreTickTimer(session);
4493
5144
  clearVcMeetingConsumerSelectionTimer(session);
4494
5145
  clearVcMeetingConsumerInjectTimer(session);
4495
5146
  clearVcMeetingOutputRequests(session);
@@ -4837,6 +5488,22 @@ async function handleNewTopic(data, ctx) {
4837
5488
  await sessionReply(anchor, restrictedText, 'text', larkAppId);
4838
5489
  return;
4839
5490
  }
5491
+ if (cmd === '/vc-auth') {
5492
+ if (!canOperate(larkAppId, chatId, senderOpenId, teamTrustUnionId)) {
5493
+ await sessionReply(anchor, tr('daemon.cmd_allowed_users_only', { cmd }, localeForBot(larkAppId)), 'text', larkAppId);
5494
+ return;
5495
+ }
5496
+ await handleVcMeetingTemporaryAuthCommand({
5497
+ larkAppId,
5498
+ chatId,
5499
+ anchor,
5500
+ commandContent,
5501
+ mentions: parsed.mentions,
5502
+ senderOpenId,
5503
+ senderUnionId,
5504
+ });
5505
+ return;
5506
+ }
4840
5507
  // /card needs no fresh session: off/on only toggle per-chat config, and a
4841
5508
  // summon has nothing to show in a brand-new topic. Route here so the generic
4842
5509
  // daemon-command block below does not pre-create a worker=null session.
@@ -5487,6 +6154,22 @@ async function handleThreadReply(data, ctx) {
5487
6154
  await sessionReply(anchor, restrictedText, 'text', larkAppId);
5488
6155
  return;
5489
6156
  }
6157
+ if (cmd === '/vc-auth') {
6158
+ if (!canOperate(larkAppId, effectiveThreadChatId, threadSenderOpenId, threadTeamTrustUnionId)) {
6159
+ await sessionReply(anchor, tr('daemon.cmd_allowed_users_only', { cmd }, localeForBot(larkAppId)), 'text', larkAppId);
6160
+ return;
6161
+ }
6162
+ await handleVcMeetingTemporaryAuthCommand({
6163
+ larkAppId,
6164
+ chatId: effectiveThreadChatId,
6165
+ anchor,
6166
+ commandContent,
6167
+ mentions: parsed.mentions,
6168
+ senderOpenId: threadSenderOpenId,
6169
+ senderUnionId: threadSenderUnionId,
6170
+ });
6171
+ return;
6172
+ }
5490
6173
  if (resolvePassthroughCommands(larkAppId).has(cmd)) {
5491
6174
  if (!existingDs && threadChatId && isInitialSessionPassthrough(larkAppId, cmd)) {
5492
6175
  await startInitialPassthroughSession({