Keep IM tool approvals in chat and add WhatsApp default model.

Claim in-flight IM turns before ownership flips active so approvals no longer stick only in DSH Web, auto-recover stale session models once, and let WhatsApp bots pick a shared default model for new sessions.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-15 20:02:45 +08:00
parent 2723e94541
commit 74c58bdcde
21 changed files with 1388 additions and 395 deletions

View file

@ -1,4 +1,9 @@
import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
import {
normalizeDefaultModelSelection,
normalizeHostModelCatalog,
SET_DEFAULT_MODEL_ENDPOINT,
} from '../../default-model.js';
import { normalizeLastMessageError } from '../../last-message-error.js';
import { normalizeAccessGrant } from '../../../../src/channels/shared/access-grant.mjs';
import { normalizeAccessPolicy } from '../../../../src/channels/shared/access-policy.mjs';
@ -21,6 +26,7 @@ export const WHATSAPP_ENDPOINTS = Object.freeze({
setGroupSessionScope: 'bot.group-session-scope.set',
setWorkspace: 'bot.workspace.set',
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
setDefaultModel: SET_DEFAULT_MODEL_ENDPOINT,
setContextEnhancement: 'bot.context-enhancement.set',
resolveChannelPeer: 'bot.session.channel-peer',
});
@ -98,6 +104,7 @@ function normalizeBot(value) {
state: connected ? 'connected' : state,
workspace: text(value.workspace, '', 4_096),
agentPreset: normalizeAgentPresetId(value.agentPreset),
defaultModel: normalizeDefaultModelSelection(value.defaultModel),
contextEnhancement: normalizeContextEnhancementConfig(value.contextEnhancement),
...(Object.hasOwn(value, 'accessPolicy')
? { accessPolicy: normalizeAccessPolicy(value.accessPolicy) }
@ -137,6 +144,7 @@ export function normalizeSnapshot(value) {
totals: { configured: bots.length, connected: bots.filter((bot) => bot.connected).length },
provisioning: source.provisioning ? normalizeProvisioning(source.provisioning) : null,
agentPresetCatalog: normalizeAgentPresetCatalog(source.agentPresetCatalog),
modelCatalog: normalizeHostModelCatalog(source.modelCatalog),
};
}

View file

@ -10,6 +10,11 @@ import {
AgentPresetEditor,
EMPTY_AGENT_PRESET_CATALOG,
} from '../../agent-preset.js';
import {
DefaultModelEditor,
EMPTY_MODEL_CATALOG,
ModelCatalogContext,
} from '../../default-model.js';
import { useWorkspaceSnapshotFence } from '../../workspace-snapshot-fence.js';
import {
BotSettingsButton,
@ -186,6 +191,7 @@ export function WhatsappAccountCard({
onReconnect,
onWorkspaceSave,
onAgentPresetSave,
onDefaultModelSave,
onContextEnhancementSave,
onRequestRemove,
onConfirmRemove,
@ -233,6 +239,11 @@ export function WhatsappAccountCard({
disabled: Boolean(busy),
onSave: onAgentPresetSave,
}),
h(DefaultModelEditor, {
defaultModel: account.defaultModel,
disabled: Boolean(busy),
onSave: onDefaultModelSave,
}),
h(ContextEnhancementEditor, {
config: account.contextEnhancement,
disabled: Boolean(busy),
@ -268,6 +279,7 @@ export function WhatsappSettingsTab({ rpcCall }) {
const [model, setModel] = React.useState({
phase: 'loading', bots: [], totals: { configured: 0, connected: 0 }, error: null,
agentPresetCatalog: EMPTY_AGENT_PRESET_CATALOG,
modelCatalog: EMPTY_MODEL_CATALOG,
});
const [provision, setProvision] = React.useState(null);
const [busy, setBusy] = React.useState(false);
@ -306,6 +318,7 @@ export function WhatsappSettingsTab({ rpcCall }) {
setModel({
phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null,
agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
modelCatalog: snapshot.modelCatalog ?? EMPTY_MODEL_CATALOG,
});
if (restore && snapshot.provisioning) setProvision({
...snapshot.provisioning,
@ -437,6 +450,7 @@ export function WhatsappSettingsTab({ rpcCall }) {
setModel({
phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null,
agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
modelCatalog: snapshot.modelCatalog ?? EMPTY_MODEL_CATALOG,
});
if (operation === 'reconnect') {
setTestNoticeByBot((current) => ({
@ -495,6 +509,12 @@ export function WhatsappSettingsTab({ rpcCall }) {
WHATSAPP_ENDPOINTS.setAgentPreset,
{ botId: account.botId, agentPreset },
),
onDefaultModelSave: (defaultModel) => botAction(
account,
'model',
WHATSAPP_ENDPOINTS.setDefaultModel,
{ botId: account.botId, defaultModel },
),
onContextEnhancementSave: (config) => botAction(
account,
'context-enhancement',
@ -515,6 +535,8 @@ export function WhatsappSettingsTab({ rpcCall }) {
return h(AgentPresetCatalogContext.Provider, {
value: model.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
}, h(ModelCatalogContext.Provider, {
value: model.modelCatalog ?? EMPTY_MODEL_CATALOG,
}, h('section', {
className: 'ddt-page dwa-page dim-channelPage',
'aria-label': 'WhatsApp 设置',
@ -552,5 +574,5 @@ export function WhatsappSettingsTab({ rpcCall }) {
: model.bots.length === 0
? h(EmptyView, { busy, onStart: () => void startProvisioning(false) })
: null,
botList)));
botList))));
}

View file

@ -0,0 +1,105 @@
import * as React from 'react';
import {
defaultModelKey,
normalizeDefaultModel,
normalizeModelCatalog,
} from '../../src/channels/shared/default-model.mjs';
import { h } from './i18n.js';
export const SET_DEFAULT_MODEL_ENDPOINT = 'bot.model.set';
export const EMPTY_MODEL_CATALOG = Object.freeze({
groups: Object.freeze([]),
failures: Object.freeze([]),
items: Object.freeze([]),
});
export const ModelCatalogContext = React.createContext(EMPTY_MODEL_CATALOG);
export function normalizeDefaultModelSelection(value) {
return normalizeDefaultModel(value);
}
export function normalizeHostModelCatalog(value) {
return normalizeModelCatalog(value);
}
export function DefaultModelEditor({ defaultModel = null, disabled = false, onSave }) {
const catalog = React.useContext(ModelCatalogContext) ?? EMPTY_MODEL_CATALOG;
const helpId = React.useId();
const current = normalizeDefaultModel(defaultModel);
const currentKey = defaultModelKey(current);
const [saving, setSaving] = React.useState(false);
const [error, setError] = React.useState(null);
const items = [];
const seen = new Set();
for (const item of Array.isArray(catalog.items) ? catalog.items : []) {
if (!item?.key || seen.has(item.key)) continue;
seen.add(item.key);
items.push(item);
}
const currentUnavailable = Boolean(currentKey && !seen.has(currentKey));
if (currentUnavailable && current) {
items.push({
key: currentKey,
provider: current.provider,
model: current.model,
label: currentKey,
unavailable: true,
});
}
const change = async (event) => {
const nextKey = event.target.value;
if (nextKey === currentKey || saving || disabled) return;
setSaving(true);
setError(null);
try {
await onSave?.(nextKey ? normalizeDefaultModel(nextKey) : null);
} catch (cause) {
setError(cause?.message ?? '默认模型修改失败,请重试。');
} finally {
setSaving(false);
}
};
return h('div', { className: 'dim-preset' },
h('div', { className: 'dim-presetHeader' },
h('span', { className: 'dim-presetTitle' },
h('span', null, '默认模型'),
h('span', { className: 'dim-presetHelp' },
h('button', {
type: 'button',
className: 'dim-presetHelpButton',
'aria-label': '查看默认模型说明',
'aria-describedby': helpId,
}, h('span', { 'aria-hidden': 'true' }, '?')),
h('span', {
id: helpId,
className: 'dim-presetTooltip',
role: 'tooltip',
}, '为该 WhatsApp 机器人的新建会话指定模型,避免每人各自 /model。已有会话需先 /new 再发消息才会生效。'))),
saving ? h('span', { className: 'dim-presetStatus' }, '保存中…') : null),
React.createElement('select', {
className: 'dim-presetSelect',
value: currentKey,
disabled: disabled || saving,
'aria-label': '默认模型',
onChange: (event) => { void change(event); },
},
h('option', { value: '' }, '跟随 Host 默认'),
...items.map((item) => h(
'option',
{ key: item.key, value: item.key },
item.unavailable ? [item.key, '(已不可用)'] : item.label,
)),
),
error || currentUnavailable ? h(
'p',
{ className: 'dim-presetError', role: error ? 'alert' : 'status' },
error ?? '当前默认模型已不可用,请选择其他模型或跟随 Host 默认。',
) : null,
);
}