mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-08 22:00:50 +08:00
Make WhatsApp member presets follow channel mapping by default.
Some checks failed
CI / Test, build, and verify package (push) Has been cancelled
Some checks failed
CI / Test, build, and verify package (push) Has been cancelled
Empty member/group Agent Preset now resolves to the bot workspace mapping (not Host global); settings label is Follow channel mapping, and /new createSession carries that mapping explicitly. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
f1d85d2985
commit
bcbe85fdb1
10 changed files with 182 additions and 131 deletions
|
|
@ -8,6 +8,9 @@ This file records the notable changes in each dsh-im release. Its format follows
|
||||||
|
|
||||||
### Fixed / 修复
|
### Fixed / 修复
|
||||||
|
|
||||||
|
- WhatsApp 成员/群 Agent Preset 空值(UI 原「跟随全局」)现在明确回落到**频道工作区映射预设**,不再让人误以为跟 Host 全局默认;设置页文案改为「跟随频道映射」。`/new` 后建会话也会带上该映射,避免改 Host 全局预设后误用坏默认导致 `SESSION_CREATE`。
|
||||||
|
Empty WhatsApp member/group Agent Preset (UI formerly “Follow global”) now clearly falls back to the **channel workspace mapping preset**, not the Host catalog default; settings copy is “Follow channel mapping”. `/new` session creation also carries that mapping so a broken Host global default after operators change it does not surface as `SESSION_CREATE`.
|
||||||
|
|
||||||
- Host 带斜杠的错误码(如 `gateway/internal`、`workspace/invalid-path`、`agent-preset/not-found`)现在会正确映射为 `SESSION_CREATE` / `WORKSPACE_UNAVAILABLE` / `PRESET_UNAVAILABLE`;`last-message-failure.json` 额外写入 workspace、Host details 与脱敏 message,便于现场对上真实 RPC。
|
- Host 带斜杠的错误码(如 `gateway/internal`、`workspace/invalid-path`、`agent-preset/not-found`)现在会正确映射为 `SESSION_CREATE` / `WORKSPACE_UNAVAILABLE` / `PRESET_UNAVAILABLE`;`last-message-failure.json` 额外写入 workspace、Host details 与脱敏 message,便于现场对上真实 RPC。
|
||||||
Host slash-branded failure codes (e.g. `gateway/internal`, `workspace/invalid-path`, `agent-preset/not-found`) now map to `SESSION_CREATE` / `WORKSPACE_UNAVAILABLE` / `PRESET_UNAVAILABLE`; `last-message-failure.json` also records workspace, Host details, and redacted messages for field triage.
|
Host slash-branded failure codes (e.g. `gateway/internal`, `workspace/invalid-path`, `agent-preset/not-found`) now map to `SESSION_CREATE` / `WORKSPACE_UNAVAILABLE` / `PRESET_UNAVAILABLE`; `last-message-failure.json` also records workspace, Host details, and redacted messages for field triage.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -571,7 +571,7 @@ var React26 = __toESM(require("react"), 1);
|
||||||
// package.json
|
// package.json
|
||||||
var package_default = {
|
var package_default = {
|
||||||
name: "dsh-im-ops",
|
name: "dsh-im-ops",
|
||||||
version: "4.9.1-ops.35",
|
version: "4.9.1-ops.36",
|
||||||
description: "Ops fork of dsh-im (all channels kept). Based on @xmanrui/dsh-im@4.9.1 - access/session policies owned here.",
|
description: "Ops fork of dsh-im (all channels kept). Based on @xmanrui/dsh-im@4.9.1 - access/session policies owned here.",
|
||||||
keywords: [
|
keywords: [
|
||||||
"deepseek-harness",
|
"deepseek-harness",
|
||||||
|
|
@ -855,6 +855,7 @@ var EN = Object.freeze({
|
||||||
"\u6E20\u9053\u7528\u6237": "Channel user",
|
"\u6E20\u9053\u7528\u6237": "Channel user",
|
||||||
"\uFF08\u4E0D\u53EF\u7528\uFF09": " (unavailable)",
|
"\uFF08\u4E0D\u53EF\u7528\uFF09": " (unavailable)",
|
||||||
"\u7FA4 Agent Preset": "Group Agent Preset",
|
"\u7FA4 Agent Preset": "Group Agent Preset",
|
||||||
|
"\u8DDF\u968F\u9891\u9053\u6620\u5C04": "Follow channel mapping",
|
||||||
"\u8DDF\u968F\u5168\u5C40": "Follow global",
|
"\u8DDF\u968F\u5168\u5C40": "Follow global",
|
||||||
"\u2014": "\u2014",
|
"\u2014": "\u2014",
|
||||||
"\u6765\u6E90": "Source",
|
"\u6765\u6E90": "Source",
|
||||||
|
|
@ -13036,7 +13037,7 @@ function AgentPresetSelect({
|
||||||
"aria-label": ariaLabel,
|
"aria-label": ariaLabel,
|
||||||
onChange: (event) => onChange(event.target.value || "")
|
onChange: (event) => onChange(event.target.value || "")
|
||||||
},
|
},
|
||||||
h2("option", { value: "" }, "\u8DDF\u968F\u5168\u5C40"),
|
h2("option", { value: "" }, localizeText("\u8DDF\u968F\u9891\u9053\u6620\u5C04")),
|
||||||
...items.map((item) => h2("option", {
|
...items.map((item) => h2("option", {
|
||||||
key: item.id,
|
key: item.id,
|
||||||
value: item.id
|
value: item.id
|
||||||
|
|
|
||||||
234
lib/index.js
234
lib/index.js
File diff suppressed because one or more lines are too long
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "dsh-im-ops",
|
"name": "dsh-im-ops",
|
||||||
"version": "4.9.1-ops.35",
|
"version": "4.9.1-ops.36",
|
||||||
"description": "Ops fork of dsh-im (all channels kept). Based on @xmanrui/dsh-im@4.9.1 - access/session policies owned here.",
|
"description": "Ops fork of dsh-im (all channels kept). Based on @xmanrui/dsh-im@4.9.1 - access/session policies owned here.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"deepseek-harness",
|
"deepseek-harness",
|
||||||
|
|
|
||||||
|
|
@ -224,7 +224,7 @@ function AgentPresetSelect({
|
||||||
'aria-label': ariaLabel,
|
'aria-label': ariaLabel,
|
||||||
onChange: (event) => onChange(event.target.value || ''),
|
onChange: (event) => onChange(event.target.value || ''),
|
||||||
},
|
},
|
||||||
h('option', { value: '' }, '跟随全局'),
|
h('option', { value: '' }, localizeText('跟随频道映射')),
|
||||||
...items.map((item) => h('option', {
|
...items.map((item) => h('option', {
|
||||||
key: item.id,
|
key: item.id,
|
||||||
value: item.id,
|
value: item.id,
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ const EN = Object.freeze({
|
||||||
'渠道用户': 'Channel user',
|
'渠道用户': 'Channel user',
|
||||||
"(不可用)": " (unavailable)",
|
"(不可用)": " (unavailable)",
|
||||||
"群 Agent Preset": "Group Agent Preset",
|
"群 Agent Preset": "Group Agent Preset",
|
||||||
|
"跟随频道映射": "Follow channel mapping",
|
||||||
"跟随全局": "Follow global",
|
"跟随全局": "Follow global",
|
||||||
"—": "—",
|
"—": "—",
|
||||||
"来源": "Source",
|
"来源": "Source",
|
||||||
|
|
|
||||||
|
|
@ -310,8 +310,9 @@ export function normalizeAccessGrant(input) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve a chat-scoped Agent Preset override from the access grant.
|
* Resolve a chat-scoped Agent Preset override from the access grant.
|
||||||
* Returns null when the chat should follow the bot-level (global) preset.
|
* Returns null when the chat should follow the bot/channel mapping preset
|
||||||
* Priority: direct-member or group override > bot global (caller applies global).
|
* (workspaces.agentPresets[botId]), not the Host catalog default.
|
||||||
|
* Priority: direct-member or group override > bot mapping (caller applies).
|
||||||
* @param {unknown} grant
|
* @param {unknown} grant
|
||||||
* @param {{ kind?: string, phone?: string, senderId?: string, groupJid?: string, conversationId?: string }} [context]
|
* @param {{ kind?: string, phone?: string, senderId?: string, groupJid?: string, conversationId?: string }} [context]
|
||||||
* @returns {string|null}
|
* @returns {string|null}
|
||||||
|
|
@ -332,6 +333,25 @@ export function resolveAccessAgentPreset(grant, context = {}) {
|
||||||
return normalizeOptionalAgentPresetId(member?.agentPreset) ?? null;
|
return normalizeOptionalAgentPresetId(member?.agentPreset) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Member/group override, else the bot's workspace-mapped preset.
|
||||||
|
* Does not fall through to Host catalog default — that is only used when the
|
||||||
|
* bot mapping itself is empty (createSession omits agentPreset).
|
||||||
|
* @param {unknown} grant
|
||||||
|
* @param {{ kind?: string, phone?: string, senderId?: string, groupJid?: string, conversationId?: string }} [context]
|
||||||
|
* @param {string|null|undefined} botAgentPreset
|
||||||
|
* @returns {string|null}
|
||||||
|
*/
|
||||||
|
export function resolveChatAgentPreset(grant, context = {}, botAgentPreset = null) {
|
||||||
|
const fromGrant = resolveAccessAgentPreset(grant, context);
|
||||||
|
if (fromGrant) return fromGrant;
|
||||||
|
if (typeof botAgentPreset === 'string') {
|
||||||
|
const trimmed = botAgentPreset.trim();
|
||||||
|
if (trimmed) return trimmed;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Seed owner phone as the first global admin when missing.
|
* Seed owner phone as the first global admin when missing.
|
||||||
* @param {object|null} grant
|
* @param {object|null} grant
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { normalizeAccessPhone, phoneFromWhatsappJid, resolveAccessAgentPreset } from './access-grant.mjs';
|
import { normalizeAccessPhone, phoneFromWhatsappJid, resolveChatAgentPreset } from './access-grant.mjs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a durable conversation binding key into chat identity fields.
|
* Parse a durable conversation binding key into chat identity fields.
|
||||||
|
|
@ -118,14 +118,13 @@ export function resolveChannelPeerFromBinding(input) {
|
||||||
const botAgentPreset = typeof input.botAgentPreset === 'string' ? input.botAgentPreset.trim() : '';
|
const botAgentPreset = typeof input.botAgentPreset === 'string' ? input.botAgentPreset.trim() : '';
|
||||||
|
|
||||||
const withPreset = (peer) => {
|
const withPreset = (peer) => {
|
||||||
const fromGrant = resolveAccessAgentPreset(grant, {
|
const agentPreset = resolveChatAgentPreset(grant, {
|
||||||
kind: peer.kind === 'group' ? 'group' : 'direct',
|
kind: peer.kind === 'group' ? 'group' : 'direct',
|
||||||
groupJid: peer.kind === 'group' ? peer.conversationId : undefined,
|
groupJid: peer.kind === 'group' ? peer.conversationId : undefined,
|
||||||
conversationId: peer.conversationId,
|
conversationId: peer.conversationId,
|
||||||
phone: peer.phone,
|
phone: peer.phone,
|
||||||
senderId: peer.senderId || peer.phone,
|
senderId: peer.senderId || peer.phone,
|
||||||
});
|
}, botAgentPreset);
|
||||||
const agentPreset = fromGrant || botAgentPreset || null;
|
|
||||||
return Object.freeze({ ...peer, agentPreset });
|
return Object.freeze({ ...peer, agentPreset });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import {
|
||||||
normalizeMessageContent,
|
normalizeMessageContent,
|
||||||
} from '@whiskeysockets/baileys';
|
} from '@whiskeysockets/baileys';
|
||||||
|
|
||||||
import { emptyAccessGrant, ensureGroupBucket, normalizeAccessPhone, phoneFromWhatsappJid, resolveAccessAgentPreset } from '../shared/access-grant.mjs';
|
import { emptyAccessGrant, ensureGroupBucket, normalizeAccessPhone, phoneFromWhatsappJid, resolveChatAgentPreset } from '../shared/access-grant.mjs';
|
||||||
import { splitMessageText } from '../shared/editable-message-stream.mjs';
|
import { splitMessageText } from '../shared/editable-message-stream.mjs';
|
||||||
import { t } from '../shared/i18n.mjs';
|
import { t } from '../shared/i18n.mjs';
|
||||||
import { ImagePromptError } from '../shared/image-prompt.mjs';
|
import { ImagePromptError } from '../shared/image-prompt.mjs';
|
||||||
|
|
@ -833,7 +833,8 @@ export class WhatsappRuntime {
|
||||||
resolveAgentPreset: (message) => {
|
resolveAgentPreset: (message) => {
|
||||||
if (!this.#workspaces || !this.#botId) return null;
|
if (!this.#workspaces || !this.#botId) return null;
|
||||||
const grant = this.#workspaces.accessGrantFor(this.#botId);
|
const grant = this.#workspaces.accessGrantFor(this.#botId);
|
||||||
return resolveAccessAgentPreset(grant, {
|
// Member/group override, else WhatsApp workspace mapping — never Host global.
|
||||||
|
return resolveChatAgentPreset(grant, {
|
||||||
kind: message?.kind === 'group' ? 'group' : 'direct',
|
kind: message?.kind === 'group' ? 'group' : 'direct',
|
||||||
groupJid: message?.conversationId,
|
groupJid: message?.conversationId,
|
||||||
conversationId: message?.conversationId,
|
conversationId: message?.conversationId,
|
||||||
|
|
@ -842,7 +843,7 @@ export class WhatsappRuntime {
|
||||||
?? phoneFromWhatsappJid(message?.senderAlternateId)
|
?? phoneFromWhatsappJid(message?.senderAlternateId)
|
||||||
?? normalizeAccessPhone(message?.senderAlternateId),
|
?? normalizeAccessPhone(message?.senderAlternateId),
|
||||||
senderId: message?.senderId,
|
senderId: message?.senderId,
|
||||||
});
|
}, this.#workspaces.agentPresetFor(this.#botId));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import {
|
||||||
resolveAccessPending,
|
resolveAccessPending,
|
||||||
upsertAccessContact,
|
upsertAccessContact,
|
||||||
resolveAccessAgentPreset,
|
resolveAccessAgentPreset,
|
||||||
|
resolveChatAgentPreset,
|
||||||
validateAccessGrant,
|
validateAccessGrant,
|
||||||
} from '../../../src/channels/shared/access-grant.mjs';
|
} from '../../../src/channels/shared/access-grant.mjs';
|
||||||
|
|
||||||
|
|
@ -233,7 +234,7 @@ test('quote approval helpers parse intent, pending id, and notify refs', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
test('resolveAccessAgentPreset prefers direct-member and group overrides over global', () => {
|
test('resolveAccessAgentPreset prefers direct-member and group overrides over channel mapping', () => {
|
||||||
const doc = grant({
|
const doc = grant({
|
||||||
directMembers: [
|
directMembers: [
|
||||||
{ phone: '8618222222222', canExecuteCommands: true, agentPreset: 'ops-user' },
|
{ phone: '8618222222222', canExecuteCommands: true, agentPreset: 'ops-user' },
|
||||||
|
|
@ -266,6 +267,31 @@ test('resolveAccessAgentPreset prefers direct-member and group overrides over gl
|
||||||
}), null);
|
}), null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('resolveChatAgentPreset falls back to bot/channel mapping, not Host global', () => {
|
||||||
|
const doc = grant({
|
||||||
|
directMembers: [
|
||||||
|
{ phone: '8618222222222', canExecuteCommands: true, agentPreset: 'ops-user' },
|
||||||
|
{ phone: '8618333333333', canExecuteCommands: true },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
assert.equal(resolveChatAgentPreset(doc, {
|
||||||
|
kind: 'direct',
|
||||||
|
phone: '8618222222222',
|
||||||
|
}, 'wa-mapped'), 'ops-user');
|
||||||
|
assert.equal(resolveChatAgentPreset(doc, {
|
||||||
|
kind: 'direct',
|
||||||
|
phone: '8618333333333',
|
||||||
|
}, 'wa-mapped'), 'wa-mapped');
|
||||||
|
assert.equal(resolveChatAgentPreset(doc, {
|
||||||
|
kind: 'direct',
|
||||||
|
phone: '8618333333333',
|
||||||
|
}, null), null);
|
||||||
|
assert.equal(resolveChatAgentPreset(null, {
|
||||||
|
kind: 'direct',
|
||||||
|
phone: '8618333333333',
|
||||||
|
}, 'wa-mapped'), 'wa-mapped');
|
||||||
|
});
|
||||||
|
|
||||||
test('validateAccessGrant keeps optional agentPreset on members and groups', () => {
|
test('validateAccessGrant keeps optional agentPreset on members and groups', () => {
|
||||||
const doc = grant({
|
const doc = grant({
|
||||||
directMembers: [{ phone: '8618222222222', canExecuteCommands: false, agentPreset: 'desk' }],
|
directMembers: [{ phone: '8618222222222', canExecuteCommands: false, agentPreset: 'desk' }],
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue