feat: 按机器人选择 Agent Preset

在九个渠道的设置页卡片上选择 Host 已有预设;未选则跟随默认,只影响之后新建的会话。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
iamyaojie 2026-08-21 19:19:45 +08:00
parent c3b3a29f27
commit 3161816cc4
51 changed files with 2227 additions and 740 deletions

View file

@ -1,3 +1,5 @@
import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
export const DINGTALK_RPC_CHANNEL = '/dingtalk';
export const DINGTALK_ENDPOINTS = Object.freeze({
@ -9,6 +11,7 @@ export const DINGTALK_ENDPOINTS = Object.freeze({
reconnectBot: 'bot.reconnect',
deleteBot: 'bot.delete',
setWorkspace: 'bot.workspace.set',
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
});
const ACCOUNT_STATES = new Set(['connected', 'connecting', 'offline', 'error']);
@ -157,6 +160,7 @@ function normalizeBot(value) {
connected,
configured: value.configured !== false,
workspace: optionalString(value.workspace, 4_096) ?? '',
agentPreset: normalizeAgentPresetId(value.agentPreset),
bot: {
name: optionalString(bot.name, 100) ?? '钉钉机器人',
clientIdMasked: optionalString(bot.clientIdMasked, 140) ?? '已安全保存',
@ -200,6 +204,7 @@ export function normalizeSnapshot(value) {
},
provisioning: source.provisioning ? normalizeProvisioning(source.provisioning) : null,
testMessage: normalizeTestMessage(source.testMessage),
agentPresetCatalog: normalizeAgentPresetCatalog(source.agentPresetCatalog),
};
}

View file

@ -3,6 +3,11 @@ import * as React from 'react';
import { CredentialActionIcon, CredentialBindingPanel, QrActionIcon } from '../../credential-binding.js';
import { h } from '../../i18n.js';
import { WorkspaceEditor } from '../../workspace-editor.js';
import {
AgentPresetCatalogContext,
AgentPresetEditor,
EMPTY_AGENT_PRESET_CATALOG,
} from '../../agent-preset.js';
import { useWorkspaceSnapshotFence } from '../../workspace-snapshot-fence.js';
import {
DINGTALK_ENDPOINTS,
@ -213,6 +218,7 @@ export function AccountCard({
removing,
onReconnect,
onWorkspaceSave,
onAgentPresetSave,
onRequestRemove,
onConfirmRemove,
onCancelRemove,
@ -241,6 +247,11 @@ export function AccountCard({
disabled: Boolean(busy),
onSave: onWorkspaceSave,
}),
h(AgentPresetEditor, {
agentPreset: account.agentPreset,
disabled: Boolean(busy),
onSave: onAgentPresetSave,
}),
h('div', { className: 'ddt-accountFooter dim-cardFooter' },
summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
feedback ? h('div', {
@ -272,6 +283,7 @@ function AccountList(props) {
removing: props.removeTarget === account.botId,
onReconnect: () => props.onReconnect(account),
onWorkspaceSave: (workspace) => props.onWorkspaceSave(account, workspace),
onAgentPresetSave: (agentPreset) => props.onAgentPresetSave(account, agentPreset),
onRequestRemove: () => props.onRequestRemove(account),
onConfirmRemove: () => props.onConfirmRemove(account),
onCancelRemove: props.onCancelRemove,
@ -283,6 +295,7 @@ const EMPTY_TOTALS = Object.freeze({ configured: 0, connected: 0 });
export function DingtalkSettingsTab({ rpcCall }) {
const [model, setModel] = React.useState({
phase: 'loading', bots: [], totals: EMPTY_TOTALS, revision: 0, error: null,
agentPresetCatalog: EMPTY_AGENT_PRESET_CATALOG,
});
const [provision, setProvision] = React.useState(null);
const [busy, setBusy] = React.useState(false);
@ -389,6 +402,7 @@ export function DingtalkSettingsTab({ rpcCall }) {
totals: snapshot.totals,
revision: snapshot.revision,
error: null,
agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
});
discardStaleFeedback(snapshot);
if (restoreProvisioning && snapshot.provisioning) {
@ -504,6 +518,7 @@ export function DingtalkSettingsTab({ rpcCall }) {
totals: snapshot.totals,
revision: snapshot.revision,
error: null,
agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
});
discardStaleFeedback(snapshot);
}
@ -638,6 +653,7 @@ export function DingtalkSettingsTab({ rpcCall }) {
totals: snapshot.totals,
revision: snapshot.revision,
error: null,
agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
});
discardStaleFeedback(snapshot);
}
@ -700,6 +716,33 @@ export function DingtalkSettingsTab({ rpcCall }) {
totals: snapshot.totals,
revision: snapshot.revision,
error: null,
agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
});
discardStaleFeedback(snapshot);
}
} finally {
const shouldRefresh = workspaceFence.endMutation();
if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
if (mountedRef.current) setBotBusy(account.botId, null);
}
}, [discardStaleFeedback, invoke, loadStatus, setBotBusy, workspaceFence]);
const saveAgentPreset = React.useCallback(async (account, agentPreset) => {
const snapshotVersion = workspaceFence.beginMutation();
setBotBusy(account.botId, 'preset');
try {
const snapshot = normalizeSnapshot(await invoke(
DINGTALK_ENDPOINTS.setAgentPreset,
{ botId: account.botId, agentPreset },
));
if (mountedRef.current && workspaceFence.canCommitMutation(snapshotVersion)) {
setModel({
phase: 'ready',
bots: snapshot.bots,
totals: snapshot.totals,
revision: snapshot.revision,
error: null,
agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
});
discardStaleFeedback(snapshot);
}
@ -762,7 +805,9 @@ export function DingtalkSettingsTab({ rpcCall }) {
})
: null;
return h('section', { className: 'ddt-page dim-channelPage', 'aria-label': '钉钉设置' },
return h(AgentPresetCatalogContext.Provider, {
value: model.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
}, h('section', { className: 'ddt-page dim-channelPage', 'aria-label': '钉钉设置' },
h(Heading, {
totals: model.totals,
adding: Boolean(provision),
@ -798,11 +843,12 @@ export function DingtalkSettingsTab({ rpcCall }) {
removeTarget,
onReconnect: (account) => void reconnect(account),
onWorkspaceSave: saveWorkspace,
onAgentPresetSave: saveAgentPreset,
onRequestRemove: (account) => setRemoveTarget(account.botId),
onConfirmRemove: (account) => void remove(account),
onCancelRemove: () => setRemoveTarget(null),
})
: null));
: null)));
}
export function apply(ctx) {