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';
const ACCOUNT_STATES = new Set(['connected', 'connecting', 'offline', 'error']);
function isRecord(value) {
@ -25,6 +27,7 @@ export const TOKEN_BOT_ENDPOINTS = Object.freeze({
reconnectBot: 'bot.reconnect',
deleteBot: 'bot.delete',
setWorkspace: 'bot.workspace.set',
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
});
export function createTokenChannelApi(channel, connectionSummary, {
@ -52,6 +55,7 @@ export function createTokenChannelApi(channel, connectionSummary, {
connected,
state: connected ? 'connected' : state,
workspace: text(value.workspace, '', 4_096),
agentPreset: normalizeAgentPresetId(value.agentPreset),
bot: {
name: text(value.bot?.name, `${channel}机器人`, 100),
username: text(value.bot?.username, '', 100),
@ -82,6 +86,7 @@ export function createTokenChannelApi(channel, connectionSummary, {
revision: Number.isSafeInteger(source.revision) ? source.revision : 0,
bots,
totals: { configured: bots.length, connected: bots.filter((bot) => bot.connected).length },
agentPresetCatalog: normalizeAgentPresetCatalog(source.agentPresetCatalog),
};
};

View file

@ -4,6 +4,11 @@ import { CredentialActionIcon, CredentialBindingPanel } from '../../credential-b
import { h } from '../../i18n.js';
import { installDingtalkStyles } from '../dingtalk/styles.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';
const Button = React.forwardRef(function Button(
@ -63,7 +68,7 @@ export function createTokenChannelSettings(definition) {
accountSettingsEndpoint = null,
} = definition;
function AccountCard({ account, busy, testNotice, removing, onReconnect, onWorkspaceSave, onAccountSettingsSave, onRequestRemove, onConfirmRemove, onCancelRemove }) {
function AccountCard({ account, busy, testNotice, removing, onReconnect, onWorkspaceSave, onAgentPresetSave, onAccountSettingsSave, onRequestRemove, onConfirmRemove, onCancelRemove }) {
const state = busy === 'reconnect' ? 'connecting' : account.state;
const tone = account.connected ? 'success' : state === 'error' ? 'error' : 'warning';
const stateLabel = account.connected ? '运行正常' : state === 'connecting' ? '正在连接' : '连接未就绪';
@ -90,6 +95,11 @@ export function createTokenChannelSettings(definition) {
disabled: Boolean(busy),
onSave: onWorkspaceSave,
}),
h(AgentPresetEditor, {
agentPreset: account.agentPreset,
disabled: Boolean(busy),
onSave: onAgentPresetSave,
}),
AccountSettings ? h(AccountSettings, {
account,
busy: Boolean(busy),
@ -122,6 +132,7 @@ export function createTokenChannelSettings(definition) {
function SettingsTab({ rpcCall }) {
const [model, setModel] = React.useState({
phase: 'loading', bots: [], totals: { configured: 0, connected: 0 }, error: null,
agentPresetCatalog: EMPTY_AGENT_PRESET_CATALOG,
});
const [credentialOpen, setCredentialOpen] = React.useState(false);
const [credentialError, setCredentialError] = React.useState(null);
@ -156,7 +167,10 @@ export function createTokenChannelSettings(definition) {
const snapshot = api.normalizeSnapshot(await invoke(endpoints.status, {}, signal));
if (!mounted.current || signal?.aborted
|| !workspaceFence.canCommitStatus(workspaceVersion)) return;
setModel({ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null });
setModel({
phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null,
agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
});
} catch (error) {
if (error?.name !== 'AbortError' && mounted.current && !signal?.aborted
&& workspaceFence.canCommitStatus(workspaceVersion)) {
@ -199,7 +213,10 @@ export function createTokenChannelSettings(definition) {
));
if (!mounted.current) return;
if (workspaceFence.canCommitMutation(snapshotVersion)) {
setModel({ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null });
setModel({
phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null,
agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
});
}
setCredentialOpen(false);
} catch (error) {
@ -218,7 +235,10 @@ export function createTokenChannelSettings(definition) {
const value = await invoke(endpoint, payload);
const snapshot = api.normalizeSnapshot(value);
if (mounted.current && workspaceFence.canCommitMutation(snapshotVersion)) {
setModel({ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null });
setModel({
phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null,
agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
});
}
if (mounted.current && operation === 'reconnect') {
setTestNoticeByBot((current) => ({
@ -267,6 +287,12 @@ export function createTokenChannelSettings(definition) {
endpoints.setWorkspace,
{ botId: account.botId, workspace },
),
onAgentPresetSave: (agentPreset) => botAction(
account,
'preset',
endpoints.setAgentPreset,
{ botId: account.botId, agentPreset },
),
onAccountSettingsSave: AccountSettings && accountSettingsEndpoint
? (payload) => botAction(
account,
@ -287,7 +313,9 @@ export function createTokenChannelSettings(definition) {
})))))
: null;
return h('section', {
return h(AgentPresetCatalogContext.Provider, {
value: model.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
}, h('section', {
className: `ddt-page ${pageClass} dim-channelPage`,
'aria-label': `${channel} 设置`,
},
@ -353,7 +381,7 @@ export function createTokenChannelSettings(definition) {
'aria-hidden': 'true',
}, h(LogoGlyph, { size: 64 }))))
: null,
botList));
botList)));
}
return { SettingsTab, AccountCard };