加固:粘贴接管改为「真的插进正文之后」才生效,插入为空时放行默认粘贴

ResourceReferenceInput 的 PASTE_COMMAND 处理器原先先 preventDefault 再去插入:插入为空时这段文字既不进我们的插入、又被挡掉了默认导入。现在先做插入,只有插入确实发生了才 preventDefault 并返回 true,否则返回 false 交给 Lexical 的默认粘贴
$insertContentAtSelection 改为返回「插进去没有」:选区取不到、或每个 part 都没落下内容时返回 false
编辑器已在一次更新里时 editor.update 会把回调排队,这时按原口径先接管(ranSync 标记区分「回调已同步跑完」与「回调被排队」),避免误判成插入为空而放行默认粘贴造成双份插入
同步 AGC 聊天素材引用功能说明的粘贴解析小节
This commit is contained in:
2026-09-23 11:55:40 +08:00
parent 3f9c698a67
commit 3391ecf7d4
2 changed files with 34 additions and 11 deletions
@@ -325,18 +325,28 @@ function $selectionOrRootEnd() {
*
* 认不出的引用 part(provider 的 `toReference` 解析不出来)退回它的 token 文本,
* 绝不静默丢掉——粘贴进来的内容一个字符都不会凭空消失。
*
* 返回「这次到底插进去没有」:调用方据此决定要不要接管这次粘贴,插入为空时必须放行
* 编辑器的默认粘贴,否则这段文字两边都不管。
*/
function $insertContentAtSelection(
content: readonly DirectCodexUserContentPart[],
providers: readonly ReferenceProvider[],
) {
): boolean {
// 每插一段都重新取一次选区:插入会移动光标,上一轮拿到的那个 RangeSelection 会过期。
if (!$selectionOrRootEnd()) return;
if (!$selectionOrRootEnd()) return false;
let inserted = false;
content.forEach((part) => {
if (part.type === 'input_text') {
part.text.split('\n').forEach((line, index) => {
if (index > 0) $selectionOrRootEnd()?.insertParagraph();
if (line) $selectionOrRootEnd()?.insertText(line);
if (index > 0) {
$selectionOrRootEnd()?.insertParagraph();
inserted = true;
}
if (line) {
$selectionOrRootEnd()?.insertText(line);
inserted = true;
}
});
return;
}
@@ -345,12 +355,17 @@ function $insertContentAtSelection(
$selectionOrRootEnd()?.insertNodes([
$createResourceReferenceNode(reference),
]);
inserted = true;
return;
}
// 解析不出的 part 已经在上游被摘掉了 token,这里必须把文本补回去,否则这段内容会静默消失。
const token = mentionTokenFromPart(providers, part);
if (token) $selectionOrRootEnd()?.insertText(token);
if (token) {
$selectionOrRootEnd()?.insertText(token);
inserted = true;
}
});
return inserted;
}
/**
@@ -567,9 +582,9 @@ function ResourceReferenceEditor({
);
}, [editor, multiline]);
// 粘贴解析:只有「粘贴文本里真的解析出了引用 token」时才接管,其余一律返回 false 放行
// Lexical 的默认导入——同 namespace 复制出来的 Lexical payload 本来就能还原真 chip,
// 不含 token 的纯文本、图片文件粘贴也都保持原行为。
// 粘贴解析:只有「粘贴文本里真的解析出了引用 token」且「这一整段真的插进了正文」时才接管,
// 其余一律返回 false 放行 Lexical 的默认导入——同 namespace 复制出来的 Lexical payload
// 本来就能还原真 chip,不含 token 的纯文本、图片文件粘贴也都保持原行为。
//
// 接管时整段文本在一次 update 内插入(与默认粘贴同样打 PASTE_TAG),所以一次 Ctrl+Z 就整体
// 回退;token 之外的每个字符逐字保留(换行落成段落分隔,与默认纯文本粘贴同形状)。
@@ -592,14 +607,22 @@ function ResourceReferenceEditor({
);
const content = buildContentFromPastedText(text, references);
if (!content) return false;
event.preventDefault();
// 先真的插进去,再决定接管这次粘贴:插入为空(取不到选区)时必须放行默认粘贴,
// 否则这段文字既没进我们的插入、又被 preventDefault 挡掉了默认导入,静默消失。
// 编辑器已经在一次更新里时 `editor.update` 会把回调排队,这时 `ranSync` 仍是 false,
// 按原口径先接管,等队列里的那次插入落地。
let ranSync = false;
let inserted = false;
editor.update(
() => {
$insertContentAtSelection(content, providersRef.current);
ranSync = true;
inserted = $insertContentAtSelection(content, providersRef.current);
},
// 与编辑器默认粘贴同一口径:粘贴是它自己的一条撤销记录。
{ tag: PASTE_TAG },
);
if (ranSync && !inserted) return false;
event.preventDefault();
return true;
},
COMMAND_PRIORITY_CRITICAL,