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 QQ_RPC_CHANNEL = '/qq';
export const QQ_ENDPOINTS = Object.freeze({
@ -9,6 +11,7 @@ export const QQ_ENDPOINTS = Object.freeze({
reconnectBot: 'bot.reconnect',
deleteBot: 'bot.delete',
setWorkspace: 'bot.workspace.set',
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
});
const PROVISION_STATES = new Set(['starting', 'pending', 'refreshing', 'connecting', 'connected', 'failed', 'cancelled']);
@ -82,6 +85,7 @@ function normalizeBot(value) {
connected,
state: connected ? 'connected' : state,
workspace: text(value.workspace, '', 4_096),
agentPreset: normalizeAgentPresetId(value.agentPreset),
bot: {
name: text(value.bot?.name, 'QQ机器人', 100),
appIdMasked: text(value.bot?.appIdMasked, '应用标识已安全保存', 140),
@ -117,6 +121,7 @@ export function normalizeSnapshot(value) {
bots,
totals: { configured: bots.length, connected: bots.filter((bot) => bot.connected).length },
provisioning: source.provisioning ? normalizeProvisioning(source.provisioning) : null,
agentPresetCatalog: normalizeAgentPresetCatalog(source.agentPresetCatalog),
...(testMessage ? { testMessage } : {}),
};
}

View file

@ -4,6 +4,11 @@ import { QqLogoGlyph } from '../../channel-logos.js';
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 { installDingtalkStyles } from '../dingtalk/styles.js';
import {
@ -156,6 +161,7 @@ export function AccountCard({
removing,
onReconnect,
onWorkspaceSave,
onAgentPresetSave,
onRequestRemove,
onConfirmRemove,
onCancelRemove,
@ -180,6 +186,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', {
@ -196,7 +207,10 @@ export function AccountCard({
}
export function QqSettingsTab({ rpcCall }) {
const [model, setModel] = React.useState({ phase: 'loading', bots: [], totals: { configured: 0, connected: 0 }, error: null });
const [model, setModel] = React.useState({
phase: 'loading', bots: [], totals: { configured: 0, connected: 0 }, error: null,
agentPresetCatalog: EMPTY_AGENT_PRESET_CATALOG,
});
const [provision, setProvision] = React.useState(null);
const [busy, setBusy] = React.useState(false);
const [busyByBot, setBusyByBot] = React.useState({});
@ -233,7 +247,10 @@ export function QqSettingsTab({ rpcCall }) {
const snapshot = normalizeSnapshot(await invoke(QQ_ENDPOINTS.status, {}, signal));
if (!mounted.current || signal?.aborted
|| !workspaceFence.canCommitStatus(workspaceVersion)) return undefined;
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 (restore && snapshot.provisioning) setProvision({
...snapshot.provisioning,
durationMs: Math.max(1, snapshot.provisioning.expiresAt - Date.now()),
@ -297,7 +314,10 @@ export function QqSettingsTab({ rpcCall }) {
));
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) {
@ -357,7 +377,10 @@ export function QqSettingsTab({ rpcCall }) {
try {
const snapshot = normalizeSnapshot(await invoke(endpoint, payload));
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,
});
}
return snapshot;
} finally {
@ -424,6 +447,12 @@ export function QqSettingsTab({ rpcCall }) {
QQ_ENDPOINTS.setWorkspace,
{ botId: account.botId, workspace },
),
onAgentPresetSave: (agentPreset) => botAction(
account,
'preset',
QQ_ENDPOINTS.setAgentPreset,
{ botId: account.botId, agentPreset },
),
onRequestRemove: () => setRemoveTarget(account.botId),
onCancelRemove: () => setRemoveTarget(null),
onConfirmRemove: async () => {
@ -447,7 +476,9 @@ export function QqSettingsTab({ rpcCall }) {
})
: null;
return h('section', { className: 'ddt-page dqq-page dim-channelPage', 'aria-label': 'QQ 设置' },
return h(AgentPresetCatalogContext.Provider, {
value: model.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
}, h('section', { className: 'ddt-page dqq-page dim-channelPage', 'aria-label': 'QQ 设置' },
h(Heading, {
totals: model.totals,
adding: Boolean(provision),
@ -465,7 +496,7 @@ export function QqSettingsTab({ rpcCall }) {
provisionView,
model.bots.length === 0 && !provision && !credentialOpen
? h(EmptyView, { busy, onStart: () => void startProvisioning() }) : null,
botList));
botList)));
}
export function apply(ctx) {