Merge pull request '修复8ceada8690f7255f318f7b335708e5dfea73d2d5的CI回归问题' (#666) from fix/codex/agc-error-convergence-regression into master
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / Frontend tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / Frontend tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Reviewed-on: #666
This commit was merged in pull request #666.
This commit is contained in:
@@ -4487,21 +4487,12 @@ pub(crate) async fn fetch_custom_llm_models(
|
||||
.await
|
||||
.map_err(|error| format!("无法连接模型端点:{error};请检查 API 地址和网络"))?;
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let body = response
|
||||
.text()
|
||||
.await
|
||||
.unwrap_or_else(|error| format!("响应正文读取失败:{error}"));
|
||||
let detail = crate::agent::redact_agent_runtime_error(
|
||||
Path::new("__agc_no_project_root__"),
|
||||
body.trim(),
|
||||
600,
|
||||
);
|
||||
return Err(if detail.is_empty() {
|
||||
format!("模型列表读取失败(HTTP {})", status.as_u16())
|
||||
} else {
|
||||
format!("模型列表读取失败(HTTP {}):{detail}", status.as_u16())
|
||||
});
|
||||
// 自定义模型端点由用户提供、响应不可信:只回报状态码,绝不回显上游正文,
|
||||
// 否则会把密钥或私有数据带进错误文案。
|
||||
return Err(format!(
|
||||
"模型列表读取失败(HTTP {})",
|
||||
response.status().as_u16()
|
||||
));
|
||||
}
|
||||
let mut body = Vec::new();
|
||||
while let Some(chunk) = response
|
||||
|
||||
@@ -53,9 +53,17 @@ fn parse_catalog_payload(bytes: &[u8]) -> Result<LlmModelsResponse, String> {
|
||||
.unwrap_or("模型列表读取失败");
|
||||
return Err(message.to_string());
|
||||
}
|
||||
return envelope
|
||||
.data
|
||||
.ok_or_else(|| "模型列表响应缺少 data,请稍后重试".to_string());
|
||||
return envelope.data.ok_or_else(|| {
|
||||
// `ok=true` 但 `data` 为空时,把服务端协议里的 error 明细带出来,
|
||||
// 否则排障只能看到笼统的「缺少 data」。
|
||||
let error_detail = envelope
|
||||
.error
|
||||
.as_ref()
|
||||
.and_then(|error| error.message.as_deref().or(error.code.as_deref()))
|
||||
.filter(|detail| !detail.trim().is_empty())
|
||||
.unwrap_or("error 字段为空");
|
||||
format!("模型列表响应缺少 data:ok=true,error={error_detail}")
|
||||
});
|
||||
}
|
||||
|
||||
serde_json::from_value::<LlmModelsResponse>(value)
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
* 落回本模块的专用兜底,并按「工作区是否已经建好」分成两条,避免用户重做一遍。
|
||||
*/
|
||||
|
||||
import { visibleClientErrorMessage } from '../../services/errorReporting';
|
||||
|
||||
/** 未分类失败的专用兜底文案。 */
|
||||
export const HOME_CREATION_UNCLASSIFIED_FAILURE = '创建未完成,请重试';
|
||||
|
||||
@@ -64,6 +66,19 @@ function isHomeCreationTransportText(text: string): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function safeHomeCreationDetail(error: unknown, code?: string) {
|
||||
const detail = visibleClientErrorMessage(error, '').trim();
|
||||
if (!detail) return '';
|
||||
if (!code) return detail;
|
||||
const withoutCode = detail
|
||||
.replace(code, '')
|
||||
.replace(/^\s*[::;;,,-]\s*/u, '')
|
||||
.trim();
|
||||
return withoutCode === code || /[::;;,,-]\s*$/u.test(withoutCode)
|
||||
? ''
|
||||
: withoutCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 由宿主错误算出用户可见文案。
|
||||
*
|
||||
@@ -76,20 +91,25 @@ export function homeCreationFailureMessage(
|
||||
): string {
|
||||
const projectCreated = options.projectCreated === true;
|
||||
const text = homeCreationDiagnosticText(error);
|
||||
const safeDetail = safeHomeCreationDetail(error);
|
||||
if (isHomeCreationTransportText(text)) {
|
||||
return projectCreated
|
||||
const base = projectCreated
|
||||
? HOME_CREATION_AFTER_PROJECT_IPC_FAILURE
|
||||
: HOME_CREATION_IPC_FAILURE;
|
||||
return safeDetail ? `${base}(${safeDetail})` : base;
|
||||
}
|
||||
const code = HOME_CREATION_HOST_CODE.exec(text)?.[0];
|
||||
if (code) {
|
||||
return projectCreated
|
||||
const base = projectCreated
|
||||
? `工作区已创建;未能进入项目开发(${code});请从「已创建的工作区」打开后重试。`
|
||||
: `创建未完成(${code});请重试。`;
|
||||
const detail = safeHomeCreationDetail(error, code);
|
||||
return detail ? `${base}(${detail})` : base;
|
||||
}
|
||||
return projectCreated
|
||||
const base = projectCreated
|
||||
? HOME_CREATION_AFTER_PROJECT_FAILURE
|
||||
: HOME_CREATION_UNCLASSIFIED_FAILURE;
|
||||
return safeDetail ? `${base}(${safeDetail})` : base;
|
||||
}
|
||||
|
||||
/** 包装成带证据的创建失败;控制器负责把 `diagnostic` 写进日志后再抛出。 */
|
||||
|
||||
@@ -9,7 +9,10 @@ import {
|
||||
|
||||
import { resolveTauriInvoke } from '../../app/tauri';
|
||||
import type { LocalProjectDirectoryStatus, TauriInvoke } from '../../app/types';
|
||||
import { visibleClientErrorMessage } from '../../services/errorReporting';
|
||||
import {
|
||||
clientErrorRawText,
|
||||
visibleClientErrorMessage,
|
||||
} from '../../services/errorReporting';
|
||||
import {
|
||||
isAbsoluteProjectPath,
|
||||
projectPathHasControlCharacter,
|
||||
@@ -78,11 +81,12 @@ async function inspectRecentWorkspace(
|
||||
]);
|
||||
return { path: workspace, status, retryable: true };
|
||||
} catch (error) {
|
||||
const message = visibleClientErrorMessage(error);
|
||||
// 分类判据看**原始**正文:脱敏会把 `C:\a\b(DACL …)` 这类无空格路径连同后面的 DACL
|
||||
// 一起吞成 `<path>`,标记消失后提权失败会被误判成可重试,等于在用户点「否」后再弹一次 UAC。
|
||||
return {
|
||||
path: workspace,
|
||||
status: null,
|
||||
retryable: recentWorkspaceFailureIsRetryable(message),
|
||||
retryable: recentWorkspaceFailureIsRetryable(clientErrorRawText(error)),
|
||||
};
|
||||
} finally {
|
||||
if (timeoutHandle !== undefined) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
|
||||
|
||||
import type { AgcPluginPanel } from '../../app/types';
|
||||
import { ThemedModal } from '../../components/modal/ThemedModal';
|
||||
import { visibleClientErrorMessage } from '../../services/errorReporting';
|
||||
import { captureClientError } from '../../services/errorReporting';
|
||||
import { readAgcPluginPanel } from '../../services/pluginHost';
|
||||
|
||||
export type PluginPanelHostProps = {
|
||||
@@ -32,8 +32,12 @@ export function PluginPanelHost({
|
||||
if (active) setHtml(result.html);
|
||||
},
|
||||
(error) => {
|
||||
if (active)
|
||||
setError(visibleClientErrorMessage(error, '插件面板加载失败'));
|
||||
// 插件面板是不可信内容:原始正文(可能含私有路径)只进错误池,用户可见文案保持通用。
|
||||
void captureClientError(error, {
|
||||
source: 'plugin-panel',
|
||||
action: 'read_agc_plugin_panel',
|
||||
}).catch(() => undefined);
|
||||
if (active) setError('插件面板加载失败');
|
||||
},
|
||||
);
|
||||
return () => {
|
||||
|
||||
@@ -96,6 +96,11 @@ function clientErrorText(error: unknown): string {
|
||||
return String(error ?? '');
|
||||
}
|
||||
|
||||
/** 未脱敏的原始错误正文:只给分类/判据用,禁止直接展示给用户。 */
|
||||
export function clientErrorRawText(error: unknown) {
|
||||
return clientErrorText(error);
|
||||
}
|
||||
|
||||
/** 用户可见的普通客户端错误正文:保留状态码/原因,替换凭据、URL、路径和长标识。 */
|
||||
export function visibleClientErrorMessage(
|
||||
error: unknown,
|
||||
@@ -106,7 +111,9 @@ export function visibleClientErrorMessage(
|
||||
0,
|
||||
400,
|
||||
);
|
||||
return safe || fallback;
|
||||
// `ClientAuthErrorWrapper` 把映射外的 Tauri 级失败序列化成 `{}`:这不是可展示的正文,落回兜底,
|
||||
// 否则登录页会停在无解释的 `{}` 上(真 Error 的 message 由上报链路单独取用)。
|
||||
return safe && safe !== '{}' ? safe : fallback;
|
||||
}
|
||||
|
||||
export async function captureClientError(
|
||||
|
||||
@@ -2553,10 +2553,12 @@ export function registerHomeProjectCreationTests() {
|
||||
});
|
||||
fireEvent.keyDown(promptInput, { key: 'Enter', code: 'Enter' });
|
||||
|
||||
// 工作区已经建好,就必须如实说「已创建」,不能覆盖成「创建未完成」;宿主原始文本
|
||||
// (这里是夹具里的英文原因)不上屏,只作为证据写进 application.log。
|
||||
// 工作区已经建好,就必须如实说「已创建」,不能覆盖成「创建未完成」;宿主原因保留脱敏后的
|
||||
// 正文(URL / 路径已替换),完整原文仍只作为证据写进 application.log。
|
||||
expect(
|
||||
await screen.findByText(HOME_CREATION_AFTER_PROJECT_FAILURE),
|
||||
await screen.findByText((content) =>
|
||||
content.startsWith(HOME_CREATION_AFTER_PROJECT_FAILURE),
|
||||
),
|
||||
).not.toBeNull();
|
||||
expect(screen.getByText(`已创建的工作区:${projectPath}`)).not.toBeNull();
|
||||
await waitFor(() => {
|
||||
|
||||
@@ -75,6 +75,7 @@ function renderWorkbench() {
|
||||
const invoke = vi.fn(
|
||||
async (command: string, args?: Record<string, unknown>) => {
|
||||
if (command === 'get_design_agent_runtime_mode') return null;
|
||||
if (command === 'get_local_game_preview_status') return null;
|
||||
if (command === 'inspect_local_project_directory') {
|
||||
return {
|
||||
projectPath: PROJECT_PATH,
|
||||
|
||||
@@ -14,7 +14,7 @@ const refreshClientAuthSession = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('../src/services/clientAuth', () => ({
|
||||
refreshClientAuthSession,
|
||||
subscribeClientAuthState: () => () => {},
|
||||
subscribeClientAuthState: async () => () => {},
|
||||
}));
|
||||
|
||||
import {
|
||||
|
||||
@@ -12,7 +12,7 @@ const refreshClientAuthSession = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('../src/services/clientAuth', () => ({
|
||||
refreshClientAuthSession,
|
||||
subscribeClientAuthState: () => () => {},
|
||||
subscribeClientAuthState: async () => () => {},
|
||||
}));
|
||||
|
||||
import {
|
||||
|
||||
@@ -1103,7 +1103,9 @@ describe('project resource live canvas integration', () => {
|
||||
within(retryPanel).getByRole('button', { name: 'AI 润色' }),
|
||||
);
|
||||
expect(
|
||||
await within(retryPanel).findByText('AI 润色失败,可重试'),
|
||||
await within(retryPanel).findByText(
|
||||
'AI 润色失败,可重试:polish unavailable in this fixture',
|
||||
),
|
||||
).not.toBeNull();
|
||||
expect(
|
||||
within(retryPanel).getByLabelText('快速编辑提示词').textContent,
|
||||
|
||||
@@ -297,7 +297,9 @@ describe('生成素材弹窗的提示词润色', () => {
|
||||
await user.type(prompt, '一段片头动画,镜头缓慢推进');
|
||||
await user.click(screen.getByRole('button', { name: 'AI 润色' }));
|
||||
|
||||
expect(await screen.findByText('AI 润色失败,可重试')).not.toBeNull();
|
||||
expect(
|
||||
await screen.findByText('AI 润色失败,可重试:platform llm timeout'),
|
||||
).not.toBeNull();
|
||||
expect(prompt.value).toBe('一段片头动画,镜头缓慢推进');
|
||||
expect(screen.queryByRole('button', { name: '恢复原文' })).toBeNull();
|
||||
});
|
||||
|
||||
@@ -790,7 +790,7 @@ describe('版本级资源替换', () => {
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByText('当前版本已不存在,请刷新项目后重试'),
|
||||
screen.getByText(/当前版本已不存在,请刷新项目后重试/u),
|
||||
).not.toBeNull(),
|
||||
);
|
||||
expect(screen.queryByRole('dialog', { name: '选择替换素材' })).toBeNull();
|
||||
@@ -1359,7 +1359,7 @@ describe('版本级资源替换', () => {
|
||||
// ② 不在权威候选里的素材:候选读取之后才登记的资源不属于这次替换目标,一律不放行
|
||||
// (判据是"候选里有没有",不在前端另算一遍兼容性)。
|
||||
await pickResourceCard('late.png');
|
||||
await waitFor(() => expect(reason()).toBe('替换素材未登记或已被删除'));
|
||||
await waitFor(() => expect(reason()).toMatch(/^替换素材未登记或已被删除/u));
|
||||
|
||||
// ③ 分类不同的候选:目标在别的栏目,替换会话要跨栏目活着。
|
||||
await openResourceBookCategory('场景与环境');
|
||||
@@ -1379,7 +1379,7 @@ describe('版本级资源替换', () => {
|
||||
observer?.triggerVisible();
|
||||
});
|
||||
await pickResourceCard('草稿.png');
|
||||
await waitFor(() => expect(reason()).toBe('替换素材未登记或已被删除'));
|
||||
await waitFor(() => expect(reason()).toMatch(/^替换素材未登记或已被删除/u));
|
||||
expect(replacementWrites(invoke)).toHaveLength(0);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user