From 3391ecf7d44ef89a3b1aec9c01659ec43c35d571 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Wed, 23 Sep 2026 11:55:40 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=A0=E5=9B=BA=EF=BC=9A=E7=B2=98=E8=B4=B4?= =?UTF-8?q?=E6=8E=A5=E7=AE=A1=E6=94=B9=E4=B8=BA=E3=80=8C=E7=9C=9F=E7=9A=84?= =?UTF-8?q?=E6=8F=92=E8=BF=9B=E6=AD=A3=E6=96=87=E4=B9=8B=E5=90=8E=E3=80=8D?= =?UTF-8?q?=E6=89=8D=E7=94=9F=E6=95=88=EF=BC=8C=E6=8F=92=E5=85=A5=E4=B8=BA?= =?UTF-8?q?=E7=A9=BA=E6=97=B6=E6=94=BE=E8=A1=8C=E9=BB=98=E8=AE=A4=E7=B2=98?= =?UTF-8?q?=E8=B4=B4=20ResourceReferenceInput=20=E7=9A=84=20PASTE=5FCOMMAN?= =?UTF-8?q?D=20=E5=A4=84=E7=90=86=E5=99=A8=E5=8E=9F=E5=85=88=E5=85=88=20pr?= =?UTF-8?q?eventDefault=20=E5=86=8D=E5=8E=BB=E6=8F=92=E5=85=A5=EF=BC=9A?= =?UTF-8?q?=E6=8F=92=E5=85=A5=E4=B8=BA=E7=A9=BA=E6=97=B6=E8=BF=99=E6=AE=B5?= =?UTF-8?q?=E6=96=87=E5=AD=97=E6=97=A2=E4=B8=8D=E8=BF=9B=E6=88=91=E4=BB=AC?= =?UTF-8?q?=E7=9A=84=E6=8F=92=E5=85=A5=E3=80=81=E5=8F=88=E8=A2=AB=E6=8C=A1?= =?UTF-8?q?=E6=8E=89=E4=BA=86=E9=BB=98=E8=AE=A4=E5=AF=BC=E5=85=A5=E3=80=82?= =?UTF-8?q?=E7=8E=B0=E5=9C=A8=E5=85=88=E5=81=9A=E6=8F=92=E5=85=A5=EF=BC=8C?= =?UTF-8?q?=E5=8F=AA=E6=9C=89=E6=8F=92=E5=85=A5=E7=A1=AE=E5=AE=9E=E5=8F=91?= =?UTF-8?q?=E7=94=9F=E4=BA=86=E6=89=8D=20preventDefault=20=E5=B9=B6?= =?UTF-8?q?=E8=BF=94=E5=9B=9E=20true=EF=BC=8C=E5=90=A6=E5=88=99=E8=BF=94?= =?UTF-8?q?=E5=9B=9E=20false=20=E4=BA=A4=E7=BB=99=20Lexical=20=E7=9A=84?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E7=B2=98=E8=B4=B4=20$insertContentAtSelectio?= =?UTF-8?q?n=20=E6=94=B9=E4=B8=BA=E8=BF=94=E5=9B=9E=E3=80=8C=E6=8F=92?= =?UTF-8?q?=E8=BF=9B=E5=8E=BB=E6=B2=A1=E6=9C=89=E3=80=8D=EF=BC=9A=E9=80=89?= =?UTF-8?q?=E5=8C=BA=E5=8F=96=E4=B8=8D=E5=88=B0=E3=80=81=E6=88=96=E6=AF=8F?= =?UTF-8?q?=E4=B8=AA=20part=20=E9=83=BD=E6=B2=A1=E8=90=BD=E4=B8=8B?= =?UTF-8?q?=E5=86=85=E5=AE=B9=E6=97=B6=E8=BF=94=E5=9B=9E=20false=20?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E5=99=A8=E5=B7=B2=E5=9C=A8=E4=B8=80=E6=AC=A1?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E9=87=8C=E6=97=B6=20editor.update=20?= =?UTF-8?q?=E4=BC=9A=E6=8A=8A=E5=9B=9E=E8=B0=83=E6=8E=92=E9=98=9F=EF=BC=8C?= =?UTF-8?q?=E8=BF=99=E6=97=B6=E6=8C=89=E5=8E=9F=E5=8F=A3=E5=BE=84=E5=85=88?= =?UTF-8?q?=E6=8E=A5=E7=AE=A1=EF=BC=88ranSync=20=E6=A0=87=E8=AE=B0?= =?UTF-8?q?=E5=8C=BA=E5=88=86=E3=80=8C=E5=9B=9E=E8=B0=83=E5=B7=B2=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=E8=B7=91=E5=AE=8C=E3=80=8D=E4=B8=8E=E3=80=8C=E5=9B=9E?= =?UTF-8?q?=E8=B0=83=E8=A2=AB=E6=8E=92=E9=98=9F=E3=80=8D=EF=BC=89=EF=BC=8C?= =?UTF-8?q?=E9=81=BF=E5=85=8D=E8=AF=AF=E5=88=A4=E6=88=90=E6=8F=92=E5=85=A5?= =?UTF-8?q?=E4=B8=BA=E7=A9=BA=E8=80=8C=E6=94=BE=E8=A1=8C=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E7=B2=98=E8=B4=B4=E9=80=A0=E6=88=90=E5=8F=8C=E4=BB=BD=E6=8F=92?= =?UTF-8?q?=E5=85=A5=20=E5=90=8C=E6=AD=A5=20AGC=20=E8=81=8A=E5=A4=A9?= =?UTF-8?q?=E7=B4=A0=E6=9D=90=E5=BC=95=E7=94=A8=E5=8A=9F=E8=83=BD=E8=AF=B4?= =?UTF-8?q?=E6=98=8E=E7=9A=84=E7=B2=98=E8=B4=B4=E8=A7=A3=E6=9E=90=E5=B0=8F?= =?UTF-8?q?=E8=8A=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ResourceReferenceInput.tsx | 43 ++++++++++++++----- .../【功能说明】AGC聊天素材引用-2026-09-08.md | 2 +- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/ResourceReferenceInput.tsx b/apps/ai-game-creator-shell/src/features/project-workspace/ResourceReferenceInput.tsx index 199b53997..633b73c26 100644 --- a/apps/ai-game-creator-shell/src/features/project-workspace/ResourceReferenceInput.tsx +++ b/apps/ai-game-creator-shell/src/features/project-workspace/ResourceReferenceInput.tsx @@ -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, diff --git a/docs/【功能说明】AGC聊天素材引用-2026-09-08.md b/docs/【功能说明】AGC聊天素材引用-2026-09-08.md index f57cbf43e..b91a0eee4 100644 --- a/docs/【功能说明】AGC聊天素材引用-2026-09-08.md +++ b/docs/【功能说明】AGC聊天素材引用-2026-09-08.md @@ -39,7 +39,7 @@ AGC 聊天输入框支持以结构化引用标记当前项目已登记素材, - 宁可不成芯片也不能认错:同名多候选(同一个 token 对应多条引用身份)一律按文本保留;未命中的 token 静默保留,不提示、也不猜文件名或路径。 - 名字为空的引用(token 只剩一个裸触发符)同样不参与解析:裸 `@` / `$` 会在正文里匹配到任何一处触发符,认下来就是把无关文字错认成引用。显示名一侧另有兜底(见下节「词干为空的素材名回退 `asset.id`」),这条是解析侧的最后一道。 - 附件与运行画面区域的 token(`@附件名` / `@区域标签`)不参与解析——这两类引用没有候选,身份来自文件与运行记录,纯文本重建不出来,所以粘贴时按文本保留。 -- 只有真的解析出引用时才接管粘贴:同 namespace 的 Lexical 负载(跨输入区复制芯片)、不含 token 的纯文本、图片文件粘贴都继续走编辑器默认导入,现有行为不变。接管时整段文本在一次编辑更新内插入,一次 Ctrl+Z 就是一次撤销。 +- 只有真的解析出引用、并且这一整段真的插进了正文时才接管粘贴:同 namespace 的 Lexical 负载(跨输入区复制芯片)、不含 token 的纯文本、图片文件粘贴都继续走编辑器默认导入,现有行为不变。取不到选区时插入为空,这时也放行默认粘贴,粘贴的文字不会「两边都不管」。接管时整段文本在一次编辑更新内插入,一次 Ctrl+Z 就是一次撤销。 - 候选来源按「模糊 / 精确」分两个口:菜单走 `fuzzyLookup(query)`(包含匹配 + 截断到候选上限),粘贴解析走 `lookup()`(就绪的全量候选,不模糊、不截断)。 - Skill 目录是应用级异步读取,仍然只在用户第一次敲出 `$` 时读:冷启动时粘贴 `$名称` 就按字面文本保留(粘贴不会为了解析去提前读盘,也不会等待目录),用户敲过一次 `$` 之后粘贴即可重建芯片。