fix(upload): address code review findings

final code review 后的修复:

1. blob 检查收紧(Important): md.contains("blob:") 误伤合法讨论 blob URL
   的代码块/正文,改为检测 markdown 图片语法 ](blob: 才算占位符泄漏
2. 重试错误原地更新(Minor): seen_error_ids 去重逻辑导致重试后再失败时
   顶部提示停留在旧错误文案;改为已存在 id 时原地更新 message
3. destroy 清理 coordinatorRef(Minor): destroy() 调 setUploadCoordinator(null),
   避免 NodeView 指向已销毁的 coordinator(防御性)
4. 删除 NodeView 未用的 editor 字段(Nit): 死代码清理

验证: cargo test (384 passed) / clippy (本次零警告) / dx check / tiptap build 全过
This commit is contained in:
xfy 2026-06-22 15:49:23 +08:00
parent a10f79d47e
commit cee4f3f3ef
3 changed files with 19 additions and 12 deletions

View File

@ -235,6 +235,8 @@ class TiptapEditorInstance {
this.editor?.destroy() this.editor?.destroy()
this.editor = null this.editor = null
this.coordinator = null this.coordinator = null
// 清除 NodeView 的 coordinator 引用,避免指向已销毁的实例
setUploadCoordinator(null)
// 清理源码模式相关引用(容器 innerHTML 已清空DOM 会随之移除) // 清理源码模式相关引用(容器 innerHTML 已清空DOM 会随之移除)
this.sourceTextarea = null this.sourceTextarea = null
this.toggleButton = null this.toggleButton = null

View File

@ -1,5 +1,5 @@
import { Image } from '@tiptap/extension-image' import { Image } from '@tiptap/extension-image'
import { mergeAttributes, type Editor } from '@tiptap/core' import { mergeAttributes } from '@tiptap/core'
import type { Node as PMNode } from '@tiptap/pm/model' import type { Node as PMNode } from '@tiptap/pm/model'
/** NodeView 按钮点击回调注入接口。 */ /** NodeView 按钮点击回调注入接口。 */
@ -20,7 +20,6 @@ export interface UploadNodeViewCallbacks {
* ProseMirror update(node)NodeView data-upload-state * ProseMirror update(node)NodeView data-upload-state
*/ */
class UploadImageNodeView { class UploadImageNodeView {
private editor: Editor
private node: PMNode private node: PMNode
private callbacks: UploadNodeViewCallbacks private callbacks: UploadNodeViewCallbacks
@ -30,12 +29,10 @@ class UploadImageNodeView {
constructor(opts: { constructor(opts: {
node: PMNode node: PMNode
editor: Editor
HTMLAttributes: Record<string, unknown> HTMLAttributes: Record<string, unknown>
callbacks: UploadNodeViewCallbacks callbacks: UploadNodeViewCallbacks
}) { }) {
this.node = opts.node this.node = opts.node
this.editor = opts.editor
this.callbacks = opts.callbacks this.callbacks = opts.callbacks
this.container = document.createElement('div') this.container = document.createElement('div')
@ -183,10 +180,9 @@ export const UploadImage = Image.configure({ allowBase64: true }).extend({
}, },
addNodeView() { addNodeView() {
return ({ node, HTMLAttributes, editor }) => { return ({ node, HTMLAttributes }) => {
return new UploadImageNodeView({ return new UploadImageNodeView({
node, node,
editor,
HTMLAttributes, HTMLAttributes,
callbacks: { callbacks: {
onRetry: (id) => coordinatorRef?.retryUpload(id), onRetry: (id) => coordinatorRef?.retryUpload(id),

View File

@ -332,14 +332,21 @@ fn write_editor(post_id: Option<i32>) -> Element {
for ev in parsed.events { for ev in parsed.events {
match ev.kind.as_str() { match ev.kind.as_str() {
"error" => { "error" => {
if !seen_error_ids.contains(&ev.upload_id) { let msg = ev.error_msg.unwrap_or_else(|| "上传失败".to_string());
seen_error_ids.insert(ev.upload_id.clone()); if seen_error_ids.insert(ev.upload_id.clone()) {
// 新失败:追加提示
upload_errors.write().push(UploadErrorEntry { upload_errors.write().push(UploadErrorEntry {
id: ev.upload_id, id: ev.upload_id,
file_name: ev.file_name, file_name: ev.file_name,
message: ev.error_msg message: msg,
.unwrap_or_else(|| "上传失败".to_string()),
}); });
} else {
// 已存在的 id重试后再失败原地更新消息
// 避免顶部提示停留在旧错误文案。
let mut errors = upload_errors.write();
if let Some(entry) = errors.iter_mut().find(|e| e.id == ev.upload_id) {
entry.message = msg;
}
} }
} }
"success" | "removed" => { "success" | "removed" => {
@ -392,8 +399,10 @@ fn write_editor(post_id: Option<i32>) -> Element {
})() })()
"#).ok().and_then(|v| v.as_string()).unwrap_or_default(); "#).ok().and_then(|v| v.as_string()).unwrap_or_default();
// 兜底:扫描残留的 blob: 或 data-upload-state轮询窗口期漏判防护 // 兜底:扫描残留的上传占位符标记(轮询窗口期漏判防护)
if md.contains("blob:") || md.contains("data-upload-state") { // 检测 ![](blob:...) 形式的泄漏图片 src而非裸 "blob:" 字符串,
// 避免误伤合法讨论 blob URL 的代码块/正文。
if md.contains("](blob:") || md.contains("data-upload-state") {
error.set(Some("检测到未完成上传的图片,请处理后保存".to_string())); error.set(Some("检测到未完成上传的图片,请处理后保存".to_string()));
return; return;
} }