vibe-coding-master 0.8.1 → 0.8.2
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/README.md +17 -8
- package/dist/backend/gateway/gateway-command-parser.js +17 -0
- package/dist/backend/gateway/gateway-service.js +168 -86
- package/dist/backend/gateway/gateway-settings-service.js +21 -8
- package/dist/backend/services/claude-hook-service.js +2 -2
- package/dist/backend/services/session-service.js +2 -1
- package/dist/backend/services/translation-service.js +1 -1
- package/dist/shared/types/session.js +9 -2
- package/dist-frontend/assets/{index-Dh7uVCmk.js → index-BDUbu5St.js} +32 -32
- package/dist-frontend/index.html +1 -1
- package/package.json +1 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getRoleDefinition, isVcmRoleName, VCM_ROLE_NAMES } from "../../shared/constants.js";
|
|
1
2
|
import { VcmError } from "../errors.js";
|
|
2
3
|
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
3
4
|
import { getTaskRuntimeRepoRoot } from "../services/task-service.js";
|
|
@@ -14,7 +15,7 @@ const DEFAULT_POLL_TIMEOUT_MS = 35_000;
|
|
|
14
15
|
const LARK_REGISTRATION_CONFIRM_TIMEOUT_MS = 15_000;
|
|
15
16
|
const GATEWAY_ROUND_FINAL_WAIT_MS = 12_000;
|
|
16
17
|
const GATEWAY_ROUND_FINAL_POLL_MS = 250;
|
|
17
|
-
const GATEWAY_TRANSLATION_FAILURE_TEXT = "
|
|
18
|
+
const GATEWAY_TRANSLATION_FAILURE_TEXT = "角色回复已收到,但翻译失败。\n发送 /retry 重新翻译。";
|
|
18
19
|
const COMMANDS_ALLOWED_WHEN_DISABLED = new Set([
|
|
19
20
|
"help",
|
|
20
21
|
"start",
|
|
@@ -33,7 +34,7 @@ export function createGatewayService(deps) {
|
|
|
33
34
|
let qrLogin = null;
|
|
34
35
|
let larkRegistrationState = null;
|
|
35
36
|
let lastFailedTranslation = null;
|
|
36
|
-
let
|
|
37
|
+
let lastGatewayInputMessageId = null;
|
|
37
38
|
// Runtime channel-connection arming switch. Not persisted: every process starts
|
|
38
39
|
// disarmed. `ensurePolling` is the single chokepoint that reads it, so no
|
|
39
40
|
// self-heal path can connect the channel while this is false.
|
|
@@ -257,7 +258,7 @@ export function createGatewayService(deps) {
|
|
|
257
258
|
}
|
|
258
259
|
async function handlePlainInbound(update, text) {
|
|
259
260
|
try {
|
|
260
|
-
const target = await
|
|
261
|
+
const target = await resolvePlainTextRoleTarget(text);
|
|
261
262
|
if (typeof target === "string") {
|
|
262
263
|
await reply(await deps.settings.loadSettings(), update.fromUserId, target);
|
|
263
264
|
await recordInbound(update, "ok", "plain");
|
|
@@ -267,16 +268,16 @@ export function createGatewayService(deps) {
|
|
|
267
268
|
await reply(await deps.settings.loadSettings(), update.fromUserId, "已收到,正在翻译...");
|
|
268
269
|
}
|
|
269
270
|
else {
|
|
270
|
-
await reply(await deps.settings.loadSettings(), update.fromUserId,
|
|
271
|
+
await reply(await deps.settings.loadSettings(), update.fromUserId, `已收到,正在发送给 ${roleLabel(target.role)}...`);
|
|
271
272
|
}
|
|
272
273
|
const englishText = target.settings.translationEnabled
|
|
273
|
-
? await
|
|
274
|
+
? await translatePlainTextForRole(target, text, update)
|
|
274
275
|
: text;
|
|
275
276
|
await submitTerminalInput(deps.runtime, target.session.id, englishText);
|
|
276
|
-
|
|
277
|
+
lastGatewayInputMessageId = update.messageId;
|
|
277
278
|
await reply(await deps.settings.loadSettings(), update.fromUserId, target.settings.translationEnabled
|
|
278
|
-
? formatGatewayInputTranslationSuccess(englishText)
|
|
279
|
-
:
|
|
279
|
+
? formatGatewayInputTranslationSuccess(target.role, englishText)
|
|
280
|
+
: `已发送给 ${roleLabel(target.role)}。`);
|
|
280
281
|
await recordInbound(update, "ok", "plain");
|
|
281
282
|
}
|
|
282
283
|
catch (error) {
|
|
@@ -288,13 +289,13 @@ export function createGatewayService(deps) {
|
|
|
288
289
|
await recordInbound(update, "error", "plain", message);
|
|
289
290
|
}
|
|
290
291
|
}
|
|
291
|
-
async function
|
|
292
|
+
async function translatePlainTextForRole(target, text, update) {
|
|
292
293
|
try {
|
|
293
294
|
return (await deps.translationService.translateUserInput({
|
|
294
295
|
repoRoot: target.project.repoRoot,
|
|
295
296
|
taskRepoRoot: getTaskRuntimeRepoRoot(target.task),
|
|
296
297
|
taskSlug: target.task.taskSlug,
|
|
297
|
-
role:
|
|
298
|
+
role: target.role,
|
|
298
299
|
text,
|
|
299
300
|
useContext: false,
|
|
300
301
|
send: false
|
|
@@ -302,7 +303,7 @@ export function createGatewayService(deps) {
|
|
|
302
303
|
}
|
|
303
304
|
catch (error) {
|
|
304
305
|
const message = errorMessage(error);
|
|
305
|
-
await reply(await deps.settings.loadSettings(), update.fromUserId, formatGatewayInputTranslationFailure(message));
|
|
306
|
+
await reply(await deps.settings.loadSettings(), update.fromUserId, formatGatewayInputTranslationFailure(target.role, message));
|
|
306
307
|
await recordInbound(update, "error", "plain", message);
|
|
307
308
|
throw new HandledGatewayInboundError();
|
|
308
309
|
}
|
|
@@ -357,7 +358,7 @@ export function createGatewayService(deps) {
|
|
|
357
358
|
return [
|
|
358
359
|
"Gateway is connected but off.",
|
|
359
360
|
"Available commands while off: /help, /start, /status, /projects, /tasks.",
|
|
360
|
-
"Turn Gateway on in desktop VCM to send
|
|
361
|
+
"Turn Gateway on in desktop VCM to send role messages or run task-changing commands."
|
|
361
362
|
].join("\n");
|
|
362
363
|
}
|
|
363
364
|
switch (command.kind) {
|
|
@@ -387,8 +388,10 @@ export function createGatewayService(deps) {
|
|
|
387
388
|
return closeTaskConfirm(command.taskSlug);
|
|
388
389
|
case "translate":
|
|
389
390
|
return setGatewayTranslation(command.enabled);
|
|
391
|
+
case "role":
|
|
392
|
+
return command.role ? setGatewayTargetRole(command.role) : gatewayRoleText(settings);
|
|
390
393
|
case "plain":
|
|
391
|
-
return
|
|
394
|
+
return sendPlainTextToRole(command.text);
|
|
392
395
|
case "unknown":
|
|
393
396
|
return `Unknown command: ${command.name}. Send /help for available commands.`;
|
|
394
397
|
}
|
|
@@ -410,6 +413,8 @@ export function createGatewayService(deps) {
|
|
|
410
413
|
await deps.settings.saveSettings({
|
|
411
414
|
...settings,
|
|
412
415
|
currentProjectId: project.repoRoot,
|
|
416
|
+
currentTaskSlug: null,
|
|
417
|
+
targetRole: "project-manager",
|
|
413
418
|
updatedAt: now()
|
|
414
419
|
});
|
|
415
420
|
}
|
|
@@ -433,6 +438,7 @@ export function createGatewayService(deps) {
|
|
|
433
438
|
...settings,
|
|
434
439
|
currentProjectId: project.repoRoot,
|
|
435
440
|
currentTaskSlug: nextTaskSlug,
|
|
441
|
+
targetRole: "project-manager",
|
|
436
442
|
updatedAt: now()
|
|
437
443
|
});
|
|
438
444
|
}
|
|
@@ -461,6 +467,7 @@ export function createGatewayService(deps) {
|
|
|
461
467
|
...settings,
|
|
462
468
|
currentProjectId: project.repoRoot,
|
|
463
469
|
currentTaskSlug: null,
|
|
470
|
+
targetRole: "project-manager",
|
|
464
471
|
updatedAt: now()
|
|
465
472
|
});
|
|
466
473
|
return `Selected project:\n${project.repoRoot}\nbranch: ${project.branch}\ncommit: ${project.shortHeadCommit ?? project.headCommit ?? "unknown"}`;
|
|
@@ -501,6 +508,7 @@ export function createGatewayService(deps) {
|
|
|
501
508
|
await deps.settings.saveSettings({
|
|
502
509
|
...settings,
|
|
503
510
|
currentTaskSlug: task.taskSlug,
|
|
511
|
+
targetRole: "project-manager",
|
|
504
512
|
updatedAt: now()
|
|
505
513
|
});
|
|
506
514
|
return `Selected task: ${task.taskSlug}\nstatus: ${task.status}\nbranch: ${task.branch}`;
|
|
@@ -520,6 +528,7 @@ export function createGatewayService(deps) {
|
|
|
520
528
|
...settings,
|
|
521
529
|
currentProjectId: project.repoRoot,
|
|
522
530
|
currentTaskSlug: task.taskSlug,
|
|
531
|
+
targetRole: "project-manager",
|
|
523
532
|
translationEnabled: preferences.translationEnabled,
|
|
524
533
|
updatedAt: now()
|
|
525
534
|
});
|
|
@@ -608,12 +617,13 @@ export function createGatewayService(deps) {
|
|
|
608
617
|
}
|
|
609
618
|
const result = await deps.taskCloseService.closeTask(project.repoRoot, taskSlug);
|
|
610
619
|
clearFailedTranslation(project.repoRoot, taskSlug);
|
|
611
|
-
const
|
|
612
|
-
|
|
620
|
+
const latestRoleReplies = Object.fromEntries(Object.entries(settings.latestRoleReplies)
|
|
621
|
+
.filter(([, reply]) => reply.repoRoot !== project.repoRoot || reply.taskSlug !== taskSlug));
|
|
613
622
|
await deps.settings.saveSettings({
|
|
614
623
|
...settings,
|
|
615
624
|
currentTaskSlug: null,
|
|
616
|
-
|
|
625
|
+
targetRole: "project-manager",
|
|
626
|
+
latestRoleReplies,
|
|
617
627
|
pendingConfirmations: {
|
|
618
628
|
...settings.pendingConfirmations,
|
|
619
629
|
closeTask: null
|
|
@@ -673,19 +683,20 @@ export function createGatewayService(deps) {
|
|
|
673
683
|
await ensurePolling();
|
|
674
684
|
const lines = [
|
|
675
685
|
"Gateway started.",
|
|
676
|
-
"Full mobile commands and
|
|
686
|
+
"Full mobile commands and role messages are now enabled.",
|
|
677
687
|
`Project: ${enabled.currentProjectId ?? "none"}`,
|
|
678
|
-
`Task: ${enabled.currentTaskSlug ?? "none"}
|
|
688
|
+
`Task: ${enabled.currentTaskSlug ?? "none"}`,
|
|
689
|
+
`Role: ${roleLabel(enabled.targetRole)}`
|
|
679
690
|
];
|
|
680
|
-
const latestReply =
|
|
691
|
+
const latestReply = getLatestRoleReply(enabled);
|
|
681
692
|
if (latestReply) {
|
|
682
|
-
const rendered = await
|
|
683
|
-
lines.push("",
|
|
693
|
+
const rendered = await renderLatestRoleReply(enabled, latestReply);
|
|
694
|
+
lines.push("", `Latest ${roleLabel(enabled.targetRole)} reply:`, rendered.text);
|
|
684
695
|
}
|
|
685
696
|
return lines.join("\n");
|
|
686
697
|
}
|
|
687
|
-
async function
|
|
688
|
-
const target = await
|
|
698
|
+
async function sendPlainTextToRole(text) {
|
|
699
|
+
const target = await resolvePlainTextRoleTarget(text);
|
|
689
700
|
if (typeof target === "string") {
|
|
690
701
|
return target;
|
|
691
702
|
}
|
|
@@ -694,16 +705,16 @@ export function createGatewayService(deps) {
|
|
|
694
705
|
repoRoot: target.project.repoRoot,
|
|
695
706
|
taskRepoRoot: getTaskRuntimeRepoRoot(target.task),
|
|
696
707
|
taskSlug: target.task.taskSlug,
|
|
697
|
-
role:
|
|
708
|
+
role: target.role,
|
|
698
709
|
text,
|
|
699
710
|
useContext: false,
|
|
700
711
|
send: false
|
|
701
712
|
})).englishPreview
|
|
702
713
|
: text;
|
|
703
714
|
await submitTerminalInput(deps.runtime, target.session.id, englishText);
|
|
704
|
-
return
|
|
715
|
+
return `Sent to ${roleLabel(target.role)}.`;
|
|
705
716
|
}
|
|
706
|
-
async function
|
|
717
|
+
async function resolvePlainTextRoleTarget(text) {
|
|
707
718
|
if (!text.trim()) {
|
|
708
719
|
return "Empty message ignored.";
|
|
709
720
|
}
|
|
@@ -717,18 +728,51 @@ export function createGatewayService(deps) {
|
|
|
717
728
|
});
|
|
718
729
|
}
|
|
719
730
|
const task = await deps.taskService.loadTask(project.repoRoot, settings.currentTaskSlug);
|
|
720
|
-
const
|
|
731
|
+
const role = settings.targetRole;
|
|
732
|
+
const session = await deps.sessionService.getRoleSession(project.repoRoot, task.taskSlug, role);
|
|
721
733
|
if (!session || session.status !== "running") {
|
|
722
734
|
throw new VcmError({
|
|
723
|
-
code: "
|
|
724
|
-
message:
|
|
735
|
+
code: "GATEWAY_TARGET_SESSION_NOT_RUNNING",
|
|
736
|
+
message: `The current task's ${roleLabel(role)} session is not running.`,
|
|
725
737
|
statusCode: 409
|
|
726
738
|
});
|
|
727
739
|
}
|
|
728
740
|
if (session.activityStatus === "running") {
|
|
729
|
-
return
|
|
741
|
+
return `${roleLabel(role)} is still working on the current turn. Please wait and send again later.`;
|
|
730
742
|
}
|
|
731
|
-
return { project, task, session, settings };
|
|
743
|
+
return { project, task, session, role, settings };
|
|
744
|
+
}
|
|
745
|
+
async function setGatewayTargetRole(role) {
|
|
746
|
+
const project = await ensureProject();
|
|
747
|
+
const settings = await syncDesktopContext(await deps.settings.loadSettings());
|
|
748
|
+
if (!settings.currentTaskSlug) {
|
|
749
|
+
throw new VcmError({
|
|
750
|
+
code: "TASK_NOT_SELECTED",
|
|
751
|
+
message: "No task is selected. Use /tasks and /use-task first.",
|
|
752
|
+
statusCode: 409
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
await deps.taskService.loadTask(project.repoRoot, settings.currentTaskSlug);
|
|
756
|
+
const updated = await deps.settings.updateSettings({ targetRole: role });
|
|
757
|
+
return `Gateway role: ${roleLabel(updated.targetRole)}.`;
|
|
758
|
+
}
|
|
759
|
+
async function gatewayRoleText(settings) {
|
|
760
|
+
const synced = await syncDesktopContext(settings);
|
|
761
|
+
const lines = [`Current Gateway role: ${roleLabel(synced.targetRole)}.`, "Available roles:"];
|
|
762
|
+
for (const role of VCM_ROLE_NAMES) {
|
|
763
|
+
let status = "not started";
|
|
764
|
+
if (synced.currentProjectId && synced.currentTaskSlug) {
|
|
765
|
+
try {
|
|
766
|
+
const session = await deps.sessionService.getRoleSession(synced.currentProjectId, synced.currentTaskSlug, role);
|
|
767
|
+
status = session?.status ?? status;
|
|
768
|
+
}
|
|
769
|
+
catch {
|
|
770
|
+
status = "missing";
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
lines.push(`- ${gatewayRoleCommandName(role)} [${status}]`);
|
|
774
|
+
}
|
|
775
|
+
return lines.join("\n");
|
|
732
776
|
}
|
|
733
777
|
async function reply(settings, userId, text) {
|
|
734
778
|
await sendGatewayText(settings, userId, text);
|
|
@@ -766,9 +810,20 @@ export function createGatewayService(deps) {
|
|
|
766
810
|
}
|
|
767
811
|
async function exposeStatus(settings) {
|
|
768
812
|
const preferences = await deps.appSettings.getPreferences();
|
|
813
|
+
let targetRoleSessionStatus = null;
|
|
814
|
+
if (settings.currentProjectId && settings.currentTaskSlug) {
|
|
815
|
+
try {
|
|
816
|
+
const session = await deps.sessionService.getRoleSession(settings.currentProjectId, settings.currentTaskSlug, settings.targetRole);
|
|
817
|
+
targetRoleSessionStatus = session?.status ?? "not_started";
|
|
818
|
+
}
|
|
819
|
+
catch {
|
|
820
|
+
targetRoleSessionStatus = "missing";
|
|
821
|
+
}
|
|
822
|
+
}
|
|
769
823
|
return {
|
|
770
824
|
...deps.settings.expose(settings, isRunning(), connectionEnabled),
|
|
771
|
-
|
|
825
|
+
targetRoleSessionStatus,
|
|
826
|
+
lastGatewayInputMessageId,
|
|
772
827
|
pauseAlertSoundEnabled: preferences.flowPauseAlerts
|
|
773
828
|
};
|
|
774
829
|
}
|
|
@@ -784,6 +839,7 @@ export function createGatewayService(deps) {
|
|
|
784
839
|
`Translation: ${synced.translationEnabled ? "on" : "off"}`,
|
|
785
840
|
`Project: ${project?.repoRoot ?? synced.currentProjectId ?? "none"}`,
|
|
786
841
|
`Task: ${synced.currentTaskSlug ?? "none"}`,
|
|
842
|
+
`Role: ${roleLabel(synced.targetRole)}`,
|
|
787
843
|
`Last poll: ${synced.lastPollStatus.state}${synced.lastPollStatus.error ? ` (${synced.lastPollStatus.error})` : ""}`
|
|
788
844
|
].join("\n");
|
|
789
845
|
}
|
|
@@ -803,7 +859,10 @@ export function createGatewayService(deps) {
|
|
|
803
859
|
return exposeStatus(settings);
|
|
804
860
|
},
|
|
805
861
|
async updateSettings(input) {
|
|
806
|
-
|
|
862
|
+
let currentSettings = await deps.settings.loadSettings();
|
|
863
|
+
if (input.enabled === true || currentSettings.enabled) {
|
|
864
|
+
currentSettings = await syncDesktopContext(currentSettings);
|
|
865
|
+
}
|
|
807
866
|
const enablingGateway = input.enabled === true && !currentSettings.enabled;
|
|
808
867
|
const gatewayStartInput = input.enabled === true
|
|
809
868
|
? { ...input, translationEnabled: true }
|
|
@@ -818,9 +877,6 @@ export function createGatewayService(deps) {
|
|
|
818
877
|
await enableGatewayTranslationRuntime({ disablePauseAlertSound: enablingGateway });
|
|
819
878
|
}
|
|
820
879
|
let settings = await deps.settings.updateSettings(updateInput);
|
|
821
|
-
if (settings.enabled) {
|
|
822
|
-
settings = await syncDesktopContext(settings);
|
|
823
|
-
}
|
|
824
880
|
if (toAccount(settings)) {
|
|
825
881
|
await ensurePolling();
|
|
826
882
|
}
|
|
@@ -1095,8 +1151,15 @@ export function createGatewayService(deps) {
|
|
|
1095
1151
|
gatewayStatus: status
|
|
1096
1152
|
};
|
|
1097
1153
|
},
|
|
1098
|
-
async
|
|
1099
|
-
if (input.session.role
|
|
1154
|
+
async handleRoleStop(input) {
|
|
1155
|
+
if (!isVcmRoleName(input.session.role)) {
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
const role = input.session.role;
|
|
1159
|
+
const settings = await syncDesktopContext(await deps.settings.loadSettings());
|
|
1160
|
+
if (settings.currentProjectId !== input.repoRoot
|
|
1161
|
+
|| settings.currentTaskSlug !== input.taskSlug
|
|
1162
|
+
|| settings.targetRole !== role) {
|
|
1100
1163
|
return;
|
|
1101
1164
|
}
|
|
1102
1165
|
const transcriptPath = resolveExistingClaudeTranscriptPath(input.session);
|
|
@@ -1104,46 +1167,52 @@ export function createGatewayService(deps) {
|
|
|
1104
1167
|
return;
|
|
1105
1168
|
}
|
|
1106
1169
|
const events = await readTranscriptTextEvents(transcriptPath);
|
|
1107
|
-
const round = await waitForGatewayRoundFinal(input.repoRoot, input.taskSlug);
|
|
1170
|
+
const round = await waitForGatewayRoundFinal(input.repoRoot, input.taskSlug, role);
|
|
1108
1171
|
if (!round) {
|
|
1109
1172
|
return;
|
|
1110
1173
|
}
|
|
1111
|
-
const
|
|
1112
|
-
|
|
1113
|
-
|
|
1174
|
+
const latestSettings = await syncDesktopContext(await deps.settings.loadSettings());
|
|
1175
|
+
if (latestSettings.currentProjectId !== input.repoRoot
|
|
1176
|
+
|| latestSettings.currentTaskSlug !== input.taskSlug
|
|
1177
|
+
|| latestSettings.targetRole !== role) {
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
const cursorKey = `${input.taskSlug}:${role}:${input.session.claudeSessionId}`;
|
|
1181
|
+
const cursor = latestSettings.pushCursors[cursorKey];
|
|
1114
1182
|
const nextEvents = selectEventsAfterCursor(events, cursor?.lastTranscriptEventId)
|
|
1115
1183
|
.filter(isFinalTurnTextEvent);
|
|
1116
1184
|
if (nextEvents.length === 0) {
|
|
1117
1185
|
return;
|
|
1118
1186
|
}
|
|
1119
|
-
const latestReply =
|
|
1187
|
+
const latestReply = toGatewayRoleReply(nextEvents[nextEvents.length - 1]);
|
|
1120
1188
|
const text = latestReply.text.trim();
|
|
1121
1189
|
if (!text) {
|
|
1122
1190
|
return;
|
|
1123
1191
|
}
|
|
1124
|
-
await
|
|
1125
|
-
const account = toAccount(
|
|
1126
|
-
const boundUserId =
|
|
1192
|
+
await saveLatestRoleReply(input, latestReply);
|
|
1193
|
+
const account = toAccount(latestSettings);
|
|
1194
|
+
const boundUserId = latestSettings.binding.boundUserId;
|
|
1127
1195
|
// A disarmed gateway never touches the channel: skip the outbound push.
|
|
1128
|
-
// The round-final
|
|
1129
|
-
if (!connectionEnabled || !
|
|
1196
|
+
// The selected role's round-final reply was already cached above and replays on the next /start.
|
|
1197
|
+
if (!connectionEnabled || !latestSettings.enabled || !account || !boundUserId) {
|
|
1130
1198
|
return;
|
|
1131
1199
|
}
|
|
1132
1200
|
const roundNotice = formatGatewayRoundNotice(round);
|
|
1133
|
-
const originalMessage =
|
|
1134
|
-
await sendGatewayText(
|
|
1201
|
+
const originalMessage = formatGatewayRoleOriginalReply(role, text, roundNotice);
|
|
1202
|
+
await sendGatewayText(latestSettings, boundUserId, originalMessage);
|
|
1135
1203
|
let output;
|
|
1136
|
-
if (
|
|
1137
|
-
output = await
|
|
1138
|
-
settings,
|
|
1204
|
+
if (latestSettings.translationEnabled) {
|
|
1205
|
+
output = await renderGatewayRoleOutput({
|
|
1206
|
+
settings: latestSettings,
|
|
1139
1207
|
repoRoot: input.repoRoot,
|
|
1140
1208
|
taskSlug: input.taskSlug,
|
|
1209
|
+
role,
|
|
1141
1210
|
sourceText: text,
|
|
1142
1211
|
sourceEntryIds: latestReply.transcriptEventId ? [latestReply.transcriptEventId] : undefined
|
|
1143
1212
|
});
|
|
1144
|
-
await sendGatewayText(
|
|
1145
|
-
?
|
|
1146
|
-
:
|
|
1213
|
+
await sendGatewayText(latestSettings, boundUserId, output.translationFailed
|
|
1214
|
+
? formatGatewayRoleTranslationFailure(role, output.translationError)
|
|
1215
|
+
: formatGatewayRoleTranslatedReply(role, output.text));
|
|
1147
1216
|
}
|
|
1148
1217
|
else {
|
|
1149
1218
|
clearFailedTranslation(input.repoRoot, input.taskSlug);
|
|
@@ -1162,16 +1231,16 @@ export function createGatewayService(deps) {
|
|
|
1162
1231
|
checkedAt: now(),
|
|
1163
1232
|
direction: "outbound",
|
|
1164
1233
|
result: output?.translationFailed ? "error" : "ok",
|
|
1165
|
-
command:
|
|
1234
|
+
command: `${role}-stop`,
|
|
1166
1235
|
preview: (output?.text ?? originalMessage).slice(0, 160),
|
|
1167
1236
|
error: output?.translationError
|
|
1168
1237
|
},
|
|
1169
1238
|
updatedAt: now()
|
|
1170
1239
|
});
|
|
1171
1240
|
await deps.audit.record({
|
|
1172
|
-
type: "gateway.
|
|
1241
|
+
type: "gateway.role_push",
|
|
1173
1242
|
result: output?.translationFailed ? "error" : "ok",
|
|
1174
|
-
command:
|
|
1243
|
+
command: `${role}-stop`,
|
|
1175
1244
|
preview: output ? `${originalMessage}\n\n${output.text}` : originalMessage,
|
|
1176
1245
|
error: output?.translationError
|
|
1177
1246
|
});
|
|
@@ -1216,17 +1285,17 @@ export function createGatewayService(deps) {
|
|
|
1216
1285
|
};
|
|
1217
1286
|
}
|
|
1218
1287
|
};
|
|
1219
|
-
function
|
|
1288
|
+
function getLatestRoleReply(settings) {
|
|
1220
1289
|
if (!settings.currentProjectId || !settings.currentTaskSlug) {
|
|
1221
1290
|
return undefined;
|
|
1222
1291
|
}
|
|
1223
|
-
return settings.
|
|
1292
|
+
return settings.latestRoleReplies[latestRoleReplyKey(settings.currentProjectId, settings.currentTaskSlug, settings.targetRole)];
|
|
1224
1293
|
}
|
|
1225
|
-
async function waitForGatewayRoundFinal(repoRoot, taskSlug) {
|
|
1294
|
+
async function waitForGatewayRoundFinal(repoRoot, taskSlug, role) {
|
|
1226
1295
|
const startedAt = Date.now();
|
|
1227
1296
|
while (Date.now() - startedAt <= GATEWAY_ROUND_FINAL_WAIT_MS) {
|
|
1228
1297
|
const round = await getGatewayRoundState(repoRoot, taskSlug);
|
|
1229
|
-
if (round && isGatewayRoundFinal(round)) {
|
|
1298
|
+
if (round && isGatewayRoundFinal(round) && round.activeRole === role) {
|
|
1230
1299
|
return round;
|
|
1231
1300
|
}
|
|
1232
1301
|
if (round && isGatewayNonFinalTerminalRound(round)) {
|
|
@@ -1295,47 +1364,48 @@ export function createGatewayService(deps) {
|
|
|
1295
1364
|
const trimmed = value.trim();
|
|
1296
1365
|
return trimmed.length > 600 ? `${trimmed.slice(0, 600)}...` : trimmed;
|
|
1297
1366
|
}
|
|
1298
|
-
function
|
|
1367
|
+
function formatGatewayRoleOriginalReply(role, text, roundNotice) {
|
|
1299
1368
|
return [
|
|
1300
|
-
|
|
1369
|
+
`${roleLabel(role)} Round Final Reply 原文:`,
|
|
1301
1370
|
"",
|
|
1302
1371
|
text.trim(),
|
|
1303
1372
|
roundNotice ? "" : undefined,
|
|
1304
1373
|
roundNotice ? `Round: ${roundNotice}` : undefined
|
|
1305
1374
|
].filter((line) => line !== undefined).join("\n");
|
|
1306
1375
|
}
|
|
1307
|
-
function
|
|
1376
|
+
function formatGatewayRoleTranslatedReply(role, text) {
|
|
1308
1377
|
return [
|
|
1309
|
-
|
|
1378
|
+
`${roleLabel(role)} Round Final Reply 翻译:`,
|
|
1310
1379
|
"",
|
|
1311
1380
|
text.trim()
|
|
1312
1381
|
].join("\n");
|
|
1313
1382
|
}
|
|
1314
|
-
function
|
|
1383
|
+
function formatGatewayRoleTranslationFailure(role, error) {
|
|
1315
1384
|
return [
|
|
1316
|
-
GATEWAY_TRANSLATION_FAILURE_TEXT
|
|
1385
|
+
`${roleLabel(role)} ${GATEWAY_TRANSLATION_FAILURE_TEXT}`,
|
|
1317
1386
|
error ? `原因:${error}` : undefined
|
|
1318
1387
|
].filter((line) => line !== undefined).join("\n");
|
|
1319
1388
|
}
|
|
1320
|
-
function formatGatewayInputTranslationSuccess(englishText) {
|
|
1389
|
+
function formatGatewayInputTranslationSuccess(role, englishText) {
|
|
1321
1390
|
return [
|
|
1322
|
-
|
|
1391
|
+
`翻译完成,已发送给 ${roleLabel(role)}:`,
|
|
1323
1392
|
"",
|
|
1324
1393
|
englishText.trim()
|
|
1325
1394
|
].join("\n");
|
|
1326
1395
|
}
|
|
1327
|
-
function formatGatewayInputTranslationFailure(error) {
|
|
1396
|
+
function formatGatewayInputTranslationFailure(role, error) {
|
|
1328
1397
|
return [
|
|
1329
|
-
|
|
1398
|
+
`翻译失败,消息未发送给 ${roleLabel(role)}。`,
|
|
1330
1399
|
`原因:${error}`,
|
|
1331
1400
|
"请重新输入后再试。"
|
|
1332
1401
|
].join("\n");
|
|
1333
1402
|
}
|
|
1334
|
-
async function
|
|
1335
|
-
const rendered = await
|
|
1403
|
+
async function renderLatestRoleReply(settings, reply) {
|
|
1404
|
+
const rendered = await renderGatewayRoleOutput({
|
|
1336
1405
|
settings,
|
|
1337
1406
|
repoRoot: reply.repoRoot,
|
|
1338
1407
|
taskSlug: reply.taskSlug,
|
|
1408
|
+
role: reply.role,
|
|
1339
1409
|
sourceText: reply.text,
|
|
1340
1410
|
sourceEntryIds: reply.transcriptEventId ? [reply.transcriptEventId] : undefined
|
|
1341
1411
|
});
|
|
@@ -1344,7 +1414,7 @@ export function createGatewayService(deps) {
|
|
|
1344
1414
|
text: reply.truncated && !rendered.translationFailed ? `${rendered.text}\n\n[truncated]` : rendered.text
|
|
1345
1415
|
};
|
|
1346
1416
|
}
|
|
1347
|
-
async function
|
|
1417
|
+
async function renderGatewayRoleOutput(input) {
|
|
1348
1418
|
if (!input.settings.translationEnabled) {
|
|
1349
1419
|
clearFailedTranslation(input.repoRoot, input.taskSlug);
|
|
1350
1420
|
return {
|
|
@@ -1356,7 +1426,7 @@ export function createGatewayService(deps) {
|
|
|
1356
1426
|
const text = await deps.translationService.translateGatewayOutput({
|
|
1357
1427
|
repoRoot: input.repoRoot,
|
|
1358
1428
|
taskSlug: input.taskSlug,
|
|
1359
|
-
role:
|
|
1429
|
+
role: input.role,
|
|
1360
1430
|
text: input.sourceText,
|
|
1361
1431
|
sourceEntryIds: input.sourceEntryIds
|
|
1362
1432
|
});
|
|
@@ -1371,7 +1441,7 @@ export function createGatewayService(deps) {
|
|
|
1371
1441
|
lastFailedTranslation = {
|
|
1372
1442
|
repoRoot: input.repoRoot,
|
|
1373
1443
|
taskSlug: input.taskSlug,
|
|
1374
|
-
role:
|
|
1444
|
+
role: input.role,
|
|
1375
1445
|
sourceText: input.sourceText,
|
|
1376
1446
|
failedAt: now(),
|
|
1377
1447
|
error: message,
|
|
@@ -1415,7 +1485,7 @@ export function createGatewayService(deps) {
|
|
|
1415
1485
|
lastFailedTranslation = null;
|
|
1416
1486
|
}
|
|
1417
1487
|
}
|
|
1418
|
-
function
|
|
1488
|
+
function toGatewayRoleReply(event) {
|
|
1419
1489
|
return {
|
|
1420
1490
|
text: event.text,
|
|
1421
1491
|
truncated: false,
|
|
@@ -1423,16 +1493,20 @@ export function createGatewayService(deps) {
|
|
|
1423
1493
|
transcriptTimestamp: event.timestamp
|
|
1424
1494
|
};
|
|
1425
1495
|
}
|
|
1426
|
-
async function
|
|
1496
|
+
async function saveLatestRoleReply(input, reply) {
|
|
1497
|
+
if (!isVcmRoleName(input.session.role)) {
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1427
1500
|
const settings = await deps.settings.loadSettings();
|
|
1428
|
-
const key =
|
|
1501
|
+
const key = latestRoleReplyKey(input.repoRoot, input.taskSlug, input.session.role);
|
|
1429
1502
|
await deps.settings.saveSettings({
|
|
1430
1503
|
...settings,
|
|
1431
|
-
|
|
1432
|
-
...settings.
|
|
1504
|
+
latestRoleReplies: {
|
|
1505
|
+
...settings.latestRoleReplies,
|
|
1433
1506
|
[key]: {
|
|
1434
1507
|
repoRoot: input.repoRoot,
|
|
1435
1508
|
taskSlug: input.taskSlug,
|
|
1509
|
+
role: input.session.role,
|
|
1436
1510
|
sessionId: input.session.id,
|
|
1437
1511
|
claudeSessionId: input.session.claudeSessionId,
|
|
1438
1512
|
transcriptEventId: reply.transcriptEventId,
|
|
@@ -1459,8 +1533,8 @@ function selectEventsAfterCursor(events, cursorId) {
|
|
|
1459
1533
|
}
|
|
1460
1534
|
return events.slice(index + 1);
|
|
1461
1535
|
}
|
|
1462
|
-
function
|
|
1463
|
-
return JSON.stringify([repoRoot, taskSlug]);
|
|
1536
|
+
function latestRoleReplyKey(repoRoot, taskSlug, role) {
|
|
1537
|
+
return JSON.stringify([repoRoot, taskSlug, role]);
|
|
1464
1538
|
}
|
|
1465
1539
|
function projectsText(projects) {
|
|
1466
1540
|
if (projects.length === 0) {
|
|
@@ -1478,7 +1552,7 @@ function helpText(enabled) {
|
|
|
1478
1552
|
"/status",
|
|
1479
1553
|
"/projects",
|
|
1480
1554
|
"/tasks",
|
|
1481
|
-
"Send /start to enable
|
|
1555
|
+
"Send /start to enable role messages and task-changing commands."
|
|
1482
1556
|
].join("\n");
|
|
1483
1557
|
}
|
|
1484
1558
|
return [
|
|
@@ -1487,6 +1561,8 @@ function helpText(enabled) {
|
|
|
1487
1561
|
"/start",
|
|
1488
1562
|
"/retry",
|
|
1489
1563
|
"/status",
|
|
1564
|
+
"/role",
|
|
1565
|
+
"/role <pm|architect|coder|tester|reviewer>",
|
|
1490
1566
|
"/projects",
|
|
1491
1567
|
"/use-project <index-or-path>",
|
|
1492
1568
|
"/pull-current",
|
|
@@ -1499,6 +1575,12 @@ function helpText(enabled) {
|
|
|
1499
1575
|
"/translate off"
|
|
1500
1576
|
].join("\n");
|
|
1501
1577
|
}
|
|
1578
|
+
function roleLabel(role) {
|
|
1579
|
+
return role === "project-manager" ? "PM" : getRoleDefinition(role).label;
|
|
1580
|
+
}
|
|
1581
|
+
function gatewayRoleCommandName(role) {
|
|
1582
|
+
return role === "project-manager" ? "pm" : role;
|
|
1583
|
+
}
|
|
1502
1584
|
function formatAheadBehind(project) {
|
|
1503
1585
|
if (!project.upstreamBranch) {
|
|
1504
1586
|
return "no upstream";
|