前端:Direct 失败说明补上宿主事实句的文案模式
- `projectRuntimeVisibleError` 增加宿主 `Display` 事实句模式:`执行通道已断开`(TransportClosed)、`等待模型回合结束达到硬上限`(TimedOut 的硬上限那档)、`宿主任务提前结束`(host-dropped),并给落盘那档补上 `收尾历史失败` / `未确认历史完整落盘` / `写入本项目对话历史失败`——改动前这几种都掉进「执行失败,请稍后重试」 - 只认宿主写死的短语、不回落原文:原文带 `exitStatus=` / `stderrClass=` 这类内部字段,`TransportClosed` 就是这种 - 修掉宿主收口文案的版本口径:解析只认 `v1`,而宿主发的是多一段 `code=` 的 `v2`,于是脱敏摘要永远命中不了;现在两版都认,并把解析结果拆成 parts,供拒单文案复用(`projectRuntimeVisibleRejectionError`,不带阶段标签——拒单这一轮没有开始) - `directTurnFailureNoticeText` 的文档注释写明「不加模式就只会看到通用文案」是有意取舍,加模式时补 `agentRuntimeModel.test.ts` 用例 - 用例:`agentRuntimeModel.test.ts` 补 v2 收口文案、拒单文案与三句宿主事实句;`directThreadChat.test.ts` 的「收尾历史失败」期望改成映射后的句子
This commit is contained in:
@@ -888,10 +888,25 @@ function redactDirectFailureMarkers(value: string) {
|
||||
.replace(/\[redacted sensitive context\]/gi, '[已隐藏敏感信息]');
|
||||
}
|
||||
|
||||
function directCodexDiagnosticFailureDetail(message: string) {
|
||||
/**
|
||||
* 宿主收口文案(`direct-codex-failure:v1|v2 …`)解析出的可展示部分。
|
||||
*
|
||||
* 两版都要认:v1 是 `stage=… retryable=… summary=…`,v2 在 `retryable` 前多一段 `code=…`
|
||||
* (宿主现在发的是 v2)。只认 v1 时这一路永远命中不了,宿主的脱敏摘要等于白写。
|
||||
*/
|
||||
type DirectDiagnosticParts = {
|
||||
stageLabel: string;
|
||||
summary: string;
|
||||
hint: string;
|
||||
retryable: boolean;
|
||||
};
|
||||
|
||||
function directCodexDiagnosticFailureParts(
|
||||
message: string,
|
||||
): DirectDiagnosticParts | null {
|
||||
const trimmed = message.trim();
|
||||
const match =
|
||||
/^direct-codex-failure:v1 stage=(request|art-preparation|code-generation|browser-validation|version-registration) retryable=(true|false) summary=(.+?);建议:(.+?);(?:已保存脱敏项目诊断|未能保存项目诊断)$/u.exec(
|
||||
/^direct-codex-failure:v[12] stage=(request|art-preparation|code-generation|browser-validation|version-registration)(?: code=[a-z0-9-]+)? retryable=(true|false) summary=(.+?);建议:(.+?);(?:已保存脱敏项目诊断|未能保存项目诊断)$/u.exec(
|
||||
trimmed,
|
||||
);
|
||||
if (!match) {
|
||||
@@ -925,17 +940,44 @@ function directCodexDiagnosticFailureDetail(message: string) {
|
||||
'browser-validation': '真实试玩未通过',
|
||||
'version-registration': '项目版本登记失败',
|
||||
}[stage];
|
||||
if (!stageLabel) {
|
||||
if (!stageLabel || !summary || !hint) {
|
||||
return null;
|
||||
}
|
||||
if (!summary || !hint) {
|
||||
return null;
|
||||
}
|
||||
return `${stageLabel}:${summary}。${hint}${
|
||||
retryable === 'true' ? '(可直接重试)' : ''
|
||||
return { stageLabel, summary, hint, retryable: retryable === 'true' };
|
||||
}
|
||||
|
||||
/** 收口文案的一句话:阶段标签只有"回合失败"才成立,拒单那边传 `null`(见下面的导出函数)。 */
|
||||
function directDiagnosticSentence(
|
||||
parts: DirectDiagnosticParts,
|
||||
stageLabel: string | null,
|
||||
) {
|
||||
return `${stageLabel ? `${stageLabel}:` : ''}${parts.summary}。${parts.hint}${
|
||||
parts.retryable ? '(可直接重试)' : ''
|
||||
}`;
|
||||
}
|
||||
|
||||
function directCodexDiagnosticFailureDetail(message: string) {
|
||||
const parts = directCodexDiagnosticFailureParts(message);
|
||||
return parts ? directDiagnosticSentence(parts, parts.stageLabel) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* **拒单**的可见文案:宿主收口文案里那段已脱敏的摘要与建议。
|
||||
*
|
||||
* 与 [`projectRuntimeVisibleError`] 的差别只有一处——**不带阶段标签**:阶段说的是"失败发生在交付的
|
||||
* 哪一步",而拒单是"这一轮没有开始",阶段只会是默认值(`code-generation`),套上去会把没发生的
|
||||
* 事讲成发生了。宿主的文案不是收口形状时退回同一份运行错误映射。
|
||||
*/
|
||||
export function projectRuntimeVisibleRejectionError(
|
||||
message: string,
|
||||
subject: string,
|
||||
) {
|
||||
const parts = directCodexDiagnosticFailureParts(message);
|
||||
return parts
|
||||
? `${subject}:${directDiagnosticSentence(parts, null)}`
|
||||
: projectRuntimeVisibleError(message, subject, true);
|
||||
}
|
||||
|
||||
function directPlatformFailureDetail(message: string) {
|
||||
const trimmed = message.trim();
|
||||
if (!trimmed) {
|
||||
@@ -1197,10 +1239,27 @@ export function projectRuntimeVisibleError(
|
||||
if (
|
||||
normalized.includes('落盘失败') ||
|
||||
normalized.includes('持久化失败') ||
|
||||
normalized.includes('写入失败')
|
||||
normalized.includes('写入失败') ||
|
||||
// 宿主"历史落盘"的事实句不带"失败"两个字:`TurnFailed` 的原因就是
|
||||
// "DirectProject 收尾历史失败:未确认历史完整落盘"这一类。
|
||||
normalized.includes('收尾历史失败') ||
|
||||
normalized.includes('未确认历史完整落盘') ||
|
||||
normalized.includes('写入本项目对话历史失败')
|
||||
) {
|
||||
return `${subject} 保存运行记录失败,请检查项目目录后重试`;
|
||||
}
|
||||
// 宿主 `Display` 的其余事实句:它们不含上面任何关键字,**不加模式就只会看到最后那句通用文案**。
|
||||
// 这里只认宿主写死的句首短语,不回落原文——原文里带 `exitStatus=` / `stderrClass=` 这类内部字段
|
||||
// (`TransportClosed` 就是这种)。
|
||||
if (visibleMessage.includes('执行通道已断开')) {
|
||||
return `${subject} 服务连接已断开,请稍后重试`;
|
||||
}
|
||||
if (visibleMessage.includes('等待模型回合结束达到硬上限')) {
|
||||
return `${subject} 响应超时,请稍后重试`;
|
||||
}
|
||||
if (visibleMessage.includes('宿主任务提前结束')) {
|
||||
return `${subject} 本轮执行已中断,请重试`;
|
||||
}
|
||||
const containsInternalDiagnostics =
|
||||
normalized.includes('agentllm.') ||
|
||||
/(?:^|[\s::])kind=/.test(normalized) ||
|
||||
|
||||
+5
@@ -44,6 +44,11 @@ export function directTurnFailureItemId(
|
||||
*
|
||||
* 事件里的 `message` 是宿主已脱敏 + 截断的原始原因,这里只做"给人看"的那一步,不再另开文案
|
||||
* 规则,也不在这里判断"这算不算失败"(那由事件载荷的有没有决定)。
|
||||
*
|
||||
* **不加模式就只会看到通用文案**:`projectRuntimeVisibleError` 只认它自己那份模式表,宿主换一句
|
||||
* 新的 `Display` 事实句而这边没跟着加模式时,用户拿到的就是"…执行失败,请稍后重试"。这是有意的
|
||||
* 取舍——宁可给通用文案,也不回落宿主原文(原文可能带 `exitStatus=` / `stderrClass=` 这类内部
|
||||
* 字段)。加了新模式就补一条 `agentRuntimeModel.test.ts` 的用例。
|
||||
*/
|
||||
export function directTurnFailureNoticeText(message: string): string {
|
||||
const raw = typeof message === 'string' ? message.trim() : '';
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
MUD_POINT_INSUFFICIENT_INTERRUPTION_MESSAGE,
|
||||
projectRuntimeVisibleCurrentWork,
|
||||
projectRuntimeVisibleError,
|
||||
projectRuntimeVisibleRejectionError,
|
||||
} from '../src/features/agent-runtime/model';
|
||||
import {
|
||||
deriveAgentStatusCards,
|
||||
@@ -431,6 +432,95 @@ describe('Agent Runtime Provider 状态投影', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('直连阶段收口文案的 v2 形状也要认出脱敏摘要', () => {
|
||||
// 宿主发的是 v2(比 v1 多一段 `code=`):只认 v1 时这一路永远命中不了,脱敏摘要等于白写。
|
||||
expect(
|
||||
projectRuntimeVisibleError(
|
||||
'direct-codex-failure:v2 stage=code-generation code=runtime-failure retryable=false summary=DirectProject 收尾历史失败:未确认历史完整落盘;建议:请检查项目目录后重试;如持续失败请检查项目诊断;已保存脱敏项目诊断',
|
||||
'陶泥儿智能创作',
|
||||
true,
|
||||
),
|
||||
).toBe(
|
||||
'陶泥儿智能创作:代码生成失败:DirectProject 收尾历史失败:未确认历史完整落盘。请检查项目目录后重试;如持续失败请检查项目诊断',
|
||||
);
|
||||
// 脱敏标记照旧要换成中文标记,带路径 / 凭据的摘要照样拒收。
|
||||
expect(
|
||||
projectRuntimeVisibleError(
|
||||
'direct-codex-failure:v2 stage=art-preparation code=runtime-failure retryable=true summary=读取陶泥儿画布资源失败:<redacted-url>;建议:请稍后重试;已保存脱敏项目诊断',
|
||||
'陶泥儿智能创作',
|
||||
true,
|
||||
),
|
||||
).toBe(
|
||||
'陶泥儿智能创作:平台资源准备失败:读取陶泥儿画布资源失败:[已隐藏链接]。请稍后重试(可直接重试)',
|
||||
);
|
||||
expect(
|
||||
projectRuntimeVisibleError(
|
||||
'direct-codex-failure:v2 stage=art-preparation code=runtime-failure retryable=true summary=读取失败:https://provider.example/private;建议:请稍后重试;已保存脱敏项目诊断',
|
||||
'陶泥儿智能创作',
|
||||
true,
|
||||
),
|
||||
).toBe('陶泥儿智能创作 执行失败,请稍后重试');
|
||||
});
|
||||
|
||||
test('拒单文案只取收口文案里的脱敏摘要与建议,不套阶段标签', () => {
|
||||
// 阶段说的是"失败发生在交付的哪一步",而拒单是"这一轮没有开始":阶段只会是默认值,
|
||||
// 套上去会把没发生的事讲成发生了。
|
||||
expect(
|
||||
projectRuntimeVisibleRejectionError(
|
||||
'direct-codex-failure:v2 stage=code-generation code=runtime-failure retryable=true summary=Codex app-server 启动失败:找不到可执行文件;建议:请重试;如持续失败请检查项目诊断;已保存脱敏项目诊断',
|
||||
'陶泥儿智能创作',
|
||||
),
|
||||
).toBe(
|
||||
'陶泥儿智能创作:Codex app-server 启动失败:找不到可执行文件。请重试;如持续失败请检查项目诊断(可直接重试)',
|
||||
);
|
||||
// 不是收口形状时退回同一份运行错误映射(宿主 `Display` 的事实句仍然只给一句可读的话)。
|
||||
expect(
|
||||
projectRuntimeVisibleRejectionError(
|
||||
'执行通道已断开,不能自动重放未确认操作:Codex app-server 已退出;exitStatus=signal: 9 (SIGKILL)',
|
||||
'陶泥儿智能创作',
|
||||
),
|
||||
).toBe('陶泥儿智能创作 服务连接已断开,请稍后重试');
|
||||
// 已知边界(本轮不动):映射里"拒绝"那条子串分支会先认领"拒绝访问"这类文件系统事实;
|
||||
// 目录锚不定的拒单在聊天里走 `Display` 原样显示(它不在上报名单里),所以摸不到这句。
|
||||
expect(
|
||||
projectRuntimeVisibleRejectionError(
|
||||
'无法锚定 Direct 调用项目目录:拒绝访问',
|
||||
'陶泥儿智能创作',
|
||||
),
|
||||
).toBe('陶泥儿智能创作 被项目权限或安全策略阻止,请检查审批配置');
|
||||
});
|
||||
|
||||
test('宿主 `Display` 的事实句不再掉进通用兜底', () => {
|
||||
expect(
|
||||
projectRuntimeVisibleError(
|
||||
'执行通道已断开,不能自动重放未确认操作:Codex app-server 已退出;exitStatus=signal: 9 (SIGKILL) stderrClass=crash',
|
||||
'陶泥儿智能创作',
|
||||
true,
|
||||
),
|
||||
).toBe('陶泥儿智能创作 服务连接已断开,请稍后重试');
|
||||
expect(
|
||||
projectRuntimeVisibleError(
|
||||
'等待模型回合结束达到硬上限,已停止本轮并核对后台操作。',
|
||||
'陶泥儿智能创作',
|
||||
true,
|
||||
),
|
||||
).toBe('陶泥儿智能创作 响应超时,请稍后重试');
|
||||
expect(
|
||||
projectRuntimeVisibleError(
|
||||
'陶泥儿回合的宿主任务提前结束(崩溃或任务被取消),本轮已按失败收口,请重试。',
|
||||
'陶泥儿智能创作',
|
||||
true,
|
||||
),
|
||||
).toBe('陶泥儿智能创作 本轮执行已中断,请重试');
|
||||
expect(
|
||||
projectRuntimeVisibleError(
|
||||
'DirectProject 收尾历史失败:未确认历史完整落盘',
|
||||
'陶泥儿智能创作',
|
||||
true,
|
||||
),
|
||||
).toBe('陶泥儿智能创作 保存运行记录失败,请检查项目目录后重试');
|
||||
});
|
||||
|
||||
test('直连平台错误保留 HTTP 诊断字段但只显示已脱敏的敏感值', () => {
|
||||
const safe = projectRuntimeVisibleError(
|
||||
'陶泥儿美术包生成失败(规范图):请求平台图片生成失败:HTTP 401;code=invalid-token;field=authorization;message=token=[redacted-secret];detail=登录态已失效',
|
||||
|
||||
@@ -312,7 +312,9 @@ describe('DirectProject 聊天 reducer', () => {
|
||||
const notice = failed.history.at(-1);
|
||||
expect(notice?.itemId).toBe('direct-codex:turn-1:user:failure');
|
||||
expect(notice?.role).toBe('assistant');
|
||||
expect(notice?.text).toBe('陶泥儿智能创作 执行失败,请稍后重试');
|
||||
expect(notice?.text).toBe(
|
||||
'陶泥儿智能创作 保存运行记录失败,请检查项目目录后重试',
|
||||
);
|
||||
// 本轮开口条目照样按身份拿到边界(失败与正常终态同源)。
|
||||
expect(selectDirectChatEntries(failed)[0]?.turnEndedAt).toBe(1_000_900);
|
||||
expect(selectDirectChatEntries(failed)[0]?.turnStartedAt).toBe(1_000_000);
|
||||
|
||||
Reference in New Issue
Block a user