mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 00:33:20 +08:00
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:
parent
7993499273
commit
2718586fda
12 changed files with 590 additions and 232 deletions
134
lib/client.js
134
lib/client.js
|
|
@ -571,7 +571,7 @@ var React24 = __toESM(require("react"), 1);
|
|||
// package.json
|
||||
var package_default = {
|
||||
name: "dsh-im-ops",
|
||||
version: "4.9.1-ops.18",
|
||||
version: "4.9.1-ops.19",
|
||||
description: "Ops fork of dsh-im (all channels kept). Based on @xmanrui/dsh-im@4.9.1 \u2014 access/session policies owned here.",
|
||||
keywords: [
|
||||
"deepseek-harness",
|
||||
|
|
@ -852,6 +852,9 @@ var EN = Object.freeze({
|
|||
"\u673A\u5668\u4EBA\u8BBE\u7F6E\u9875\u7B7E": "Bot settings tabs",
|
||||
"\u6295\u9012\u8BBE\u7F6E": "Delivery settings",
|
||||
"\u8BBF\u95EE\u8BBE\u7F6E": "Access settings",
|
||||
"\uFF08\u4E0D\u53EF\u7528\uFF09": " (unavailable)",
|
||||
"\u7FA4 Agent Preset": "Group Agent Preset",
|
||||
"\u8DDF\u968F\u5168\u5C40": "Follow global",
|
||||
"\u2014": "\u2014",
|
||||
"\u6765\u6E90": "Source",
|
||||
"\u6682\u65E0\u7BA1\u7406\u5458\uFF0C\u53EF\u70B9\u4E0B\u65B9\u65B0\u589E\u3002": "No admins yet. Add one below.",
|
||||
|
|
@ -11119,7 +11122,13 @@ var ACCESS_GRANT_VERSION = 1;
|
|||
var ACCESS_PENDING_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
||||
var ACCESS_GRANT_PHONE_MAX_LENGTH = 32;
|
||||
var ACCESS_GRANT_GROUP_JID_PATTERN = /^\d{5,32}@g\.us$/;
|
||||
var AGENT_PRESET_ID = /^[a-z0-9][a-z0-9-]*$/;
|
||||
var CONTROL_CHARACTERS2 = /[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/;
|
||||
function normalizeOptionalAgentPresetId(value) {
|
||||
if (typeof value !== "string") return null;
|
||||
const id5 = value.trim();
|
||||
return AGENT_PRESET_ID.test(id5) ? id5 : null;
|
||||
}
|
||||
function invalidGrant(message) {
|
||||
const error = new TypeError(message);
|
||||
error.code = "access-grant-invalid";
|
||||
|
|
@ -11159,7 +11168,18 @@ function validateMember(input) {
|
|||
}
|
||||
const phone = validateAccessPhone(input.phone ?? input.id);
|
||||
const canExecuteCommands = input.canExecuteCommands === void 0 ? true : Boolean(input.canExecuteCommands);
|
||||
return Object.freeze({ phone, canExecuteCommands });
|
||||
const agentPreset = optionalAgentPreset(input.agentPreset);
|
||||
return Object.freeze({
|
||||
phone,
|
||||
canExecuteCommands,
|
||||
...agentPreset ? { agentPreset } : {}
|
||||
});
|
||||
}
|
||||
function optionalAgentPreset(value) {
|
||||
if (value == null || value === "") return void 0;
|
||||
const id5 = normalizeOptionalAgentPresetId(value);
|
||||
if (!id5) invalidGrant("Agent Preset \u65E0\u6548\u3002");
|
||||
return id5;
|
||||
}
|
||||
function validatePhoneList(input, { emptyMessage, label }) {
|
||||
if (!Array.isArray(input)) invalidGrant(`${label}\u5FC5\u987B\u662F\u6570\u7EC4\u3002`);
|
||||
|
|
@ -11178,8 +11198,10 @@ function validateGroupGrant(input) {
|
|||
invalidGrant("\u7FA4\u6210\u5458\u4E0D\u80FD\u5305\u542B\u91CD\u590D\u7535\u8BDD\u3002");
|
||||
}
|
||||
const title = typeof input.title === "string" ? input.title.trim().slice(0, 128) : "";
|
||||
const agentPreset = optionalAgentPreset(input.agentPreset);
|
||||
return Object.freeze({
|
||||
...title ? { title } : {},
|
||||
...agentPreset ? { agentPreset } : {},
|
||||
admins,
|
||||
members
|
||||
});
|
||||
|
|
@ -12597,6 +12619,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 }))
|
||||
}])),
|
||||
|
|
@ -12715,14 +12738,52 @@ function PhoneTypeahead({
|
|||
) : null
|
||||
);
|
||||
}
|
||||
function AgentPresetSelect({
|
||||
value,
|
||||
catalog,
|
||||
disabled,
|
||||
onChange,
|
||||
ariaLabel = "Agent Preset"
|
||||
}) {
|
||||
const current = typeof value === "string" ? value : "";
|
||||
const items = [];
|
||||
const seen = /* @__PURE__ */ 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 h2(
|
||||
"select",
|
||||
{
|
||||
className: "dim-accessCompactSelect",
|
||||
value: current,
|
||||
disabled,
|
||||
title: ariaLabel,
|
||||
"aria-label": ariaLabel,
|
||||
onChange: (event) => onChange(event.target.value || "")
|
||||
},
|
||||
h2("option", { value: "" }, "\u8DDF\u968F\u5168\u5C40"),
|
||||
...items.map((item) => h2("option", {
|
||||
key: item.id,
|
||||
value: item.id
|
||||
}, item.unavailable ? item.label + localizeText("\uFF08\u4E0D\u53EF\u7528\uFF09") : item.label))
|
||||
);
|
||||
}
|
||||
function MemberRows({
|
||||
title,
|
||||
members,
|
||||
contacts,
|
||||
disabled,
|
||||
onChange,
|
||||
nested = false
|
||||
nested = false,
|
||||
showAgentPreset = false,
|
||||
presetCatalog = EMPTY_AGENT_PRESET_CATALOG
|
||||
}) {
|
||||
const columns = showAgentPreset ? 5 : 4;
|
||||
return h2(
|
||||
nested ? "div" : "fieldset",
|
||||
{
|
||||
|
|
@ -12735,7 +12796,10 @@ function MemberRows({
|
|||
{ className: "dim-accessTableWrap" },
|
||||
h2(
|
||||
"table",
|
||||
{ className: "dim-accessTable", "data-kind": "member" },
|
||||
{
|
||||
className: "dim-accessTable",
|
||||
"data-kind": showAgentPreset ? "member-preset" : "member"
|
||||
},
|
||||
h2(
|
||||
"thead",
|
||||
null,
|
||||
|
|
@ -12745,10 +12809,14 @@ function MemberRows({
|
|||
h2("th", { scope: "col" }, "\u6635\u79F0"),
|
||||
h2("th", { scope: "col" }, "\u7535\u8BDD"),
|
||||
h2("th", { scope: "col" }, "\u547D\u4EE4\u6743\u9650"),
|
||||
showAgentPreset ? h2("th", { scope: "col" }, "Agent Preset") : null,
|
||||
h2("th", { scope: "col", className: "dim-accessTableActions" }, "\u64CD\u4F5C")
|
||||
)
|
||||
),
|
||||
h2("tbody", null, members.length === 0 ? h2("tr", null, h2("td", { colSpan: 4, className: "dim-accessTableEmpty" }, "\u6682\u65E0\u7528\u6237\uFF0C\u53EF\u70B9\u4E0B\u65B9\u65B0\u589E\u3002")) : members.map((member, index) => h2(
|
||||
h2("tbody", null, members.length === 0 ? h2("tr", null, h2("td", {
|
||||
colSpan: columns,
|
||||
className: "dim-accessTableEmpty"
|
||||
}, "\u6682\u65E0\u7528\u6237\uFF0C\u53EF\u70B9\u4E0B\u65B9\u65B0\u589E\u3002")) : members.map((member, index) => h2(
|
||||
"tr",
|
||||
{ key: `member-${index}` },
|
||||
h2(
|
||||
|
|
@ -12784,6 +12852,23 @@ function MemberRows({
|
|||
h2("option", { value: "deny" }, "\u4E0D\u53EF\u6267\u884C\u547D\u4EE4")
|
||||
)
|
||||
),
|
||||
showAgentPreset ? h2(
|
||||
"td",
|
||||
{ className: "dim-accessTablePreset" },
|
||||
h2(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,
|
||||
h2(
|
||||
"td",
|
||||
{ className: "dim-accessTableActions" },
|
||||
|
|
@ -12906,6 +12991,10 @@ function AccessGrantSettingsPage({ channel: channel4, account, rpcCall, onSaved
|
|||
const [feedback, setFeedback] = React21.useState(null);
|
||||
const [newGroupJid, setNewGroupJid] = React21.useState("");
|
||||
const [loadingGrant, setLoadingGrant] = React21.useState(!initialGrant);
|
||||
const contextCatalog = React21.useContext(AgentPresetCatalogContext);
|
||||
const [presetCatalog, setPresetCatalog] = React21.useState(() => normalizeAgentPresetCatalog(
|
||||
account?.agentPresetCatalog ?? contextCatalog ?? EMPTY_AGENT_PRESET_CATALOG
|
||||
));
|
||||
React21.useEffect(() => {
|
||||
setDraft(cloneGrant(normalizeAccessGrant(account?.accessGrant), ownerPhone));
|
||||
setGroupSessionScope(normalizeGroupSessionScope(account?.groupSessionScope));
|
||||
|
|
@ -12918,6 +13007,9 @@ function AccessGrantSettingsPage({ channel: channel4, account, rpcCall, onSaved
|
|||
try {
|
||||
const value = unwrapRpcResult11(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;
|
||||
let grant = normalizeAccessGrant(bot?.accessGrant);
|
||||
if (grant) {
|
||||
|
|
@ -13072,6 +13164,8 @@ function AccessGrantSettingsPage({ channel: channel4, account, rpcCall, onSaved
|
|||
members: draft.directMembers,
|
||||
contacts,
|
||||
disabled: saving,
|
||||
showAgentPreset: true,
|
||||
presetCatalog,
|
||||
onChange: (directMembers) => {
|
||||
setDraft((current) => ({ ...current, directMembers }));
|
||||
setFeedback(null);
|
||||
|
|
@ -13183,6 +13277,30 @@ function AccessGrantSettingsPage({ channel: channel4, account, rpcCall, onSaved
|
|||
})
|
||||
}, "\u540C\u6B65\u7FA4\u540D")
|
||||
),
|
||||
h2(
|
||||
"label",
|
||||
{ className: "dim-accessField" },
|
||||
h2("span", null, "\u7FA4 Agent Preset"),
|
||||
h2(AgentPresetSelect, {
|
||||
value: group.agentPreset ?? "",
|
||||
catalog: presetCatalog,
|
||||
disabled: saving,
|
||||
ariaLabel: "\u7FA4 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
|
||||
}
|
||||
};
|
||||
})
|
||||
})
|
||||
),
|
||||
h2(AdminPhones, {
|
||||
title: "\u672C\u7FA4\u7BA1\u7406\u5458",
|
||||
help: "\u672A\u914D\u7F6E\u65F6\uFF0C\u672C\u7FA4\u7533\u8BF7\u56DE\u843D\u7ED9\u5168\u5C40\u7BA1\u7406\u5458\u5BA1\u6279\uFF08\u4ECD\u53EA\u6388\u4E88\u672C\u7FA4\u6743\uFF09\u3002",
|
||||
|
|
@ -14799,6 +14917,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%; }
|
||||
|
|
|
|||
436
lib/index.js
436
lib/index.js
File diff suppressed because one or more lines are too long
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "dsh-im-ops",
|
||||
"version": "4.9.1-ops.18",
|
||||
"version": "4.9.1-ops.19",
|
||||
"description": "Ops fork of dsh-im (all channels kept). Based on @xmanrui/dsh-im@4.9.1 — access/session policies owned here.",
|
||||
"keywords": [
|
||||
"deepseek-harness",
|
||||
|
|
|
|||
|
|
@ -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: '未配置时,本群申请回落给全局管理员审批(仍只授予本群权)。',
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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%; }
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ export function publicAccessGrant(grant) {
|
|||
directMembers: normalized.directMembers.map((m) => ({ ...m })),
|
||||
groups: Object.fromEntries(Object.entries(normalized.groups).map(([jid, group]) => [jid, {
|
||||
...(group.title ? { title: group.title } : {}),
|
||||
...(group.agentPreset ? { agentPreset: group.agentPreset } : {}),
|
||||
admins: [...group.admins],
|
||||
members: group.members.map((m) => ({ ...m })),
|
||||
}])),
|
||||
|
|
|
|||
|
|
@ -7,12 +7,19 @@ export const ACCESS_GRANT_VERSION = 1;
|
|||
export const ACCESS_PENDING_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
||||
export const ACCESS_GRANT_PHONE_MAX_LENGTH = 32;
|
||||
export const ACCESS_GRANT_GROUP_JID_PATTERN = /^\d{5,32}@g\.us$/;
|
||||
const AGENT_PRESET_ID = /^[a-z0-9][a-z0-9-]*$/;
|
||||
|
||||
const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/;
|
||||
|
||||
/** @typedef {'direct' | 'group'} AccessGrantScene */
|
||||
/** @typedef {'approve' | 'deny'} AccessPendingAction */
|
||||
|
||||
function normalizeOptionalAgentPresetId(value) {
|
||||
if (typeof value !== 'string') return null;
|
||||
const id = value.trim();
|
||||
return AGENT_PRESET_ID.test(id) ? id : null;
|
||||
}
|
||||
|
||||
function invalidGrant(message) {
|
||||
const error = new TypeError(message);
|
||||
error.code = 'access-grant-invalid';
|
||||
|
|
@ -75,7 +82,7 @@ export function validateGroupJid(value) {
|
|||
|
||||
/**
|
||||
* @param {unknown} input
|
||||
* @returns {{ phone: string, canExecuteCommands: boolean }}
|
||||
* @returns {{ phone: string, canExecuteCommands: boolean, agentPreset?: string }}
|
||||
*/
|
||||
function validateMember(input) {
|
||||
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
||||
|
|
@ -85,7 +92,24 @@ function validateMember(input) {
|
|||
const canExecuteCommands = input.canExecuteCommands === undefined
|
||||
? true
|
||||
: Boolean(input.canExecuteCommands);
|
||||
return Object.freeze({ phone, canExecuteCommands });
|
||||
const agentPreset = optionalAgentPreset(input.agentPreset);
|
||||
return Object.freeze({
|
||||
phone,
|
||||
canExecuteCommands,
|
||||
...(agentPreset ? { agentPreset } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional Agent Preset override. Empty/null means follow the bot-level preset.
|
||||
* @param {unknown} value
|
||||
* @returns {string|undefined}
|
||||
*/
|
||||
function optionalAgentPreset(value) {
|
||||
if (value == null || value === '') return undefined;
|
||||
const id = normalizeOptionalAgentPresetId(value);
|
||||
if (!id) invalidGrant('Agent Preset 无效。');
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -119,8 +143,10 @@ function validateGroupGrant(input) {
|
|||
invalidGrant('群成员不能包含重复电话。');
|
||||
}
|
||||
const title = typeof input.title === 'string' ? input.title.trim().slice(0, 128) : '';
|
||||
const agentPreset = optionalAgentPreset(input.agentPreset);
|
||||
return Object.freeze({
|
||||
...(title ? { title } : {}),
|
||||
...(agentPreset ? { agentPreset } : {}),
|
||||
admins,
|
||||
members,
|
||||
});
|
||||
|
|
@ -282,6 +308,30 @@ export function normalizeAccessGrant(input) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a chat-scoped Agent Preset override from the access grant.
|
||||
* Returns null when the chat should follow the bot-level (global) preset.
|
||||
* Priority: direct-member or group override > bot global (caller applies global).
|
||||
* @param {unknown} grant
|
||||
* @param {{ kind?: string, phone?: string, senderId?: string, groupJid?: string, conversationId?: string }} [context]
|
||||
* @returns {string|null}
|
||||
*/
|
||||
export function resolveAccessAgentPreset(grant, context = {}) {
|
||||
const doc = normalizeAccessGrant(grant);
|
||||
if (!doc) return null;
|
||||
if (context.kind === 'group') {
|
||||
const groupJid = typeof context.groupJid === 'string' && context.groupJid.trim()
|
||||
? context.groupJid.trim()
|
||||
: typeof context.conversationId === 'string' ? context.conversationId.trim() : '';
|
||||
if (!groupJid || !ACCESS_GRANT_GROUP_JID_PATTERN.test(groupJid)) return null;
|
||||
return normalizeOptionalAgentPresetId(doc.groups[groupJid]?.agentPreset) ?? null;
|
||||
}
|
||||
const phone = normalizeAccessPhone(context.phone ?? context.senderId);
|
||||
if (!phone) return null;
|
||||
const member = doc.directMembers.find((entry) => entry.phone === phone);
|
||||
return normalizeOptionalAgentPresetId(member?.agentPreset) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed owner phone as the first global admin when missing.
|
||||
* @param {object|null} grant
|
||||
|
|
|
|||
|
|
@ -1288,9 +1288,12 @@ export function createBotWorkspaceScope(
|
|||
throw error;
|
||||
}
|
||||
const generation = workspaces.generationFor(botId);
|
||||
const agentPreset = workspaces.agentPresetFor(botId);
|
||||
const { agentPreset: requestedPreset, ...createOptions } = options ?? {};
|
||||
const agentPreset = requestedPreset !== undefined
|
||||
? requestedPreset
|
||||
: workspaces.agentPresetFor(botId);
|
||||
const sessionId = await target.createSession({
|
||||
...options,
|
||||
...createOptions,
|
||||
workspace: workspaces.workspaceFor(botId),
|
||||
...(agentPreset == null ? {} : { agentPreset }),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -148,6 +148,8 @@ export class TextHarnessBridge {
|
|||
#logger;
|
||||
#replyTimeoutMs;
|
||||
#signal;
|
||||
/** @type {((message: object) => string|null|undefined)|null} */
|
||||
#resolveAgentPreset;
|
||||
#queues = new Map();
|
||||
#pendingInteractions = new Map();
|
||||
#interactionKeys = new Map();
|
||||
|
|
@ -170,6 +172,7 @@ export class TextHarnessBridge {
|
|||
logger = console,
|
||||
replyTimeoutMs = 600_000,
|
||||
signal,
|
||||
resolveAgentPreset,
|
||||
}) {
|
||||
if (!descriptor?.key || !descriptor?.label) throw new TypeError('A channel descriptor is required');
|
||||
if (!bot || typeof bot.sendText !== 'function') throw new TypeError('A bot client is required');
|
||||
|
|
@ -185,6 +188,7 @@ export class TextHarnessBridge {
|
|||
this.#logger = logger;
|
||||
this.#replyTimeoutMs = replyTimeoutMs;
|
||||
this.#signal = signal;
|
||||
this.#resolveAgentPreset = typeof resolveAgentPreset === 'function' ? resolveAgentPreset : null;
|
||||
this.#approvals = new HarnessApprovalQueue({
|
||||
label: descriptor.key,
|
||||
logger,
|
||||
|
|
@ -735,7 +739,13 @@ export class TextHarnessBridge {
|
|||
text,
|
||||
content,
|
||||
contextEnhanced,
|
||||
createOptions: this.#signal ? { signal: this.#signal } : undefined,
|
||||
createOptions: (() => {
|
||||
const agentPreset = this.#resolveAgentPreset?.(message);
|
||||
const options = {};
|
||||
if (this.#signal) options.signal = this.#signal;
|
||||
if (typeof agentPreset === 'string' && agentPreset) options.agentPreset = agentPreset;
|
||||
return Object.keys(options).length > 0 ? options : undefined;
|
||||
})(),
|
||||
existsOptions: this.#signal ? { signal: this.#signal } : undefined,
|
||||
askOptions: {
|
||||
timeoutMs: this.#replyTimeoutMs,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import {
|
|||
normalizeMessageContent,
|
||||
} from '@whiskeysockets/baileys';
|
||||
|
||||
import { emptyAccessGrant, ensureGroupBucket } from '../shared/access-grant.mjs';
|
||||
import { emptyAccessGrant, ensureGroupBucket, normalizeAccessPhone, phoneFromWhatsappJid, resolveAccessAgentPreset } from '../shared/access-grant.mjs';
|
||||
import { splitMessageText } from '../shared/editable-message-stream.mjs';
|
||||
import { t } from '../shared/i18n.mjs';
|
||||
import { ImagePromptError } from '../shared/image-prompt.mjs';
|
||||
|
|
@ -823,6 +823,20 @@ export class WhatsappRuntime {
|
|||
logger: this.#logger,
|
||||
replyTimeoutMs: this.#replyTimeoutMs,
|
||||
signal: controller.signal,
|
||||
resolveAgentPreset: (message) => {
|
||||
if (!this.#workspaces || !this.#botId) return null;
|
||||
const grant = this.#workspaces.accessGrantFor(this.#botId);
|
||||
return resolveAccessAgentPreset(grant, {
|
||||
kind: message?.kind === 'group' ? 'group' : 'direct',
|
||||
groupJid: message?.conversationId,
|
||||
conversationId: message?.conversationId,
|
||||
phone: phoneFromWhatsappJid(message?.senderId)
|
||||
?? normalizeAccessPhone(message?.senderId)
|
||||
?? phoneFromWhatsappJid(message?.senderAlternateId)
|
||||
?? normalizeAccessPhone(message?.senderAlternateId),
|
||||
senderId: message?.senderId,
|
||||
});
|
||||
},
|
||||
});
|
||||
const now = Date.now();
|
||||
this.#status.ready = true;
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {
|
|||
parsePendingIdFromNotifyText,
|
||||
resolveAccessPending,
|
||||
upsertAccessContact,
|
||||
resolveAccessAgentPreset,
|
||||
validateAccessGrant,
|
||||
} from '../../../src/channels/shared/access-grant.mjs';
|
||||
|
||||
|
|
@ -194,3 +195,55 @@ test('quote approval helpers parse intent, pending id, and notify refs', () => {
|
|||
};
|
||||
assert.equal(findPendingByNotifyMessageId(withRef, 'MSG1')?.id, 'p_abc123');
|
||||
});
|
||||
|
||||
|
||||
test('resolveAccessAgentPreset prefers direct-member and group overrides over global', () => {
|
||||
const doc = grant({
|
||||
directMembers: [
|
||||
{ phone: '8618222222222', canExecuteCommands: true, agentPreset: 'ops-user' },
|
||||
{ phone: '8618333333333', canExecuteCommands: true },
|
||||
],
|
||||
groups: {
|
||||
[GROUP_A]: {
|
||||
title: 'A',
|
||||
agentPreset: 'ops-group',
|
||||
admins: [],
|
||||
members: [{ phone: '8618444444444', canExecuteCommands: true }],
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.equal(resolveAccessAgentPreset(doc, {
|
||||
kind: 'direct',
|
||||
phone: '8618222222222',
|
||||
}), 'ops-user');
|
||||
assert.equal(resolveAccessAgentPreset(doc, {
|
||||
kind: 'direct',
|
||||
phone: '8618333333333',
|
||||
}), null);
|
||||
assert.equal(resolveAccessAgentPreset(doc, {
|
||||
kind: 'group',
|
||||
groupJid: GROUP_A,
|
||||
}), 'ops-group');
|
||||
assert.equal(resolveAccessAgentPreset(doc, {
|
||||
kind: 'group',
|
||||
groupJid: GROUP_B,
|
||||
}), null);
|
||||
});
|
||||
|
||||
test('validateAccessGrant keeps optional agentPreset on members and groups', () => {
|
||||
const doc = grant({
|
||||
directMembers: [{ phone: '8618222222222', canExecuteCommands: false, agentPreset: 'desk' }],
|
||||
groups: {
|
||||
[GROUP_A]: {
|
||||
agentPreset: 'night',
|
||||
admins: ['8618111111111'],
|
||||
members: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.equal(doc.directMembers[0].agentPreset, 'desk');
|
||||
assert.equal(doc.groups[GROUP_A].agentPreset, 'night');
|
||||
assert.throws(() => grant({
|
||||
directMembers: [{ phone: '8618222222222', canExecuteCommands: true, agentPreset: 'BAD Preset' }],
|
||||
}), /Agent Preset/);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue