收敛 Direct 控制器错误正文

统一取消、上传、历史读取和前置异常的精确脱敏展示

保留 IPC 与操作系统错误并补充回归和文档
This commit is contained in:
kdletters
2026-10-05 10:10:32 +08:00
parent 315d734b39
commit 7369956762
5 changed files with 58 additions and 14 deletions
@@ -35,6 +35,10 @@ import {
enqueueSystemFailureNoticeText,
invokeTurnEnqueue,
} from '../conversation/directTurnEnqueueFailure';
import {
visibleDirectControllerError,
visibleDirectFailureDetail,
} from '../conversation/directTurnFailure';
import type { HistorySlice } from '../generated/HistorySlice';
import type { QueueRemovalOutcome } from '../generated/QueueRemovalOutcome';
import type { ThreadItem } from '../generated/ThreadItem';
@@ -322,9 +326,7 @@ export function useDirectProjectChatController({
} catch (error) {
if (projectPathRef.current !== nextProjectPath) return;
setComposerNotice(
`取消待发消息失败:${
error instanceof Error ? error.message : String(error)
}`,
visibleDirectControllerError(error, '取消待发消息失败'),
);
}
}
@@ -394,7 +396,7 @@ export function useDirectProjectChatController({
} catch (error) {
if (projectPathRef.current === projectPath) {
setAttachmentNotice(
error instanceof Error ? error.message : String(error),
visibleDirectControllerError(error, '文件上传失败'),
);
}
return [];
@@ -491,7 +493,13 @@ export function useDirectProjectChatController({
// 写权限门等前置步骤抛出时不能只留一个未处理的 rejection:回合会静默失败,
// 这一轮的任何说明都没有出口。
if (projectPathRef.current === nextProjectPath) {
onRuntimeError(error instanceof Error ? error.message : String(error));
onRuntimeError(
projectRuntimeVisibleError(
error instanceof Error ? error.message : String(error),
'陶泥儿智能创作',
true,
),
);
}
draftSafe = true;
} finally {
@@ -625,9 +633,7 @@ export function useDirectProjectChatController({
setComposerNotice(message);
}
} catch (error) {
onRuntimeError(
`终止失败:${error instanceof Error ? error.message : String(error)}`,
);
onRuntimeError(visibleDirectControllerError(error, '终止失败'));
setComposerNotice('');
} finally {
setTurnCancelling(false);
@@ -680,10 +686,10 @@ export function useDirectProjectChatController({
if (pages.error) throw pages.error;
setStatusNotice('');
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (projectPathRef.current !== nextProjectPath) return;
const message = visibleDirectControllerError(error, '项目对话读取失败');
onRuntimeError(message);
setStatusNotice(`项目对话读取失败:${message}`);
setStatusNotice(message);
}
}
@@ -734,9 +740,12 @@ export function useDirectProjectChatController({
} catch (error) {
// 与加载成功路径同样按世代守卫:切项目后在飞的读取失败不能算到新项目头上。
if (isStaleLoad()) return;
const message =
(error instanceof Error ? error.message : String(error)) ||
DIRECT_HISTORY_LOAD_ERROR_FALLBACK;
const detail = visibleDirectFailureDetail(
error instanceof Error ? error.message : String(error),
);
const message = detail
? `读取更早的对话历史失败:${detail}`
: DIRECT_HISTORY_LOAD_ERROR_FALLBACK;
historyErrorRef.current = message;
setHistoryError(message);
} finally {
@@ -65,6 +65,16 @@ export function visibleDirectFailureDetail(
: safe;
}
/** 控制器非回合错误的统一可见正文:只替换敏感值,空错误也保留专用未分类提示。 */
export function visibleDirectControllerError(
error: unknown,
subject: string,
): string {
const raw = error instanceof Error ? error.message : String(error ?? '');
const detail = visibleDirectFailureDetail(raw);
return detail ? `${subject}:${detail}` : `${subject}:未提供具体错误正文`;
}
function withVisibleFailureDetail(
message: string,
detail: string | null | undefined,
@@ -1,6 +1,9 @@
import { describe, expect, it } from 'vitest';
import { directTurnFailureNoticeText } from '../src/view/project-development/chat/conversation/directTurnFailure';
import {
directTurnFailureNoticeText,
visibleDirectControllerError,
} from '../src/view/project-development/chat/conversation/directTurnFailure';
describe('DirectProject 上游失败文案', () => {
it.each([401, 403, 408, 429, 500, 502, 503, 504])(
@@ -121,4 +124,23 @@ describe('DirectProject 上游失败文案', () => {
expect(text).toContain('旧错误事件未记录具体原因');
expect(text).not.toContain('服务连接');
});
it('控制器错误也保留正文并只替换敏感值', () => {
const text = visibleDirectControllerError(
new Error(
'IPC invoke failed: EPIPE;Authorization: Bearer fixture-secret;C:\\Users\\demo\\game',
),
'终止失败',
);
expect(text).toContain('EPIPE');
expect(text).toContain('[已隐藏凭据]');
expect(text).not.toContain('fixture-secret');
expect(text).not.toContain('C:\\Users\\demo');
});
it('空控制器错误使用专用未分类正文提示', () => {
expect(visibleDirectControllerError(new Error(''), '读取失败')).toBe(
'读取失败:未提供具体错误正文',
);
});
});
@@ -109,6 +109,7 @@ DirectProject 已经解决过同一类问题([`【ADR】DirectProject命令接
- Claude Code(cc)侧车的非零退出、RPC `error`、stdout JSON/UTF-8 解析失败、stderr 和静默超时也走同一映射;侧车 stderr 只在有界收口窗口内读取并进入同一脱敏 detail,不再只写裸 `eprintln!`。
- HTTP 409 只有明确包含泥点不足事实时才映射为 `paidCreditsInsufficient`;Claude Code 的普通 409 冲突保留为 `upstreamFailed`。
- `hostDropped` 携带可选的脱敏 `detail`:panic hook 能取得的负载和位置随原回合占用进入失败事件;普通 Drop 仅记录“退出时未写终态、未观察到 panic”,不推断为网络中断。旧版本无 `detail` 的事件继续可读,界面明确标注旧事件未记录原因。
- DirectProject 控制器的取消待发、终止、附件上传、项目历史读取和发送前置异常也复用同一份前端精确脱敏出口;这些非回合错误不得直接把 `Error.message` 原文写入状态栏、composer 或历史错误行。
## 后果与边界
@@ -13,6 +13,7 @@ Parent Milestone: `【里程碑】AGC错误具体文本展示-2026-10-04.md`
4. 补充 upstream、transport、stream、IPC、内存不足与 HostDropped 边界测试。
5. 更新 ADR 的当前行为口径。
6. HostDropped 的可选 detail 由 Rust 单点脱敏;panic hook 把可读负载和位置关联到原回合占用,Drop 与旧无字段事件分别验证,不更改回合排队、重试或计费行为。
7. DirectProject 控制器的取消、上传、历史读取和前置异常统一复用前端精确脱敏出口,保留 IPC/OS 正文并替换凭据值、URL 与路径。
## 实现顺序
@@ -33,5 +34,6 @@ Parent Milestone: `【里程碑】AGC错误具体文本展示-2026-10-04.md`
- 真实 AGC dev smoke 复现并定位 `Reached maximum number of turns (8)` → `transport-closed`;修复后客户端已热重编译重启,未再次触发付费 Provider 请求。
- Rust 应用日志/启动诊断精确脱敏回归:通过。
- Rust HostDropped panic detail + 旧无字段载荷兼容回归:定向通过。
- DirectProject 控制器错误正文:新增脱敏与空错误专用提示回归,前端定向测试通过。
- 真实 Provider、IPC 断链和内存压力尚未执行。
- 2026-10-05 当前代码真实 `npm run agc` 构建启动通过;客户端与 Runner 已启动,前端/后台/API/worker/数据库健康检查通过,停止本次自有客户端后保留原有后端。