Skip to content

Commit 2c7c460

Browse files
committed
修复本地环境修复卡在准备状态
宿主修复请求增加完整异常兜底,确保无效 action、修复执行异常、复检异常都会回传失败结果及日志路径。 修复服务将准备阶段目录创建、日志路径解析和初始状态写入纳入失败结果,并回写 completed/failed 状态。 Dashboard 在修复 Promise reject 时将对应进度卡片切到 failed,并保留现有命令输出和日志上下文。 新增和更新 .NET 与 Vitest 用例,覆盖早期准备失败、宿主结果回传、前端 reject 失败态和桥接超时错误。
1 parent 863c85c commit 2c7c460

7 files changed

Lines changed: 418 additions & 58 deletions

File tree

resources/webui/upstream/source/src/pages/DashboardPage.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,17 @@ export function DashboardPage() {
503503
);
504504
} catch (error) {
505505
const message = error instanceof Error ? error.message : t('common.unknown_error');
506+
setLocalRepairProgressItemId(itemId);
507+
setLocalRepairProgress((current) => ({
508+
actionId,
509+
phase: 'failed',
510+
message,
511+
commandLine: current?.actionId === actionId ? current.commandLine : null,
512+
recentOutput: current?.actionId === actionId ? current.recentOutput : [],
513+
logPath: current?.actionId === actionId ? current.logPath : null,
514+
updatedAt: new Date().toISOString(),
515+
exitCode: current?.actionId === actionId ? current.exitCode : null,
516+
}));
506517
showNotification(message, 'error');
507518
} finally {
508519
setRepairingTarget((current) =>

resources/webui/upstream/source/tests/desktop/localDependencyBridge.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,24 @@ describe('local dependency desktop bridge', () => {
207207
});
208208
});
209209

210+
it('rejects a repair request with the desktop timeout error when no result arrives', async () => {
211+
vi.useFakeTimers();
212+
const repairRequestIds: string[] = [];
213+
const bridge = await loadBridge({
214+
runRepair: (_actionId, requestId) => {
215+
repairRequestIds.push(requestId);
216+
return true;
217+
},
218+
});
219+
220+
const request = bridge.runLocalDependencyRepair('repair-user-path');
221+
const rejected = expect(request).rejects.toThrow('桌面端响应超时');
222+
expect(repairRequestIds).toHaveLength(1);
223+
await vi.advanceTimersByTimeAsync(35 * 60_000);
224+
225+
await rejected;
226+
});
227+
210228
it('passes required environment repair action through the existing repair channel', async () => {
211229
const repairCalls: Array<{ actionId: string; requestId: string }> = [];
212230
const bridge = await loadBridge({

resources/webui/upstream/source/tests/pages/DashboardPage.localEnvironment.test.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,10 @@ describe('DashboardPage local environment loading', () => {
509509
expect(
510510
within(nodeNpmRow).getByRole('button', { name: 'dashboard.local_environment_repair' })
511511
).not.toBeDisabled();
512+
expect(
513+
within(nodeNpmRow).getByText('dashboard.local_environment_repair_phase_failed')
514+
).toBeInTheDocument();
515+
expect(within(nodeNpmRow).getByText('用户取消了管理员授权。')).toBeInTheDocument();
512516
});
513517
});
514518

src/CodexCliPlus.App/MainWindow.WebViewHost.cs

Lines changed: 101 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1094,56 +1094,87 @@ private async Task RunLocalDependencyRepairAsync(string? requestId, string? acti
10941094
string.IsNullOrWhiteSpace(actionId) || !LocalDependencyRepairActionIds.IsKnown(actionId)
10951095
)
10961096
{
1097+
var invalidActionResult = CreateLocalDependencyRepairFailure(
1098+
actionId ?? string.Empty,
1099+
"未知修复动作。",
1100+
"桌面端只接受内置白名单 action id。"
1101+
);
10971102
PostWebUiCommand(
10981103
new
10991104
{
11001105
type = "localDependencyRepairResult",
11011106
requestId,
1102-
result = new
1103-
{
1104-
actionId = actionId ?? string.Empty,
1105-
succeeded = false,
1106-
summary = "未知修复动作。",
1107-
detail = "桌面端只接受内置白名单 action id。",
1108-
},
1107+
result = invalidActionResult,
11091108
}
11101109
);
11111110
return;
11121111
}
11131112

1114-
PostWebUiCommand(
1115-
new
1116-
{
1117-
type = "localDependencyRepairStarted",
1118-
requestId,
1119-
actionId,
1120-
}
1121-
);
1113+
LocalDependencyRepairResult result;
1114+
LocalDependencySnapshot? snapshot = null;
1115+
try
1116+
{
1117+
PostWebUiCommand(
1118+
new
1119+
{
1120+
type = "localDependencyRepairStarted",
1121+
requestId,
1122+
actionId,
1123+
}
1124+
);
11221125

1123-
var result = await _localDependencyRepairService.RunElevatedRepairAsync(
1124-
actionId,
1125-
progress =>
1126-
Dispatcher.InvokeAsync(() =>
1127-
PostWebUiCommand(
1128-
new
1129-
{
1130-
type = "localDependencyRepairProgress",
1131-
requestId,
1132-
progress,
1133-
}
1126+
result = await _localDependencyRepairService.RunElevatedRepairAsync(
1127+
actionId,
1128+
progress =>
1129+
Dispatcher.InvokeAsync(() =>
1130+
PostWebUiCommand(
1131+
new
1132+
{
1133+
type = "localDependencyRepairProgress",
1134+
requestId,
1135+
progress,
1136+
}
1137+
)
11341138
)
1135-
)
1136-
);
1137-
var snapshot = await _localDependencyHealthService.CheckAsync();
1138-
PostWebUiCommand(
1139-
new
1139+
);
1140+
try
11401141
{
1141-
type = "localDependencyRepairResult",
1142-
requestId,
1143-
result,
1144-
snapshot,
1142+
snapshot = await _localDependencyHealthService.CheckAsync();
1143+
}
1144+
catch (Exception exception)
1145+
{
1146+
_logger.LogError("Failed to check local dependency health after repair.", exception);
1147+
result = CreateLocalDependencyRepairFailure(
1148+
actionId,
1149+
"修复后检测失败。",
1150+
exception.Message,
1151+
result.LogPath
1152+
);
11451153
}
1146-
);
1154+
1155+
PostWebUiCommand(
1156+
new
1157+
{
1158+
type = "localDependencyRepairResult",
1159+
requestId,
1160+
result,
1161+
snapshot,
1162+
}
1163+
);
1164+
}
1165+
catch (Exception exception)
1166+
{
1167+
_logger.LogError($"Local dependency repair '{actionId}' failed before result.", exception);
1168+
result = CreateLocalDependencyRepairFailure(actionId, "本地环境修复失败。", exception.Message);
1169+
PostWebUiCommand(
1170+
new
1171+
{
1172+
type = "localDependencyRepairResult",
1173+
requestId,
1174+
result,
1175+
}
1176+
);
1177+
}
11471178

11481179
if (result.Succeeded)
11491180
{
@@ -1156,6 +1187,40 @@ private async Task RunLocalDependencyRepairAsync(string? requestId, string? acti
11561187
}
11571188
}
11581189

1190+
private LocalDependencyRepairResult CreateLocalDependencyRepairFailure(
1191+
string actionId,
1192+
string summary,
1193+
string detail,
1194+
string? logPath = null
1195+
)
1196+
{
1197+
return new LocalDependencyRepairResult
1198+
{
1199+
ActionId = actionId,
1200+
Succeeded = false,
1201+
Summary = summary,
1202+
Detail = detail,
1203+
LogPath = string.IsNullOrWhiteSpace(logPath)
1204+
? GetLocalDependencyRepairLogPath()
1205+
: logPath,
1206+
};
1207+
}
1208+
1209+
private string GetLocalDependencyRepairLogPath()
1210+
{
1211+
try
1212+
{
1213+
return Path.Combine(
1214+
_pathService.Directories.LogsDirectory,
1215+
"local-environment-repair.log"
1216+
);
1217+
}
1218+
catch
1219+
{
1220+
return "local-environment-repair.log";
1221+
}
1222+
}
1223+
11591224
private static string? ReadRequestId(JsonElement root)
11601225
{
11611226
return

src/CodexCliPlus.Infrastructure/LocalEnvironment/LocalDependencyRepairService.cs

Lines changed: 71 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -120,19 +120,41 @@ public async Task<LocalDependencyRepairResult> RunElevatedRepairAsync(
120120
return Failure(actionId, "未知修复动作。", "前端只能调用内置白名单动作。");
121121
}
122122

123-
await _pathService.EnsureCreatedAsync(cancellationToken);
124-
var statusPath = Path.Combine(
125-
_pathService.Directories.RuntimeDirectory,
126-
$"local-environment-repair-{Guid.NewGuid():N}.json"
127-
);
128-
var initialProgress = CreateProgress(
129-
actionId,
130-
"starting",
131-
"正在准备修复。",
132-
logPath: GetRepairLogPath()
133-
);
134-
await WriteStatusAsync(statusPath, initialProgress, cancellationToken);
135-
progressReporter?.Invoke(initialProgress);
123+
string? statusPath = null;
124+
LocalDependencyRepairProgress? initialProgress = null;
125+
try
126+
{
127+
statusPath = Path.Combine(
128+
_pathService.Directories.RuntimeDirectory,
129+
$"local-environment-repair-{Guid.NewGuid():N}.json"
130+
);
131+
await _pathService.EnsureCreatedAsync(cancellationToken);
132+
initialProgress = CreateProgress(
133+
actionId,
134+
"starting",
135+
"正在准备修复。",
136+
logPath: GetRepairLogPath()
137+
);
138+
await WriteStatusAsync(statusPath, initialProgress, cancellationToken);
139+
progressReporter?.Invoke(initialProgress);
140+
}
141+
catch (Exception exception) when (exception is not OperationCanceledException)
142+
{
143+
_logger.LogError($"Failed to prepare local dependency repair '{actionId}'.", exception);
144+
var failure = Failure(
145+
actionId,
146+
"准备修复失败。",
147+
exception.Message,
148+
TryGetRepairLogPath()
149+
);
150+
if (!string.IsNullOrWhiteSpace(statusPath))
151+
{
152+
await TryWriteCompletedStatusAsync(statusPath, failure, cancellationToken);
153+
}
154+
155+
TryReportProgress(progressReporter, CreateCompletedProgress(failure, initialProgress));
156+
return failure;
157+
}
136158

137159
if (IsCurrentProcessRunningAsAdministrator())
138160
{
@@ -460,6 +482,23 @@ CancellationToken cancellationToken
460482
}
461483
}
462484

485+
private void TryReportProgress(
486+
Action<LocalDependencyRepairProgress>? progressReporter,
487+
LocalDependencyRepairProgress progress
488+
)
489+
{
490+
try
491+
{
492+
progressReporter?.Invoke(progress);
493+
}
494+
catch (Exception exception) when (exception is not OperationCanceledException)
495+
{
496+
_logger.Warn(
497+
$"Failed to report local dependency repair progress: {exception.Message}"
498+
);
499+
}
500+
}
501+
463502
private async Task TryWriteCompletedStatusAsync(
464503
string statusPath,
465504
LocalDependencyRepairResult result,
@@ -1017,8 +1056,25 @@ await AtomicFileWriter.WriteUtf8NoBomTextAsync(
10171056

10181057
private string GetRepairLogPath()
10191058
{
1020-
Directory.CreateDirectory(_pathService.Directories.LogsDirectory);
1021-
return Path.Combine(_pathService.Directories.LogsDirectory, "local-environment-repair.log");
1059+
var logPath = GetRepairLogPathWithoutCreatingDirectory();
1060+
Directory.CreateDirectory(Path.GetDirectoryName(logPath)!);
1061+
return logPath;
1062+
}
1063+
1064+
private string GetRepairLogPathWithoutCreatingDirectory() =>
1065+
Path.Combine(_pathService.Directories.LogsDirectory, "local-environment-repair.log");
1066+
1067+
private string? TryGetRepairLogPath()
1068+
{
1069+
try
1070+
{
1071+
return GetRepairLogPathWithoutCreatingDirectory();
1072+
}
1073+
catch (Exception exception) when (exception is not OperationCanceledException)
1074+
{
1075+
_logger.Warn($"Failed to resolve local dependency repair log path: {exception.Message}");
1076+
return null;
1077+
}
10221078
}
10231079

10241080
private static LocalDependencyRepairProgress CreateProgress(

0 commit comments

Comments
 (0)