feat: RAG 知识库真实数据 + 渠道端到端接线(并行∦wire.go)+ 主会话收口
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

【渠道端到端·接线(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:
Aska committed 2026-09-06 20:09:09 +08:00
1 parent 518bd0a230
commit dbcf3df8a3
13 files changed
+1510 -104

No files matched your search

+105 -46
View File
@@ -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>
);
}
+25
View File
@@ -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 }),
});
}
+25
View File
@@ -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;