Add per-DM and per-group Agent Preset overrides (4.9.1-ops.19).

Access grants can pin an agentPreset on direct members and groups; new sessions resolve override over the bot-global preset.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-05 18:53:43 +08:00
parent 7993499273
commit 2718586fda
12 changed files with 590 additions and 232 deletions

View file

@ -10,6 +10,11 @@ import {
normalizeGroupSessionScope,
validateGroupSessionScope,
} from '../../src/channels/shared/session-scope.mjs';
import {
AgentPresetCatalogContext,
EMPTY_AGENT_PRESET_CATALOG,
normalizeAgentPresetCatalog,
} from './agent-preset.js';
import { h, localizeText } from './i18n.js';
export const ACCESS_GRANT_ENDPOINT = 'bot.access-grant.set';
@ -58,6 +63,7 @@ function cloneGrant(grant, ownerPhone) {
directMembers: (base.directMembers ?? []).map((m) => ({ ...m })),
groups: Object.fromEntries(Object.entries(base.groups ?? {}).map(([jid, group]) => [jid, {
title: group.title ?? '',
...(group.agentPreset ? { agentPreset: group.agentPreset } : {}),
admins: [...(group.admins ?? [])],
members: (group.members ?? []).map((m) => ({ ...m })),
}])),
@ -191,6 +197,40 @@ function PhoneTypeahead({
}, contactLabel(contact))))) : null);
}
function AgentPresetSelect({
value,
catalog,
disabled,
onChange,
ariaLabel = 'Agent Preset',
}) {
const current = typeof value === 'string' ? value : '';
const items = [];
const seen = new Set();
for (const item of Array.isArray(catalog?.items) ? catalog.items : []) {
if (!item?.id || seen.has(item.id)) continue;
seen.add(item.id);
items.push(item);
}
if (current && !seen.has(current)) {
items.push({ id: current, label: current, unavailable: true });
}
return h('select', {
className: 'dim-accessCompactSelect',
value: current,
disabled,
title: ariaLabel,
'aria-label': ariaLabel,
onChange: (event) => onChange(event.target.value || ''),
},
h('option', { value: '' }, '跟随全局'),
...items.map((item) => h('option', {
key: item.id,
value: item.id,
}, item.unavailable ? item.label + localizeText('(不可用)') : item.label)));
}
function MemberRows({
title,
members,
@ -198,22 +238,32 @@ function MemberRows({
disabled,
onChange,
nested = false,
showAgentPreset = false,
presetCatalog = EMPTY_AGENT_PRESET_CATALOG,
}) {
const columns = showAgentPreset ? 5 : 4;
return h(nested ? 'div' : 'fieldset', {
className: nested ? 'dim-accessSubblock' : 'dim-accessScene',
...(nested ? {} : { disabled }),
},
nested ? h('h4', { className: 'dim-accessSubblockTitle' }, title) : h('legend', null, title),
h('div', { className: 'dim-accessTableWrap' },
h('table', { className: 'dim-accessTable', 'data-kind': 'member' },
h('table', {
className: 'dim-accessTable',
'data-kind': showAgentPreset ? 'member-preset' : 'member',
},
h('thead', null,
h('tr', null,
h('th', { scope: 'col' }, '昵称'),
h('th', { scope: 'col' }, '电话'),
h('th', { scope: 'col' }, '命令权限'),
showAgentPreset ? h('th', { scope: 'col' }, 'Agent Preset') : null,
h('th', { scope: 'col', className: 'dim-accessTableActions' }, '操作'))),
h('tbody', null, members.length === 0
? h('tr', null, h('td', { colSpan: 4, className: 'dim-accessTableEmpty' }, '暂无用户,可点下方新增。'))
? h('tr', null, h('td', {
colSpan: columns,
className: 'dim-accessTableEmpty',
}, '暂无用户,可点下方新增。'))
: members.map((member, index) =>
h('tr', { key: `member-${index}` },
h('td', { className: 'dim-accessTableNick' },
@ -243,6 +293,20 @@ function MemberRows({
},
h('option', { value: 'allow' }, '可执行命令'),
h('option', { value: 'deny' }, '不可执行命令'))),
showAgentPreset ? h('td', { className: 'dim-accessTablePreset' },
h(AgentPresetSelect, {
value: member.agentPreset ?? '',
catalog: presetCatalog,
disabled,
ariaLabel: 'Agent Preset',
onChange: (preset) => onChange(members.map((entry, i) => {
if (i !== index) return entry;
const next = { ...entry };
if (preset) next.agentPreset = preset;
else delete next.agentPreset;
return next;
})),
})) : null,
h('td', { className: 'dim-accessTableActions' },
h('button', {
type: 'button',
@ -339,6 +403,10 @@ export function AccessGrantSettingsPage({ channel, account, rpcCall, onSaved })
const [feedback, setFeedback] = React.useState(null);
const [newGroupJid, setNewGroupJid] = React.useState('');
const [loadingGrant, setLoadingGrant] = React.useState(!initialGrant);
const contextCatalog = React.useContext(AgentPresetCatalogContext);
const [presetCatalog, setPresetCatalog] = React.useState(() => normalizeAgentPresetCatalog(
account?.agentPresetCatalog ?? contextCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
));
React.useEffect(() => {
setDraft(cloneGrant(normalizeAccessGrant(account?.accessGrant), ownerPhone));
@ -354,6 +422,9 @@ export function AccessGrantSettingsPage({ channel, account, rpcCall, onSaved })
try {
const value = unwrapRpcResult(await rpcCall('connection.status', {}));
if (cancelled) return;
if (value?.agentPresetCatalog) {
setPresetCatalog(normalizeAgentPresetCatalog(value.agentPresetCatalog));
}
const bot = Array.isArray(value?.bots)
? value.bots.find((entry) => entry?.botId === account.botId)
: null;
@ -521,6 +592,8 @@ export function AccessGrantSettingsPage({ channel, account, rpcCall, onSaved })
members: draft.directMembers,
contacts,
disabled: saving,
showAgentPreset: true,
presetCatalog,
onChange: (directMembers) => {
setDraft((current) => ({ ...current, directMembers }));
setFeedback(null);
@ -618,6 +691,27 @@ export function AccessGrantSettingsPage({ channel, account, rpcCall, onSaved })
}
}),
}, '同步群名')),
h('label', { className: 'dim-accessField' },
h('span', null, '群 Agent Preset'),
h(AgentPresetSelect, {
value: group.agentPreset ?? '',
catalog: presetCatalog,
disabled: saving,
ariaLabel: '群 Agent Preset',
onChange: (preset) => setDraft((current) => {
const existing = current.groups[groupJid] ?? { title: '', admins: [], members: [] };
const nextGroup = { ...existing };
if (preset) nextGroup.agentPreset = preset;
else delete nextGroup.agentPreset;
return {
...current,
groups: {
...current.groups,
[groupJid]: nextGroup,
},
};
}),
})),
h(AdminPhones, {
title: '本群管理员',
help: '未配置时,本群申请回落给全局管理员审批(仍只授予本群权)。',

View file

@ -11,6 +11,9 @@ const EN = Object.freeze({
'机器人设置页签': 'Bot settings tabs',
'投递设置': 'Delivery settings',
'访问设置': 'Access settings',
"(不可用)": " (unavailable)",
"群 Agent Preset": "Group Agent Preset",
"跟随全局": "Follow global",
"—": "—",
"来源": "Source",
"暂无管理员,可点下方新增。": "No admins yet. Add one below.",

View file

@ -484,6 +484,12 @@ body[data-ds-dark-theme] .dim-page select option, body[data-ds-dark-theme] .dim-
.dim-accessTable[data-kind="member"] th:nth-child(2), .dim-accessTable[data-kind="member"] td:nth-child(2) { width: 36%; }
.dim-accessTable[data-kind="member"] th:nth-child(3), .dim-accessTable[data-kind="member"] td:nth-child(3) { width: 28%; }
.dim-accessTable[data-kind="member"] th:nth-child(4), .dim-accessTable[data-kind="member"] td:nth-child(4) { width: 16%; }
.dim-accessTable[data-kind="member-preset"] th:nth-child(1), .dim-accessTable[data-kind="member-preset"] td:nth-child(1) { width: 16%; }
.dim-accessTable[data-kind="member-preset"] th:nth-child(2), .dim-accessTable[data-kind="member-preset"] td:nth-child(2) { width: 26%; }
.dim-accessTable[data-kind="member-preset"] th:nth-child(3), .dim-accessTable[data-kind="member-preset"] td:nth-child(3) { width: 18%; }
.dim-accessTable[data-kind="member-preset"] th:nth-child(4), .dim-accessTable[data-kind="member-preset"] td:nth-child(4) { width: 26%; }
.dim-accessTable[data-kind="member-preset"] th:nth-child(5), .dim-accessTable[data-kind="member-preset"] td:nth-child(5) { width: 14%; }
.dim-accessTablePreset { min-width: 0; }
.dim-accessTable[data-kind="contact"] th:nth-child(1), .dim-accessTable[data-kind="contact"] td:nth-child(1) { width: 18%; }
.dim-accessTable[data-kind="contact"] th:nth-child(2), .dim-accessTable[data-kind="contact"] td:nth-child(2) { width: 26%; }
.dim-accessTable[data-kind="contact"] th:nth-child(3), .dim-accessTable[data-kind="contact"] td:nth-child(3) { width: 16%; }