Files
Cyrene_v2/frontend/web/src/admin/modules/Plugins.tsx
T
Aska 6dbb4a2e18
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat: 唤醒式架构批次——capacity/L4/会话编排/管理面板写CRUD/前端集成(77-81)
【capacity 唤醒式核心(77)】internal/runtime/capacity:(Decider L1启发式+L2上下文+人格覆盖→三态静息/唤醒/全能→允许工具集+capacity_awaken事件)。loop 外围允许集过滤(不硬阻断=能力≠边界红线)。config 加 Capacity(Enabled/MinConfidence/ToolSets)。wire buildCapacity 接入 loop。

【L4 记忆驱动意图(80)】recall.go:从记忆召回用户习惯(搞一下xx=办事)→唤醒L1/L2漏掉/升置信;群聊陌生人不读owner习惯(隔离红线);RecordIntent闭环(判定写回记忆越用越准);默认保守静息。

【会话消息编排(78)】gateway/session_orchestrator.go:每会话(模式/pending有界5/tail上下文尾巴20);Submit非阻塞(idle立即/busy入队ack/满拒绝);完成回调按序续接(tail+新消息合并增量注入非全量重放);Interrupt中断;REST 202/409+WS session_busy/pending_ack事件;不重造任务引擎复用loop。

【前端集成(79)】ws.ts 加 capacity_awaken/session_busy/pending_ack 解析;store 加 awakenState/sessionBusy;TopBar/ChatShell 加唤醒态徽标+忙态提示(不阻塞输入框);状态组件 status.tsx;Miuix风格。

【管理面板写CRUD(81)】20个写端点(POST/PUT/DELETE /admin/*,owner+审计落库):personas(真持久化YAML)/users apikey(签发掩码+撤销)/iot控制(真下发)/automations(建/触发/删)/llm(test真调用+临时)/channels(connect/close/身份测试actor白名单)/plugins(卸载)/memories(search真检索)/sessions(export)。无存储如实501不伪造。拆4文件满足≤500红线。前端9模块接真写。

【wire收口】buildCapacity接入loop+OnCapacity事件;server.go newOrchestrator接hub。

实机验证:iot ac.living control 200 真实改变 on:true;observability/metrics 200、health ok;全仓 build/vet/test 47包绿+前端tsc/vite。
2026-09-06 21:54:40 +08:00

139 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from "react";
import { useAsyncData } from "../../lib/useAsyncData";
import { adminListPlugins, adminSetPluginPolicy, adminPluginInstall, adminPluginUninstall, getStoredRole } from "../../lib/api";
import type { AdminPluginRecord } from "../../lib/types";
import { useAutoReload } from "../hooks";
import { useAuditStore } from "../audit";
import { AdminTable, Notice, PageHeader, type AdminColumn } from "../components";
import { Button, Surface, Switch, Tag } from "../../components/primitives";
export function PluginsModule(): React.ReactNode {
const { data, loading, error, reload } = useAsyncData(adminListPlugins, []);
useAutoReload(reload, 10000);
const isOwner = getStoredRole() === "owner";
const log = useAuditStore((s) => s.log);
const [busy, setBusy] = useState<string | null>(null);
const [fail, setFail] = useState<string | null>(null);
const [msg, setMsg] = useState<string | null>(null);
const install = () => {
setBusy("__install__");
setFail(null);
setMsg(null);
adminPluginInstall({ name: "sample-plugin", kind: "tool", description: "手工安装示例" })
.then(() => {
log("plugin.install", "ok");
setMsg("插件安装成功");
reload();
})
.catch((err: unknown) => {
log("plugin.install", "error");
setFail(err instanceof Error ? err.message : "插件安装失败");
})
.finally(() => setBusy(null));
};
const uninstall = (name: string) => {
setBusy(name);
setFail(null);
setMsg(null);
adminPluginUninstall({ name })
.then(() => {
log("plugin.uninstall", "ok");
setMsg(`已卸载 ${name}`);
reload();
})
.catch((err: unknown) => {
log("plugin.uninstall", "error");
setFail(err instanceof Error ? err.message : "卸载失败");
})
.finally(() => setBusy(null));
};
const setPolicy = (name: string, allowInGroup: boolean, allowInStranger: boolean) => {
setBusy(name);
setFail(null);
adminSetPluginPolicy(name, allowInGroup, allowInStranger)
.then(() => {
log(`policy.set:${name}`, "ok");
reload();
})
.catch((err: unknown) => {
log(`policy.set:${name}`, "error");
setFail(err instanceof Error ? err.message : "策略更新失败");
})
.finally(() => setBusy(null));
};
const columns: AdminColumn<AdminPluginRecord>[] = [
{ key: "name", title: "名称", render: (p) => <span className="font-medium">{p.name}</span> },
{ key: "kind", title: "类型", render: (p) => <Tag tone="accent">{p.kind}</Tag> },
{ key: "desc", title: "描述", render: (p) => <span className="block max-w-sm truncate text-[var(--mu-text-mid)]">{p.description || "—"}</span> },
{
key: "policy",
title: "环境授权(owner)",
render: (p) => (
<span className="flex items-center gap-4">
<label className="flex items-center gap-1.5">
<Switch
checked={p.allowInGroup}
disabled={!isOwner || busy === p.name}
onChange={(v) => setPolicy(p.name, v, p.allowInStranger)}
/>
<span className="text-xs text-[var(--mu-text-mid)]">群聊</span>
</label>
<label className="flex items-center gap-1.5">
<Switch
checked={p.allowInStranger}
disabled={!isOwner || busy === p.name}
onChange={(v) => setPolicy(p.name, p.allowInGroup, v)}
/>
<span className="text-xs text-[var(--mu-text-mid)]">陌生人</span>
</label>
{busy === p.name ? <span className="text-xs text-[var(--mu-text-lo)]">保存中…</span> : null}
</span>
),
},
{
key: "actions",
title: "操作",
render: (p) => (
<Button size="sm" variant="ghost" disabled={!isOwner || busy !== null} onClick={() => uninstall(p.name)}>
卸载
</Button>
),
},
];
return (
<div className="space-y-5">
<PageHeader
title="插件与能力"
subtitle="GET /api/v1/admin/plugins · 启停/安装/卸载/MCP 连接 · POST /api/v1/admin/plugins/policy (owner-only)"
actions={
<Button variant="primary" disabled={busy !== null} onClick={install}>
安装插件
</Button>
}
/>
{!isOwner ? <Notice tone="warn">当前角色为 {getStoredRole()},工具策略仅 owner 可写(读取/查看可见)。</Notice> : null}
{fail ? <Notice tone="err">{fail}</Notice> : null}
{msg ? <Notice tone="ok">{msg}</Notice> : null}
<Surface className="space-y-2">
<AdminTable
columns={columns}
rows={loading ? null : data}
loading={loading}
error={error}
emptyTitle="无插件数据"
rowKey={(p) => p.name}
/>
</Surface>
<Notice>工具 schema / 技能 / MCP 服务器列表为后端 M10+ 提供;此页展示已注册能力及其环境可见性标记(群聊 / 陌生人默认关闭)。</Notice>
</div>
);
}