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(