feat(knowledge): 知识能挂附件,新建时就能传
CI / backend (push) Canceled after 0s

知识是备忘录,备忘录不能只有字:手册、日志、导出表格都得能挂上去。
附件本来就落在 item 上(`ALLOWED_KINDS` 早含 knowledge),缺的是"新建时
还没有条目 id"这一段。

- 懒建草稿:`KnowledgeComposer` 第一次传附件(或往正文贴图)时先 `POST /knowledge`
  存一条草稿拿到 id,`draftingRef` 去重防止贴图与上传同时打出两条草稿;
  之后的保存走 `PATCH /knowledge/{id}`。没传过附件就取消,库里不留空条目。
- `RichTextEditor` 增 `ensureItemId`:没有 itemId 时先把草稿存下来再传图,
  于是"新建时不能贴图"这条限制也没了;工具栏上传按钮按是否有落点显隐。
- 新建面板加了附件区:有草稿 id 就是正常的 `AttachmentPanel`(上传/下载/删),
  没有就先给一个「添加附件」按钮,点一下建草稿。
- 安卓知识详情加附件区:列表(文件名 + 大小)、`OpenDocument` 上传任意文件、
  点「打开」用浏览器看签名直链。编辑器原本只能插图,现在手册/日志也能传。
- 文档:API.md 说明附件三类事项共用同一套接口,以及懒建草稿的先后顺序;
  ROADMAP 加 3.13。

验证:后端 pytest 94 passed、ruff 全绿;Web `tsc --noEmit` 通过、`vite build` 通过;
`knowledge-notebook.mjs` 15 项全绿(新增「新建面板里能上传附件」「详情抽屉里能看到
刚传的附件」「附件挂在知识上,详情回读得到」)。安卓端仍是 JDK 25 + Gradle 8.7
不兼容,本次未编译验证。
This commit is contained in:
Steven
2026-10-30 16:18:32 +08:00
parent ffae7e380f
commit 7c84ff3b8d
7 changed files with 250 additions and 14 deletions
+26 -7
View File
@@ -20,6 +20,11 @@ interface Props {
wsId: string;
/** 新建场景还没有条目 id:正文照样能写,只是暂时不能贴图(贴图前必须先存草稿) */
itemId?: string;
/**
* 新建场景的兜底:要贴图时先老老实实把草稿存下来,拿到条目 id 再传。
* 返回 null 表示没存成(由调用方自己提示),编辑器不会继续上传。
*/
ensureItemId?: () => Promise<string | null>;
placeholder?: string;
disabled?: boolean;
/** 编辑器初始高度(px) */
@@ -46,6 +51,7 @@ export function RichTextEditor({
onChange,
wsId,
itemId,
ensureItemId,
placeholder = "写点什么…支持粘贴/拖入图片,也可以用工具栏排版",
disabled = false,
minHeight = 240,
@@ -62,6 +68,7 @@ export function RichTextEditor({
const onErrorRef = useRef(onError);
const resourcesRef = useRef(resources);
const onUploadedRef = useRef(onUploaded);
const ensureItemIdRef = useRef(ensureItemId);
const [ready, setReady] = useState(false);
const [uploading, setUploading] = useState(false);
@@ -70,6 +77,7 @@ export function RichTextEditor({
onErrorRef.current = onError;
resourcesRef.current = resources;
onUploadedRef.current = onUploaded;
ensureItemIdRef.current = ensureItemId;
const disabledRef = useRef(disabled);
disabledRef.current = disabled;
@@ -113,8 +121,8 @@ export function RichTextEditor({
"line",
"code",
"inline-code",
// 没有条目 id 就没有上传落点,索性不摆这个按钮,免得点了才报错
...(itemId ? ["upload"] : []),
// 没有落点就不摆这个按钮,免得点了才报错;新建时靠 ensureItemId 先存草稿
...(itemId || ensureItemId ? ["upload"] : []),
"table",
"|",
"undo",
@@ -131,9 +139,18 @@ export function RichTextEditor({
accept: "image/*",
multiple: true,
handler: async (files: File[]) => {
if (!itemId) {
onErrorRef.current?.("先保存这条内容,再回来贴图或传附件");
return null;
// 新建时还没有条目 id:先让调用方把草稿存下来,拿到落点再传
let targetId = itemId;
if (!targetId) {
const ensured = await ensureItemIdRef.current?.();
if (!ensured) {
// 存草稿失败的原因由 ensureItemId 自己报,这里不叠一层噪音
if (!ensureItemIdRef.current) {
onErrorRef.current?.("先保存这条内容,再回来贴图或传附件");
}
return null;
}
targetId = ensured;
}
const images = files.filter((file) => file.type.startsWith("image/"));
if (images.length === 0) {
@@ -145,7 +162,7 @@ export function RichTextEditor({
try {
for (const file of images) {
const created = await uploadFile<Attachment>(
`/workspaces/${wsId}/items/${itemId}/attachments`,
`/workspaces/${wsId}/items/${targetId}/attachments`,
file,
);
// 自己插入,不用 Vditor 代劳:它对 handler 的返回值只会
@@ -219,7 +236,9 @@ export function RichTextEditor({
? "正在上传图片…"
: itemId
? "支持直接粘贴或拖入图片;内容按 Markdown 保存,模型和导出都读得懂"
: "内容按 Markdown 保存;存下来之后就能直接贴图、传附件"}
: ensureItemId
? "支持直接粘贴或拖入图片:会先自动存成草稿,再传上去"
: "内容按 Markdown 保存;存下来之后就能直接贴图、传附件"}
</p>
</div>
);