修复:候选菜单的 key 改用引用身份,同名素材不再撞 key
- 导出 resourceReferences 的 chatReferenceKey(只认稳定身份、不含显示名的身份键),并加注释说明它为什么不能省 - ReferenceMentionOption 由 super(chatReferenceMentionToken(...)) 改为 super(chatReferenceKey(...)):characters/hero.png 与 enemies/hero.png 都展开成 @hero,拿显示 token 当 key 会让 React 撞 key、复用错 DOM,键盘高亮与选中落到另一条候选上 - referenceSourceProviders 用例补一批同名素材:断言两条候选的显示 token 相同、身份键不同
This commit is contained in:
+4
-1
@@ -57,6 +57,7 @@ import {
|
||||
type ChatComposerDraft,
|
||||
type ChatReference,
|
||||
chatReferenceDisplayHint,
|
||||
chatReferenceKey,
|
||||
chatReferenceMentionToken,
|
||||
chatReferenceToContentPart,
|
||||
dedupeChatReferences,
|
||||
@@ -121,7 +122,9 @@ class ReferenceMentionOption extends MenuOption {
|
||||
hint: string;
|
||||
|
||||
constructor(reference: ChatReference) {
|
||||
super(chatReferenceMentionToken(reference));
|
||||
// key 必须是引用的唯一身份:显示 token(`@显示名`)不唯一,同名素材会撞 key 并让
|
||||
// React 复用错 DOM、键盘高亮落到别的候选上。显示 token 只留给 `label`。
|
||||
super(chatReferenceKey(reference));
|
||||
this.reference = reference;
|
||||
this.label = chatReferenceMentionToken(reference);
|
||||
this.hint = chatReferenceDisplayHint(reference);
|
||||
|
||||
@@ -788,7 +788,13 @@ function runtimeRegionReferenceDiscriminators(
|
||||
}:${reference.width ?? ''}:${reference.height ?? ''}:${resourceIds}`;
|
||||
}
|
||||
|
||||
function chatReferenceKey(reference: ChatReference) {
|
||||
/**
|
||||
* 引用的身份键:只认稳定身份,**不含显示名**。
|
||||
*
|
||||
* 同一个显示名可以来自两条不同引用(`characters/hero.png` 与 `enemies/hero.png` 都展开成
|
||||
* `@hero`),所以去重与候选菜单的 React key 都必须用它,而不是显示 token。
|
||||
*/
|
||||
export function chatReferenceKey(reference: ChatReference) {
|
||||
if (reference.type === 'resource') {
|
||||
return `resource:${reference.resourceId}:${reference.source}`;
|
||||
}
|
||||
|
||||
@@ -10,11 +10,13 @@ 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 {
|
||||
AttachmentReference,
|
||||
ChatReference,
|
||||
ResourceReference,
|
||||
RuntimeRegionReference,
|
||||
import {
|
||||
type AttachmentReference,
|
||||
type ChatReference,
|
||||
chatReferenceKey,
|
||||
chatReferenceMentionToken,
|
||||
type ResourceReference,
|
||||
type RuntimeRegionReference,
|
||||
} from '../src/features/project-workspace/resourceReferences';
|
||||
import type { DirectCodexUserContentPart } from '../src/view/project-development/chat/generated/DirectCodexUserContentPart';
|
||||
|
||||
@@ -106,6 +108,25 @@ describe('资源引用 provider', () => {
|
||||
expect(provider.match?.('missing')).toEqual([]);
|
||||
});
|
||||
|
||||
it('同名不同目录的素材是两条候选:显示名相同,但身份键不同', () => {
|
||||
const sameName = createResourceReferenceProvider({
|
||||
assets: [
|
||||
asset('asset-hero-a', 'character', 'image/png', 'characters/hero.png'),
|
||||
asset('asset-hero-b', 'character', 'image/png', 'enemies/hero.png'),
|
||||
],
|
||||
});
|
||||
|
||||
const candidates = sameName.match?.('') ?? [];
|
||||
// 显示 token 会撞(都是 `@hero`),所以候选菜单的 key 不能拿 token 当身份。
|
||||
expect(candidates.map((item) => chatReferenceMentionToken(item))).toEqual([
|
||||
'@hero',
|
||||
'@hero',
|
||||
]);
|
||||
expect(new Set(candidates.map((item) => chatReferenceKey(item))).size).toBe(
|
||||
2,
|
||||
);
|
||||
});
|
||||
|
||||
it('`toReference` 只认资源 part:资产已删除时不合成引用', () => {
|
||||
expect(provider.toReference({ type: 'input_text', text: '看素材' })).toBe(
|
||||
null,
|
||||
|
||||
Reference in New Issue
Block a user