From 14f11e77a2b4319b2697419b360e1dbbc2d28532 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Tue, 22 Sep 2026 19:54:36 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E7=B2=98=E8=B4=B4?= =?UTF-8?q?=E8=A7=A3=E6=9E=90=E5=87=BA=E7=9A=84=E5=BC=95=E7=94=A8=E8=8B=A5?= =?UTF-8?q?=E8=A7=A3=E6=9E=90=E4=B8=8D=E5=87=BA=E6=9D=A5=EF=BC=8C=E9=80=80?= =?UTF-8?q?=E5=8C=96=E6=88=90=20token=20=E6=96=87=E6=9C=AC=E8=80=8C?= =?UTF-8?q?=E4=B8=8D=E6=98=AF=E6=95=B4=E6=9D=A1=E4=B8=A2=E6=8E=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit $insertContentAtSelection 的引用分支补 else:referenceFromPart 返回 null 时插入该 part 的 token 文本 此前上游 buildContentFromPastedText 已把 token 摘走,插入端再丢一次就会让这段内容静默消失 新增用例:provider 认得出 token 但插入时已解析不出引用,断言正文保持 `看 @hero 一眼`、不产生芯片 变异验证:去掉该兜底后同一用例变红(正文变成 `看 一眼`),恢复即绿 --- .../ResourceReferenceInput.tsx | 7 +++++ .../tests/resourceReferenceInput.test.tsx | 31 +++++++++++++++++++ 2 files changed, 38 insertions(+) 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 ffbbe4035..199b53997 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 @@ -322,6 +322,9 @@ function $selectionOrRootEnd() { * 与 `applyContentToRoot`(整根替换,供初始草稿与润色回写使用)的区别只在替换范围: * 文本 part 的 `\n` 落成真正的段落分隔(与编辑器默认的纯文本粘贴同一形状),引用 part 落成 * chip;不加任何补白,token 原位替换、token 之外的每个字符照原样保留。 + * + * 认不出的引用 part(provider 的 `toReference` 解析不出来)退回它的 token 文本, + * 绝不静默丢掉——粘贴进来的内容一个字符都不会凭空消失。 */ function $insertContentAtSelection( content: readonly DirectCodexUserContentPart[], @@ -342,7 +345,11 @@ function $insertContentAtSelection( $selectionOrRootEnd()?.insertNodes([ $createResourceReferenceNode(reference), ]); + return; } + // 解析不出的 part 已经在上游被摘掉了 token,这里必须把文本补回去,否则这段内容会静默消失。 + const token = mentionTokenFromPart(providers, part); + if (token) $selectionOrRootEnd()?.insertText(token); }); } diff --git a/apps/ai-game-creator-shell/tests/resourceReferenceInput.test.tsx b/apps/ai-game-creator-shell/tests/resourceReferenceInput.test.tsx index ea1feaa2a..0fdbd3298 100644 --- a/apps/ai-game-creator-shell/tests/resourceReferenceInput.test.tsx +++ b/apps/ai-game-creator-shell/tests/resourceReferenceInput.test.tsx @@ -33,6 +33,7 @@ import { attachmentReferenceProvider } from '../src/features/project-workspace/r import { createResourceReferenceProvider } from '../src/features/project-workspace/reference-source/resourceReferenceProvider'; import { runtimeRegionReferenceProvider } from '../src/features/project-workspace/reference-source/runtimeRegionReferenceProvider'; import { useSkillReferenceProvider } from '../src/features/project-workspace/reference-source/skillReferenceProvider'; +import type { ReferenceProvider } from '../src/features/project-workspace/reference-source/types'; import { ResourceReferenceInput, type ResourceReferenceInputHandle, @@ -458,6 +459,36 @@ describe('ResourceReferenceInput', () => { expect(editorTextSize()).toBe('看 '.length + 1 + ' 这一版'.length); }); + test('粘贴解析出的引用若已无法解析,退化成 token 文本而不是整条丢掉', async () => { + const ref = createRef(); + // provider 认得出这个 token(lookup 有候选),但插入那一刻已经解析不出引用(例如资产刚被删)。 + const ghostProvider: ReferenceProvider = { + trigger: '@', + fuzzy_lookup: () => [], + lookup: () => [resourceReferenceFromAsset(assets[0]!, 'asset-picker')], + toReference: () => null, + refresh: (reference) => reference, + mentionToken: (part) => + part.type === 'agc_resource_reference' ? '@hero' : null, + }; + render( + , + ); + + pasteComposerText('看 @hero 一眼'); + + await waitFor(() => { + expect(draftText(ref.current?.getDraft())).toBe('看 @hero 一眼'); + }); + expect(draftResourceIds(ref.current?.getDraft())).toEqual([]); + }); + test('粘贴未命中的 token:整段按字面落进正文,不接管、不提示', async () => { const ref = createRef(); render(