feat: RAG 知识库真实数据 + 渠道端到端接线(并行∦wire.go)+ 主会话收口
【渠道端到端·接线(agent-72)】 - 新建 internal/services/channelbridge/:桥接服务(Start 对配置渠道 Connect(10s超时/warn)+SetMessageHandler+ReadMessages; 入站 UnifiedMessage→sessions.GetOrCreate(ResolveActor 白名单结构性识别)→loop.Run→SendMessage 回执,chat 文本)。 - cmd/cyrene/channels.go:从 CHANNEL_* env 读 4 渠道凭据(telegram/qq_obv11/weixin_ilink/webhook)。 - wire.go:app 加 channelBridge 字段,wireChannels 构造适配器+bridge.Start(),close() 收尾。 - 无真凭据降级:无 CHANNEL_* → configured()false 跳过不 dial;Connect 失败 Warn 不阻启动; webhook 仅入站(回执 ErrUnsupported 记 debug)。身份识别绝不靠提示词。收发端到端待凭据验证。 【RAG 知识库(agent-71)】 - internal/runtime/knowledge/:真实数据源(doc=资料/记忆=经历 分离 design/03 §3③)。 ListDocuments 走 projectindex.ListProjects(+ListChapters)、Search 走 projectindex.Search(标题/描述+章节 命中行+上下文)+retriever.Retrieve(配 vector 真语义,命中标 semantic:true)。 - gateway/server.go:RagStore 接口+Deps.RAG 可空+3 路由(GET /admin/rag、POST /admin/rag/query、/delete)。 - admin.go:adminListRag/adminSearchRag(空 query 400)/adminDeleteRag(写 501 not_implemented,M10)。 - projectindex/store.go:ListProjects/ListChapters。 - 前端 Rag.tsx:docs 从 GET /admin/rag 拉、检索走 POST /admin/rag/query、删除调真端点(501 显示 Notice), 上传/分块/嵌入显示 M10 Notice;api.ts/types.ts 加 adminListRag/adminSearchRag/adminDeleteRag。 【主会话收口 wire.go】projStore 提到 app struct+wireMemory 赋值;knowledge.New(...) 注入 gateway.Deps.RAG。 实机验证:admin/rag 200(documents[] 空表非壳)、channels 枚举 4 渠道、health ok。 go build/vet/test 全绿 + 前端 tsc。wire.go 567 行。 遗留(记录):① 群聊/陌生人 MemWritable 记忆隔离未接 loop(AGENTS.md 红线,待修) ② webhook InboundHandler 需 gateway 挂载 ③ RAG 语义路径 persona-scoped(project 记忆其他 persona 不浮现)。
This commit is contained in:
1 parent
518bd0a230
commit
dbcf3df8a3
13 files changed
+1510
-104
No files matched your search
@@ -1,37 +1,60 @@
|
||||
import { useState } from "react";
|
||||
import { useAsyncData } from "../../lib/useAsyncData";
|
||||
import { adminDeleteRag, adminListRag, adminSearchRag } from "../../lib/api";
|
||||
import type { AdminRagDoc, AdminRagHit } from "../../lib/types";
|
||||
import { useAuditStore } from "../audit";
|
||||
import { Notice, PageHeader } from "../components";
|
||||
import { Button, Surface, Tag } from "../../components/primitives";
|
||||
|
||||
interface DocRow {
|
||||
id: string;
|
||||
name: string;
|
||||
chunks: number;
|
||||
embedded: boolean;
|
||||
}
|
||||
|
||||
const docs: DocRow[] = [
|
||||
{ id: "1", name: "owner_identity.md", chunks: 1, embedded: false },
|
||||
{ id: "2", name: "开发时间线.md", chunks: 6, embedded: false },
|
||||
{ id: "3", name: "design/10-后台管理面板.md", chunks: 2, embedded: false },
|
||||
];
|
||||
const WRITE_M10 = "上传/分块/嵌入为后端 M10+ 提供;当前只读列文档与检索,删除接口在 M10 落地。";
|
||||
|
||||
export function RagModule(): React.ReactNode {
|
||||
const log = useAuditStore((s) => s.log);
|
||||
const { data, loading, error, reload } = useAsyncData(() => adminListRag(), []);
|
||||
const [query, setQuery] = useState("");
|
||||
const [hits, setHits] = useState<string[]>([]);
|
||||
const [hits, setHits] = useState<AdminRagHit[]>([]);
|
||||
const [searching, setSearching] = useState(false);
|
||||
const [notice, setNotice] = useState<string | null>(null);
|
||||
|
||||
const docs: AdminRagDoc[] = data?.documents ?? [];
|
||||
|
||||
const search = async () => {
|
||||
log("rag.search", "ok");
|
||||
setSearching(true);
|
||||
setNotice(null);
|
||||
try {
|
||||
const res = await adminSearchRag(query.trim());
|
||||
setHits(res.hits);
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : "检索失败";
|
||||
setNotice(msg);
|
||||
setHits([]);
|
||||
} finally {
|
||||
setSearching(false);
|
||||
}
|
||||
};
|
||||
|
||||
const remove = async (d: AdminRagDoc) => {
|
||||
log("rag.delete", "ok");
|
||||
setNotice(null);
|
||||
try {
|
||||
await adminDeleteRag(d.id);
|
||||
} catch (err: unknown) {
|
||||
setNotice(err instanceof Error ? err.message : WRITE_M10);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<PageHeader
|
||||
title="知识库 RAG"
|
||||
subtitle="文档列表(分块/嵌入状态) + 索引 + 检索测试"
|
||||
subtitle="GET /api/v1/admin/rag · 文档列表(分块/嵌入状态) + 检索测试"
|
||||
actions={
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
log("rag.upload", "ok");
|
||||
alert("上传文档(占位):后端 M10+ 提供分块/嵌入。");
|
||||
setNotice(WRITE_M10);
|
||||
}}
|
||||
>
|
||||
上传文档
|
||||
@@ -39,30 +62,50 @@ export function RagModule(): React.ReactNode {
|
||||
}
|
||||
/>
|
||||
|
||||
{notice ? <Notice tone="warn">{notice}</Notice> : null}
|
||||
|
||||
<Surface className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-sm font-semibold">文档列表</h3>
|
||||
<span className="text-xs text-[var(--mu-text-lo)]">{docs.length} 篇</span>
|
||||
<span className="text-xs text-[var(--mu-text-lo)]">
|
||||
{loading ? "加载中…" : `${docs.length} 篇`}
|
||||
</span>
|
||||
</div>
|
||||
{docs.map((d) => (
|
||||
<div key={d.id} className="flex items-center gap-2 rounded-[var(--mu-radius)] bg-[var(--mu-surface-2)] px-3 py-2 text-sm">
|
||||
<span className="font-medium">{d.name}</span>
|
||||
<span className="text-xs text-[var(--mu-text-lo)]">{d.chunks} 分块</span>
|
||||
<span className="ml-auto">
|
||||
<Tag tone={d.embedded ? "ok" : "warn"}>{d.embedded ? "已嵌入" : "待嵌入"}</Tag>
|
||||
</span>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
log("rag.delete", "ok");
|
||||
alert(`删除 ${d.name}(占位)`);
|
||||
}}
|
||||
{loading ? (
|
||||
<p className="text-xs text-[var(--mu-text-lo)]">正在从项目索引加载知识文档…</p>
|
||||
) : error ? (
|
||||
<p className="text-xs text-[var(--mu-text-lo)]">{error}</p>
|
||||
) : docs.length === 0 ? (
|
||||
<p className="text-xs text-[var(--mu-text-lo)]">暂无知识文档(项目索引为空)。</p>
|
||||
) : (
|
||||
docs.map((d) => (
|
||||
<div
|
||||
key={d.id}
|
||||
className="flex items-center gap-2 rounded-[var(--mu-radius)] bg-[var(--mu-surface-2)] px-3 py-2 text-sm"
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<div className="min-w-0">
|
||||
<span className="font-medium">{d.name}</span>
|
||||
{d.desc ? (
|
||||
<span className="ml-2 truncate text-xs text-[var(--mu-text-lo)]">{d.desc}</span>
|
||||
) : null}
|
||||
</div>
|
||||
<span className="text-xs text-[var(--mu-text-lo)]">{d.chunks} 分块</span>
|
||||
<span className="ml-auto">
|
||||
<Tag tone={d.embedded ? "ok" : "warn"}>{d.embedded ? "已嵌入" : "待嵌入"}</Tag>
|
||||
</span>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
if (!confirm(`删除 ${d.name}?`)) return;
|
||||
void remove(d);
|
||||
}}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</Surface>
|
||||
|
||||
<Surface className="space-y-3">
|
||||
@@ -71,29 +114,45 @@ export function RagModule(): React.ReactNode {
|
||||
<input
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="输入查询…"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && query.trim()) void search();
|
||||
}}
|
||||
placeholder="输入查询(命中项目索引或项目记忆)…"
|
||||
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"
|
||||
onClick={() => {
|
||||
log("rag.search", "ok");
|
||||
setHits(query ? [`片段 1(命中 "${query}")`, `片段 2(命中 "${query}")`] : []);
|
||||
}}
|
||||
>
|
||||
检索
|
||||
<Button variant="primary" onClick={() => void search()} disabled={searching || !query.trim()}>
|
||||
{searching ? "检索中…" : "检索"}
|
||||
</Button>
|
||||
</div>
|
||||
{hits.length > 0 ? (
|
||||
<div className="space-y-1.5">
|
||||
{hits.map((h) => (
|
||||
<div key={h} className="rounded-[var(--mu-radius-sm)] bg-[var(--mu-surface-2)] px-3 py-2 text-xs text-[var(--mu-text-mid)]">{h}</div>
|
||||
{hits.map((h, i) => (
|
||||
<div
|
||||
key={`${h.docId}-${h.chapter ?? "mem"}-${i}`}
|
||||
className="rounded-[var(--mu-radius-sm)] bg-[var(--mu-surface-2)] px-3 py-2 text-xs"
|
||||
>
|
||||
<div className="mb-1 flex items-center gap-2">
|
||||
<span className="font-medium text-[var(--mu-text)]">{h.docName}</span>
|
||||
{h.chapter ? (
|
||||
<span className="text-[var(--mu-text-lo)]">#{h.chapter}</span>
|
||||
) : null}
|
||||
<span className="ml-auto text-[var(--mu-text-lo)]">
|
||||
{h.semantic ? "语义" : "关键词"} · {h.score.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="whitespace-pre-wrap text-[var(--mu-text-mid)]">{h.text}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : hits.length === 0 && query.trim() && !searching ? (
|
||||
<p className="text-xs text-[var(--mu-text-lo)]">无命中。</p>
|
||||
) : null}
|
||||
</Surface>
|
||||
|
||||
<Notice>分块/嵌入状态、向量索引与语义检索为后端 M10+ 提供;配置了 pgvector 增强后必须真实走语义路径(非空壳)。</Notice>
|
||||
<Notice>
|
||||
分块/嵌入状态与语义检索:配置了 pgvector / SQLite 向量增强后,后端经真实语义/向量路径返回(非空壳);
|
||||
未配置时走项目索引关键词 + 行上下文。上传/删除为后端 M10 写操作。
|
||||
</Notice>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import type {
|
||||
AdminIoTDevice,
|
||||
AdminLlmConfig,
|
||||
AdminPluginRecord,
|
||||
AdminRagHit,
|
||||
AdminRagResponse,
|
||||
AdminUserRecord,
|
||||
ApiErrorShape,
|
||||
ChatSyncResult,
|
||||
@@ -244,3 +246,26 @@ export function adminGetLlm(): Promise<AdminLlmConfig> {
|
||||
export function adminListAutomations(): Promise<AdminAutomationRecord[]> {
|
||||
return request<AdminAutomationRecord[]>("/admin/automations");
|
||||
}
|
||||
/** GET /api/v1/admin/rag — lists knowledge documents; when query is present the
|
||||
* backend also returns matching hits. */
|
||||
export function adminListRag(query?: string): Promise<AdminRagResponse> {
|
||||
const qs = query ? `?query=${encodeURIComponent(query)}` : "";
|
||||
return request<AdminRagResponse>(`/admin/rag${qs}`);
|
||||
}
|
||||
/** POST /api/v1/admin/rag/query — runs a real knowledge-base query. */
|
||||
export function adminSearchRag(
|
||||
query: string
|
||||
): Promise<{ query: string; hits: AdminRagHit[] }> {
|
||||
return request<{ query: string; hits: AdminRagHit[] }>("/admin/rag/query", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ query }),
|
||||
});
|
||||
}
|
||||
/** POST /api/v1/admin/rag/delete — deletes a document. The backend returns a
|
||||
* clear 501 until M10 lands the write path (upload/chunk/embed/delete). */
|
||||
export function adminDeleteRag(id: string): Promise<{ id: string }> {
|
||||
return request<{ id: string }>("/admin/rag/delete", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ id }),
|
||||
});
|
||||
}
|
||||
@@ -249,6 +249,31 @@ export interface AdminLogRecord {
|
||||
msg: string;
|
||||
}
|
||||
|
||||
/** A knowledge-base document (GET /api/v1/admin/rag). */
|
||||
export interface AdminRagDoc {
|
||||
id: string;
|
||||
name: string;
|
||||
desc?: string;
|
||||
chunks: number;
|
||||
embedded: boolean;
|
||||
}
|
||||
|
||||
/** One RAG retrieval hit (GET /api/v1/admin/rag?query= / POST /rag/query). */
|
||||
export interface AdminRagHit {
|
||||
docId: string;
|
||||
docName: string;
|
||||
chapter?: string;
|
||||
text: string;
|
||||
score: number;
|
||||
semantic: boolean;
|
||||
}
|
||||
|
||||
/** The GET /api/v1/admin/rag response shape. */
|
||||
export interface AdminRagResponse {
|
||||
documents: AdminRagDoc[];
|
||||
hits?: AdminRagHit[];
|
||||
}
|
||||
|
||||
/** One audit entry: who / what / when / result. */
|
||||
export interface AuditRecord {
|
||||
id: string;
|
||||
|
||||
Reference in new issue
Block a user