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

@ -0,0 +1,101 @@
import * as React from 'react';
import { h } from './i18n.js';
export const SET_AGENT_PRESET_ENDPOINT = 'bot.preset.set';
const PRESET_ID = /^[a-z0-9][a-z0-9-]*$/;
export const EMPTY_AGENT_PRESET_CATALOG = Object.freeze({
defaultId: '',
items: Object.freeze([]),
});
export const AgentPresetCatalogContext = React.createContext(EMPTY_AGENT_PRESET_CATALOG);
export function normalizeAgentPresetId(value) {
if (typeof value !== 'string') return '';
const id = value.trim();
return PRESET_ID.test(id) ? id : '';
}
export function normalizeAgentPresetCatalog(value) {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return { defaultId: '', items: [] };
}
const items = [];
const seen = new Set();
for (const entry of Array.isArray(value.items) ? value.items : []) {
const id = typeof entry === 'string'
? normalizeAgentPresetId(entry)
: normalizeAgentPresetId(entry?.id);
if (!id || seen.has(id)) continue;
seen.add(id);
const label = typeof entry?.label === 'string' && entry.label.trim()
? entry.label.trim().slice(0, 128)
: typeof entry?.name === 'string' && entry.name.trim()
? entry.name.trim().slice(0, 128)
: id;
items.push({ id, label });
}
return {
defaultId: normalizeAgentPresetId(value.defaultId),
items,
};
}
export function AgentPresetEditor({ agentPreset = '', disabled = false, onSave }) {
const catalog = React.useContext(AgentPresetCatalogContext) ?? EMPTY_AGENT_PRESET_CATALOG;
const current = normalizeAgentPresetId(agentPreset);
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?.id || seen.has(item.id)) continue;
seen.add(item.id);
items.push(item);
}
if (current && !seen.has(current)) {
items.push({ id: current, label: current });
}
const inheritLabel = '跟随 Host 默认';
const change = async (event) => {
const next = event.target.value;
if (next === current || saving || disabled) return;
setSaving(true);
setError(null);
try {
await onSave?.(next || null);
} catch (cause) {
setError(cause?.message ?? 'Agent Preset 修改失败,请重试。');
} finally {
setSaving(false);
}
};
return h('div', { className: 'dim-preset' },
h('div', { className: 'dim-presetHeader' },
h('span', null, 'Agent Preset'),
saving ? h('span', { className: 'dim-presetStatus' }, '保存中…') : null),
React.createElement('select', {
className: 'dim-presetSelect',
value: current,
disabled: disabled || saving,
'aria-label': 'Agent Preset',
onChange: (event) => { void change(event); },
},
h('option', { value: '' }, inheritLabel),
...items.map((item) => h(
'option',
{ key: item.id, value: item.id },
item.label && item.label !== item.id ? `${item.label}(${item.id})` : item.id,
)),
),
h('small', { className: 'dim-presetHelp' }, '只影响之后新建的会话。'),
error ? h('p', { className: 'dim-presetError', role: 'alert' }, error) : null,
);
}