Repository navigation
feat: add knowledge base RAG v0 #222
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
30fc8ea
ef4ed10
f6a8a68
d6a176d
023822a
41812ea
b891add
f711874
cb6028b
3c8ae95
898d828
5ff619b
3954bc7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,6 +3,7 @@ import { BookOpen, Send, WifiOff } from "lucide-react"; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useCallback, useEffect, useState } from "react"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| askKnowledge, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| downloadKnowledgeImage, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| getPublicPreventionCard, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| listLearningCategories, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| listLearningQuestions, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -75,6 +76,20 @@ export function LearningCenterPage() { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Record<string, { isCorrect: boolean; explanation: string }> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| >({}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [answeringQuestion, setAnsweringQuestion] = useState(""); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [imageUrls, setImageUrls] = useState<Record<string, string>>({}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!token || !answer) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let cancelled = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const loadImages = async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const entries = await Promise.all(answer.sources.flatMap((source) => (source.images ?? []).map(async (image) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { const blob = await downloadKnowledgeImage(token, source.knowledge_item_id, image.id); return [image.id, URL.createObjectURL(blob)] as const; } catch { return null; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }))); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!cancelled) setImageUrls(Object.fromEntries(entries.filter((entry): entry is readonly [string, string] => Boolean(entry)))); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| void loadImages(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return () => { cancelled = true; }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, [answer, token]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+81
to
+92
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 释放已创建的 Blob URL。 清理函数只设置 如果下载在取消后完成,当前代码仍会创建对象 URL,但不会保存或释放它。跟踪本次 effect 创建的 URL。在清理时释放它们。在检测到取消后立即释放新创建的 URL。 建议修复 useEffect(() => {
- if (!token || !answer) return;
+ if (!token || !answer) {
+ setImageUrls({});
+ return;
+ }
let cancelled = false;
+ const objectUrls = new Set<string>();
const loadImages = async () => {
const entries = await Promise.all(answer.sources.flatMap((source) => (source.images ?? []).map(async (image) => {
- try { const blob = await downloadKnowledgeImage(token, source.knowledge_item_id, image.id); return [image.id, URL.createObjectURL(blob)] as const; } catch { return null; }
+ try {
+ const blob = await downloadKnowledgeImage(token, source.knowledge_item_id, image.id);
+ const url = URL.createObjectURL(blob);
+ if (cancelled) {
+ URL.revokeObjectURL(url);
+ return null;
+ }
+ objectUrls.add(url);
+ return [image.id, url] as const;
+ } catch { return null; }
})));
if (!cancelled) setImageUrls(Object.fromEntries(entries.filter((entry): entry is readonly [string, string] => Boolean(entry))));
};
void loadImages();
- return () => { cancelled = true; };
+ return () => {
+ cancelled = true;
+ objectUrls.forEach((url) => URL.revokeObjectURL(url));
+ };
}, [answer, token]);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const load = useCallback(async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!token) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -483,12 +498,27 @@ export function LearningCenterPage() { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {answer.human_review_notice} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {answer.sources.length > 0 && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <p className="mb-0 mt-2 text-xs text-slate-500"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 引用来源(均为已审核发布): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {answer.sources | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .map((source) => `${source.title} v${source.version}`) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .join("、")} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="mt-3 grid gap-3"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <p className="m-0 text-xs text-slate-500"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 引用来源(均为已审核发布): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {answer.sources | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .map((source) => `${source.title} v${source.version}`) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .join("、")} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {answer.sources.flatMap((source) => source.images ?? []).length > 0 && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="grid grid-cols-2 gap-2 sm:grid-cols-3"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {answer.sources.flatMap((source) => source.images ?? []).map((image) => ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <img | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| key={image.id} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| src={imageUrls[image.id] ?? ""} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| alt="Knowledge source image" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="aspect-video w-full rounded-md border border-slate-200 object-cover" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| loading="lazy" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ))} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+508
to
+518
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win 图片的 两个问题:
♻️ 建议改动- {answer.sources.flatMap((source) => source.images ?? []).length > 0 && (
- <div className="grid grid-cols-2 gap-2 sm:grid-cols-3">
- {answer.sources.flatMap((source) => source.images ?? []).map((image) => (
- <img
- key={image.id}
- src={image.storage_path}
- alt="Knowledge source image"
- className="aspect-video w-full rounded-md border border-slate-200 object-cover"
- loading="lazy"
- />
- ))}
- </div>
- )}
+ {sourceImages.length > 0 && (
+ <div className="grid grid-cols-2 gap-2 sm:grid-cols-3">
+ {sourceImages.map(({ image, title }) => (
+ <img
+ key={image.id}
+ src={image.storage_path}
+ alt={`来源「${title}」的配图`}
+ className="aspect-video w-full rounded-md border border-slate-200 object-cover"
+ loading="lazy"
+ />
+ ))}
+ </div>
+ )}在组件的 const sourceImages =
answer?.sources.flatMap((source) =>
(source.images ?? []).map((image) => ({ image, title: source.title })),
) ?? [];📝 Committable suggestion
Suggested change
🤖 Prompt for AI AgentsSource: Path instructions |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,7 @@ const mocked = vi.hoisted(() => ({ | |
| transitionResource: vi.fn(), | ||
| listCategories: vi.fn(), | ||
| transitionCategory: vi.fn(), | ||
| listKnowledgeBases: vi.fn().mockResolvedValue([]), | ||
| })); | ||
|
|
||
| vi.mock("../auth/useAuth", () => ({ | ||
|
|
@@ -38,6 +39,14 @@ vi.mock("../api/learning", () => ({ | |
| transitionManagedLearningQuestion: vi.fn(), | ||
| createManagedLearningResource: vi.fn(), | ||
| createManagedLearningQuestion: vi.fn(), | ||
| listKnowledgeBases: (...args: unknown[]) => mocked.listKnowledgeBases(...args), | ||
| createKnowledgeBase: vi.fn(), | ||
| previewKnowledgeImport: vi.fn(), | ||
| confirmKnowledgeImport: vi.fn(), | ||
| cancelKnowledgeImport: vi.fn(), | ||
| createKnowledgeItem: vi.fn(), | ||
| listKnowledgeItems: vi.fn(), | ||
| transitionKnowledgeItem: vi.fn(), | ||
|
Comment on lines
+42
to
+49
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win 补充知识库管理与导入状态机的关键路径测试。 请覆盖资料库加载与创建、CSV 预览逐行状态展示、确认/取消/重复确认、重复内容行、可见性隔离及 📍 Affects 2 files
🤖 Prompt for AI AgentsSource: Path instructions |
||
| })); | ||
|
|
||
| function submittedResource(submittedBy = "admin-1") { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
certainty新增"rule_based",但页面判断逻辑没有同步更新。knowledge_service::chat在有检索结果时返回"rule_based",在无结果时返回"insufficient_sources"。它从不返回"source_backed"。frontend/src/pages/LearningCenterPage.tsx的 Line 478-481 只判断"source_backed",其余情况一律显示「资料不足,无法形成行动建议」。结果是:即使答案有已审核来源支持,界面也会告诉用户资料不足。修复位置在页面组件,详见该文件的评论。
🤖 Prompt for AI Agents