message / apps /web /src /components /features /NativeConfigurationEditor.tsx
dot
Deliver native DSH market and authenticated cloud MCP with HF deployment
1140c43
Raw History Blame Contribute Delete
5.35 kB
import { useState, type FormEvent } from 'react';
import { Button } from '@components/ui/button';
import { Label } from '@components/ui/label';
import type { NativePlugin } from '@typings/native';
const field = 'w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring';
const reasons: Record<string, string> = {
'transport-managed-by-host': 'MCP 传输由宿主管理,不能通过插件配置修改',
'management-required': '系统核心条目由宿主管理',
unaddressable: '此条目没有可写的 profile 配置行',
};
const entryId = (plugin: NativePlugin) => plugin.entry_id ?? plugin.entryId ?? '';
interface ConfigurationEditorProps {
plugins: NativePlugin[];
busy: boolean;
onConfigure: (id: string, name: string, config: Record<string, unknown>) => void;
}
/** 配置写入期望状态;用户明确提交后才进入异步确认流程。 */
export function NativeConfigurationEditor({ plugins, busy, onConfigure }: ConfigurationEditorProps) {
const [selectedId, setSelectedId] = useState('');
const [draft, setDraft] = useState('{}');
const [error, setError] = useState('');
const selected = plugins.find(plugin => entryId(plugin) === selectedId);
const readOnlyReason = selected?.readOnlyReason
? reasons[selected.readOnlyReason] ?? selected.readOnlyReason
: !selected || !entryId(selected) ? '请选择具有完整条目标识的插件'
: !selected.enabled ? '请先在期望配置中启用该 bundle,再编辑配置'
: selected.config === undefined ? '服务端尚未提供当前配置,请刷新后重试,避免覆盖已有值' : '';
const restore = (plugin = selected) => {
setDraft(JSON.stringify(plugin?.config ?? {}, null, 2));
setError('');
};
const submit = (event: FormEvent) => {
event.preventDefault();
if (!selected || readOnlyReason || busy) return;
try {
const value: unknown = JSON.parse(draft);
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error();
setError('');
onConfigure(entryId(selected), selected.moduleName, value as Record<string, unknown>);
} catch { setError('配置必须是有效的 JSON 对象,例如 {"prefix":"示例:"}'); }
};
return <form onSubmit={submit} className="space-y-3 border-t border-console-border pt-5" aria-labelledby="plugin-config-heading">
<h3 id="plugin-config-heading" className="text-sm font-semibold">编辑插件配置</h3>
<p className="text-sm text-console-text-secondary">选择可编辑的插件条目,提交会替换该条目的完整 JSON 配置。通用 JSON 禁止填写字面密码、token、key 等真实凭据;需要凭据的字段请使用环境变量引用。变更先进入期望状态,不立即中断会话,重启后才生效。</p>
<div className="rounded-md bg-console-surface-raised p-3 text-xs leading-relaxed text-console-text-secondary">
<p>引用必须是精确的 JSON 对象:<span className="font-mono">{'{"$env":"SERVICE_TOKEN"}'}</span>。这里只填写引用名,请勿输入真实值。</p>
<p className="mt-2">名称必须位于运营者 MESSAGE_PLUGIN_ENV_ALLOWLIST_JSON 为当前 profile 配置的允许名单中。实际值由运营者环境提供,仅注入该 profile 的 worker;配置界面、API 和操作历史只保存或显示引用。引用未获允许或实际值缺失时,启动将失败。</p>
<p className="mt-2">页面只检查 JSON 对象结构;引用规则及插件配置类型由后端验证。这里不提供凭据保存功能。</p>
</div>
<div><Label htmlFor="config-plugin">配置目标条目</Label>
<select id="config-plugin" className={`${field} mt-2`} value={selectedId} disabled={busy} onChange={event => {
const id = event.target.value; setSelectedId(id); restore(plugins.find(plugin => entryId(plugin) === id));
}}><option value="">请选择插件条目</option>{plugins.map((plugin, index) => <option key={entryId(plugin) || index} value={entryId(plugin)} disabled={!entryId(plugin)}>{plugin.moduleName} · {entryId(plugin) || '无条目标识'}{plugin.readOnlyReason ? '(只读)' : ''}</option>)}</select>
</div>
{selected && <>
<p className="break-all text-xs text-console-text-secondary">条目 {entryId(selected)} · 配置行 {selected.row_id ?? selected.patchId ?? '—'}</p>
{readOnlyReason && <p role="note" className="rounded-md bg-console-surface-raised p-3 text-sm">只读:{readOnlyReason}</p>}
<div><Label htmlFor="plugin-config-json">插件配置 JSON</Label><textarea id="plugin-config-json" className={`${field} mt-2 font-mono`} rows={8} spellCheck={false} value={draft} onChange={event => { setDraft(event.target.value); setError(''); }} readOnly={Boolean(readOnlyReason)} disabled={busy} /></div>
{error && <p role="alert" className="text-sm text-red-700">{error}</p>}
<div className="flex flex-wrap gap-2"><Button type="submit" disabled={busy || Boolean(readOnlyReason)}>提交配置</Button><Button type="button" variant="outline" onClick={() => restore()} disabled={busy}>恢复当前配置</Button></div>
</>}
{!plugins.length && <p className="text-sm text-console-text-secondary">暂无插件条目,请先安装 bundle 并读取 profile 状态</p>}
</form>;
}