diff --git a/CHANGELOG.md b/CHANGELOG.md
index c2256785..7725bf7f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,11 @@
## [Unreleased]
+### 修复
+
+- 知识库文本文档编辑抽屉在「编辑」模式下点「保存」无响应(`name`/`format` 字段未挂载时 `validateFields` 缺值导致抛错被吞掉;#592)
+- 知识库文本文档编辑内容未改时点「保存」只关闭抽屉,不触发更新与重建索引
+
## [0.9.32] - 2026-09-06
### 新增
diff --git a/dashboard/src/pages/KnowledgeBases/TextDocumentEditorModal.test.tsx b/dashboard/src/pages/KnowledgeBases/TextDocumentEditorModal.test.tsx
new file mode 100644
index 00000000..c67e674a
--- /dev/null
+++ b/dashboard/src/pages/KnowledgeBases/TextDocumentEditorModal.test.tsx
@@ -0,0 +1,101 @@
+/**
+ * Edit-mode Save used to call `values.name.trim()` even though the name field
+ * is not mounted — validateFields() omits it, so the click threw and was
+ * swallowed (GitHub #592).
+ */
+
+import { describe, it, expect, vi } from "vitest";
+import { render, screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+
+import TextDocumentEditorModal from "./TextDocumentEditorModal";
+
+describe("", () => {
+ it("submits edit-mode content without a mounted name field", async () => {
+ const user = userEvent.setup();
+ const onSubmit = vi.fn().mockResolvedValue(undefined);
+
+ render(
+ undefined}
+ onSubmit={onSubmit}
+ />,
+ );
+
+ const editor = screen.getByPlaceholderText(
+ "knowledgeBases.fileContentPlaceholder",
+ );
+ await user.clear(editor);
+ await user.type(editor, "updated body");
+
+ await user.click(screen.getByRole("button", { name: "common.save" }));
+
+ await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1));
+ expect(onSubmit).toHaveBeenCalledWith({
+ name: "高德地图.md",
+ format: "md",
+ content: "updated body",
+ });
+ });
+
+ it("closes without submitting when edit-mode content is unchanged", async () => {
+ const user = userEvent.setup();
+ const onCancel = vi.fn();
+ const onSubmit = vi.fn().mockResolvedValue(undefined);
+
+ render(
+ ,
+ );
+
+ await user.click(screen.getByRole("button", { name: "common.save" }));
+
+ await waitFor(() => expect(onCancel).toHaveBeenCalledTimes(1));
+ expect(onSubmit).not.toHaveBeenCalled();
+ });
+
+ it("submits when edit-mode content changes by a single character", async () => {
+ const user = userEvent.setup();
+ const onCancel = vi.fn();
+ const onSubmit = vi.fn().mockResolvedValue(undefined);
+
+ render(
+ ,
+ );
+
+ const editor = screen.getByPlaceholderText(
+ "knowledgeBases.fileContentPlaceholder",
+ );
+ await user.type(editor, "!");
+
+ await user.click(screen.getByRole("button", { name: "common.save" }));
+
+ await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1));
+ expect(onSubmit).toHaveBeenCalledWith({
+ name: "高德地图.md",
+ format: "md",
+ content: "same body!",
+ });
+ expect(onCancel).not.toHaveBeenCalled();
+ });
+});
diff --git a/dashboard/src/pages/KnowledgeBases/TextDocumentEditorModal.tsx b/dashboard/src/pages/KnowledgeBases/TextDocumentEditorModal.tsx
index 4ca4ef31..af745204 100644
--- a/dashboard/src/pages/KnowledgeBases/TextDocumentEditorModal.tsx
+++ b/dashboard/src/pages/KnowledgeBases/TextDocumentEditorModal.tsx
@@ -79,14 +79,36 @@ export default function TextDocumentEditorModal({
const handleOk = async () => {
try {
const values = await form.validateFields();
+ const name = String(values.name ?? initialName ?? "").trim();
+ const format = values.format ?? initialFormat;
+ const nextContent = values.content ?? "";
+
+ // Edit mode: unchanged content → just close; skip API / reindex.
+ if (mode === "edit" && nextContent === initialContent) {
+ onCancel();
+ return;
+ }
+
setSubmitting(true);
await onSubmit({
- name: values.name.trim(),
- format: values.format,
- content: values.content ?? "",
+ // Edit mode does not mount name/format fields, so validateFields() may
+ // omit them — fall back to the props used to open the drawer.
+ name,
+ format,
+ content: nextContent,
});
- } catch {
- // validation errors stay in the form
+ } catch (error) {
+ // Ant Design validation rejects with `{ errorFields }`; keep those silent.
+ // Real submit failures are handled inside `onSubmit` (toast) and must not
+ // be mistaken for a no-op click.
+ if (
+ error &&
+ typeof error === "object" &&
+ "errorFields" in error &&
+ Array.isArray((error as { errorFields?: unknown }).errorFields)
+ ) {
+ return;
+ }
} finally {
setSubmitting(false);
}