feat: 管理面板补齐(RAG导入/记忆添加/会话接history/诚实表述) + design/16 §4.6 IM多会话隔离+第三方身份+黑白名单
agent-98 面板批次:
- 会话管理面板改接 history.ListSessions(主web+次级) + 新建次级会话按钮
- 记忆面板手动添加入口 POST /admin/memories(owner)
- 知识库真正实现 IndexDocument(分块+落projectindex) + POST /admin/rag 导入端点
- 三面板空态诚实表述(去除'后端M10+提供')
- RAG嵌入降级到keyword/line(无直属embedder,不伪造语义,符合红线)
agent-99 design/16 §4.6:
- model.Message.SenderName + 迁移0006 sender_name 列
- 第三方身份边界: session.ResolveActor判定(非LLM猜) + [第三方·昵称]前缀(不改源content)
- 群聊/陌生人落独立sess-xxx(persistIsolated),不写主会话,不广播
- 黑/白名单(allowGroups/denyGroups/allowUsers/denyUsers)最前拦截,不进LLM
- 多账号 CHANNELS_JSON 配置 + 每平台多实例
- 管理只读 GET /admin/channels/{platform}/messages(该平台非主人消息流)
实机验证(JWT): 会话管理返回主会话web / 记忆添加成功 / RAG导入documents非空+Cyrene命中; 后端build/vet/test全绿 + 前端build绿
This commit is contained in:
1 parent
37e98ec584
commit
9c2a37c4ac
32 files changed
+1768
-136
No files matched your search
@@ -1,18 +1,18 @@
|
||||
import { useState } from "react";
|
||||
import { useAsyncData } from "../../lib/useAsyncData";
|
||||
import { adminListSessions, adminExportSession } from "../../lib/api";
|
||||
import type { SessionRecord } from "../../lib/types";
|
||||
import { adminListSessions, createSubSession } from "../../lib/api";
|
||||
import type { SessionMeta } from "../../lib/types";
|
||||
import { useAutoReload } from "../hooks";
|
||||
import { useAuditStore } from "../audit";
|
||||
import { AdminTable, Notice, PageHeader, type AdminColumn } from "../components";
|
||||
import { Button, Surface, Tag } from "../../components/primitives";
|
||||
|
||||
function exportBlob(sess: SessionRecord): void {
|
||||
function exportMeta(sess: SessionMeta): void {
|
||||
const blob = new Blob([JSON.stringify(sess, null, 2)], { type: "application/json" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `session-${sess.sessionId}.json`;
|
||||
a.download = `session-${sess.id}.json`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
@@ -29,49 +29,45 @@ export function SessionsModule(): React.ReactNode {
|
||||
if (!query.trim()) return true;
|
||||
const q = query.toLowerCase();
|
||||
return (
|
||||
s.platform.toLowerCase().includes(q) ||
|
||||
s.sessionId.toLowerCase().includes(q) ||
|
||||
s.channelId.toLowerCase().includes(q) ||
|
||||
s.actorRole.toLowerCase().includes(q)
|
||||
s.id.toLowerCase().includes(q) ||
|
||||
s.type.toLowerCase().includes(q) ||
|
||||
(s.title ?? "").toLowerCase().includes(q)
|
||||
);
|
||||
});
|
||||
|
||||
const exportSession = (s: SessionRecord) => {
|
||||
setBusy(s.sessionId);
|
||||
const newSubSession = () => {
|
||||
setBusy("new");
|
||||
setFail(null);
|
||||
adminExportSession(s.sessionId)
|
||||
.then(({ session }) => {
|
||||
log("session.export", "ok");
|
||||
exportBlob(session);
|
||||
createSubSession()
|
||||
.then(() => {
|
||||
log("session.create", "ok");
|
||||
reload();
|
||||
})
|
||||
.catch((e: unknown) => {
|
||||
log("session.export", "error");
|
||||
setFail(e instanceof Error ? e.message : "导出失败");
|
||||
log("session.create", "error");
|
||||
setFail(e instanceof Error ? e.message : "新建次级会话失败");
|
||||
})
|
||||
.finally(() => setBusy(null));
|
||||
};
|
||||
|
||||
const columns: AdminColumn<SessionRecord>[] = [
|
||||
{ key: "platform", title: "渠道", render: (s) => <Tag tone="accent">{s.platform}</Tag> },
|
||||
{ key: "actor", title: "actor 元数据", render: (s) => <Tag tone="neutral">{s.actorRole}</Tag> },
|
||||
const columns: AdminColumn<SessionMeta>[] = [
|
||||
{ key: "type", title: "类型", render: (s) => <Tag tone={s.type === "main" ? "ok" : "accent"}>{s.type === "main" ? "主会话" : "次级会话"}</Tag> },
|
||||
{ key: "title", title: "标题", render: (s) => <span>{s.title ? s.title : <span className="text-[var(--mu-text-lo)]">(未命名)</span>}</span> },
|
||||
{
|
||||
key: "flags",
|
||||
title: "标记",
|
||||
render: (s) => (
|
||||
<span className="flex flex-wrap gap-1">
|
||||
{s.owner ? <Tag tone="ok">主人</Tag> : null}
|
||||
{s.isGroup ? <Tag tone="warn">群聊</Tag> : null}
|
||||
<Tag tone={s.memWritable ? "ok" : "neutral"}>{s.memWritable ? "记忆可写" : "记忆只读"}</Tag>
|
||||
</span>
|
||||
),
|
||||
key: "id",
|
||||
title: "会话 ID",
|
||||
render: (s) => <span className="font-mono text-xs">{s.id}</span>,
|
||||
},
|
||||
{
|
||||
key: "createdAt",
|
||||
title: "创建时间",
|
||||
render: (s) => <span className="text-xs">{new Date(s.createdAt).toLocaleString()}</span>,
|
||||
},
|
||||
{ key: "channel", title: "channelId", render: (s) => <span className="font-mono text-xs text-[var(--mu-text-lo)]">{s.channelId}</span> },
|
||||
{ key: "session", title: "sessionId", render: (s) => <span className="font-mono text-xs">{s.sessionId}</span> },
|
||||
{
|
||||
key: "actions",
|
||||
title: "操作",
|
||||
render: (s) => (
|
||||
<Button size="sm" variant="ghost" disabled={busy !== null} onClick={() => exportSession(s)}>
|
||||
<Button size="sm" variant="ghost" disabled={busy !== null} onClick={() => exportMeta(s)}>
|
||||
导出
|
||||
</Button>
|
||||
),
|
||||
@@ -82,7 +78,7 @@ export function SessionsModule(): React.ReactNode {
|
||||
<div className="space-y-5">
|
||||
<PageHeader
|
||||
title="会话管理"
|
||||
subtitle="GET /api/v1/admin/sessions + GET /sessions/{id}/export · 渠道/模式/角色/分页 · 身份识别靠 channel 层 actor 元数据"
|
||||
subtitle="GET /api/v1/admin/sessions · 持久化主会话 + 次级会话(design/16 §2)· 主会话固定存在,次级会话独立保留上下文 · 身份识别靠 channel 层 actor 元数据"
|
||||
actions={
|
||||
<Button variant="soft" disabled={busy !== null} onClick={() => reload()}>
|
||||
刷新
|
||||
@@ -93,23 +89,30 @@ export function SessionsModule(): React.ReactNode {
|
||||
{fail ? <Notice tone="err">{fail}</Notice> : null}
|
||||
|
||||
<Surface className="space-y-3">
|
||||
<input
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="按渠道 / sessionId / channelId / actor 过滤…"
|
||||
className="w-full rounded-[var(--mu-radius)] bg-[var(--mu-surface-2)] px-3 py-2 text-sm outline-none focus:ring-1 focus:ring-[var(--mu-accent)]"
|
||||
/>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<input
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="按会话 ID / 类型 / 标题过滤…"
|
||||
className="min-w-0 flex-1 rounded-[var(--mu-radius)] bg-[var(--mu-surface-2)] px-3 py-2 text-sm outline-none focus:ring-1 focus:ring-[var(--mu-accent)]"
|
||||
/>
|
||||
<Button variant="primary" disabled={busy !== null} onClick={newSubSession}>
|
||||
新建次级会话
|
||||
</Button>
|
||||
</div>
|
||||
<AdminTable
|
||||
columns={columns}
|
||||
rows={loading ? null : filtered}
|
||||
loading={loading}
|
||||
error={error}
|
||||
emptyTitle="无会话数据"
|
||||
rowKey={(s) => s.sessionId}
|
||||
emptyTitle="暂无会话:主会话 web 固定存在;用侧栏新建次级会话"
|
||||
rowKey={(s) => s.id}
|
||||
/>
|
||||
</Surface>
|
||||
|
||||
<Notice>导出当前为会话身份元数据 JSON(完整会话消息存储由后端 M10+ 提供)。身份判定永远由 actor 元数据 + 白名单 + 策略引擎完成,不依赖提示词。</Notice>
|
||||
<Notice>
|
||||
列表来自后端持久化会话(主会话 + 次级会话),主会话 id 固定。导出为会话元数据 JSON;身份判定永远由 actor 元数据 + 白名单 + 策略引擎完成,不依赖提示词。
|
||||
</Notice>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user