mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 00:33:20 +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
|
|
@ -22,6 +22,7 @@ export const 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",
|
||||
|
|
@ -70,6 +71,10 @@ function optionalTimestamp(value) {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
export function normalizeGroupResponseMode(value) {
|
||||
return value === "all" ? "all" : "mention";
|
||||
}
|
||||
|
||||
function clamp(value, min, max, fallback) {
|
||||
return typeof value === "number" && Number.isFinite(value)
|
||||
? Math.min(max, Math.max(min, value))
|
||||
|
|
@ -186,6 +191,7 @@ export function normalizeBotConnection(value, fallbackBotId) {
|
|||
configured: value.configured !== false,
|
||||
workspace: optionalString(value.workspace)?.slice(0, 4_096) ?? "",
|
||||
agentPreset: normalizeAgentPresetId(value.agentPreset),
|
||||
groupResponseMode: normalizeGroupResponseMode(value.groupResponseMode),
|
||||
bot: normalizeBot(value.bot),
|
||||
health: normalizeHealth(value.health, connected),
|
||||
error: normalizeError(value.error),
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
FEISHU_RPC_CHANNEL,
|
||||
formatRemaining,
|
||||
normalizeBotsSnapshot,
|
||||
normalizeGroupResponseMode,
|
||||
normalizePollResult,
|
||||
normalizeProvisioning,
|
||||
presentError,
|
||||
|
|
@ -413,6 +414,50 @@ function RemoveConfirmation({ bot, busy, onConfirm, onCancel }) {
|
|||
);
|
||||
}
|
||||
|
||||
function GroupResponseModeEditor({ value, disabled = false, onSave }) {
|
||||
const current = normalizeGroupResponseMode(value);
|
||||
const [saving, setSaving] = React.useState(false);
|
||||
const [error, setError] = React.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 ?? "群聊响应方式修改失败,请重试。");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return h("div", { className: "bxf-responseMode dim-responseMode" },
|
||||
h("div", { className: "bxf-responseModeHeader dim-responseModeHeader" },
|
||||
h("span", null, "群聊响应方式"),
|
||||
saving ? h("span", { className: "bxf-responseModeStatus dim-responseModeStatus" }, "保存中…") : null),
|
||||
h("select", {
|
||||
className: "bxf-responseModeSelect dim-responseModeSelect",
|
||||
value: current,
|
||||
disabled: disabled || saving,
|
||||
"aria-label": "群聊响应方式",
|
||||
onChange: (event) => { void change(event); },
|
||||
},
|
||||
h("option", { value: "mention" }, "仅在 @机器人时响应(推荐)"),
|
||||
h("option", { value: "all" }, "响应所有群消息(需飞书敏感权限)"),
|
||||
),
|
||||
h("small", { className: "bxf-responseModeHelp dim-responseModeHelp" },
|
||||
current === "mention"
|
||||
? "私聊始终响应;群聊仅处理明确 @当前机器人的消息。"
|
||||
: "需在飞书为该机器人开通“获取群组中所有消息”权限(im:message.group_msg);开通后,机器人会处理群聊中的所有可见消息。"),
|
||||
error ? h("p", {
|
||||
className: "bxf-responseModeError dim-responseModeError",
|
||||
role: "alert",
|
||||
}, error) : null,
|
||||
);
|
||||
}
|
||||
|
||||
export function BotCard({
|
||||
connection,
|
||||
busy,
|
||||
|
|
@ -424,6 +469,7 @@ export function BotCard({
|
|||
onRepairCallback,
|
||||
onWorkspaceSave,
|
||||
onAgentPresetSave,
|
||||
onGroupResponseModeSave,
|
||||
onRequestRemove,
|
||||
onConfirmRemove,
|
||||
onCancelRemove,
|
||||
|
|
@ -479,6 +525,11 @@ export function BotCard({
|
|||
disabled: Boolean(busy),
|
||||
onSave: onAgentPresetSave,
|
||||
}),
|
||||
h(GroupResponseModeEditor, {
|
||||
value: connection.groupResponseMode,
|
||||
disabled: Boolean(busy),
|
||||
onSave: onGroupResponseModeSave,
|
||||
}),
|
||||
h("div", { className: "bxf-connectedFooter dim-cardFooter" },
|
||||
summary ? h("div", { className: "bxf-healthSummary dim-cardSummary", "data-error": actionError || connection.error ? "true" : undefined },
|
||||
summary) : null,
|
||||
|
|
@ -536,6 +587,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,
|
||||
|
|
@ -1101,6 +1153,26 @@ export function FeishuSettingsTab({ rpcCall }) {
|
|||
}
|
||||
}, [invoke, loadStatus, mergeSnapshot, setBotBusy, setBotError, workspaceFence]);
|
||||
|
||||
const saveGroupResponseMode = React.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 = React.useCallback((connection) => {
|
||||
setRemoveTargetId(connection.botId);
|
||||
}, []);
|
||||
|
|
@ -1254,6 +1326,7 @@ export function FeishuSettingsTab({ rpcCall }) {
|
|||
onRepairCallback: repairCallback,
|
||||
onWorkspaceSave: saveWorkspace,
|
||||
onAgentPresetSave: saveAgentPreset,
|
||||
onGroupResponseModeSave: saveGroupResponseMode,
|
||||
onRequestRemove: requestRemove,
|
||||
onConfirmRemove: (bot) => void confirmRemove(bot),
|
||||
onCancelRemove: cancelRemove,
|
||||
|
|
|
|||
|
|
@ -407,6 +407,17 @@ const CSS = 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); }
|
||||
|
|
|
|||
|
|
@ -68,6 +68,12 @@ const EN = Object.freeze({
|
|||
'WhatsApp账号': 'WhatsApp account',
|
||||
'微信设置': 'WeChat settings',
|
||||
'飞书机器人设置': 'Feishu bot settings',
|
||||
'群聊响应方式': 'Group response mode',
|
||||
'仅在 @机器人时响应(推荐)': 'Only respond when @mentioned (recommended)',
|
||||
'响应所有群消息(需飞书敏感权限)': 'Respond to all group messages (requires a sensitive Feishu scope)',
|
||||
'私聊始终响应;群聊仅处理明确 @当前机器人的消息。': 'Direct messages always work; group chats require an explicit @mention of this bot.',
|
||||
'需在飞书为该机器人开通“获取群组中所有消息”权限(im:message.group_msg);开通后,机器人会处理群聊中的所有可见消息。': 'Grant this bot the “Read all messages in associated group chat” Feishu scope (im:message.group_msg); once granted, it will process every visible group message.',
|
||||
'群聊响应方式修改失败,请重试。': 'Could not update the group response mode. Try again.',
|
||||
'钉钉设置': 'DingTalk settings',
|
||||
'企业微信设置': 'WeCom settings',
|
||||
'扫码接入机器人': 'Scan QR code',
|
||||
|
|
|
|||
|
|
@ -144,6 +144,8 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|||
appId: botConfig.appId,
|
||||
appSecret,
|
||||
domain: botConfig.domain,
|
||||
botOpenId: botConfig.botOpenId,
|
||||
groupResponseMode: botConfig.groupResponseMode,
|
||||
ownerOpenIds: botConfig.ownerOpenIds ?? [botConfig.ownerOpenId],
|
||||
harness: workspaceScope.harness,
|
||||
state: workspaceScope.state,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,10 @@ import { publicConnectionTestResult } from '../../../../src/channels/shared/conn
|
|||
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
||||
import { publicWorkspaceError, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
|
||||
import { validAgentPresetPayload } from '../shared/agent-preset-rpc.mjs';
|
||||
import {
|
||||
isFeishuGroupResponseMode,
|
||||
normalizeFeishuGroupResponseMode,
|
||||
} from '../../../../src/channels/feishu/group-response-mode.mjs';
|
||||
import {
|
||||
FEISHU_ENDPOINTS,
|
||||
FEISHU_RPC_CHANNEL,
|
||||
|
|
@ -259,6 +263,7 @@ function publicBotEntry(entry) {
|
|||
connected,
|
||||
configured: source.configured === true,
|
||||
agentPreset: normalizeAgentPresetId(source.agentPreset),
|
||||
groupResponseMode: normalizeFeishuGroupResponseMode(source.groupResponseMode),
|
||||
bot: publicBot(source.bot),
|
||||
health: publicHealth(source, connected),
|
||||
};
|
||||
|
|
@ -389,6 +394,13 @@ function validPayload(endpoint, payload) {
|
|||
return validAgentPresetPayload(payload)
|
||||
? null : '请选择 Agent Preset。';
|
||||
}
|
||||
if (endpoint === FEISHU_ENDPOINTS.setGroupResponseMode) {
|
||||
return hasOnlyKeys(payload, new Set(['botId', 'groupResponseMode']))
|
||||
&& safeOpaqueId(payload.botId)
|
||||
&& isFeishuGroupResponseMode(payload.groupResponseMode)
|
||||
? null
|
||||
: '请选择群聊响应方式。';
|
||||
}
|
||||
return 'Unknown Feishu endpoint.';
|
||||
}
|
||||
|
||||
|
|
@ -623,6 +635,14 @@ export function createFeishuRpcHandler(controller, { encodeQr = qrCodeDataUrl }
|
|||
await controller.updateAgentPreset(payload.botId, payload.agentPreset),
|
||||
{ encodeQr: cachedEncodeQr },
|
||||
);
|
||||
} else if (endpoint === FEISHU_ENDPOINTS.setGroupResponseMode) {
|
||||
if (typeof controller.updateGroupResponseMode !== 'function') {
|
||||
throw new Error('Group response mode update is unavailable');
|
||||
}
|
||||
value = await toPublicFeishuStatus(
|
||||
await controller.updateGroupResponseMode(payload.botId, payload.groupResponseMode),
|
||||
{ encodeQr: cachedEncodeQr },
|
||||
);
|
||||
} else {
|
||||
if (typeof controller.deleteBot !== 'function') throw new Error('Multi-bot delete is unavailable');
|
||||
value = await toPublicFeishuStatus(await controller.deleteBot(payload.botId), { encodeQr: cachedEncodeQr });
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue