Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

### 变更

- 知识库桌面 / 首次打开默认可用:打开「知识库」先看到本地文件夹挂接与云知识库挂接(ima 等),不必先找开关。向量模型、图片识别、扫描件 OCR 仍在「基础设置」里,留给后续清洗、标注、蒸馏。原文件夹只读;解析结果仍写入用户另选的新文件夹。
- 侧栏「组织 OS」更名为「组织」(英文 Organization)。


Expand Down
41 changes: 29 additions & 12 deletions dashboard/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -416,15 +416,32 @@
},
"knowledgeBases": {
"title": "Knowledge Bases",
"subtitle": "Index documents for chat retrieval. Optionally share them with users on this instance.",
"subtitle": "Mount a local folder or cloud knowledge base first. Cleaning, labeling, and distillation come later.",
"featureTitle": "Enable Knowledge Bases",
"settingsTitle": "Knowledge base settings",
"settingsLead": "Instance-wide switch. When on, configure an embedding model. When off, knowledge bases are unavailable for everyone.",
"settingsOpen": "Enable knowledge bases",
"settingsFoundation": "Foundation settings",
"settingsFoundationLead": "These settings are for later cleaning, labeling, and distillation. You can mount sources first without changing them.",
"settingsTabVector": "Vector model",
"settingsTabImage": "Images",
"settingsTabOcr": "Scanned-document OCR",
"settingsLead": "Advanced switch. It is on by default on the desktop. Turning it off hides knowledge bases from colleagues.",
"settingsOpen": "Knowledge base feature",
"homeTitle": "Mount your sources first",
"homeDesc": "Pick a folder on this computer, or attach a cloud knowledge base such as ima. The original folder stays read-only. Parse results go to a new folder you choose. Colleagues can look them up in chat.",
"homeAdvancedHint": "Vector, image, and scanned-document OCR settings are for later cleaning, labeling, and distillation.",
"defaultLocalName": "Local files",
"defaultCloudName": "Cloud knowledge",
"pickFolder": "Choose folder",
"pickFolderUnavailable": "This window cannot open a folder picker. Type the path instead.",
"imageSettingsTitle": "Read text in images",
"imageSettingsDesc": "Turn this on to recognize text in pictures, so you can clean and label them later.",
"ocrFoundationHint": "Turn this on so scanned PDFs can be read as text for later distillation.",
"disabledTitle": "Knowledge bases are turned off",
"disabledDesc": "You can turn the feature back on in foundation settings. After that, mount a local folder or a cloud knowledge base to start.",
"manageModels": "Manage models",
"unavailableTitle": "Knowledge bases are not ready",
"unavailableDescription": "Open Settings in the top-right to enable knowledge bases and configure an embedding model.",
"unavailableDescriptionNonAdmin": "Knowledge bases are not enabled or not configured yet. Please contact an administrator.",
"unavailableDescription": "Open Foundation settings in the top-right to turn knowledge bases on.",
"unavailableDescriptionNonAdmin": "Knowledge bases are turned off. Please contact an administrator.",
"create": "Create knowledge base",
"edit": "Edit knowledge base",
"addToChat": "Add to chat",
Expand All @@ -449,12 +466,12 @@
"enableGuideStepModel": "Configure an embedding model",
"enableGuideStepModelDetail": "Choose local ONNX (download the model first) or an online embedding provider, then save.",
"noDescription": "No description",
"selectBase": "Select a knowledge base to view its documents",
"mountTitle": "Local directory mount",
"mountHint": "Mount a folder as read-only. FreeOS never deletes or edits files in that folder. Parse copies must go to a new folder you choose.",
"selectBase": "Select a knowledge base, or mount a source first",
"mountTitle": "Local folder mount",
"mountHint": "Attach a folder on this computer. It stays read-only. Parse or distillation results must go to a new folder you choose.",
"mountSource": "Local folder path",
"mountSourceRequired": "Enter a local folder to mount",
"mountAction": "Mount read-only",
"mountSourceRequired": "Choose a local folder to mount",
"mountAction": "Mount folder",
"mountSaved": "Folder mounted (read-only)",
"mountFailed": "Could not mount the folder",
"mountDir": "Folder",
Expand All @@ -463,8 +480,8 @@
"distillDest": "Choose a new folder for parse results (not the mounted folder)",
"distillDone": "Copied {{count}} files to {{path}}",
"distillFailed": "Could not copy parse results",
"cloudMountTitle": "Cloud knowledge (ima and similar)",
"cloudMountHint": "Attach a cloud corpus such as ima. FreeOS does not download or parse those files locally. Distillation writes a pointer into a new folder you choose, for later agent strengthening.",
"cloudMountTitle": "Cloud knowledge mount",
"cloudMountHint": "Attach a cloud knowledge base such as ima. Files are not parsed locally. Distillation writes a pointer into a new folder you choose, for later assistant strengthening.",
"cloudUrl": "Cloud knowledge URL",
"cloudUrlRequired": "Enter a cloud knowledge URL",
"cloudProvider": "Provider (ima…)",
Expand Down
41 changes: 29 additions & 12 deletions dashboard/src/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -416,15 +416,32 @@
},
"knowledgeBases": {
"title": "知识库",
"subtitle": "上传文档建立索引,在对话中检索引用;可按需共享给同实例用户。",
"subtitle": "先挂接本地文件夹或云知识库,再按需做清洗、标注和蒸馏。",
"featureTitle": "启用知识库",
"settingsTitle": "知识库设置",
"settingsLead": "实例级开关。启用后需配置向量模型;关闭后全实例不可使用知识库。",
"settingsOpen": "启用知识库",
"settingsFoundation": "基础设置",
"settingsFoundationLead": "这些是后续清洗、标注、蒸馏用的基础设置。先挂接资料即可开始,不必先改这里。",
"settingsTabVector": "向量模型",
"settingsTabImage": "图片",
"settingsTabOcr": "扫描件 OCR",
"settingsLead": "高级开关。桌面默认已打开。关掉后,同事都无法再使用知识库。",
"settingsOpen": "知识库功能",
"homeTitle": "先把资料挂进来",
"homeDesc": "选电脑上的文件夹,或挂接 ima 等云知识库。原来的文件夹只读,不会被改动;解析结果会存到你另选的新文件夹。同事可以在对话里查阅。",
"homeAdvancedHint": "向量模型、图片和扫描件 OCR 留给后续清洗、标注、蒸馏。",
"defaultLocalName": "本地资料",
"defaultCloudName": "云知识库",
"pickFolder": "选择文件夹",
"pickFolderUnavailable": "当前窗口无法弹出文件夹选择,请直接填写路径。",
"imageSettingsTitle": "识别图片里的文字",
"imageSettingsDesc": "打开后,可以把图片里的字认出来,方便以后清洗和标注。",
"ocrFoundationHint": "打开后,扫描件 PDF 也能认成文字,方便以后蒸馏。",
"disabledTitle": "知识库功能已关闭",
"disabledDesc": "可以在基础设置里重新打开。打开后,先挂接本地文件夹或云知识库即可使用。",
"manageModels": "管理模型",
"unavailableTitle": "知识库尚未就绪",
"unavailableDescription": "请先在右上角设置中启用知识库,并配置可用的向量模型。",
"unavailableDescriptionNonAdmin": "知识库尚未启用或未配置向量模型,请联系管理员。",
"unavailableDescription": "请先在右上角基础设置中打开知识库。",
"unavailableDescriptionNonAdmin": "知识库尚未打开,请联系管理员。",
"create": "创建知识库",
"edit": "编辑知识库",
"addToChat": "添加到会话",
Expand All @@ -449,12 +466,12 @@
"enableGuideStepModel": "配置向量模型",
"enableGuideStepModelDetail": "选择本地 ONNX(需先下载模型)或在线 Embedding 提供商,保存后即可使用。",
"noDescription": "暂无描述",
"selectBase": "选择一个知识库以查看文档",
"mountTitle": "挂载本地目录",
"mountHint": "以只读方式挂载本地文件夹。FreeOS 不会删除或修改该目录中的文件。解析结果必须保存到你另外指定的新文件夹。",
"selectBase": "选一个知识库查看文档,或先挂接资料",
"mountTitle": "本地文件夹挂接",
"mountHint": "把电脑上的文件夹挂进来,只读,不会改原来的文件。解析或蒸馏结果必须存到你另选的新文件夹。",
"mountSource": "本地文件夹路径",
"mountSourceRequired": "请输入要挂载的本地文件夹",
"mountAction": "只读挂载",
"mountSourceRequired": "请选择要挂接的本地文件夹",
"mountAction": "挂接文件夹",
"mountSaved": "已只读挂载该文件夹",
"mountFailed": "挂载失败",
"mountDir": "文件夹",
Expand All @@ -463,8 +480,8 @@
"distillDest": "请选择保存解析结果的新文件夹(不能是已挂载的目录)",
"distillDone": "已复制 {{count}} 个文件到 {{path}}",
"distillFailed": "无法复制解析结果",
"cloudMountTitle": "云知识库(ima 等)",
"cloudMountHint": "挂载 ima 等云端语料。FreeOS 不会在本地解析这些文件。蒸馏时只在你指定的新文件夹写入指针,供后续强化智能体。",
"cloudMountTitle": "云知识库挂接",
"cloudMountHint": "挂接 ima 等云知识库。不会在本地解析原文。蒸馏时只在你另选的新文件夹写入指针,供以后强化智能助手。",
"cloudUrl": "云知识库地址",
"cloudUrlRequired": "请输入云知识库地址",
"cloudProvider": "来源(ima…)",
Expand Down
50 changes: 39 additions & 11 deletions dashboard/src/pages/KnowledgeBases/CloudMountPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,33 @@ import { Button, Input, Space, Typography } from "antd";
import { useTranslation } from "react-i18next";
import { knowledgeBasesApi } from "../../api/modules/knowledgeBases";
import { message } from "../../utils/antdMessage";
import {
canPickKnowledgeFolder,
pickKnowledgeFolder,
} from "./pickKnowledgeFolder";

interface CloudMountPanelProps {
kbId: string;
kbId?: string;
ensureKb?: () => Promise<string>;
onMounted?: () => void;
prominent?: boolean;
}

export function CloudMountPanel({ kbId }: CloudMountPanelProps) {
export function CloudMountPanel({
kbId,
ensureKb,
onMounted,
prominent = false,
}: CloudMountPanelProps) {
const { t } = useTranslation();
const [url, setUrl] = useState("");
const [provider, setProvider] = useState("ima");
const [mounted, setMounted] = useState(false);
const [busy, setBusy] = useState(false);
const canPick = canPickKnowledgeFolder();

const load = async () => {
const next = await knowledgeBasesApi.getMount(kbId);
const load = async (id: string) => {
const next = await knowledgeBasesApi.getMount(id);
if (next.kind === "cloud") {
setMounted(next.mounted);
setUrl(next.cloud_url || "");
Expand All @@ -25,23 +38,34 @@ export function CloudMountPanel({ kbId }: CloudMountPanelProps) {
};

useEffect(() => {
void load().catch(() => undefined);
if (!kbId) return;
void load(kbId).catch(() => undefined);
}, [kbId]);

const resolveKb = async () => {
if (kbId) return kbId;
if (!ensureKb) {
throw new Error(t("knowledgeBases.cloudMountFailed"));
}
return ensureKb();
};

const mount = async () => {
if (!url.trim()) {
message.error(t("knowledgeBases.cloudUrlRequired"));
return;
}
setBusy(true);
try {
await knowledgeBasesApi.setMount(kbId, "", "", {
const id = await resolveKb();
await knowledgeBasesApi.setMount(id, "", "", {
kind: "cloud",
cloud_url: url.trim(),
cloud_provider: provider.trim() || "ima",
});
message.success(t("knowledgeBases.cloudMountSaved"));
await load();
await load(id);
onMounted?.();
} catch (err) {
message.error(
err instanceof Error
Expand All @@ -54,11 +78,15 @@ export function CloudMountPanel({ kbId }: CloudMountPanelProps) {
};

const distill = async () => {
const folder = window.prompt(t("knowledgeBases.cloudDistillDest"));
let folder = await pickKnowledgeFolder();
if (!folder && !canPick) {
folder = window.prompt(t("knowledgeBases.cloudDistillDest")) || "";
}
if (!folder) return;
setBusy(true);
try {
const result = await knowledgeBasesApi.distillMount(kbId, folder);
const id = await resolveKb();
const result = await knowledgeBasesApi.distillMount(id, folder);
message.success(
t("knowledgeBases.cloudDistillDone", { path: result.distill_path }),
);
Expand All @@ -72,7 +100,7 @@ export function CloudMountPanel({ kbId }: CloudMountPanelProps) {
};

return (
<div style={{ margin: "12px 0 16px" }}>
<div style={prominent ? undefined : { margin: "12px 0 16px" }}>
<Typography.Text strong>
{t("knowledgeBases.cloudMountTitle")}
</Typography.Text>
Expand All @@ -90,7 +118,7 @@ export function CloudMountPanel({ kbId }: CloudMountPanelProps) {
value={url}
onChange={(event) => setUrl(event.target.value)}
placeholder={t("knowledgeBases.cloudUrl")}
style={{ minWidth: 280 }}
style={{ minWidth: 240 }}
/>
<Button type="primary" loading={busy} onClick={() => void mount()}>
{t("knowledgeBases.cloudMountAction")}
Expand Down
53 changes: 53 additions & 0 deletions dashboard/src/pages/KnowledgeBases/KnowledgeMountHome.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { describe, expect, it, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

import { KnowledgeMountHome } from "./KnowledgeMountHome";

vi.mock("../../api/modules/knowledgeBases", () => ({
knowledgeBasesApi: {
getMount: vi.fn().mockResolvedValue({ mounted: false, entries: [] }),
setMount: vi.fn(),
distillMount: vi.fn(),
},
}));

describe("<KnowledgeMountHome />", () => {
it("shows local and cloud mount setup immediately", () => {
render(
<KnowledgeMountHome
canConfigure
onOpenSettings={() => undefined}
ensureKb={vi.fn()}
onMounted={() => undefined}
/>,
);

expect(screen.getByText("knowledgeBases.homeTitle")).toBeInTheDocument();
expect(screen.getByText("knowledgeBases.mountTitle")).toBeInTheDocument();
expect(
screen.getByText("knowledgeBases.cloudMountTitle"),
).toBeInTheDocument();
expect(
screen.getByRole("button", { name: "knowledgeBases.settingsFoundation" }),
).toBeInTheDocument();
});

it("opens foundation settings from the secondary link", async () => {
const user = userEvent.setup();
const onOpenSettings = vi.fn();
render(
<KnowledgeMountHome
canConfigure
onOpenSettings={onOpenSettings}
ensureKb={vi.fn()}
onMounted={() => undefined}
/>,
);

await user.click(
screen.getByRole("button", { name: "knowledgeBases.settingsFoundation" }),
);
expect(onOpenSettings).toHaveBeenCalledTimes(1);
});
});
72 changes: 72 additions & 0 deletions dashboard/src/pages/KnowledgeBases/KnowledgeMountHome.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { Button, Typography } from "antd";
import { Cloud, FolderUp, Settings } from "lucide-react";
import { useTranslation } from "react-i18next";

import { XyaiMascot } from "../../components/EmptyState";
import { CloudMountPanel } from "./CloudMountPanel";
import { LocalMountPanel } from "./LocalMountPanel";
import styles from "./index.module.less";

interface KnowledgeMountHomeProps {
canConfigure: boolean;
onOpenSettings: () => void;
ensureKb: (name: string) => Promise<string>;
onMounted: () => void;
}

export function KnowledgeMountHome({
canConfigure,
onOpenSettings,
ensureKb,
onMounted,
}: KnowledgeMountHomeProps) {
const { t } = useTranslation();

return (
<div className={styles.mountHome}>
<XyaiMascot size={96} className={styles.setupMascot} />
<Typography.Title level={3} className={styles.mountHomeTitle}>
{t("knowledgeBases.homeTitle")}
</Typography.Title>
<Typography.Paragraph className={styles.mountHomeLead}>
{t("knowledgeBases.homeDesc")}
</Typography.Paragraph>
<div className={styles.mountHomeCards}>
<section className={styles.mountCard}>
<div className={styles.mountCardIcon} aria-hidden>
<FolderUp size={22} strokeWidth={1.8} />
</div>
<LocalMountPanel
prominent
ensureKb={() => ensureKb(t("knowledgeBases.defaultLocalName"))}
onMounted={onMounted}
/>
</section>
<section className={styles.mountCard}>
<div className={styles.mountCardIcon} aria-hidden>
<Cloud size={22} strokeWidth={1.8} />
</div>
<CloudMountPanel
prominent
ensureKb={() => ensureKb(t("knowledgeBases.defaultCloudName"))}
onMounted={onMounted}
/>
</section>
</div>
{canConfigure ? (
<div className={styles.mountHomeAdvanced}>
<Typography.Text type="secondary">
{t("knowledgeBases.homeAdvancedHint")}
</Typography.Text>
<Button
type="link"
icon={<Settings size={14} />}
onClick={onOpenSettings}
>
{t("knowledgeBases.settingsFoundation")}
</Button>
</div>
) : null}
</div>
);
}
Loading
Loading