vibe-coding-master 0.6.1 → 0.6.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.
@@ -21,6 +21,8 @@ const COMMANDS_ALLOWED_WHEN_DISABLED = new Set([
21
21
  "projects",
22
22
  "tasks"
23
23
  ]);
24
+ class HandledGatewayInboundError extends Error {
25
+ }
24
26
  export function createGatewayService(deps) {
25
27
  const now = deps.now ?? (() => new Date().toISOString());
26
28
  const larkRegistration = deps.larkRegistration ?? createLarkRegistrationClient();
@@ -219,6 +221,10 @@ export function createGatewayService(deps) {
219
221
  return;
220
222
  }
221
223
  const command = parseGatewayCommand(update.text);
224
+ if (command.kind === "plain" && settings.enabled) {
225
+ await handlePlainInbound(update, command.text);
226
+ return;
227
+ }
222
228
  try {
223
229
  const output = await executeCommand(command, settings);
224
230
  await reply(await deps.settings.loadSettings(), update.fromUserId, output);
@@ -247,6 +253,69 @@ export function createGatewayService(deps) {
247
253
  });
248
254
  }
249
255
  }
256
+ async function handlePlainInbound(update, text) {
257
+ try {
258
+ const target = await resolvePlainTextPmTarget(text);
259
+ if (typeof target === "string") {
260
+ await reply(await deps.settings.loadSettings(), update.fromUserId, target);
261
+ await recordInbound(update, "ok", "plain");
262
+ return;
263
+ }
264
+ if (target.settings.translationEnabled) {
265
+ await reply(await deps.settings.loadSettings(), update.fromUserId, "已收到,正在翻译...");
266
+ }
267
+ else {
268
+ await reply(await deps.settings.loadSettings(), update.fromUserId, "已收到,正在发送给 PM...");
269
+ }
270
+ const englishText = target.settings.translationEnabled
271
+ ? await translatePlainTextForPm(target, text, update)
272
+ : text;
273
+ await submitTerminalInput(deps.runtime, target.session.id, englishText);
274
+ await reply(await deps.settings.loadSettings(), update.fromUserId, target.settings.translationEnabled
275
+ ? formatGatewayInputTranslationSuccess(englishText)
276
+ : "已发送给 PM。");
277
+ await recordInbound(update, "ok", "plain");
278
+ }
279
+ catch (error) {
280
+ if (error instanceof HandledGatewayInboundError) {
281
+ return;
282
+ }
283
+ const message = errorMessage(error);
284
+ await reply(await deps.settings.loadSettings(), update.fromUserId, `Error: ${message}`);
285
+ await recordInbound(update, "error", "plain", message);
286
+ }
287
+ }
288
+ async function translatePlainTextForPm(target, text, update) {
289
+ try {
290
+ return (await deps.translationService.translateUserInput({
291
+ repoRoot: target.project.repoRoot,
292
+ taskRepoRoot: getTaskRuntimeRepoRoot(target.task),
293
+ taskSlug: target.task.taskSlug,
294
+ role: "project-manager",
295
+ text,
296
+ useContext: false,
297
+ send: false
298
+ })).englishPreview;
299
+ }
300
+ catch (error) {
301
+ const message = errorMessage(error);
302
+ await reply(await deps.settings.loadSettings(), update.fromUserId, formatGatewayInputTranslationFailure(message));
303
+ await recordInbound(update, "error", "plain", message);
304
+ throw new HandledGatewayInboundError();
305
+ }
306
+ }
307
+ async function recordInbound(update, result, command, error) {
308
+ await recordMessageStatus("inbound", result, update.text, error, command);
309
+ await deps.audit.record({
310
+ type: "gateway.command",
311
+ result,
312
+ messageId: update.messageId,
313
+ userId: update.fromUserId,
314
+ command,
315
+ preview: update.text,
316
+ error
317
+ });
318
+ }
250
319
  async function saveInboundMetadata(settings, update, options = {}) {
251
320
  const bindMode = options.bind ?? "never";
252
321
  return deps.settings.saveSettings({
@@ -633,6 +702,25 @@ export function createGatewayService(deps) {
633
702
  return lines.join("\n");
634
703
  }
635
704
  async function sendPlainTextToPm(text) {
705
+ const target = await resolvePlainTextPmTarget(text);
706
+ if (typeof target === "string") {
707
+ return target;
708
+ }
709
+ const englishText = target.settings.translationEnabled
710
+ ? (await deps.translationService.translateUserInput({
711
+ repoRoot: target.project.repoRoot,
712
+ taskRepoRoot: getTaskRuntimeRepoRoot(target.task),
713
+ taskSlug: target.task.taskSlug,
714
+ role: "project-manager",
715
+ text,
716
+ useContext: false,
717
+ send: false
718
+ })).englishPreview
719
+ : text;
720
+ await submitTerminalInput(deps.runtime, target.session.id, englishText);
721
+ return "Sent to PM.";
722
+ }
723
+ async function resolvePlainTextPmTarget(text) {
636
724
  if (!text.trim()) {
637
725
  return "Empty message ignored.";
638
726
  }
@@ -657,21 +745,13 @@ export function createGatewayService(deps) {
657
745
  if (session.activityStatus === "running") {
658
746
  return "PM is still working on the current turn. Please wait and send again later.";
659
747
  }
660
- const englishText = settings.translationEnabled
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.";
748
+ return { project, task, session, settings };
673
749
  }
674
750
  async function reply(settings, userId, text) {
751
+ await sendGatewayText(settings, userId, text);
752
+ await recordMessageStatus("outbound", "ok", text);
753
+ }
754
+ async function sendGatewayText(settings, userId, text) {
675
755
  const account = toAccount(settings);
676
756
  if (!account) {
677
757
  return;
@@ -685,7 +765,6 @@ export function createGatewayService(deps) {
685
765
  contextToken,
686
766
  text
687
767
  });
688
- await recordMessageStatus("outbound", "ok", text);
689
768
  }
690
769
  async function recordMessageStatus(direction, result, preview, error, command) {
691
770
  const settings = await deps.settings.loadSettings();
@@ -1055,19 +1134,25 @@ export function createGatewayService(deps) {
1055
1134
  if (!text) {
1056
1135
  return;
1057
1136
  }
1058
- const output = await renderGatewayPmOutput({
1059
- settings,
1060
- repoRoot: input.repoRoot,
1061
- taskSlug: input.taskSlug,
1062
- sourceText: text
1063
- });
1064
- await resolveChannel(settings).sendText({
1065
- account,
1066
- toUserId: boundUserId,
1067
- chatId: settings.binding.chatIds[boundUserId] ?? settings.binding.homeChatId ?? undefined,
1068
- contextToken: settings.binding.contextTokens[boundUserId],
1069
- text: output.text
1070
- });
1137
+ const roundNotice = await getGatewayRoundNotice(input.repoRoot, input.taskSlug);
1138
+ const originalMessage = formatGatewayPmOriginalReply(text, roundNotice);
1139
+ await sendGatewayText(settings, boundUserId, originalMessage);
1140
+ let output;
1141
+ if (settings.translationEnabled) {
1142
+ output = await renderGatewayPmOutput({
1143
+ settings,
1144
+ repoRoot: input.repoRoot,
1145
+ taskSlug: input.taskSlug,
1146
+ sourceText: text,
1147
+ sourceEntryIds: nextEvents.map((event) => event.id)
1148
+ });
1149
+ await sendGatewayText(settings, boundUserId, output.translationFailed
1150
+ ? formatGatewayPmTranslationFailure(output.translationError)
1151
+ : formatGatewayPmTranslatedReply(output.text));
1152
+ }
1153
+ else {
1154
+ clearFailedTranslation(input.repoRoot, input.taskSlug);
1155
+ }
1071
1156
  const lastEvent = nextEvents.at(-1);
1072
1157
  const current = await deps.settings.loadSettings();
1073
1158
  await deps.settings.saveSettings({
@@ -1082,19 +1167,19 @@ export function createGatewayService(deps) {
1082
1167
  lastMessageStatus: {
1083
1168
  checkedAt: now(),
1084
1169
  direction: "outbound",
1085
- result: output.translationFailed ? "error" : "ok",
1170
+ result: output?.translationFailed ? "error" : "ok",
1086
1171
  command: "pm-stop",
1087
- preview: output.text.slice(0, 160),
1088
- error: output.translationError
1172
+ preview: (output?.text ?? originalMessage).slice(0, 160),
1173
+ error: output?.translationError
1089
1174
  },
1090
1175
  updatedAt: now()
1091
1176
  });
1092
1177
  await deps.audit.record({
1093
1178
  type: "gateway.pm_push",
1094
- result: output.translationFailed ? "error" : "ok",
1179
+ result: output?.translationFailed ? "error" : "ok",
1095
1180
  command: "pm-stop",
1096
- preview: output.text,
1097
- error: output.translationError
1181
+ preview: output ? `${originalMessage}\n\n${output.text}` : originalMessage,
1182
+ error: output?.translationError
1098
1183
  });
1099
1184
  },
1100
1185
  getDiagnostics() {
@@ -1109,12 +1194,75 @@ export function createGatewayService(deps) {
1109
1194
  }
1110
1195
  return settings.latestPmReplies[latestPmReplyKey(settings.currentProjectId, settings.currentTaskSlug)];
1111
1196
  }
1197
+ async function getGatewayRoundNotice(repoRoot, taskSlug) {
1198
+ try {
1199
+ const [projectConfig, task] = await Promise.all([
1200
+ deps.projectService.loadConfig(repoRoot),
1201
+ deps.taskService.loadTask(repoRoot, taskSlug)
1202
+ ]);
1203
+ const round = await deps.roundService.getSessionRoundState({
1204
+ repoRoot,
1205
+ stateRepoRoot: getTaskRuntimeRepoRoot(task),
1206
+ stateRoot: projectConfig.stateRoot,
1207
+ taskSlug
1208
+ });
1209
+ return formatGatewayRoundNotice(round);
1210
+ }
1211
+ catch {
1212
+ return undefined;
1213
+ }
1214
+ }
1215
+ function formatGatewayRoundNotice(round) {
1216
+ const roundLabel = round.roundSequence ? `第 ${round.roundSequence} 轮` : "当前 round";
1217
+ if (round.status === "stopped") {
1218
+ return `${roundLabel}已结束。现在需要你给出下一步指令。`;
1219
+ }
1220
+ const activeRole = round.activeRole ? `,当前角色:${round.activeRole}` : "";
1221
+ return `${roundLabel}运行中${activeRole}。`;
1222
+ }
1223
+ function formatGatewayPmOriginalReply(text, roundNotice) {
1224
+ return [
1225
+ "PM final reply 原文:",
1226
+ "",
1227
+ text.trim(),
1228
+ roundNotice ? "" : undefined,
1229
+ roundNotice ? `Round: ${roundNotice}` : undefined
1230
+ ].filter((line) => line !== undefined).join("\n");
1231
+ }
1232
+ function formatGatewayPmTranslatedReply(text) {
1233
+ return [
1234
+ "PM final reply 翻译:",
1235
+ "",
1236
+ text.trim()
1237
+ ].join("\n");
1238
+ }
1239
+ function formatGatewayPmTranslationFailure(error) {
1240
+ return [
1241
+ GATEWAY_TRANSLATION_FAILURE_TEXT,
1242
+ error ? `原因:${error}` : undefined
1243
+ ].filter((line) => line !== undefined).join("\n");
1244
+ }
1245
+ function formatGatewayInputTranslationSuccess(englishText) {
1246
+ return [
1247
+ "翻译完成,已发送给 PM:",
1248
+ "",
1249
+ englishText.trim()
1250
+ ].join("\n");
1251
+ }
1252
+ function formatGatewayInputTranslationFailure(error) {
1253
+ return [
1254
+ "翻译失败,消息未发送给 PM。",
1255
+ `原因:${error}`,
1256
+ "请重新输入后再试。"
1257
+ ].join("\n");
1258
+ }
1112
1259
  async function renderLatestPmReply(settings, reply) {
1113
1260
  const rendered = await renderGatewayPmOutput({
1114
1261
  settings,
1115
1262
  repoRoot: reply.repoRoot,
1116
1263
  taskSlug: reply.taskSlug,
1117
- sourceText: reply.text
1264
+ sourceText: reply.text,
1265
+ sourceEntryIds: reply.transcriptEventId ? [reply.transcriptEventId] : undefined
1118
1266
  });
1119
1267
  return {
1120
1268
  ...rendered,
@@ -1134,7 +1282,8 @@ export function createGatewayService(deps) {
1134
1282
  repoRoot: input.repoRoot,
1135
1283
  taskSlug: input.taskSlug,
1136
1284
  role: "project-manager",
1137
- text: input.sourceText
1285
+ text: input.sourceText,
1286
+ sourceEntryIds: input.sourceEntryIds
1138
1287
  });
1139
1288
  clearFailedTranslation(input.repoRoot, input.taskSlug);
1140
1289
  return {
@@ -13,6 +13,8 @@ const TRANSLATION_MODEL = "translator";
13
13
  const OUTPUT_TRANSLATION_BATCH_DELAY_MS = 10000;
14
14
  const TRANSCRIPT_REPLAY_GRACE_MS = 5000;
15
15
  const TRANSLATION_TASK_FEED_RETENTION_LIMIT = 2000;
16
+ const GATEWAY_TRANSLATION_REUSE_GRACE_MS = 1500;
17
+ const GATEWAY_TRANSLATION_REUSE_POLL_MS = 50;
16
18
  export function createTranslationService(deps) {
17
19
  const now = deps.now ?? (() => new Date().toISOString());
18
20
  const id = deps.id ?? (() => `tr_${Date.now()}_${Math.random().toString(16).slice(2)}`);
@@ -1004,6 +1006,10 @@ export function createTranslationService(deps) {
1004
1006
  },
1005
1007
  async translateGatewayOutput(input) {
1006
1008
  const config = await loadConfig();
1009
+ const reusable = await findReusableGatewayOutputTranslation(input, config);
1010
+ if (reusable) {
1011
+ return reusable.trim();
1012
+ }
1007
1013
  const translation = await translateText({
1008
1014
  repoRoot: input.repoRoot,
1009
1015
  taskSlug: input.taskSlug,
@@ -1033,6 +1039,148 @@ export function createTranslationService(deps) {
1033
1039
  };
1034
1040
  }
1035
1041
  };
1042
+ async function findReusableGatewayOutputTranslation(input, config) {
1043
+ const graceDeadline = Date.now() + (input.sourceEntryIds?.length ? GATEWAY_TRANSLATION_REUSE_GRACE_MS : 0);
1044
+ while (true) {
1045
+ const lookup = await lookupGatewayOutputTranslation(input);
1046
+ if (lookup.kind === "translated") {
1047
+ return lookup.text;
1048
+ }
1049
+ if (lookup.kind === "active") {
1050
+ return waitForReusableGatewayOutputTranslation(input, config);
1051
+ }
1052
+ if (!input.sourceEntryIds?.length || Date.now() >= graceDeadline) {
1053
+ return undefined;
1054
+ }
1055
+ await delay(GATEWAY_TRANSLATION_REUSE_POLL_MS);
1056
+ }
1057
+ }
1058
+ async function waitForReusableGatewayOutputTranslation(input, config) {
1059
+ const deadline = Date.now() + config.requestTimeoutMs;
1060
+ while (Date.now() <= deadline) {
1061
+ const lookup = await lookupGatewayOutputTranslation(input);
1062
+ if (lookup.kind === "translated") {
1063
+ return lookup.text;
1064
+ }
1065
+ if (lookup.kind !== "active") {
1066
+ return undefined;
1067
+ }
1068
+ await delay(GATEWAY_TRANSLATION_REUSE_POLL_MS);
1069
+ }
1070
+ throw new VcmError({
1071
+ code: "TRANSLATION_TIMEOUT",
1072
+ message: "Gateway output translation timed out while waiting for the existing PM reply translation.",
1073
+ statusCode: 504
1074
+ });
1075
+ }
1076
+ async function lookupGatewayOutputTranslation(input) {
1077
+ const states = await getGatewayOutputCandidateStates(input);
1078
+ return selectGatewayOutputTranslation(states, input);
1079
+ }
1080
+ async function getGatewayOutputCandidateStates(input) {
1081
+ const states = [];
1082
+ const seen = new Set();
1083
+ const add = (state) => {
1084
+ if (seen.has(state) || !isGatewayOutputCandidateState(state, input)) {
1085
+ return;
1086
+ }
1087
+ seen.add(state);
1088
+ states.push(state);
1089
+ };
1090
+ const roleSession = await getGatewayRoleSession(input);
1091
+ if (roleSession) {
1092
+ const state = await prepareCache({
1093
+ repoRoot: roleSession.cwd,
1094
+ baseRepoRoot: input.repoRoot,
1095
+ taskSlug: input.taskSlug,
1096
+ role: input.role,
1097
+ sessionId: roleSession.id
1098
+ });
1099
+ if (roleSession.status === "running") {
1100
+ startTranscriptTail(roleSession);
1101
+ }
1102
+ add(state);
1103
+ }
1104
+ for (const state of sessionStates.values()) {
1105
+ add(state);
1106
+ }
1107
+ return states;
1108
+ }
1109
+ async function getGatewayRoleSession(input) {
1110
+ try {
1111
+ return await deps.sessionService.getRoleSession(input.repoRoot, input.taskSlug, input.role);
1112
+ }
1113
+ catch {
1114
+ return undefined;
1115
+ }
1116
+ }
1117
+ function isGatewayOutputCandidateState(state, input) {
1118
+ if (state.taskSlug !== input.taskSlug || state.role !== input.role) {
1119
+ return false;
1120
+ }
1121
+ return state.baseRepoRoot === input.repoRoot
1122
+ || state.repoRoot === input.repoRoot
1123
+ || Boolean(state.repoRoot?.startsWith(`${input.repoRoot}${path.sep}`));
1124
+ }
1125
+ function selectGatewayOutputTranslation(states, input) {
1126
+ const sourceEntryIds = normalizeGatewaySourceEntryIds(input.sourceEntryIds);
1127
+ if (sourceEntryIds.length > 0) {
1128
+ const entries = sourceEntryIds
1129
+ .map((entryId) => findGatewayOutputEntryById(states, input, entryId));
1130
+ const foundEntries = entries.filter((entry) => Boolean(entry));
1131
+ if (foundEntries.length === sourceEntryIds.length) {
1132
+ if (foundEntries.some(isActiveTranslationEntry)) {
1133
+ return { kind: "active" };
1134
+ }
1135
+ if (foundEntries.every(isReusableGatewayOutputEntry)) {
1136
+ return {
1137
+ kind: "translated",
1138
+ text: foundEntries.map((entry) => entry.translatedText).join("\n\n")
1139
+ };
1140
+ }
1141
+ }
1142
+ else if (foundEntries.some(isActiveTranslationEntry)) {
1143
+ return { kind: "active" };
1144
+ }
1145
+ }
1146
+ const normalizedSourceText = normalizeGatewaySourceText(input.text);
1147
+ const sourceMatches = states
1148
+ .flatMap((state) => state.entries)
1149
+ .filter((entry) => isGatewayOutputEntry(entry, input)
1150
+ && normalizeGatewaySourceText(entry.sourceText) === normalizedSourceText);
1151
+ const translated = [...sourceMatches].reverse().find(isReusableGatewayOutputEntry);
1152
+ if (translated) {
1153
+ return { kind: "translated", text: translated.translatedText };
1154
+ }
1155
+ if (sourceMatches.some(isActiveTranslationEntry)) {
1156
+ return { kind: "active" };
1157
+ }
1158
+ return { kind: "missing" };
1159
+ }
1160
+ function findGatewayOutputEntryById(states, input, entryId) {
1161
+ for (const state of states) {
1162
+ const entry = state.entries.find((candidate) => candidate.id === entryId && isGatewayOutputEntry(candidate, input));
1163
+ if (entry) {
1164
+ return entry;
1165
+ }
1166
+ }
1167
+ return undefined;
1168
+ }
1169
+ function isGatewayOutputEntry(entry, input) {
1170
+ return entry.taskSlug === input.taskSlug
1171
+ && entry.role === input.role
1172
+ && entry.direction === "cc-output-to-user"
1173
+ && entry.sourceKind === "prose";
1174
+ }
1175
+ function isReusableGatewayOutputEntry(entry) {
1176
+ return entry.status === "translated" && Boolean(entry.translatedText.trim());
1177
+ }
1178
+ function normalizeGatewaySourceEntryIds(sourceEntryIds) {
1179
+ return Array.from(new Set((sourceEntryIds ?? []).map((entryId) => entryId.trim()).filter(Boolean)));
1180
+ }
1181
+ function normalizeGatewaySourceText(text) {
1182
+ return text.trim();
1183
+ }
1036
1184
  async function writeToCurrentRole(repoRoot, taskSlug, role, text) {
1037
1185
  const record = await deps.sessionService.getRoleSession(repoRoot, taskSlug, role);
1038
1186
  if (!record || record.status !== "running") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibe-coding-master",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "description": "Local GUI session cockpit for Claude Code role sessions.",
5
5
  "type": "module",
6
6
  "files": [