修复:润色回写与粘贴共用同一条落点兜底,认不出的引用落文本而不是消失
ResourceReferenceInput 新增 mentionTokenOrText:provider 的 mentionToken,答不出来(契约被破坏)时退 contentPartText 的通用文本形态;粘贴插入、润色回写的候选扫描、整根替换的落点共用这一条 润色回写的候选不再跳过「答不出 token 的 part」:这类 part 在回包里没被提到时作为末尾孤儿补回来,而不是在翻译阶段直接消失 applyContentToRoot(初始草稿与润色回写共用)解析不出引用时落文本,不再是整条吃掉 新增两处输入区用例:provider 只答得出引用答不出 token 时润色回写仍保住引用;初始草稿里已解析不出的引用落成 @resourceId 文本。两处去掉兜底即变红 同步 AGC 聊天素材引用功能说明、引用粘贴解析实施计划与决策记录(落点兜底一条,含初始草稿行为变化的记录)
This commit is contained in:
+22
-4
@@ -237,6 +237,20 @@ function mentionTokenFromPart(
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* part 落进正文时用的文本:provider 的 `mentionToken`,它答不出来(provider 契约被破坏)时退到
|
||||
* `contentPartText` 的通用文本形态。文本 part 自己就是文本,返回 `null`。
|
||||
*
|
||||
* 粘贴插入、润色回写的候选扫描与落点都走这一条:只要 part 不是文本,就一定有一段正常文本可落,
|
||||
* 「认不出的引用走文本、绝不静默丢」在几个入口是同一份实现,不是各写一遍兜底。
|
||||
*/
|
||||
function mentionTokenOrText(
|
||||
providers: readonly ReferenceProvider[],
|
||||
part: DirectCodexUserContentPart,
|
||||
): string | null {
|
||||
return mentionTokenFromPart(providers, part) ?? contentPartText(part);
|
||||
}
|
||||
|
||||
/**
|
||||
* 草稿的展示文本:把每个引用 part 按注入的 provider 展开成它的 token,其余文本逐字保留。
|
||||
*
|
||||
@@ -267,8 +281,10 @@ function applyPolishedTextToRoot(
|
||||
) {
|
||||
// 候选按 canonical content 原顺序取:引用、Skill、runtime 区域与附件共用一套 token 扫描,
|
||||
// 与出站给润色服务的文本口径一致,因此回包保留下来的 token 能原位换回真 part。
|
||||
// provider 答不出 token 的 part 也照样进候选(token 退到 `mentionTokenOrText` 的通用文本形态):
|
||||
// 它在回包里没被提到时会作为末尾孤儿补回来,而不是从这门翻译里直接消失。
|
||||
const candidates = current.content.flatMap((part) => {
|
||||
const token = mentionTokenFromPart(providers, part);
|
||||
const token = mentionTokenOrText(providers, part);
|
||||
return token ? [{ token, part }] : [];
|
||||
});
|
||||
applyContentToRoot(buildContentFromTextTokens(value, candidates), providers);
|
||||
@@ -297,7 +313,11 @@ function applyContentToRoot(
|
||||
const reference = referenceFromPart(providers, part);
|
||||
if (reference) {
|
||||
paragraph.append($createResourceReferenceNode(reference));
|
||||
return;
|
||||
}
|
||||
// 解析不出引用的 part 落它的文本(与粘贴同一条兜底链),整根替换同样不许把内容吃掉。
|
||||
const token = mentionTokenOrText(providers, part);
|
||||
if (token) paragraph.append($createTextNode(token));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -361,9 +381,7 @@ function $insertContentAtSelection(
|
||||
return;
|
||||
}
|
||||
// 解析不出的 part 已经在上游被摘掉了 token,这里必须把文本补回去,否则这段内容会静默消失。
|
||||
// provider 连 token 都答不出来(契约被破坏)时落它的通用文本形态,绝不留空。
|
||||
const token =
|
||||
mentionTokenFromPart(providers, part) ?? contentPartText(part);
|
||||
const token = mentionTokenOrText(providers, part);
|
||||
if (token) {
|
||||
$selectionOrRootEnd()?.insertText(token);
|
||||
inserted = true;
|
||||
|
||||
@@ -864,6 +864,68 @@ describe('ResourceReferenceInput', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test('润色回写时 provider 答不出 token 的 part 仍留在草稿里,不在这门翻译里消失', async () => {
|
||||
const onChange = vi.fn<(draft: ChatComposerDraft) => void>();
|
||||
const reference = resourceReferenceFromAsset(assets[0]!, 'asset-picker');
|
||||
// provider 契约被破坏:解析得出引用,但答不出 token。它仍然必须活过这一轮润色回写。
|
||||
const tokenlessProvider: ReferenceProvider = {
|
||||
trigger: '@',
|
||||
fuzzy_lookup: () => [],
|
||||
lookup: () => [reference],
|
||||
toReference: (part) =>
|
||||
part.type === 'agc_resource_reference' ? reference : null,
|
||||
refresh: (part) => part,
|
||||
mentionToken: () => null,
|
||||
};
|
||||
const composerRef = createRef<ResourceReferenceInputHandle>();
|
||||
render(
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => composerRef.current?.replaceText('润色后的需求')}
|
||||
>
|
||||
模拟润色
|
||||
</button>
|
||||
<ResourceReferenceInput
|
||||
ref={composerRef}
|
||||
providers={[tokenlessProvider]}
|
||||
initialContent={[chatReferenceToContentPart(reference)]}
|
||||
projectPath="C:/project"
|
||||
ariaLabel="聊天"
|
||||
onChange={onChange}
|
||||
/>
|
||||
</>,
|
||||
);
|
||||
await settleComposer();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '模拟润色' }));
|
||||
await settleComposer();
|
||||
expect(draftResourceIds(composerRef.current?.getDraft())).toEqual(['hero']);
|
||||
expect(draftText(composerRef.current?.getDraft())).toBe(
|
||||
'润色后的需求 @hero ',
|
||||
);
|
||||
});
|
||||
|
||||
test('初始草稿里解析不出的引用落文本形态,不从草稿里消失', async () => {
|
||||
const composerRef = createRef<ResourceReferenceInputHandle>();
|
||||
render(
|
||||
<ComposerHost
|
||||
ref={composerRef}
|
||||
initialContent={[
|
||||
{ type: 'agc_resource_reference', resourceId: 'missing-asset' },
|
||||
]}
|
||||
assets={assets}
|
||||
projectPath="C:/project"
|
||||
ariaLabel="聊天"
|
||||
onChange={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
await settleComposer();
|
||||
expect(composerRef.current?.getDraft().content).toEqual([
|
||||
{ type: 'input_text', text: '@missing-asset' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('引用浮层打开时 Enter 不提交表单,关掉后恢复提交', async () => {
|
||||
const onSubmit = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
Reference in New Issue
Block a user