mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-10 20:55:59 +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
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue