Compare commits

..
1 Commits
7 changed files with 92 additions and 6 deletions
Generated
+7
View File
@@ -1780,6 +1780,12 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "http-range"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573"
[[package]]
name = "httparse"
version = "1.10.1"
@@ -4125,6 +4131,7 @@ dependencies = [
"gtk",
"heck 0.5.0",
"http",
"http-range",
"jni",
"libc",
"log",
+1 -1
View File
@@ -17,7 +17,7 @@ custom-protocol = ["tauri/custom-protocol"]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
tauri = { version = "2", features = ["protocol-asset"] }
tauri-plugin-opener = "2"
tauri-plugin-autostart = "2"
tauri-plugin-dialog = "2"
+5 -2
View File
@@ -21,7 +21,11 @@
}
],
"security": {
"csp": null
"csp": null,
"assetProtocol": {
"enable": true,
"scope": ["**"]
}
}
},
"bundle": {
@@ -35,4 +39,3 @@
]
}
}
+1
View File
@@ -98,6 +98,7 @@ ui/src/
- `run()`:初始化数据目录(`%APPDATA%\XianrenStudio`)、日志、panic 钩子;构建 Tauri 应用并注册 `invoke_handler`
- 注册插件:`tauri-plugin-opener``tauri-plugin-autostart`(开机启动,Windows 写注册表启动项)、`tauri-plugin-dialog`(原生文件/文件夹选择对话框,知识库目录来源「浏览…」使用)。
- `tauri.conf.json` 启用 `assetProtocol`scope `**`):知识库媒体文档(图片/视频/音频)预览通过 `convertFileSrc` 直读本地文件渲染/播放。
- `setup` 末尾启动**自动加载模型**任务(`auto_load_models`:按设置列表顺序逐个部署,最后一个保持运行)。
- `setup` 末尾启动**定时计划后台调度循环**tokio interval 每 30 秒调用 `run_due_scheduled_tasks`)。
- 数据目录:`logs/``models/``engines/``recommendations/`
+2
View File
@@ -141,6 +141,7 @@
- 同一目录重复添加自动更新配置不产生重复来源;按文件路径去重,可一键「重新扫描」增量导入新增文件;删除目录来源会同时删除它导入的文档。
- **扫描进度**:添加目录 / 重新扫描时显示进度条与处理统计(处理 X / Y 个文件、成功、失败、忽略、分块数),失败文件在结尾汇总提示。
- **文档操作**:文档列表每页 10 条,带分页控件;展示类型 / 大小 / 字数 / 分块数;支持全文预览、「重切」(按最新分块设置重新切分,适用于修改设置后)、单条删除;标题「文档(共 x 个)」右侧提供「清空」按钮,一键清空该知识库全部文档(含分块与检索索引),清空前弹窗警告确认。
- **媒体预览**:图片 / 视频 / 音频文档点击「预览」直接展示实际内容——图片原图显示、视频/音频用原生播放器播放(经 Tauri asset protocol 直接读取本地文件,无需整文件过 IPC);文件被移动/删除时提示无法加载。
- **检索**:全文检索基于 SQLite FTS5(trigram 分词,对中文友好),按 BM25 相关性排序;支持单库检索(也可全库检索),结果每页 10 条带分页控件,展示来源文档与分块序号,可一键复制分块内容用于对话。
- **页面布局**:知识库主区内自上而下依次为「目录来源」→「文档列表」→「检索」。
- 数据表:`knowledge_bases``kb_documents`(含提取后的纯文本)、`kb_chunks`(分块)、`kb_chunks_fts`FTS5 全文索引,contentless-delete 模式 + 触发器同步)。
@@ -232,6 +233,7 @@
### 2026-08-17
- 知识库文档预览支持多媒体:图片/视频/音频点击「预览」直接展示实际内容(asset protocol 直读本地文件,视频音频可播放)。
- 知识库文档列表标题增加「清空」按钮:一键清空当前知识库全部文档(分块与索引级联删除),清空前弹出警告确认。
- 知识库页调整:文档列表与检索结果增加分页控件;目录来源、文档列表区块移到检索上方;目录扫描增加进度条与处理统计(处理/成功/失败/忽略/分块数,事件驱动实时刷新)。
- 修复:知识库「添加目录 → 浏览…」无响应——为 dialog 插件在 capabilities 中补充权限(`dialog:default`),并让选择器异常在前端可见。
+2
View File
@@ -221,6 +221,8 @@ export interface KbDocument {
file_size: number;
char_count: number;
chunk_count: number;
file_path: string;
source_id: string | null;
created_at: string;
updated_at: string;
}
+74 -3
View File
@@ -1,5 +1,6 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { open as openDialog } from "@tauri-apps/plugin-dialog";
import { convertFileSrc } from "@tauri-apps/api/core";
import {
api,
KbDocument,
@@ -16,6 +17,24 @@ import Icon from "../components/Icon";
const DOC_PAGE_SIZE = 10;
const SEARCH_PAGE_SIZE = 10;
const IMAGE_EXTS = [
"png", "jpg", "jpeg", "gif", "webp", "bmp", "svg", "tiff", "tif", "ico", "avif", "heic",
];
const VIDEO_EXTS = [
"mp4", "mkv", "avi", "mov", "webm", "flv", "wmv", "m4v", "mpg", "mpeg", "3gp",
];
const AUDIO_EXTS = [
"mp3", "wav", "flac", "ogg", "oga", "m4a", "aac", "wma", "opus", "aiff", "aif",
];
function mediaKind(ext: string): "image" | "video" | "audio" | null {
const e = ext.toLowerCase();
if (IMAGE_EXTS.includes(e)) return "image";
if (VIDEO_EXTS.includes(e)) return "video";
if (AUDIO_EXTS.includes(e)) return "audio";
return null;
}
function fmtSize(bytes: number) {
if (bytes >= 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
if (bytes >= 1024) return `${(bytes / 1024).toFixed(0)} KB`;
@@ -697,9 +716,28 @@ export default function KnowledgeBasePage() {
</button>
</div>
<pre className="flex-1 overflow-auto whitespace-pre-wrap break-words bg-panel-2/40 p-4 text-xs leading-relaxed text-slate-300">
{preview.content}
</pre>
{(() => {
const kind = mediaKind(preview.file_type);
if (kind && preview.file_path) {
return (
<div className="flex flex-1 items-center justify-center overflow-auto bg-panel-2/40 p-4">
<MediaView kind={kind} filePath={preview.file_path} name={preview.name} />
</div>
);
}
if (kind) {
return (
<div className="flex flex-1 items-center justify-center bg-panel-2/40 p-4 text-xs text-slate-500">
</div>
);
}
return (
<pre className="flex-1 overflow-auto whitespace-pre-wrap break-words bg-panel-2/40 p-4 text-xs leading-relaxed text-slate-300">
{preview.content}
</pre>
);
})()}
</div>
</div>
) : null}
@@ -742,6 +780,39 @@ function Pager({
);
}
function MediaView({
kind,
filePath,
name,
}: {
kind: "image" | "video" | "audio";
filePath: string;
name: string;
}) {
const [error, setError] = useState<string | null>(null);
const src = convertFileSrc(filePath);
const commonProps = {
src,
onError: () => setError("无法加载文件,文件可能已被移动或删除"),
};
if (error) {
return <div className="text-xs text-red-400">{error}</div>;
}
if (kind === "image") {
return (
<img
{...commonProps}
alt={name}
className="max-h-full max-w-full object-contain"
/>
);
}
if (kind === "video") {
return <video {...commonProps} controls className="max-h-full max-w-full" />;
}
return <audio {...commonProps} controls className="w-full" />;
}
function SearchHitRow({ hit }: { hit: KbSearchHit }) {
const [copied, setCopied] = useState(false);
return (