From 3f9c698a67eb82972243a27e65452d62a2fd1e13 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:54:09 +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=E8=B7=B3=E8=BF=87=E5=8F=AA=E6=9C=89=E8=A7=A6?= =?UTF-8?q?=E5=8F=91=E7=AC=A6=E7=9A=84=E9=80=80=E5=8C=96=20token=EF=BC=8C?= =?UTF-8?q?=E4=B8=8D=E5=86=8D=E9=94=99=E8=AE=A4=E6=AD=A3=E6=96=87=E9=87=8C?= =?UTF-8?q?=E7=9A=84=E8=A3=B8=20`@`=20buildContentFromPastedText=20?= =?UTF-8?q?=E7=BB=84=E8=A3=85=E5=80=99=E9=80=89=E6=97=B6=E8=B7=B3=E8=BF=87?= =?UTF-8?q?=20token=20=E9=95=BF=E5=BA=A6=E5=B0=8F=E4=BA=8E=E7=AD=89?= =?UTF-8?q?=E4=BA=8E=201=20=E7=9A=84=E6=9D=A1=E7=9B=AE=EF=BC=88=E5=BC=95?= =?UTF-8?q?=E7=94=A8=E5=90=8D=E4=B8=BA=E7=A9=BA=E6=97=B6=20token=20?= =?UTF-8?q?=E5=B0=B1=E6=98=AF=E8=A3=B8=20`@`=20/=20`$`=EF=BC=89=EF=BC=9A?= =?UTF-8?q?=E8=BE=B9=E7=95=8C=E8=A7=84=E5=88=99=E4=BC=9A=E8=AE=A9=E5=AE=83?= =?UTF-8?q?=E5=9C=A8=E6=AD=A3=E6=96=87=E9=87=8C=E5=8C=B9=E9=85=8D=E5=88=B0?= =?UTF-8?q?=E4=BB=BB=E4=BD=95=E4=B8=80=E5=A4=84=E8=A7=A6=E5=8F=91=E7=AC=A6?= =?UTF-8?q?=EF=BC=8C=E8=AE=A4=E4=B8=8B=E6=9D=A5=E7=AD=89=E4=BA=8E=E6=8A=8A?= =?UTF-8?q?=E6=97=A0=E5=85=B3=E6=96=87=E5=AD=97=E9=94=99=E8=AE=A4=E6=88=90?= =?UTF-8?q?=E4=B8=80=E6=9D=A1=E5=BC=95=E7=94=A8=20=E5=87=BD=E6=95=B0?= =?UTF-8?q?=E6=B3=A8=E9=87=8A=E7=9A=84=E4=BF=9D=E5=AE=88=E8=A7=84=E5=88=99?= =?UTF-8?q?=E4=BB=8E=E4=B8=A4=E6=9D=A1=E8=A1=A5=E6=88=90=E4=B8=89=E6=9D=A1?= =?UTF-8?q?=EF=BC=8C=E5=86=99=E6=98=8E=E7=A9=BA=E5=90=8D=E5=AD=97=E6=8C=89?= =?UTF-8?q?=E3=80=8C=E5=AE=81=E5=8F=AF=E4=B8=8D=E6=88=90=20chip=E3=80=8D?= =?UTF-8?q?=E7=9A=84=E5=90=8C=E4=B8=80=E5=8F=A3=E5=BE=84=E8=B7=B3=E8=BF=87?= =?UTF-8?q?=20=E6=96=B0=E5=A2=9E=E7=94=A8=E4=BE=8B=EF=BC=9A=E7=A9=BA?= =?UTF-8?q?=E5=90=8D=E5=AD=97=E5=BC=95=E7=94=A8=E7=9A=84=20token=20?= =?UTF-8?q?=E6=98=AF=20`@`=20=E6=97=B6=E4=B8=8D=E8=AE=A4=E9=A2=86=E6=AD=A3?= =?UTF-8?q?=E6=96=87=E9=87=8C=E7=9A=84=E8=A3=B8=20`@`=E3=80=81=E5=85=B6?= =?UTF-8?q?=E4=BD=99=20token=20=E7=85=A7=E5=B8=B8=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=EF=BC=9B=E5=8E=BB=E6=8E=89=E8=AF=A5=E8=B7=B3=E8=BF=87=E5=88=86?= =?UTF-8?q?=E6=94=AF=E5=90=8E=E7=94=A8=E4=BE=8B=E5=8F=98=E7=BA=A2=20?= =?UTF-8?q?=E5=90=8C=E6=AD=A5=20AGC=20=E8=81=8A=E5=A4=A9=E7=B4=A0=E6=9D=90?= =?UTF-8?q?=E5=BC=95=E7=94=A8=E5=8A=9F=E8=83=BD=E8=AF=B4=E6=98=8E=E7=9A=84?= =?UTF-8?q?=E7=B2=98=E8=B4=B4=E8=A7=A3=E6=9E=90=E5=B0=8F=E8=8A=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../project-workspace/resourceReferences.ts | 7 +++++- .../tests/resourceReferences.test.ts | 23 +++++++++++++++++++ .../【功能说明】AGC聊天素材引用-2026-09-08.md | 1 + 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/resourceReferences.ts b/apps/ai-game-creator-shell/src/features/project-workspace/resourceReferences.ts index bef2cd660..08bfe5250 100644 --- a/apps/ai-game-creator-shell/src/features/project-workspace/resourceReferences.ts +++ b/apps/ai-game-creator-shell/src/features/project-workspace/resourceReferences.ts @@ -464,9 +464,11 @@ function countMentionTokenOccurrences(value: string, token: string) { * `chatReferenceMentionToken`——与显示口径逐字同一个字符串,所以「从气泡复制再粘贴」不需要 * 任何兼容别名:`@显示名` / `$名称` 认得出,`@hero.png`、resourceId、大小写变体一律不认。 * - * 两条保守规则: + * 三条保守规则: * * - 同名多候选(同一个 token 对应多条引用身份)一律按文本保留——宁可不成 chip,也不能认错引用。 + * - 退化 token(只有触发符的裸 `@` / `$`,即引用名为空)不参与解析:边界规则会把正文里 + * 任何一处裸 `@` 当成它,等于把无关文字错认成一条引用。 * - 只把正文里真的出现过的 token 交给 `buildContentFromTextTokens`,不做「未命中候选补到末尾」 * (那是润色回包的语义,照搬会把整份清单追加到粘贴文本后面)。同一 token 出现几次就展开几条 * 候选,所以重复出现的引用会各自原位成 chip。 @@ -492,6 +494,9 @@ export function buildContentFromPastedText( } const candidates: ContentTokenCandidate[] = []; for (const [token, bucket] of referencesByToken) { + // 名字为空的引用只会给出一个裸触发符:它能在正文里匹配到任何一处 `@` / `$`, + // 认下来就是错认。空名字是上游数据问题,这里按「宁可不成 chip」的同一口径跳过。 + if (token.length <= 1) continue; if (bucket.length !== 1) continue; const reference = bucket[0]!; const part = chatReferenceToContentPart(reference); diff --git a/apps/ai-game-creator-shell/tests/resourceReferences.test.ts b/apps/ai-game-creator-shell/tests/resourceReferences.test.ts index aee75e632..3ed6d96c5 100644 --- a/apps/ai-game-creator-shell/tests/resourceReferences.test.ts +++ b/apps/ai-game-creator-shell/tests/resourceReferences.test.ts @@ -252,6 +252,29 @@ describe('粘贴文本反解析', () => { ).toBe('dotenv'); }); + it('退化 token(名字为空只剩触发符)不参与解析,正文里的裸 `@` 逐字保留', () => { + // 上游仍然可能送来空名字的引用(显示名兜底只是把常见路径堵上),裸 `@` token 会在正文里 + // 匹配到任何一处 `@`,认下来就是错认:这里必须按「宁可不成 chip」跳过。 + const nameless: ChatReference = { + ...resourceReferenceFromAsset( + assetEntry('dotenv', '.env'), + 'asset-picker', + ), + label: '', + }; + expect(chatReferenceMentionToken(nameless)).toBe('@'); + expect( + buildContentFromPastedText('@ 单独一个 @ 符号,看 @hero', [ + nameless, + hero, + ]), + ).toEqual([ + { type: 'input_text', text: '@ 单独一个 @ 符号,看 ' }, + { type: 'agc_resource_reference', resourceId: 'hero' }, + ]); + expect(buildContentFromPastedText('@ 只有裸符号', [nameless])).toBeNull(); + }); + it('与展示口径互为逆运算:用户气泡文本再粘贴回来得到同一份 content', () => { const content: DirectCodexUserContentPart[] = [ { type: 'input_text', text: '看 ' }, diff --git a/docs/【功能说明】AGC聊天素材引用-2026-09-08.md b/docs/【功能说明】AGC聊天素材引用-2026-09-08.md index 549cb3e06..f57cbf43e 100644 --- a/docs/【功能说明】AGC聊天素材引用-2026-09-08.md +++ b/docs/【功能说明】AGC聊天素材引用-2026-09-08.md @@ -37,6 +37,7 @@ AGC 聊天输入框支持以结构化引用标记当前项目已登记素材, - 只认与显示口径逐字一致的 token:`@显示名`(素材)与 `$名称`(Skill)。`@hero.png`、资源 ID、大小写变体、全角 `@` 都不解析;token 前后必须是行首 / 行尾或空白(与出站文本「token 前后各留一个空白」自洽),所以从用户消息气泡复制出来的那段文字粘回来会重建同一批芯片。 - 宁可不成芯片也不能认错:同名多候选(同一个 token 对应多条引用身份)一律按文本保留;未命中的 token 静默保留,不提示、也不猜文件名或路径。 +- 名字为空的引用(token 只剩一个裸触发符)同样不参与解析:裸 `@` / `$` 会在正文里匹配到任何一处触发符,认下来就是把无关文字错认成引用。显示名一侧另有兜底(见下节「词干为空的素材名回退 `asset.id`」),这条是解析侧的最后一道。 - 附件与运行画面区域的 token(`@附件名` / `@区域标签`)不参与解析——这两类引用没有候选,身份来自文件与运行记录,纯文本重建不出来,所以粘贴时按文本保留。 - 只有真的解析出引用时才接管粘贴:同 namespace 的 Lexical 负载(跨输入区复制芯片)、不含 token 的纯文本、图片文件粘贴都继续走编辑器默认导入,现有行为不变。接管时整段文本在一次编辑更新内插入,一次 Ctrl+Z 就是一次撤销。 - 候选来源按「模糊 / 精确」分两个口:菜单走 `fuzzyLookup(query)`(包含匹配 + 截断到候选上限),粘贴解析走 `lookup()`(就绪的全量候选,不模糊、不截断)。