vibe-coding-master 0.6.1 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/backend/gateway/gateway-service.js +289 -47
- package/dist/backend/server.js +1 -0
- package/dist/backend/services/claude-hook-service.js +52 -9
- package/dist/backend/services/translation-service.js +261 -9
- package/dist/shared/types/app-settings.js +1 -0
- package/dist-frontend/assets/{index-D5LBogZG.js → index-UT2aoyEt.js} +1 -1
- package/dist-frontend/index.html +1 -1
- package/package.json +1 -1
|
@@ -13,6 +13,8 @@ const POLL_LONG_BACKOFF_MS = 30_000;
|
|
|
13
13
|
const MAX_FAILURES_BEFORE_LONG_BACKOFF = 3;
|
|
14
14
|
const DEFAULT_POLL_TIMEOUT_MS = 35_000;
|
|
15
15
|
const LARK_REGISTRATION_CONFIRM_TIMEOUT_MS = 15_000;
|
|
16
|
+
const GATEWAY_ROUND_FINAL_WAIT_MS = 12_000;
|
|
17
|
+
const GATEWAY_ROUND_FINAL_POLL_MS = 250;
|
|
16
18
|
const GATEWAY_TRANSLATION_FAILURE_TEXT = "PM 回复已收到,但翻译失败。\n发送 /retry 重新翻译。";
|
|
17
19
|
const COMMANDS_ALLOWED_WHEN_DISABLED = new Set([
|
|
18
20
|
"help",
|
|
@@ -21,6 +23,8 @@ const COMMANDS_ALLOWED_WHEN_DISABLED = new Set([
|
|
|
21
23
|
"projects",
|
|
22
24
|
"tasks"
|
|
23
25
|
]);
|
|
26
|
+
class HandledGatewayInboundError extends Error {
|
|
27
|
+
}
|
|
24
28
|
export function createGatewayService(deps) {
|
|
25
29
|
const now = deps.now ?? (() => new Date().toISOString());
|
|
26
30
|
const larkRegistration = deps.larkRegistration ?? createLarkRegistrationClient();
|
|
@@ -219,6 +223,10 @@ export function createGatewayService(deps) {
|
|
|
219
223
|
return;
|
|
220
224
|
}
|
|
221
225
|
const command = parseGatewayCommand(update.text);
|
|
226
|
+
if (command.kind === "plain" && settings.enabled) {
|
|
227
|
+
await handlePlainInbound(update, command.text);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
222
230
|
try {
|
|
223
231
|
const output = await executeCommand(command, settings);
|
|
224
232
|
await reply(await deps.settings.loadSettings(), update.fromUserId, output);
|
|
@@ -247,6 +255,69 @@ export function createGatewayService(deps) {
|
|
|
247
255
|
});
|
|
248
256
|
}
|
|
249
257
|
}
|
|
258
|
+
async function handlePlainInbound(update, text) {
|
|
259
|
+
try {
|
|
260
|
+
const target = await resolvePlainTextPmTarget(text);
|
|
261
|
+
if (typeof target === "string") {
|
|
262
|
+
await reply(await deps.settings.loadSettings(), update.fromUserId, target);
|
|
263
|
+
await recordInbound(update, "ok", "plain");
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (target.settings.translationEnabled) {
|
|
267
|
+
await reply(await deps.settings.loadSettings(), update.fromUserId, "已收到,正在翻译...");
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
await reply(await deps.settings.loadSettings(), update.fromUserId, "已收到,正在发送给 PM...");
|
|
271
|
+
}
|
|
272
|
+
const englishText = target.settings.translationEnabled
|
|
273
|
+
? await translatePlainTextForPm(target, text, update)
|
|
274
|
+
: text;
|
|
275
|
+
await submitTerminalInput(deps.runtime, target.session.id, englishText);
|
|
276
|
+
await reply(await deps.settings.loadSettings(), update.fromUserId, target.settings.translationEnabled
|
|
277
|
+
? formatGatewayInputTranslationSuccess(englishText)
|
|
278
|
+
: "已发送给 PM。");
|
|
279
|
+
await recordInbound(update, "ok", "plain");
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
if (error instanceof HandledGatewayInboundError) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const message = errorMessage(error);
|
|
286
|
+
await reply(await deps.settings.loadSettings(), update.fromUserId, `Error: ${message}`);
|
|
287
|
+
await recordInbound(update, "error", "plain", message);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
async function translatePlainTextForPm(target, text, update) {
|
|
291
|
+
try {
|
|
292
|
+
return (await deps.translationService.translateUserInput({
|
|
293
|
+
repoRoot: target.project.repoRoot,
|
|
294
|
+
taskRepoRoot: getTaskRuntimeRepoRoot(target.task),
|
|
295
|
+
taskSlug: target.task.taskSlug,
|
|
296
|
+
role: "project-manager",
|
|
297
|
+
text,
|
|
298
|
+
useContext: false,
|
|
299
|
+
send: false
|
|
300
|
+
})).englishPreview;
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
const message = errorMessage(error);
|
|
304
|
+
await reply(await deps.settings.loadSettings(), update.fromUserId, formatGatewayInputTranslationFailure(message));
|
|
305
|
+
await recordInbound(update, "error", "plain", message);
|
|
306
|
+
throw new HandledGatewayInboundError();
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
async function recordInbound(update, result, command, error) {
|
|
310
|
+
await recordMessageStatus("inbound", result, update.text, error, command);
|
|
311
|
+
await deps.audit.record({
|
|
312
|
+
type: "gateway.command",
|
|
313
|
+
result,
|
|
314
|
+
messageId: update.messageId,
|
|
315
|
+
userId: update.fromUserId,
|
|
316
|
+
command,
|
|
317
|
+
preview: update.text,
|
|
318
|
+
error
|
|
319
|
+
});
|
|
320
|
+
}
|
|
250
321
|
async function saveInboundMetadata(settings, update, options = {}) {
|
|
251
322
|
const bindMode = options.bind ?? "never";
|
|
252
323
|
return deps.settings.saveSettings({
|
|
@@ -594,12 +665,15 @@ export function createGatewayService(deps) {
|
|
|
594
665
|
}
|
|
595
666
|
async function enableGatewayTranslationRuntime() {
|
|
596
667
|
const preferences = await deps.appSettings.getPreferences();
|
|
597
|
-
if (preferences.translationEnabled &&
|
|
668
|
+
if (preferences.translationEnabled &&
|
|
669
|
+
preferences.translationAutoSendEnabled &&
|
|
670
|
+
preferences.translationOutputMode === "round-final") {
|
|
598
671
|
return;
|
|
599
672
|
}
|
|
600
673
|
await deps.appSettings.updatePreferences({
|
|
601
674
|
translationEnabled: true,
|
|
602
|
-
translationAutoSendEnabled: true
|
|
675
|
+
translationAutoSendEnabled: true,
|
|
676
|
+
translationOutputMode: "round-final"
|
|
603
677
|
});
|
|
604
678
|
}
|
|
605
679
|
async function startGateway() {
|
|
@@ -633,6 +707,25 @@ export function createGatewayService(deps) {
|
|
|
633
707
|
return lines.join("\n");
|
|
634
708
|
}
|
|
635
709
|
async function sendPlainTextToPm(text) {
|
|
710
|
+
const target = await resolvePlainTextPmTarget(text);
|
|
711
|
+
if (typeof target === "string") {
|
|
712
|
+
return target;
|
|
713
|
+
}
|
|
714
|
+
const englishText = target.settings.translationEnabled
|
|
715
|
+
? (await deps.translationService.translateUserInput({
|
|
716
|
+
repoRoot: target.project.repoRoot,
|
|
717
|
+
taskRepoRoot: getTaskRuntimeRepoRoot(target.task),
|
|
718
|
+
taskSlug: target.task.taskSlug,
|
|
719
|
+
role: "project-manager",
|
|
720
|
+
text,
|
|
721
|
+
useContext: false,
|
|
722
|
+
send: false
|
|
723
|
+
})).englishPreview
|
|
724
|
+
: text;
|
|
725
|
+
await submitTerminalInput(deps.runtime, target.session.id, englishText);
|
|
726
|
+
return "Sent to PM.";
|
|
727
|
+
}
|
|
728
|
+
async function resolvePlainTextPmTarget(text) {
|
|
636
729
|
if (!text.trim()) {
|
|
637
730
|
return "Empty message ignored.";
|
|
638
731
|
}
|
|
@@ -657,21 +750,13 @@ export function createGatewayService(deps) {
|
|
|
657
750
|
if (session.activityStatus === "running") {
|
|
658
751
|
return "PM is still working on the current turn. Please wait and send again later.";
|
|
659
752
|
}
|
|
660
|
-
|
|
661
|
-
? (await deps.translationService.translateUserInput({
|
|
662
|
-
repoRoot: project.repoRoot,
|
|
663
|
-
taskRepoRoot: getTaskRuntimeRepoRoot(task),
|
|
664
|
-
taskSlug: task.taskSlug,
|
|
665
|
-
role: "project-manager",
|
|
666
|
-
text,
|
|
667
|
-
useContext: false,
|
|
668
|
-
send: false
|
|
669
|
-
})).englishPreview
|
|
670
|
-
: text;
|
|
671
|
-
await submitTerminalInput(deps.runtime, session.id, englishText);
|
|
672
|
-
return "Sent to PM.";
|
|
753
|
+
return { project, task, session, settings };
|
|
673
754
|
}
|
|
674
755
|
async function reply(settings, userId, text) {
|
|
756
|
+
await sendGatewayText(settings, userId, text);
|
|
757
|
+
await recordMessageStatus("outbound", "ok", text);
|
|
758
|
+
}
|
|
759
|
+
async function sendGatewayText(settings, userId, text) {
|
|
675
760
|
const account = toAccount(settings);
|
|
676
761
|
if (!account) {
|
|
677
762
|
return;
|
|
@@ -685,7 +770,6 @@ export function createGatewayService(deps) {
|
|
|
685
770
|
contextToken,
|
|
686
771
|
text
|
|
687
772
|
});
|
|
688
|
-
await recordMessageStatus("outbound", "ok", text);
|
|
689
773
|
}
|
|
690
774
|
async function recordMessageStatus(direction, result, preview, error, command) {
|
|
691
775
|
const settings = await deps.settings.loadSettings();
|
|
@@ -1032,19 +1116,12 @@ export function createGatewayService(deps) {
|
|
|
1032
1116
|
return;
|
|
1033
1117
|
}
|
|
1034
1118
|
const events = await readTranscriptTextEvents(transcriptPath);
|
|
1035
|
-
const
|
|
1036
|
-
if (
|
|
1037
|
-
await saveLatestPmReply(input, latestReply);
|
|
1038
|
-
}
|
|
1039
|
-
const settings = await deps.settings.loadSettings();
|
|
1040
|
-
const account = toAccount(settings);
|
|
1041
|
-
const boundUserId = settings.binding.boundUserId;
|
|
1042
|
-
// A disarmed gateway never touches the channel: skip the outbound push.
|
|
1043
|
-
// The latest reply was already cached above and replays on the next /start.
|
|
1044
|
-
if (!connectionEnabled || !settings.enabled || !account || !boundUserId) {
|
|
1119
|
+
const round = await waitForGatewayRoundFinal(input.repoRoot, input.taskSlug);
|
|
1120
|
+
if (!round) {
|
|
1045
1121
|
return;
|
|
1046
1122
|
}
|
|
1047
1123
|
const cursorKey = `${input.taskSlug}:project-manager:${input.session.claudeSessionId}`;
|
|
1124
|
+
const settings = await deps.settings.loadSettings();
|
|
1048
1125
|
const cursor = settings.pushCursors[cursorKey];
|
|
1049
1126
|
const nextEvents = selectEventsAfterCursor(events, cursor?.lastTranscriptEventId)
|
|
1050
1127
|
.filter(isFinalTurnTextEvent);
|
|
@@ -1055,19 +1132,36 @@ export function createGatewayService(deps) {
|
|
|
1055
1132
|
if (!text) {
|
|
1056
1133
|
return;
|
|
1057
1134
|
}
|
|
1058
|
-
const
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1135
|
+
const latestReply = selectLatestTurnReply(nextEvents, input.session);
|
|
1136
|
+
if (latestReply) {
|
|
1137
|
+
await saveLatestPmReply(input, latestReply);
|
|
1138
|
+
}
|
|
1139
|
+
const account = toAccount(settings);
|
|
1140
|
+
const boundUserId = settings.binding.boundUserId;
|
|
1141
|
+
// A disarmed gateway never touches the channel: skip the outbound push.
|
|
1142
|
+
// The round-final PM reply was already cached above and replays on the next /start.
|
|
1143
|
+
if (!connectionEnabled || !settings.enabled || !account || !boundUserId) {
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
const roundNotice = formatGatewayRoundNotice(round);
|
|
1147
|
+
const originalMessage = formatGatewayPmOriginalReply(text, roundNotice);
|
|
1148
|
+
await sendGatewayText(settings, boundUserId, originalMessage);
|
|
1149
|
+
let output;
|
|
1150
|
+
if (settings.translationEnabled) {
|
|
1151
|
+
output = await renderGatewayPmOutput({
|
|
1152
|
+
settings,
|
|
1153
|
+
repoRoot: input.repoRoot,
|
|
1154
|
+
taskSlug: input.taskSlug,
|
|
1155
|
+
sourceText: text,
|
|
1156
|
+
sourceEntryIds: nextEvents.map((event) => event.id)
|
|
1157
|
+
});
|
|
1158
|
+
await sendGatewayText(settings, boundUserId, output.translationFailed
|
|
1159
|
+
? formatGatewayPmTranslationFailure(output.translationError)
|
|
1160
|
+
: formatGatewayPmTranslatedReply(output.text));
|
|
1161
|
+
}
|
|
1162
|
+
else {
|
|
1163
|
+
clearFailedTranslation(input.repoRoot, input.taskSlug);
|
|
1164
|
+
}
|
|
1071
1165
|
const lastEvent = nextEvents.at(-1);
|
|
1072
1166
|
const current = await deps.settings.loadSettings();
|
|
1073
1167
|
await deps.settings.saveSettings({
|
|
@@ -1082,19 +1176,53 @@ export function createGatewayService(deps) {
|
|
|
1082
1176
|
lastMessageStatus: {
|
|
1083
1177
|
checkedAt: now(),
|
|
1084
1178
|
direction: "outbound",
|
|
1085
|
-
result: output
|
|
1179
|
+
result: output?.translationFailed ? "error" : "ok",
|
|
1086
1180
|
command: "pm-stop",
|
|
1087
|
-
preview: output
|
|
1088
|
-
error: output
|
|
1181
|
+
preview: (output?.text ?? originalMessage).slice(0, 160),
|
|
1182
|
+
error: output?.translationError
|
|
1089
1183
|
},
|
|
1090
1184
|
updatedAt: now()
|
|
1091
1185
|
});
|
|
1092
1186
|
await deps.audit.record({
|
|
1093
1187
|
type: "gateway.pm_push",
|
|
1094
|
-
result: output
|
|
1188
|
+
result: output?.translationFailed ? "error" : "ok",
|
|
1095
1189
|
command: "pm-stop",
|
|
1096
|
-
preview: output.text,
|
|
1097
|
-
error: output
|
|
1190
|
+
preview: output ? `${originalMessage}\n\n${output.text}` : originalMessage,
|
|
1191
|
+
error: output?.translationError
|
|
1192
|
+
});
|
|
1193
|
+
},
|
|
1194
|
+
async handleRoleStopFailure(input) {
|
|
1195
|
+
const round = await getGatewayRoundState(input.repoRoot, input.taskSlug);
|
|
1196
|
+
if (round?.stopReason === "manual-interrupt") {
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
const settings = await deps.settings.loadSettings();
|
|
1200
|
+
const account = toAccount(settings);
|
|
1201
|
+
const boundUserId = settings.binding.boundUserId;
|
|
1202
|
+
if (!connectionEnabled || !settings.enabled || !account || !boundUserId) {
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
const message = formatGatewayRoleStopFailure(input, round);
|
|
1206
|
+
await sendGatewayText(settings, boundUserId, message);
|
|
1207
|
+
const current = await deps.settings.loadSettings();
|
|
1208
|
+
await deps.settings.saveSettings({
|
|
1209
|
+
...current,
|
|
1210
|
+
lastMessageStatus: {
|
|
1211
|
+
checkedAt: now(),
|
|
1212
|
+
direction: "outbound",
|
|
1213
|
+
result: "error",
|
|
1214
|
+
command: "role-stop-failure",
|
|
1215
|
+
preview: message.slice(0, 160),
|
|
1216
|
+
error: input.error
|
|
1217
|
+
},
|
|
1218
|
+
updatedAt: now()
|
|
1219
|
+
});
|
|
1220
|
+
await deps.audit.record({
|
|
1221
|
+
type: "gateway.role_stop_failure",
|
|
1222
|
+
result: "error",
|
|
1223
|
+
command: "role-stop-failure",
|
|
1224
|
+
preview: message,
|
|
1225
|
+
error: input.error
|
|
1098
1226
|
});
|
|
1099
1227
|
},
|
|
1100
1228
|
getDiagnostics() {
|
|
@@ -1109,12 +1237,122 @@ export function createGatewayService(deps) {
|
|
|
1109
1237
|
}
|
|
1110
1238
|
return settings.latestPmReplies[latestPmReplyKey(settings.currentProjectId, settings.currentTaskSlug)];
|
|
1111
1239
|
}
|
|
1240
|
+
async function waitForGatewayRoundFinal(repoRoot, taskSlug) {
|
|
1241
|
+
const startedAt = Date.now();
|
|
1242
|
+
while (Date.now() - startedAt <= GATEWAY_ROUND_FINAL_WAIT_MS) {
|
|
1243
|
+
const round = await getGatewayRoundState(repoRoot, taskSlug);
|
|
1244
|
+
if (round && isGatewayRoundFinal(round)) {
|
|
1245
|
+
return round;
|
|
1246
|
+
}
|
|
1247
|
+
if (round && isGatewayNonFinalTerminalRound(round)) {
|
|
1248
|
+
return undefined;
|
|
1249
|
+
}
|
|
1250
|
+
await delay(GATEWAY_ROUND_FINAL_POLL_MS);
|
|
1251
|
+
}
|
|
1252
|
+
return undefined;
|
|
1253
|
+
}
|
|
1254
|
+
async function getGatewayRoundState(repoRoot, taskSlug) {
|
|
1255
|
+
try {
|
|
1256
|
+
const [projectConfig, task] = await Promise.all([
|
|
1257
|
+
deps.projectService.loadConfig(repoRoot),
|
|
1258
|
+
deps.taskService.loadTask(repoRoot, taskSlug)
|
|
1259
|
+
]);
|
|
1260
|
+
const round = await deps.roundService.getSessionRoundState({
|
|
1261
|
+
repoRoot,
|
|
1262
|
+
stateRepoRoot: getTaskRuntimeRepoRoot(task),
|
|
1263
|
+
stateRoot: projectConfig.stateRoot,
|
|
1264
|
+
taskSlug
|
|
1265
|
+
});
|
|
1266
|
+
return round;
|
|
1267
|
+
}
|
|
1268
|
+
catch {
|
|
1269
|
+
return undefined;
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
function isGatewayRoundFinal(round) {
|
|
1273
|
+
return round.status === "stopped"
|
|
1274
|
+
&& Boolean(round.roundId)
|
|
1275
|
+
&& !round.stopReason
|
|
1276
|
+
&& round.roleRecovery?.status !== "failed"
|
|
1277
|
+
&& round.flowPause?.reason !== "role-recovery-failed";
|
|
1278
|
+
}
|
|
1279
|
+
function isGatewayNonFinalTerminalRound(round) {
|
|
1280
|
+
return round.status === "stopped" && (Boolean(round.stopReason) ||
|
|
1281
|
+
round.roleRecovery?.status === "failed" ||
|
|
1282
|
+
round.flowPause?.reason === "role-recovery-failed");
|
|
1283
|
+
}
|
|
1284
|
+
function formatGatewayRoundNotice(round) {
|
|
1285
|
+
const roundLabel = round.roundSequence ? `第 ${round.roundSequence} 轮` : "当前 round";
|
|
1286
|
+
if (round.status === "stopped") {
|
|
1287
|
+
return `${roundLabel}已结束。现在需要你给出下一步指令。`;
|
|
1288
|
+
}
|
|
1289
|
+
const activeRole = round.activeRole ? `,当前角色:${round.activeRole}` : "";
|
|
1290
|
+
return `${roundLabel}运行中${activeRole}。`;
|
|
1291
|
+
}
|
|
1292
|
+
function formatGatewayRoleStopFailure(input, round) {
|
|
1293
|
+
const roundLabel = round?.roundSequence ? `第 ${round.roundSequence} 轮` : "当前 round";
|
|
1294
|
+
const retryLine = input.maxAttempts !== undefined
|
|
1295
|
+
? `Retry: ${input.attempt ?? 0}/${input.maxAttempts}`
|
|
1296
|
+
: undefined;
|
|
1297
|
+
return [
|
|
1298
|
+
"VCM 角色异常中断,流程已暂停。",
|
|
1299
|
+
`Task: ${input.taskSlug}`,
|
|
1300
|
+
`Role: ${input.role}`,
|
|
1301
|
+
`Round: ${roundLabel}`,
|
|
1302
|
+
input.error ? `Reason: ${input.error}` : undefined,
|
|
1303
|
+
retryLine,
|
|
1304
|
+
input.errorDetails ? `Details: ${truncateGatewayFailureDetails(input.errorDetails)}` : undefined,
|
|
1305
|
+
"",
|
|
1306
|
+
"请在 VCM 中检查状态,或修复问题后继续给出下一步指令。"
|
|
1307
|
+
].filter((line) => line !== undefined).join("\n");
|
|
1308
|
+
}
|
|
1309
|
+
function truncateGatewayFailureDetails(value) {
|
|
1310
|
+
const trimmed = value.trim();
|
|
1311
|
+
return trimmed.length > 600 ? `${trimmed.slice(0, 600)}...` : trimmed;
|
|
1312
|
+
}
|
|
1313
|
+
function formatGatewayPmOriginalReply(text, roundNotice) {
|
|
1314
|
+
return [
|
|
1315
|
+
"PM final reply 原文:",
|
|
1316
|
+
"",
|
|
1317
|
+
text.trim(),
|
|
1318
|
+
roundNotice ? "" : undefined,
|
|
1319
|
+
roundNotice ? `Round: ${roundNotice}` : undefined
|
|
1320
|
+
].filter((line) => line !== undefined).join("\n");
|
|
1321
|
+
}
|
|
1322
|
+
function formatGatewayPmTranslatedReply(text) {
|
|
1323
|
+
return [
|
|
1324
|
+
"PM final reply 翻译:",
|
|
1325
|
+
"",
|
|
1326
|
+
text.trim()
|
|
1327
|
+
].join("\n");
|
|
1328
|
+
}
|
|
1329
|
+
function formatGatewayPmTranslationFailure(error) {
|
|
1330
|
+
return [
|
|
1331
|
+
GATEWAY_TRANSLATION_FAILURE_TEXT,
|
|
1332
|
+
error ? `原因:${error}` : undefined
|
|
1333
|
+
].filter((line) => line !== undefined).join("\n");
|
|
1334
|
+
}
|
|
1335
|
+
function formatGatewayInputTranslationSuccess(englishText) {
|
|
1336
|
+
return [
|
|
1337
|
+
"翻译完成,已发送给 PM:",
|
|
1338
|
+
"",
|
|
1339
|
+
englishText.trim()
|
|
1340
|
+
].join("\n");
|
|
1341
|
+
}
|
|
1342
|
+
function formatGatewayInputTranslationFailure(error) {
|
|
1343
|
+
return [
|
|
1344
|
+
"翻译失败,消息未发送给 PM。",
|
|
1345
|
+
`原因:${error}`,
|
|
1346
|
+
"请重新输入后再试。"
|
|
1347
|
+
].join("\n");
|
|
1348
|
+
}
|
|
1112
1349
|
async function renderLatestPmReply(settings, reply) {
|
|
1113
1350
|
const rendered = await renderGatewayPmOutput({
|
|
1114
1351
|
settings,
|
|
1115
1352
|
repoRoot: reply.repoRoot,
|
|
1116
1353
|
taskSlug: reply.taskSlug,
|
|
1117
|
-
sourceText: reply.text
|
|
1354
|
+
sourceText: reply.text,
|
|
1355
|
+
sourceEntryIds: reply.transcriptEventId ? [reply.transcriptEventId] : undefined
|
|
1118
1356
|
});
|
|
1119
1357
|
return {
|
|
1120
1358
|
...rendered,
|
|
@@ -1134,7 +1372,8 @@ export function createGatewayService(deps) {
|
|
|
1134
1372
|
repoRoot: input.repoRoot,
|
|
1135
1373
|
taskSlug: input.taskSlug,
|
|
1136
1374
|
role: "project-manager",
|
|
1137
|
-
text: input.sourceText
|
|
1375
|
+
text: input.sourceText,
|
|
1376
|
+
sourceEntryIds: input.sourceEntryIds
|
|
1138
1377
|
});
|
|
1139
1378
|
clearFailedTranslation(input.repoRoot, input.taskSlug);
|
|
1140
1379
|
return {
|
|
@@ -1293,6 +1532,9 @@ function sleep(ms, signal) {
|
|
|
1293
1532
|
}, { once: true });
|
|
1294
1533
|
});
|
|
1295
1534
|
}
|
|
1535
|
+
function delay(ms) {
|
|
1536
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1537
|
+
}
|
|
1296
1538
|
function normalizeBaseUrl(input, fallback) {
|
|
1297
1539
|
const trimmed = input.trim();
|
|
1298
1540
|
if (!trimmed) {
|
package/dist/backend/server.js
CHANGED
|
@@ -241,15 +241,27 @@ export function createClaudeHookService(deps) {
|
|
|
241
241
|
}
|
|
242
242
|
const failure = parseStopFailureDiagnostic(input.event);
|
|
243
243
|
if (!failure.retryable) {
|
|
244
|
+
if (await isManualInterruptedStopFailure(context, input.role)) {
|
|
245
|
+
return recordTurnEnd(input, context, eventName, {
|
|
246
|
+
dispatchRouteFiles: false,
|
|
247
|
+
notifyGateway: false,
|
|
248
|
+
settleGuard: false
|
|
249
|
+
});
|
|
250
|
+
}
|
|
244
251
|
await markStopFailureRecoveryFailed(input, context, failure, 0);
|
|
245
252
|
return recordTurnEnd(input, context, eventName, {
|
|
246
253
|
dispatchRouteFiles: false,
|
|
247
254
|
notifyGateway: false,
|
|
248
|
-
settleGuard: false
|
|
255
|
+
settleGuard: false,
|
|
256
|
+
gatewayStopFailure: {
|
|
257
|
+
...failure,
|
|
258
|
+
attempt: 0,
|
|
259
|
+
maxAttempts: MAX_ROLE_RETRY_ATTEMPTS
|
|
260
|
+
}
|
|
249
261
|
});
|
|
250
262
|
}
|
|
251
|
-
const
|
|
252
|
-
if (
|
|
263
|
+
const retryResult = await scheduleStopFailureRetry(input, context, failure);
|
|
264
|
+
if (retryResult === "scheduled") {
|
|
253
265
|
return {
|
|
254
266
|
ok: true,
|
|
255
267
|
eventName,
|
|
@@ -262,7 +274,8 @@ export function createClaudeHookService(deps) {
|
|
|
262
274
|
return recordTurnEnd(input, context, eventName, {
|
|
263
275
|
dispatchRouteFiles: false,
|
|
264
276
|
notifyGateway: false,
|
|
265
|
-
settleGuard: false
|
|
277
|
+
settleGuard: false,
|
|
278
|
+
...(retryResult === "manual-interrupt" ? {} : { gatewayStopFailure: failure })
|
|
266
279
|
});
|
|
267
280
|
}
|
|
268
281
|
async function processPostCompactHook(input) {
|
|
@@ -344,6 +357,17 @@ export function createClaudeHookService(deps) {
|
|
|
344
357
|
session
|
|
345
358
|
}).catch(() => undefined);
|
|
346
359
|
}
|
|
360
|
+
if (boundToTask && options.gatewayStopFailure) {
|
|
361
|
+
void deps.gatewayService?.handleRoleStopFailure({
|
|
362
|
+
repoRoot: context.project.repoRoot,
|
|
363
|
+
taskSlug: context.taskSlug,
|
|
364
|
+
role: input.role,
|
|
365
|
+
error: options.gatewayStopFailure.error,
|
|
366
|
+
errorDetails: options.gatewayStopFailure.errorDetails,
|
|
367
|
+
attempt: options.gatewayStopFailure.attempt,
|
|
368
|
+
maxAttempts: options.gatewayStopFailure.maxAttempts
|
|
369
|
+
}).catch(() => undefined);
|
|
370
|
+
}
|
|
347
371
|
const dispatched = options.dispatchRouteFiles
|
|
348
372
|
? await deps.messageService.scanAndDispatchPendingRouteFiles(scopedRouteDispatchInput)
|
|
349
373
|
: [];
|
|
@@ -370,14 +394,14 @@ export function createClaudeHookService(deps) {
|
|
|
370
394
|
async function scheduleStopFailureRetry(input, context, failure) {
|
|
371
395
|
const preferences = await deps.appSettings.getPreferences();
|
|
372
396
|
if (!preferences.roleRetryEnabled || !deps.runtime) {
|
|
373
|
-
return
|
|
397
|
+
return "not-scheduled";
|
|
374
398
|
}
|
|
375
399
|
const stateInput = createRoundStateInput(context);
|
|
376
400
|
const currentRoundState = await deps.roundService.getSessionRoundState(stateInput);
|
|
377
401
|
if (currentRoundState.stopReason === "manual-interrupt"
|
|
378
402
|
&& currentRoundState.status === "stopped"
|
|
379
403
|
&& currentRoundState.activeRole === input.role) {
|
|
380
|
-
return
|
|
404
|
+
return "manual-interrupt";
|
|
381
405
|
}
|
|
382
406
|
const previousAttempt = currentRoundState.roleRecovery?.role === input.role &&
|
|
383
407
|
currentRoundState.roleRecovery.status !== "failed"
|
|
@@ -388,7 +412,7 @@ export function createClaudeHookService(deps) {
|
|
|
388
412
|
if (attempt > MAX_ROLE_RETRY_ATTEMPTS) {
|
|
389
413
|
clearStopFailureRetryTimer(context.project.repoRoot, context.taskSlug, input.role);
|
|
390
414
|
await markStopFailureRecoveryFailed(input, context, failure, MAX_ROLE_RETRY_ATTEMPTS, timestamp);
|
|
391
|
-
return
|
|
415
|
+
return "not-scheduled";
|
|
392
416
|
}
|
|
393
417
|
const nextRetryAt = new Date(Date.parse(timestamp) + attempt * ROLE_RETRY_BASE_DELAY_MS).toISOString();
|
|
394
418
|
await deps.roundService.setRoleRecovery({
|
|
@@ -408,7 +432,18 @@ export function createClaudeHookService(deps) {
|
|
|
408
432
|
});
|
|
409
433
|
await deps.sessionService.markRoleActivityRunning(context.project.repoRoot, context.taskSlug, input.role);
|
|
410
434
|
scheduleStopFailureRetryTimer(input, context, attempt, nextRetryAt);
|
|
411
|
-
return
|
|
435
|
+
return "scheduled";
|
|
436
|
+
}
|
|
437
|
+
async function isManualInterruptedStopFailure(context, role) {
|
|
438
|
+
try {
|
|
439
|
+
const state = await deps.roundService.getSessionRoundState(createRoundStateInput(context));
|
|
440
|
+
return state.status === "stopped" &&
|
|
441
|
+
state.stopReason === "manual-interrupt" &&
|
|
442
|
+
state.activeRole === role;
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
412
447
|
}
|
|
413
448
|
function scheduleStopFailureRetryTimer(input, context, attempt, nextRetryAt) {
|
|
414
449
|
const key = stopFailureRecoveryKey(context.project.repoRoot, context.taskSlug, input.role);
|
|
@@ -441,7 +476,15 @@ export function createClaudeHookService(deps) {
|
|
|
441
476
|
await recordTurnEnd(input, context, "StopFailure", {
|
|
442
477
|
dispatchRouteFiles: false,
|
|
443
478
|
notifyGateway: false,
|
|
444
|
-
settleGuard: false
|
|
479
|
+
settleGuard: false,
|
|
480
|
+
gatewayStopFailure: {
|
|
481
|
+
error: recovery.error ?? "stop_failure",
|
|
482
|
+
errorDetails: recovery.errorDetails,
|
|
483
|
+
lastAssistantMessage: recovery.lastAssistantMessage,
|
|
484
|
+
retryable: recovery.retryable ?? true,
|
|
485
|
+
attempt: recovery.attempt,
|
|
486
|
+
maxAttempts: recovery.maxAttempts
|
|
487
|
+
}
|
|
445
488
|
});
|
|
446
489
|
return;
|
|
447
490
|
}
|