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.
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +2 -1
- package/dist/core/command-handler.js.map +1 -1
- package/dist/core/passthrough-commands.d.ts.map +1 -1
- package/dist/core/passthrough-commands.js +1 -1
- package/dist/core/passthrough-commands.js.map +1 -1
- package/dist/daemon.d.ts +31 -8
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +750 -67
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard/settings-write-applier.d.ts +5 -0
- package/dist/dashboard/settings-write-applier.d.ts.map +1 -1
- package/dist/dashboard/settings-write-applier.js +2 -2
- package/dist/dashboard/settings-write-applier.js.map +1 -1
- package/dist/dashboard/web/settings-page.d.ts.map +1 -1
- package/dist/dashboard/web/settings-page.js +13 -4
- package/dist/dashboard/web/settings-page.js.map +1 -1
- package/dist/dashboard-web/app.js +1 -1
- package/dist/dashboard-web/chunks/settings-page-R23I3IZG.js +2 -0
- package/dist/dashboard-web/style.css +11 -0
- package/dist/dashboard.js +239 -62
- package/dist/dashboard.js.map +1 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +1 -0
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/zh.d.ts.map +1 -1
- package/dist/i18n/zh.js +1 -0
- package/dist/i18n/zh.js.map +1 -1
- package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
- package/dist/im/lark/event-dispatcher.js +11 -0
- package/dist/im/lark/event-dispatcher.js.map +1 -1
- package/dist/im/lark/message-parser.d.ts +6 -0
- package/dist/im/lark/message-parser.d.ts.map +1 -1
- package/dist/im/lark/message-parser.js +14 -0
- package/dist/im/lark/message-parser.js.map +1 -1
- package/dist/services/vc-meeting-runtime-store.d.ts +4 -0
- package/dist/services/vc-meeting-runtime-store.d.ts.map +1 -1
- package/dist/services/vc-meeting-runtime-store.js +26 -0
- package/dist/services/vc-meeting-runtime-store.js.map +1 -1
- package/dist/setup/open-platform-automation.d.ts +20 -0
- package/dist/setup/open-platform-automation.d.ts.map +1 -1
- package/dist/setup/open-platform-automation.js +78 -1
- package/dist/setup/open-platform-automation.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/vc-agent/cards.d.ts +2 -0
- package/dist/vc-agent/cards.d.ts.map +1 -1
- package/dist/vc-agent/cards.js +18 -2
- package/dist/vc-agent/cards.js.map +1 -1
- package/dist/vc-agent/normalizer.d.ts.map +1 -1
- package/dist/vc-agent/normalizer.js +59 -0
- package/dist/vc-agent/normalizer.js.map +1 -1
- package/dist/vc-agent/polling-source.d.ts +28 -0
- package/dist/vc-agent/polling-source.d.ts.map +1 -1
- package/dist/vc-agent/polling-source.js +57 -5
- package/dist/vc-agent/polling-source.js.map +1 -1
- package/dist/vc-agent/types.d.ts +1 -0
- package/dist/vc-agent/types.d.ts.map +1 -1
- package/dist/worker.js +43 -6
- package/dist/worker.js.map +1 -1
- package/package.json +1 -1
- 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 =
|
|
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
|
-
|
|
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
|
|
2659
|
-
|
|
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
|
|
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' &&
|
|
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
|
-
|
|
3236
|
-
|
|
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
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
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(
|
|
3464
|
-
if (
|
|
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
|
|
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(
|
|
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(
|
|
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, {
|
|
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({
|