修复排队消息 chip 的 @ 引用露出内部 resourceId(review 第 2 条)

- queuedChatTurnLabel 增加 assets 入参并透传给 directCodexContentToPromptText,@ 引用按 manifest 显示名展开,与聊天输入区文案口径一致
- ComposerTurnQueue 增加 assets 属性,ProjectSupervisorView 把 chatProjectAssets 传下去
- chat-composer 用例新增「队列 chip 的 @ 引用按 manifest 显示名展开」,钉住显示名文案与不传清单时的稳定 id 兜底
This commit is contained in:
2026-09-16 21:25:38 +08:00
parent 280a2dc5d1
commit 869abad879
4 changed files with 50 additions and 5 deletions
@@ -19,6 +19,7 @@ import {
import type { RefObject } from 'react';
import { useEffect, useRef, useState } from 'react';
import type { GameCreationAppAssetManifestEntry } from '../../../../../packages/shared/src/contracts/gameCreationApp';
import { resolveTauriInvoke } from '../../app/tauri';
import type {
GameCreatorAppConfigView,
@@ -192,9 +193,12 @@ export function ComposerPendingAttachments({
/** 队列 chip:回合运行中入队的消息,按 FIFO 顺序展示,可单条取消。 */
export function ComposerTurnQueue({
turns,
assets,
onCancel,
}: {
turns: readonly QueuedChatTurn[];
/** 与聊天输入区同源的素材清单:chip 文案里的 `@` 引用按它展开成显示名。 */
assets?: readonly GameCreationAppAssetManifestEntry[];
onCancel: (id: string) => void;
}) {
if (turns.length === 0) {
@@ -211,11 +215,11 @@ export function ComposerTurnQueue({
{index + 1}
</span>
<span className="project-supervisor-composer-queue-text">
{queuedChatTurnLabel(turn)}
{queuedChatTurnLabel(turn, assets)}
</span>
<button
type="button"
aria-label={`取消排队消息 ${queuedChatTurnLabel(turn)}`}
aria-label={`取消排队消息 ${queuedChatTurnLabel(turn, assets)}`}
title="取消这条排队消息"
onClick={() => onCancel(turn.id)}
>
@@ -889,6 +889,7 @@ export function ProjectSupervisorView({
>
<ComposerTurnQueue
turns={directCodex ? queuedTurns : []}
assets={chatProjectAssets}
onCancel={(id) => onCancelQueuedTurn?.(id)}
/>
<ComposerPendingAttachments
@@ -4,6 +4,7 @@
* 回合运行中用户再次发送时,消息进入 FIFO 队列而不是被丢弃;当前回合结束后按入队顺序
* 依次发出。队列项能在输入盒上方单独取消。这里只放与 React 无关的纯逻辑,便于单测。
*/
import type { GameCreationAppAssetManifestEntry } from '../../../../../packages/shared/src/contracts/gameCreationApp';
import type { DirectCodexUserItem } from './generated';
import { directCodexContentToPromptText } from './resourceReferences';
@@ -73,9 +74,17 @@ export function chatQueueFullNotice(): string {
return `队列已满(最多 ${MAX_QUEUED_CHAT_TURNS} 条),请等当前回合结束后再发送`;
}
/** 队列 chip 上显示的文字:单行、有长度上限。 */
export function queuedChatTurnLabel(turn: QueuedChatTurn): string {
const text = directCodexContentToPromptText(turn.userItem.content)
/**
* 队列 chip 上显示的文字:单行、有长度上限。
*
* `assets` 必须与聊天输入区同源(`manifest.assets`):`@` 引用要按显示名展开,否则
* chip 上会露出 `@asset:…` 这样的内部 id,与用户输入时看到的 `@显示名` 对不上。
*/
export function queuedChatTurnLabel(
turn: QueuedChatTurn,
assets: readonly GameCreationAppAssetManifestEntry[] = [],
): string {
const text = directCodexContentToPromptText(turn.userItem.content, assets)
.trim()
.replace(/\s+/gu, ' ');
if (text) {
@@ -196,6 +196,37 @@ function queuedTurn(
}
export function registerChatComposerControlTests() {
it('队列 chip 的 @ 引用按 manifest 显示名展开,不露出内部 resourceId', () => {
const turn = createQueuedChatTurn({
id: 'turn-ref',
clientTurnId: 'client-ref',
userItem: directCodexUserItemFromContent(
[
{ type: 'input_text', text: '用这张图改一下' },
{ type: 'agc_resource_reference', resourceId: 'asset:hero' },
],
'client-ref:user',
),
createdAt: 1,
});
const manifestAssets = [
{
id: 'asset:hero',
kind: 'character',
mediaType: 'image/png',
localPath: 'assets/hero.png',
source: { kind: 'uploaded' as const },
},
];
// chip 文案与聊天输入区同口径:用户看到的是 `@显示名`,不是 `@内部 id`。
expect(queuedChatTurnLabel(turn, manifestAssets)).toBe(
'用这张图改一下@hero',
);
// 素材清单不传(老调用方)时退回稳定 id,不抛错。
expect(queuedChatTurnLabel(turn)).toBe('用这张图改一下@asset:hero');
});
it('keeps queued chat turns in FIFO order and drops only the cancelled one', () => {
const first = queuedTurn('turn-1', 'client-1', '第一条', 1);
const second = queuedTurn('turn-2', 'client-2', '第二条', 2);