File size: 5,348 Bytes
1140c43
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
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>;
}