mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 03:03:24 +08:00
feat(feishu): add configurable group response mode
This commit is contained in:
parent
1a86fa5013
commit
9950261a49
19 changed files with 615 additions and 150 deletions
|
|
@ -40,6 +40,10 @@ import {
|
|||
workspaceListCard,
|
||||
} from './feishu-cards.mjs';
|
||||
import { MAX_WATCHES_PER_KEY } from './state-store.mjs';
|
||||
import {
|
||||
FEISHU_GROUP_RESPONSE_MODES,
|
||||
normalizeFeishuGroupResponseMode,
|
||||
} from './group-response-mode.mjs';
|
||||
|
||||
const INTERACTION_RESOLVED_TEXT = '这个问题已在其他客户端处理,无需再次回答。';
|
||||
const RESOLVED_REPLY_TTL_MS = 30 * 60_000;
|
||||
|
|
@ -249,6 +253,8 @@ export class FeishuHarnessBridge {
|
|||
#signal;
|
||||
#botId;
|
||||
#appId;
|
||||
#botOpenId;
|
||||
#groupResponseMode;
|
||||
#repair;
|
||||
#repairOwnerOpenIds;
|
||||
#repairAttempt = null;
|
||||
|
|
@ -275,6 +281,8 @@ export class FeishuHarnessBridge {
|
|||
allowedSenderOpenIds = new Set(),
|
||||
botId,
|
||||
appId,
|
||||
botOpenId,
|
||||
groupResponseMode = FEISHU_GROUP_RESPONSE_MODES.ALL,
|
||||
repair,
|
||||
repairOwnerOpenIds,
|
||||
repairPollIntervalMs = REPAIR_POLL_INTERVAL_MS,
|
||||
|
|
@ -308,6 +316,8 @@ export class FeishuHarnessBridge {
|
|||
this.#allowedSenderOpenIds = allowedSenderOpenIds;
|
||||
this.#botId = nonEmptyString(botId);
|
||||
this.#appId = nonEmptyString(appId);
|
||||
this.#botOpenId = nonEmptyString(botOpenId);
|
||||
this.#groupResponseMode = normalizeFeishuGroupResponseMode(groupResponseMode);
|
||||
this.#repair = repair ?? null;
|
||||
const repairOwners = repairOwnerOpenIds ?? allowedSenderOpenIds;
|
||||
this.#repairOwnerOpenIds = new Set(
|
||||
|
|
@ -327,6 +337,18 @@ export class FeishuHarnessBridge {
|
|||
}
|
||||
}
|
||||
|
||||
setGroupResponseMode(value) {
|
||||
this.#groupResponseMode = normalizeFeishuGroupResponseMode(value);
|
||||
}
|
||||
|
||||
#isAddressed(event) {
|
||||
if (event?.message?.chat_type === 'p2p') return true;
|
||||
const mentions = Array.isArray(event?.message?.mentions) ? event.message.mentions : [];
|
||||
if (!this.#botOpenId) return mentions.length > 0;
|
||||
return mentions.some((mention) => mention?.id?.open_id === this.#botOpenId
|
||||
|| mention?.open_id === this.#botOpenId);
|
||||
}
|
||||
|
||||
accept(event) {
|
||||
if (this.#signal?.aborted) return Promise.resolve();
|
||||
const messageId = nonEmptyString(event?.message?.message_id);
|
||||
|
|
@ -337,6 +359,12 @@ export class FeishuHarnessBridge {
|
|||
this.#logger.warn?.('[dsh-feishu] ignored a message from a sender outside the allowlist');
|
||||
return Promise.resolve();
|
||||
}
|
||||
const addressed = this.#isAddressed(event);
|
||||
if (event?.message?.chat_type !== 'p2p'
|
||||
&& this.#groupResponseMode === FEISHU_GROUP_RESPONSE_MODES.MENTION
|
||||
&& !addressed) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (this.#state.hasSeen(messageId) || this.#acceptedMessageIds.has(messageId)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
|
@ -362,8 +390,6 @@ export class FeishuHarnessBridge {
|
|||
const commandRunner = isControlCommand(commandText)
|
||||
? runControlCommand
|
||||
: (isModelCommand(commandText) ? runModelCommand : null);
|
||||
const addressed = event?.message?.chat_type === 'p2p'
|
||||
|| (Array.isArray(event?.message?.mentions) && event.message.mentions.length > 0);
|
||||
if (commandRunner && addressed) {
|
||||
const processing = this.#processFastCommand(
|
||||
event,
|
||||
|
|
@ -407,8 +433,7 @@ export class FeishuHarnessBridge {
|
|||
actor: senderOpenId(event),
|
||||
messageId,
|
||||
text: extractText(event) ?? '',
|
||||
addressed: event?.message?.chat_type === 'p2p'
|
||||
|| (Array.isArray(event?.message?.mentions) && event.message.mentions.length > 0),
|
||||
addressed,
|
||||
hasPendingQuestion: Boolean(pending),
|
||||
questionCompletion: pending?.submitting || pending?.claimedReplyMessageId
|
||||
? pending.queue
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { randomUUID } from 'node:crypto';
|
|||
import { FeishuHarnessBridge } from './bridge.mjs';
|
||||
import { cardActionProbeCard } from './feishu-cards.mjs';
|
||||
import { VerifiedFeishuChannel } from './feishu-channel.mjs';
|
||||
import { normalizeFeishuGroupResponseMode } from './group-response-mode.mjs';
|
||||
import {
|
||||
connectionTestTargetUnavailable,
|
||||
sendRememberedConnectionTest,
|
||||
|
|
@ -87,6 +88,8 @@ export class FeishuRuntime {
|
|||
#appId;
|
||||
#appSecret;
|
||||
#domain;
|
||||
#botOpenId;
|
||||
#groupResponseMode;
|
||||
#ownerOpenIds;
|
||||
#harness;
|
||||
#state;
|
||||
|
|
@ -109,6 +112,8 @@ export class FeishuRuntime {
|
|||
appId,
|
||||
appSecret,
|
||||
domain = 'feishu',
|
||||
botOpenId,
|
||||
groupResponseMode,
|
||||
ownerOpenId,
|
||||
ownerOpenIds,
|
||||
harness,
|
||||
|
|
@ -138,6 +143,8 @@ export class FeishuRuntime {
|
|||
this.#appId = appId;
|
||||
this.#appSecret = appSecret;
|
||||
this.#domain = domain;
|
||||
this.#botOpenId = nonEmptyString(botOpenId);
|
||||
this.#groupResponseMode = normalizeFeishuGroupResponseMode(groupResponseMode);
|
||||
this.#ownerOpenIds = normalizedOwners;
|
||||
this.#harness = harness;
|
||||
this.#state = state;
|
||||
|
|
@ -153,6 +160,11 @@ export class FeishuRuntime {
|
|||
return structuredClone(this.#status);
|
||||
}
|
||||
|
||||
setGroupResponseMode(value) {
|
||||
this.#groupResponseMode = normalizeFeishuGroupResponseMode(value);
|
||||
this.#bridge?.setGroupResponseMode(this.#groupResponseMode);
|
||||
}
|
||||
|
||||
async start() {
|
||||
if (this.#wsClient && this.#status.ready) return this.status;
|
||||
if (this.#starting) return this.#starting;
|
||||
|
|
@ -202,6 +214,8 @@ export class FeishuRuntime {
|
|||
allowedSenderOpenIds: new Set(this.#ownerOpenIds),
|
||||
botId: this.#botId,
|
||||
appId: this.#appId,
|
||||
botOpenId: this.#botOpenId,
|
||||
groupResponseMode: this.#groupResponseMode,
|
||||
repair: this.#repair,
|
||||
repairOwnerOpenIds: new Set(this.#ownerOpenIds.filter((value) => value !== '*')),
|
||||
replyTimeoutMs: this.#replyTimeoutMs,
|
||||
|
|
|
|||
15
src/channels/feishu/group-response-mode.mjs
Normal file
15
src/channels/feishu/group-response-mode.mjs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
export const FEISHU_GROUP_RESPONSE_MODES = Object.freeze({
|
||||
MENTION: 'mention',
|
||||
ALL: 'all',
|
||||
});
|
||||
|
||||
export function normalizeFeishuGroupResponseMode(value) {
|
||||
return value === FEISHU_GROUP_RESPONSE_MODES.ALL
|
||||
? FEISHU_GROUP_RESPONSE_MODES.ALL
|
||||
: FEISHU_GROUP_RESPONSE_MODES.MENTION;
|
||||
}
|
||||
|
||||
export function isFeishuGroupResponseMode(value) {
|
||||
return value === FEISHU_GROUP_RESPONSE_MODES.MENTION
|
||||
|| value === FEISHU_GROUP_RESPONSE_MODES.ALL;
|
||||
}
|
||||
|
|
@ -6,6 +6,10 @@ import {
|
|||
CallbackRepairManager,
|
||||
} from './repair-manager.mjs';
|
||||
import { REQUIRED_TENANT_SCOPES } from './plugin-controller.mjs';
|
||||
import {
|
||||
isFeishuGroupResponseMode,
|
||||
normalizeFeishuGroupResponseMode,
|
||||
} from './group-response-mode.mjs';
|
||||
|
||||
const ACTIVE_REGISTRATION_STATES = new Set([
|
||||
'starting', 'qr_ready', 'polling', 'slow_down', 'domain_switched',
|
||||
|
|
@ -76,6 +80,7 @@ function configuredBotFingerprint(config) {
|
|||
botName: config.botName,
|
||||
botOpenId: config.botOpenId,
|
||||
activated: config.activated,
|
||||
groupResponseMode: normalizeFeishuGroupResponseMode(config.groupResponseMode),
|
||||
deletionPending: config.deletionPending === true,
|
||||
connectedAt: config.connectedAt ?? null,
|
||||
createdAt: config.createdAt ?? null,
|
||||
|
|
@ -473,6 +478,20 @@ export class MultiBotDshFeishuController {
|
|||
});
|
||||
}
|
||||
|
||||
async updateGroupResponseMode(botId, groupResponseMode) {
|
||||
this.#assertOpen();
|
||||
if (!isFeishuGroupResponseMode(groupResponseMode)) {
|
||||
throw new TypeError('Invalid Feishu group response mode');
|
||||
}
|
||||
return this.#serializeConfig(() => this.#withBotTransition(botId, async () => {
|
||||
const config = this.#requireBot(botId);
|
||||
const saved = await this.#configStore.saveBot({ ...config, groupResponseMode });
|
||||
this.#runtimes.get(botId)?.setGroupResponseMode?.(saved.groupResponseMode);
|
||||
this.#touch();
|
||||
return this.status(botId);
|
||||
}));
|
||||
}
|
||||
|
||||
async deleteBot(botId) {
|
||||
this.#assertOpen();
|
||||
return this.#serializeConfig(() => this.#withBotTransition(botId, async () => {
|
||||
|
|
@ -534,6 +553,7 @@ export class MultiBotDshFeishuController {
|
|||
phase: botPhase({ connected, error, connection }),
|
||||
connected,
|
||||
configured: true,
|
||||
groupResponseMode: normalizeFeishuGroupResponseMode(config.groupResponseMode),
|
||||
bot: publicBot(config),
|
||||
connection,
|
||||
error,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { createHash } from 'node:crypto';
|
||||
import { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
|
||||
import { dirname } from 'node:path';
|
||||
import { normalizeFeishuGroupResponseMode } from './group-response-mode.mjs';
|
||||
|
||||
export const LEGACY_FEISHU_SECRET_REF = 'DSH_FEISHU_APP_SECRET';
|
||||
|
||||
|
|
@ -43,6 +44,7 @@ function normalizeBot(value, { legacy = false } = {}) {
|
|||
botName: cleanString(value.botName),
|
||||
botOpenId: cleanString(value.botOpenId),
|
||||
activated: value.activated ?? null,
|
||||
groupResponseMode: normalizeFeishuGroupResponseMode(value.groupResponseMode),
|
||||
deletionPending: value.deletionPending === true,
|
||||
connectedAt: cleanString(value.connectedAt),
|
||||
createdAt: cleanString(value.createdAt) ?? cleanString(value.connectedAt),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue