feat(feishu): add configurable group response mode

This commit is contained in:
xmanrui 2026-08-22 01:26:29 +08:00
parent 1a86fa5013
commit 9950261a49
19 changed files with 615 additions and 150 deletions

View file

@ -258,6 +258,12 @@ var EN = Object.freeze({
"WhatsApp\u8D26\u53F7": "WhatsApp account",
"\u5FAE\u4FE1\u8BBE\u7F6E": "WeChat settings",
"\u98DE\u4E66\u673A\u5668\u4EBA\u8BBE\u7F6E": "Feishu bot settings",
"\u7FA4\u804A\u54CD\u5E94\u65B9\u5F0F": "Group response mode",
"\u4EC5\u5728 @\u673A\u5668\u4EBA\u65F6\u54CD\u5E94\uFF08\u63A8\u8350\uFF09": "Only respond when @mentioned (recommended)",
"\u54CD\u5E94\u6240\u6709\u7FA4\u6D88\u606F\uFF08\u9700\u98DE\u4E66\u654F\u611F\u6743\u9650\uFF09": "Respond to all group messages (requires a sensitive Feishu scope)",
"\u79C1\u804A\u59CB\u7EC8\u54CD\u5E94\uFF1B\u7FA4\u804A\u4EC5\u5904\u7406\u660E\u786E @\u5F53\u524D\u673A\u5668\u4EBA\u7684\u6D88\u606F\u3002": "Direct messages always work; group chats require an explicit @mention of this bot.",
"\u9700\u5728\u98DE\u4E66\u4E3A\u8BE5\u673A\u5668\u4EBA\u5F00\u901A\u201C\u83B7\u53D6\u7FA4\u7EC4\u4E2D\u6240\u6709\u6D88\u606F\u201D\u6743\u9650\uFF08im:message.group_msg\uFF09\uFF1B\u5F00\u901A\u540E\uFF0C\u673A\u5668\u4EBA\u4F1A\u5904\u7406\u7FA4\u804A\u4E2D\u7684\u6240\u6709\u53EF\u89C1\u6D88\u606F\u3002": "Grant this bot the \u201CRead all messages in associated group chat\u201D Feishu scope (im:message.group_msg); once granted, it will process every visible group message.",
"\u7FA4\u804A\u54CD\u5E94\u65B9\u5F0F\u4FEE\u6539\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5\u3002": "Could not update the group response mode. Try again.",
"\u9489\u9489\u8BBE\u7F6E": "DingTalk settings",
"\u4F01\u4E1A\u5FAE\u4FE1\u8BBE\u7F6E": "WeCom settings",
"\u626B\u7801\u63A5\u5165\u673A\u5668\u4EBA": "Scan QR code",
@ -3286,6 +3292,7 @@ var FEISHU_ENDPOINTS = Object.freeze({
deleteBot: "bot.delete",
setWorkspace: "bot.workspace.set",
setAgentPreset: "bot.preset.set",
setGroupResponseMode: "bot.group-response-mode.set",
// Kept for rolling upgrades. The multi-bot UI never calls these endpoints.
testConnection: "connection.test",
disconnect: "connection.disconnect"
@ -3325,6 +3332,9 @@ function optionalTimestamp(value) {
}
return void 0;
}
function normalizeGroupResponseMode(value) {
return value === "all" ? "all" : "mention";
}
function clamp2(value, min, max, fallback) {
return typeof value === "number" && Number.isFinite(value) ? Math.min(max, Math.max(min, value)) : fallback;
}
@ -3420,6 +3430,7 @@ function normalizeBotConnection(value, fallbackBotId) {
configured: value.configured !== false,
workspace: optionalString2(value.workspace)?.slice(0, 4096) ?? "",
agentPreset: normalizeAgentPresetId(value.agentPreset),
groupResponseMode: normalizeGroupResponseMode(value.groupResponseMode),
bot: normalizeBot2(value.bot),
health: normalizeHealth(value.health, connected),
error: normalizeError2(value.error)
@ -4000,6 +4011,17 @@ var CSS3 = String.raw`
.bxf-metric dt { color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 11px; line-height: 17px; }
.bxf-metric dd { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsw-alias-label-primary, #1f2329); font-size: 12px; line-height: 18px; font-weight: 550; margin: 3px 0 0; }
.bxf-responseMode { min-width: 0; display: grid; grid-template-columns: minmax(0, 1fr) max-content; align-items: center; column-gap: 10px; row-gap: 5px; margin-top: 6px; padding: 8px 10px; border: 1px solid var(--dsw-alias-border-l1, #eef0f3); border-radius: 9px; background: var(--dsw-alias-bg-module-platform, #f7f8fa); }
.bxf-responseModeHeader { display: contents; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: normal; }
.bxf-responseModeHeader > span:first-child { grid-column: 1; grid-row: 1; white-space: nowrap; }
.bxf-responseModeStatus { grid-column: 2; grid-row: 1; color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 12px; white-space: nowrap; }
.bxf-responseModeSelect { min-width: 0; width: 100%; grid-column: 1 / -1; grid-row: 2; height: 32px; padding: 0 30px 0 9px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 7px; color: var(--dsw-alias-label-primary, #1f2329); background-color: var(--dsw-alias-bg-layer-1, #fff); font: inherit; font-size: 12px; cursor: pointer; transition: border-color .16s ease, box-shadow .16s ease, background-color .16s ease; }
.bxf-responseModeSelect:hover:not(:disabled) { border-color: color-mix(in srgb, var(--bxf-accent) 45%, var(--dsw-alias-border-l2, #dfe1e5)); }
.bxf-responseModeSelect:focus-visible { outline: none; border-color: var(--bxf-accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--bxf-accent) 16%, transparent); }
.bxf-responseModeSelect:disabled { cursor: not-allowed; opacity: .55; }
.bxf-responseModeHelp { grid-column: 1 / -1; grid-row: 3; color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 11px; line-height: 1.45; }
.bxf-responseModeError { grid-column: 1 / -1; grid-row: 4; color: var(--bxf-error); font-size: 12px; line-height: 1.4; margin: 0; }
.bxf-connectedFooter { display: flex; align-items: center; justify-content: space-between; gap: 15px; margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--dsw-alias-border-l1, #eef0f3); }
.bxf-healthSummary { min-width: 0; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: 18px; }
.bxf-healthSummary[data-error="true"] { color: var(--bxf-error); }
@ -4482,6 +4504,57 @@ function RemoveConfirmation2({ bot, busy, onConfirm, onCancel }) {
)
);
}
function GroupResponseModeEditor({ value, disabled = false, onSave }) {
const current = normalizeGroupResponseMode(value);
const [saving, setSaving] = React11.useState(false);
const [error, setError] = React11.useState(null);
const change = async (event) => {
const next = normalizeGroupResponseMode(event.target.value);
if (next === current || saving || disabled) return;
setSaving(true);
setError(null);
try {
await onSave?.(next);
} catch (cause) {
setError(cause?.message ?? "\u7FA4\u804A\u54CD\u5E94\u65B9\u5F0F\u4FEE\u6539\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5\u3002");
} finally {
setSaving(false);
}
};
return h2(
"div",
{ className: "bxf-responseMode dim-responseMode" },
h2(
"div",
{ className: "bxf-responseModeHeader dim-responseModeHeader" },
h2("span", null, "\u7FA4\u804A\u54CD\u5E94\u65B9\u5F0F"),
saving ? h2("span", { className: "bxf-responseModeStatus dim-responseModeStatus" }, "\u4FDD\u5B58\u4E2D\u2026") : null
),
h2(
"select",
{
className: "bxf-responseModeSelect dim-responseModeSelect",
value: current,
disabled: disabled || saving,
"aria-label": "\u7FA4\u804A\u54CD\u5E94\u65B9\u5F0F",
onChange: (event) => {
void change(event);
}
},
h2("option", { value: "mention" }, "\u4EC5\u5728 @\u673A\u5668\u4EBA\u65F6\u54CD\u5E94\uFF08\u63A8\u8350\uFF09"),
h2("option", { value: "all" }, "\u54CD\u5E94\u6240\u6709\u7FA4\u6D88\u606F\uFF08\u9700\u98DE\u4E66\u654F\u611F\u6743\u9650\uFF09")
),
h2(
"small",
{ className: "bxf-responseModeHelp dim-responseModeHelp" },
current === "mention" ? "\u79C1\u804A\u59CB\u7EC8\u54CD\u5E94\uFF1B\u7FA4\u804A\u4EC5\u5904\u7406\u660E\u786E @\u5F53\u524D\u673A\u5668\u4EBA\u7684\u6D88\u606F\u3002" : "\u9700\u5728\u98DE\u4E66\u4E3A\u8BE5\u673A\u5668\u4EBA\u5F00\u901A\u201C\u83B7\u53D6\u7FA4\u7EC4\u4E2D\u6240\u6709\u6D88\u606F\u201D\u6743\u9650\uFF08im:message.group_msg\uFF09\uFF1B\u5F00\u901A\u540E\uFF0C\u673A\u5668\u4EBA\u4F1A\u5904\u7406\u7FA4\u804A\u4E2D\u7684\u6240\u6709\u53EF\u89C1\u6D88\u606F\u3002"
),
error ? h2("p", {
className: "bxf-responseModeError dim-responseModeError",
role: "alert"
}, error) : null
);
}
function BotCard({
connection,
busy,
@ -4493,6 +4566,7 @@ function BotCard({
onRepairCallback,
onWorkspaceSave,
onAgentPresetSave,
onGroupResponseModeSave,
onRequestRemove,
onConfirmRemove,
onCancelRemove,
@ -4567,6 +4641,11 @@ function BotCard({
disabled: Boolean(busy),
onSave: onAgentPresetSave
}),
h2(GroupResponseModeEditor, {
value: connection.groupResponseMode,
disabled: Boolean(busy),
onSave: onGroupResponseModeSave
}),
h2(
"div",
{ className: "bxf-connectedFooter dim-cardFooter" },
@ -4641,6 +4720,7 @@ function BotList(props) {
onRepairCallback: () => props.onRepairCallback(bot),
onWorkspaceSave: (workspace) => props.onWorkspaceSave(bot, workspace),
onAgentPresetSave: (agentPreset) => props.onAgentPresetSave(bot, agentPreset),
onGroupResponseModeSave: (groupResponseMode) => props.onGroupResponseModeSave(bot, groupResponseMode),
onRequestRemove: () => props.onRequestRemove(bot),
onConfirmRemove: () => props.onConfirmRemove(bot),
onCancelRemove: props.onCancelRemove,
@ -5151,6 +5231,25 @@ function FeishuSettingsTab({ rpcCall }) {
if (mountedRef.current) setBotBusy(botId, null);
}
}, [invoke, loadStatus, mergeSnapshot, setBotBusy, setBotError, workspaceFence]);
const saveGroupResponseMode = React11.useCallback(async (connection, groupResponseMode) => {
const { botId } = connection;
const snapshotVersion = workspaceFence.beginMutation();
setBotBusy(botId, "group-response-mode");
setBotError(botId, null);
try {
const snapshot = normalizeBotsSnapshot(await invoke(
FEISHU_ENDPOINTS.setGroupResponseMode,
{ botId, groupResponseMode }
));
if (mountedRef.current && workspaceFence.canCommitMutation(snapshotVersion)) {
mergeSnapshot(snapshot);
}
} finally {
const shouldRefresh = workspaceFence.endMutation();
if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
if (mountedRef.current) setBotBusy(botId, null);
}
}, [invoke, loadStatus, mergeSnapshot, setBotBusy, setBotError, workspaceFence]);
const requestRemove = React11.useCallback((connection) => {
setRemoveTargetId(connection.botId);
}, []);
@ -5303,6 +5402,7 @@ function FeishuSettingsTab({ rpcCall }) {
onRepairCallback: repairCallback,
onWorkspaceSave: saveWorkspace,
onAgentPresetSave: saveAgentPreset,
onGroupResponseModeSave: saveGroupResponseMode,
onRequestRemove: requestRemove,
onConfirmRemove: (bot) => void confirmRemove(bot),
onCancelRemove: cancelRemove,

File diff suppressed because one or more lines are too long