接入前端 canonical 用户 Response item

按 Lexical 顺序构造 message.content

DirectProject payload 改为发送 userItem

策略重试保留同一 canonical item
This commit is contained in:
2026-09-15 16:12:56 +08:00
committed by kdletters
parent 4ed342d882
commit 5576acdc11
3 changed files with 120 additions and 9 deletions
+36 -5
View File
@@ -222,6 +222,7 @@ import {
} from './features/project-workspace/agentRunTrace';
import { DeveloperProjectPanels } from './features/project-workspace/DeveloperProjectPanels';
import { DeveloperRuntimePanels } from './features/project-workspace/DeveloperRuntimePanels';
import type { DirectCodexUserContentPart } from './features/project-workspace/generated';
import {
appendMemoryContent,
memoryScopeLabel,
@@ -259,6 +260,7 @@ import type {
ChatReference,
} from './features/project-workspace/resourceReferences';
import {
chatComposerDraftToDirectCodexUserItem,
RESOURCE_REFERENCE_INSERT_EVENT,
type ResourceReferenceInsertEventDetail,
} from './features/project-workspace/resourceReferences';
@@ -555,6 +557,7 @@ type ExecuteChatAgentReplyInput = {
attachments?: DirectCodexTurnAttachment[];
directPolicyChecked?: boolean;
references?: ChatReference[];
userItem?: ReturnType<typeof chatComposerDraftToDirectCodexUserItem>;
};
/**
@@ -743,6 +746,9 @@ export function App({
: '',
);
const [chatReferences, setChatReferences] = useState<ChatReference[]>([]);
const [chatContent, setChatContent] = useState<DirectCodexUserContentPart[]>(
[],
);
const chatComposerRef = useRef<ResourceReferenceInputHandle | null>(null);
const [chatAgentBusy, setChatAgentBusy] = useState(false);
const [directCodexProgress, setDirectCodexProgress] = useState('');
@@ -3711,6 +3717,7 @@ export function App({
setChatInput(readSupervisorChatDraft(openedProject.projectPath));
}
setChatReferences([]);
setChatContent([]);
setManifest(openedProject.manifest);
setProjectFiles([]);
setProjectCheckpoints([]);
@@ -3951,12 +3958,14 @@ export function App({
function prepareChatCommandDraft(commandDraft: string) {
setChatInput(commandDraft);
setChatReferences([]);
setChatContent([]);
window.setTimeout(() => chatInputRef.current?.focus(), 0);
}
function handleChatComposerChange(draft: ChatComposerDraft) {
setChatInput(draft.text);
setChatReferences(draft.references);
setChatContent(draft.content ?? []);
}
useEffect(() => {
@@ -4996,6 +5005,7 @@ export function App({
setChatInput('');
setChatReferences([]);
setChatContent([]);
setMessages((current) => [
...current,
{
@@ -6083,7 +6093,12 @@ export function App({
return;
}
void executeChatAgentReply({ prompt, references });
const clientTurnId = createDirectCodexConversationTurnId();
const userItem = chatComposerDraftToDirectCodexUserItem(
{ text: prompt, references, content: chatContent },
directCodexConversationMessageId(clientTurnId, 'user'),
);
void executeChatAgentReply({ prompt, references, userItem, clientTurnId });
}
async function executeLlmConfigStatus() {
@@ -6312,6 +6327,7 @@ export function App({
attachments,
directPolicyChecked = false,
references,
userItem,
}: ExecuteChatAgentReplyInput) {
if (planningV2ActiveRef.current || planningStartMode) {
const nextProjectPath = resolveChatProjectPath(localProject);
@@ -6342,6 +6358,12 @@ export function App({
if (directProjectPath && directInvoke) {
const clientTurnId =
directConversationTurnId ?? createDirectCodexConversationTurnId();
const effectiveUserItem =
userItem ??
chatComposerDraftToDirectCodexUserItem(
{ text: prompt, references: references ?? [], content: [] },
directCodexConversationMessageId(clientTurnId, 'user'),
);
if (
!directPolicyChecked &&
projectConversationWriteConfirmedRef.current !== directProjectPath
@@ -6353,6 +6375,7 @@ export function App({
creationType,
attachments,
references,
userItem: effectiveUserItem,
});
try {
const policyPaused = await queueProjectPolicyConfirmationIfNeeded(
@@ -6477,11 +6500,12 @@ export function App({
clientTurnId: string;
creationType?: HomeCreationType;
attachments?: DirectCodexTurnAttachment[];
references?: ChatReference[];
userItem: ReturnType<typeof chatComposerDraftToDirectCodexUserItem>;
} = {
projectPath: directProjectPath,
prompt,
clientTurnId,
userItem: effectiveUserItem,
};
if (creationType) {
directTurnInput.creationType = creationType;
@@ -6489,9 +6513,7 @@ export function App({
if (attachments?.length) {
directTurnInput.attachments = attachments;
}
if (references?.length) {
directTurnInput.references = references;
}
directTurnInput.userItem = effectiveUserItem;
const reply = await withDirectCodexSessionRefresh(() => {
// 每次调用都会新建 Rust 事件流;续期重试需重新接收同一回合的进度。
activeDirectCodexTurnRef.current = {
@@ -11839,6 +11861,7 @@ export function App({
}
setChatInput('');
setChatReferences([]);
setChatContent([]);
void loadProjectConversation(nextProjectPath, false, 'replace');
return;
}
@@ -11868,8 +11891,15 @@ export function App({
const directConversationTurnId = directCodexProductRuntime
? createDirectCodexConversationTurnId()
: undefined;
const directUserItem = directConversationTurnId
? chatComposerDraftToDirectCodexUserItem(
{ text: prompt, references, content: chatContent },
directCodexConversationMessageId(directConversationTurnId, 'user'),
)
: undefined;
setChatInput('');
setChatReferences([]);
setChatContent([]);
setMessages((current) => [
...current,
{
@@ -11891,6 +11921,7 @@ export function App({
prompt,
clientTurnId: directConversationTurnId,
references,
userItem: directUserItem,
});
}
@@ -70,6 +70,7 @@ import {
writeChatPromptPolishReminderDisabled,
} from './chatPromptPolish';
import { ChatPromptPolishReminder } from './ChatPromptPolishReminder';
import type { DirectCodexUserContentPart } from './generated';
import {
$createResourceReferenceNode,
$isResourceReferenceNode,
@@ -176,7 +177,8 @@ function referenceListKey(references: ChatReference[]) {
function sameDraft(left: ChatComposerDraft, right: ChatComposerDraft) {
return (
left.text === right.text &&
referenceListKey(left.references) === referenceListKey(right.references)
referenceListKey(left.references) === referenceListKey(right.references) &&
JSON.stringify(left.content ?? []) === JSON.stringify(right.content ?? [])
);
}
@@ -184,26 +186,50 @@ function collectDraftParts(
node: LexicalNode,
textParts: string[],
references: ChatReference[],
content: DirectCodexUserContentPart[],
) {
if ($isTextNode(node)) {
textParts.push(node.getTextContent());
const text = node.getTextContent();
textParts.push(text);
if (text) content.push({ type: 'input_text', text });
return;
}
if ($isLineBreakNode(node)) {
textParts.push('\n');
content.push({ type: 'input_text', text: '\n' });
return;
}
if ($isResourceReferenceNode(node)) {
textParts.push(`@${node.__reference.label}`);
references.push(node.__reference);
content.push(
node.__reference.type === 'resource'
? {
type: 'agc_resource_reference',
resourceId: node.__reference.resourceId,
}
: {
type: 'agc_runtime_region_reference',
label: node.__reference.label,
runId: node.__reference.runId,
versionId: node.__reference.versionId,
elementTag: node.__reference.elementTag,
elementRole: node.__reference.elementRole,
text: node.__reference.text,
width: node.__reference.width,
height: node.__reference.height,
resourceIds: node.__reference.resourceIds,
},
);
return;
}
if ($isElementNode(node)) {
node.getChildren().forEach((child, index) => {
if (index > 0 && node.getType() === 'root') {
textParts.push('\n');
content.push({ type: 'input_text', text: '\n' });
}
collectDraftParts(child, textParts, references);
collectDraftParts(child, textParts, references, content);
});
}
}
@@ -212,10 +238,12 @@ function collectDraftParts(
function readDraftFromNodes(): ChatComposerDraft {
const textParts: string[] = [];
const references: ChatReference[] = [];
collectDraftParts($getRoot(), textParts, references);
const content: DirectCodexUserContentPart[] = [];
collectDraftParts($getRoot(), textParts, references, content);
return {
text: textParts.join('').trim(),
references: dedupeChatReferences(references),
content,
};
}
@@ -10,6 +10,11 @@ import {
buildGameCreationAppAssetTagLibrary,
type GameCreationAppAssetTagLibraryEntry,
} from '../../../../../packages/shared/src/contracts/gameCreationAppAssetTagLibrary';
import type {
DirectCodexUserContentPart,
DirectCodexUserItem,
DirectCodexUserMessageItem,
} from './generated';
export type ResourceReferenceSource =
| 'asset-picker'
@@ -47,6 +52,8 @@ export type ChatReference = ResourceReference | RuntimeRegionReference;
export type ChatComposerDraft = {
text: string;
references: ChatReference[];
/** Lexical 顺序对应的 canonical user content;仅由编辑器读回时提供。 */
content?: DirectCodexUserContentPart[];
};
export const RESOURCE_REFERENCE_INSERT_EVENT = 'agc-resource-reference-insert';
@@ -84,8 +91,53 @@ export function isResourceReferenceOverlayTarget(target: EventTarget | null) {
export const EMPTY_CHAT_COMPOSER_DRAFT: ChatComposerDraft = {
text: '',
references: [],
content: [],
};
export function chatReferenceToContentPart(
reference: ChatReference,
): DirectCodexUserContentPart {
if (reference.type === 'resource') {
return { type: 'agc_resource_reference', resourceId: reference.resourceId };
}
return {
type: 'agc_runtime_region_reference',
label: reference.label,
runId: reference.runId,
versionId: reference.versionId,
elementTag: reference.elementTag,
elementRole: reference.elementRole,
text: reference.text,
width: reference.width,
height: reference.height,
resourceIds: reference.resourceIds,
};
}
export function chatComposerDraftToDirectCodexUserItem(
draft: ChatComposerDraft,
id: string,
): DirectCodexUserItem {
const content = draft.content?.length
? draft.content
: draft.references.length > 0
? [
...(draft.text
? [{ type: 'input_text' as const, text: draft.text }]
: []),
...draft.references.map(chatReferenceToContentPart),
]
: draft.text
? [{ type: 'input_text' as const, text: draft.text }]
: [];
return {
type: 'message',
role: 'user',
content,
id,
} satisfies DirectCodexUserMessageItem;
}
export function resourceDisplayName(asset: GameCreationAppAssetManifestEntry) {
const fileName = asset.localPath.split(/[\\/]/u).pop() ?? asset.id;
return fileName.replace(/\.[^.]+$/u, '').trim() || asset.id;