vibe-coding-master 0.4.30 → 0.4.32

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.
@@ -1,3 +1,4 @@
1
+ import { randomInt } from "node:crypto";
1
2
  import { readFile } from "node:fs/promises";
2
3
  import { CORE_VCM_ROLE_DEFINITIONS, GATE_REVIEWER_ROLE_DEFINITION, VCM_ROLE_NAMES } from "../../shared/constants.js";
3
4
  import { VcmError } from "../errors.js";
@@ -5,8 +6,8 @@ import { submitTerminalInput } from "../runtime/terminal-submit.js";
5
6
  import { getTaskRuntimeRepoRoot } from "../services/task-service.js";
6
7
  import { parseAssistantContent, resolveExistingClaudeTranscriptPath } from "../services/claude-transcript-service.js";
7
8
  import { parseGatewayCommand } from "./gateway-command-parser.js";
8
- const DEFAULT_BASE_URL = "https://ilinkai.weixin.qq.com";
9
9
  const QR_LOGIN_TTL_MS = 8 * 60 * 1000;
10
+ const PAIRING_CODE_TTL_MS = 10 * 60 * 1000;
10
11
  const CLOSE_CONFIRM_TTL_MS = 10 * 60 * 1000;
11
12
  const POLL_ERROR_BACKOFF_MS = 2_000;
12
13
  const POLL_LONG_BACKOFF_MS = 30_000;
@@ -41,7 +42,7 @@ export function createGatewayService(deps) {
41
42
  }
42
43
  pollStartingPromise = (async () => {
43
44
  const settings = await deps.settings.loadSettings();
44
- if (!settings.binding.token || isRunning()) {
45
+ if (!toAccount(settings) || isRunning()) {
45
46
  return;
46
47
  }
47
48
  const controller = new AbortController();
@@ -72,14 +73,24 @@ export function createGatewayService(deps) {
72
73
  pollAbort = null;
73
74
  }
74
75
  }
76
+ function resolveChannel(settings) {
77
+ return deps.channels.get(settings.channel);
78
+ }
75
79
  function toAccount(settings) {
76
- if (!settings.binding.token) {
80
+ const channel = resolveChannel(settings);
81
+ if (settings.channel === "weixin-ilink" && !settings.binding.token) {
82
+ return undefined;
83
+ }
84
+ if (settings.channel === "lark" && (!settings.binding.appId || !settings.binding.appSecret)) {
77
85
  return undefined;
78
86
  }
79
87
  return {
80
88
  accountId: settings.binding.accountId,
81
- baseUrl: settings.binding.baseUrl || DEFAULT_BASE_URL,
82
- token: settings.binding.token
89
+ baseUrl: settings.binding.baseUrl || channel.defaultBaseUrl,
90
+ token: settings.binding.token,
91
+ appId: settings.binding.appId,
92
+ appSecret: settings.binding.appSecret,
93
+ homeChatId: settings.binding.homeChatId
83
94
  };
84
95
  }
85
96
  async function pollLoop(signal) {
@@ -93,8 +104,9 @@ export function createGatewayService(deps) {
93
104
  await savePollStatus("idle");
94
105
  return;
95
106
  }
107
+ const channel = resolveChannel(settings);
96
108
  try {
97
- const result = await deps.channel.getUpdates({
109
+ const result = await channel.getUpdates({
98
110
  account,
99
111
  cursor: settings.binding.getUpdatesBuf,
100
112
  timeoutMs,
@@ -128,7 +140,7 @@ export function createGatewayService(deps) {
128
140
  }
129
141
  consecutiveFailures += 1;
130
142
  const message = errorMessage(error);
131
- const expired = message.toLowerCase().includes("expired");
143
+ const expired = channel.isSessionExpiredError?.(error) ?? message.toLowerCase().includes("expired");
132
144
  const settingsAfterError = await deps.settings.loadSettings();
133
145
  await deps.settings.saveSettings({
134
146
  ...settingsAfterError,
@@ -186,25 +198,38 @@ export function createGatewayService(deps) {
186
198
  });
187
199
  return;
188
200
  }
189
- settings = await deps.settings.saveSettings({
190
- ...settings,
191
- binding: {
192
- ...settings.binding,
193
- boundUserId: settings.binding.boundUserId ?? update.fromUserId,
194
- contextTokens: update.contextToken
195
- ? {
196
- ...settings.binding.contextTokens,
197
- [update.fromUserId]: update.contextToken
198
- }
199
- : settings.binding.contextTokens
200
- },
201
- dedupe: {
202
- recentInboundMessageIds: [...settings.dedupe.recentInboundMessageIds, update.messageId].slice(-1000)
203
- },
204
- updatedAt: now()
205
- });
201
+ if (settings.channel === "lark" && !settings.binding.boundUserId) {
202
+ settings = await saveInboundMetadata(settings, update);
203
+ const pairing = parseLarkPairingCommand(update.text);
204
+ if (!pairing || !isValidPairingCode(settings, pairing)) {
205
+ await reply(settings, update.fromUserId, "Lark Gateway is not paired. Generate a pairing code in desktop VCM, then send /bind CODE here.");
206
+ await recordMessageStatus("inbound", "ignored", update.text, "lark gateway not paired");
207
+ return;
208
+ }
209
+ await deps.settings.saveSettings({
210
+ ...settings,
211
+ binding: {
212
+ ...settings.binding,
213
+ boundUserId: update.fromUserId,
214
+ loginUserId: update.fromUserId,
215
+ pairingCode: null,
216
+ pairingCodeExpiresAt: null,
217
+ chatIds: update.chatId
218
+ ? {
219
+ ...settings.binding.chatIds,
220
+ [update.fromUserId]: update.chatId
221
+ }
222
+ : settings.binding.chatIds
223
+ },
224
+ updatedAt: now()
225
+ });
226
+ await reply(await deps.settings.loadSettings(), update.fromUserId, "Lark Gateway bound. Send /help for available commands.");
227
+ await recordMessageStatus("inbound", "ok", update.text, undefined, "bind");
228
+ return;
229
+ }
230
+ settings = await saveInboundMetadata(settings, update, { bindIfMissing: true });
206
231
  if (settings.binding.boundUserId !== update.fromUserId) {
207
- await reply(settings, update.fromUserId, "This VCM gateway is already bound to another Weixin DM.");
232
+ await reply(settings, update.fromUserId, "This VCM gateway is already bound to another user.");
208
233
  await recordMessageStatus("inbound", "ignored", update.text, "unbound user");
209
234
  return;
210
235
  }
@@ -237,6 +262,31 @@ export function createGatewayService(deps) {
237
262
  });
238
263
  }
239
264
  }
265
+ async function saveInboundMetadata(settings, update, options = {}) {
266
+ return deps.settings.saveSettings({
267
+ ...settings,
268
+ binding: {
269
+ ...settings.binding,
270
+ boundUserId: options.bindIfMissing ? settings.binding.boundUserId ?? update.fromUserId : settings.binding.boundUserId,
271
+ contextTokens: update.contextToken
272
+ ? {
273
+ ...settings.binding.contextTokens,
274
+ [update.fromUserId]: update.contextToken
275
+ }
276
+ : settings.binding.contextTokens,
277
+ chatIds: update.chatId
278
+ ? {
279
+ ...settings.binding.chatIds,
280
+ [update.fromUserId]: update.chatId
281
+ }
282
+ : settings.binding.chatIds
283
+ },
284
+ dedupe: {
285
+ recentInboundMessageIds: [...settings.dedupe.recentInboundMessageIds, update.messageId].slice(-1000)
286
+ },
287
+ updatedAt: now()
288
+ });
289
+ }
240
290
  async function executeCommand(command, settings) {
241
291
  if (!settings.enabled && !COMMANDS_ALLOWED_WHEN_DISABLED.has(command.kind)) {
242
292
  return [
@@ -581,8 +631,10 @@ export function createGatewayService(deps) {
581
631
  }
582
632
  async function startGateway() {
583
633
  const settings = await deps.settings.loadSettings();
584
- if (!settings.binding.token) {
585
- return "Gateway is not bound. Start QR login from desktop VCM first.";
634
+ if (!toAccount(settings)) {
635
+ return settings.channel === "lark"
636
+ ? "Gateway is not configured. Save Lark App ID and App Secret in desktop VCM first."
637
+ : "Gateway is not bound. Start QR login from desktop VCM first.";
586
638
  }
587
639
  if (settings.enabled) {
588
640
  return "Gateway is already on.";
@@ -647,9 +699,11 @@ export function createGatewayService(deps) {
647
699
  return;
648
700
  }
649
701
  const contextToken = settings.binding.contextTokens[userId];
650
- await deps.channel.sendText({
702
+ const chatId = settings.binding.chatIds[userId] ?? settings.binding.homeChatId ?? undefined;
703
+ await resolveChannel(settings).sendText({
651
704
  account,
652
705
  toUserId: userId,
706
+ chatId,
653
707
  contextToken,
654
708
  text
655
709
  });
@@ -682,6 +736,15 @@ export function createGatewayService(deps) {
682
736
  `Last poll: ${synced.lastPollStatus.state}${synced.lastPollStatus.error ? ` (${synced.lastPollStatus.error})` : ""}`
683
737
  ].join("\n");
684
738
  }
739
+ function parseLarkPairingCommand(text) {
740
+ const match = text.trim().match(/^\/bind\s+([A-Z0-9]{6,12})$/i);
741
+ return match?.[1]?.toUpperCase() ?? null;
742
+ }
743
+ function isValidPairingCode(settings, code) {
744
+ const expected = settings.binding.pairingCode?.toUpperCase();
745
+ const expiresAt = settings.binding.pairingCodeExpiresAt;
746
+ return Boolean(expected && expected === code.toUpperCase() && expiresAt && Date.parse(expiresAt) > Date.now());
747
+ }
685
748
  return {
686
749
  async start() {
687
750
  await ensurePolling();
@@ -699,7 +762,7 @@ export function createGatewayService(deps) {
699
762
  if (settings.enabled) {
700
763
  settings = await syncDesktopContext(settings);
701
764
  }
702
- if (settings.binding.token) {
765
+ if (toAccount(settings)) {
703
766
  await ensurePolling();
704
767
  }
705
768
  else {
@@ -713,15 +776,58 @@ export function createGatewayService(deps) {
713
776
  const settings = await deps.settings.resetBinding();
714
777
  return deps.settings.expose(settings, isRunning());
715
778
  },
779
+ async createPairingCode() {
780
+ const settings = await deps.settings.loadSettings();
781
+ if (settings.channel !== "lark") {
782
+ throw new VcmError({
783
+ code: "GATEWAY_PAIRING_UNSUPPORTED",
784
+ message: "Pairing codes are only available for Lark Gateway.",
785
+ statusCode: 409
786
+ });
787
+ }
788
+ if (!settings.binding.appId || !settings.binding.appSecret) {
789
+ throw new VcmError({
790
+ code: "GATEWAY_LARK_CONFIG_MISSING",
791
+ message: "Save Lark App ID and App Secret before creating a pairing code.",
792
+ statusCode: 409
793
+ });
794
+ }
795
+ const code = randomPairingCode();
796
+ const expiresAt = new Date(Date.now() + PAIRING_CODE_TTL_MS).toISOString();
797
+ const nextSettings = await deps.settings.saveSettings({
798
+ ...settings,
799
+ binding: {
800
+ ...settings.binding,
801
+ pairingCode: code,
802
+ pairingCodeExpiresAt: expiresAt
803
+ },
804
+ updatedAt: now()
805
+ });
806
+ await ensurePolling();
807
+ return {
808
+ code,
809
+ expiresAt,
810
+ status: deps.settings.expose(nextSettings, isRunning())
811
+ };
812
+ },
716
813
  async startQrLogin() {
717
814
  const settings = await deps.settings.loadSettings();
718
- const login = await deps.channel.startQrLogin({
815
+ const channel = resolveChannel(settings);
816
+ if (!channel.startQrLogin) {
817
+ throw new VcmError({
818
+ code: "GATEWAY_QR_UNSUPPORTED",
819
+ message: `${channel.label} Gateway does not support QR login.`,
820
+ statusCode: 409
821
+ });
822
+ }
823
+ const login = await channel.startQrLogin({
719
824
  localTokenList: settings.binding.token ? [settings.binding.token] : []
720
825
  });
721
826
  qrLogin = {
827
+ channel: settings.channel,
722
828
  qrcode: login.qrcode,
723
829
  qrcodeUrl: login.qrcodeUrl,
724
- baseUrl: settings.binding.baseUrl || DEFAULT_BASE_URL,
830
+ baseUrl: settings.binding.baseUrl || channel.defaultBaseUrl,
725
831
  expiresAt: new Date(Date.now() + QR_LOGIN_TTL_MS).toISOString()
726
832
  };
727
833
  return {
@@ -738,7 +844,15 @@ export function createGatewayService(deps) {
738
844
  message: "QR login expired. Start a new QR login."
739
845
  };
740
846
  }
741
- const result = await deps.channel.checkQrLogin({
847
+ const channel = deps.channels.get(qrLogin.channel);
848
+ if (!channel.checkQrLogin) {
849
+ throw new VcmError({
850
+ code: "GATEWAY_QR_UNSUPPORTED",
851
+ message: `${channel.label} Gateway does not support QR login.`,
852
+ statusCode: 409
853
+ });
854
+ }
855
+ const result = await channel.checkQrLogin({
742
856
  baseUrl: qrLogin.baseUrl,
743
857
  qrcode: qrLogin.qrcode,
744
858
  verifyCode: input.verifyCode
@@ -746,21 +860,22 @@ export function createGatewayService(deps) {
746
860
  if (result.status === "scaned_but_redirect" && result.redirectHost) {
747
861
  qrLogin = {
748
862
  ...qrLogin,
749
- baseUrl: normalizeBaseUrl(result.redirectHost)
863
+ baseUrl: normalizeBaseUrl(result.redirectHost, channel.defaultBaseUrl)
750
864
  };
751
865
  }
752
866
  if (result.status === "confirmed" || result.status === "binded_redirect") {
753
867
  const settings = await deps.settings.loadSettings();
754
868
  const token = result.token ?? settings.binding.token;
869
+ const boundUserId = result.boundUserId ?? result.loginUserId;
755
870
  if (token) {
756
871
  await deps.settings.saveSettings({
757
872
  ...settings,
758
873
  binding: {
759
874
  ...settings.binding,
760
875
  accountId: result.accountId ?? settings.binding.accountId,
761
- baseUrl: normalizeBaseUrl(result.baseUrl ?? settings.binding.baseUrl),
876
+ baseUrl: normalizeBaseUrl(result.baseUrl ?? settings.binding.baseUrl, channel.defaultBaseUrl),
762
877
  loginUserId: result.loginUserId ?? settings.binding.loginUserId,
763
- boundUserId: result.loginUserId ?? settings.binding.boundUserId,
878
+ boundUserId: boundUserId ?? settings.binding.boundUserId,
764
879
  token
765
880
  },
766
881
  updatedAt: now()
@@ -769,11 +884,12 @@ export function createGatewayService(deps) {
769
884
  await ensurePolling();
770
885
  }
771
886
  }
887
+ const boundUserId = result.boundUserId ?? result.loginUserId;
772
888
  return {
773
889
  status: result.status,
774
890
  qrcodeUrl: qrLogin?.qrcodeUrl,
775
891
  accountId: result.accountId,
776
- boundUserId: result.loginUserId,
892
+ boundUserId,
777
893
  loginUserId: result.loginUserId
778
894
  };
779
895
  },
@@ -812,9 +928,10 @@ export function createGatewayService(deps) {
812
928
  taskSlug: input.taskSlug,
813
929
  sourceText: text
814
930
  });
815
- await deps.channel.sendText({
931
+ await resolveChannel(settings).sendText({
816
932
  account,
817
933
  toUserId: boundUserId,
934
+ chatId: settings.binding.chatIds[boundUserId] ?? settings.binding.homeChatId ?? undefined,
818
935
  contextToken: settings.binding.contextTokens[boundUserId],
819
936
  text: output.text
820
937
  });
@@ -1095,6 +1212,14 @@ function formatAheadBehind(project) {
1095
1212
  }
1096
1213
  return ahead > 0 ? `ahead ${ahead}` : `behind ${behind}`;
1097
1214
  }
1215
+ function randomPairingCode() {
1216
+ const alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
1217
+ let out = "";
1218
+ for (let index = 0; index < 8; index += 1) {
1219
+ out += alphabet[randomInt(alphabet.length)];
1220
+ }
1221
+ return out;
1222
+ }
1098
1223
  function sleep(ms, signal) {
1099
1224
  return new Promise((resolve) => {
1100
1225
  if (signal.aborted) {
@@ -1108,10 +1233,10 @@ function sleep(ms, signal) {
1108
1233
  }, { once: true });
1109
1234
  });
1110
1235
  }
1111
- function normalizeBaseUrl(input) {
1236
+ function normalizeBaseUrl(input, fallback) {
1112
1237
  const trimmed = input.trim();
1113
1238
  if (!trimmed) {
1114
- return DEFAULT_BASE_URL;
1239
+ return fallback;
1115
1240
  }
1116
1241
  if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) {
1117
1242
  return trimmed.replace(/\/+$/, "");
@@ -1,27 +1,32 @@
1
1
  import path from "node:path";
2
2
  import { resolveVcmDataDir } from "../vcm-data-dir.js";
3
3
  const DEFAULT_BASE_URL = "https://ilinkai.weixin.qq.com";
4
+ const DEFAULT_GATEWAY_CHANNEL = "weixin-ilink";
4
5
  const MAX_DEDUPE_IDS = 1000;
5
6
  export function createGatewaySettingsService(deps) {
6
7
  const dataDir = resolveVcmDataDir();
7
8
  const settingsPath = deps.settingsPath ?? path.join(dataDir, "gateway", "settings.json");
8
9
  const auditPath = deps.auditPath ?? path.join(dataDir, "gateway", "audit.jsonl");
9
10
  const now = deps.now ?? (() => new Date().toISOString());
11
+ const normalizeOptions = {
12
+ defaultChannel: deps.defaultChannel ?? DEFAULT_GATEWAY_CHANNEL,
13
+ defaultBaseUrl: deps.defaultBaseUrl ?? DEFAULT_BASE_URL
14
+ };
10
15
  let cachedSettings = null;
11
16
  async function loadSettings() {
12
17
  if (cachedSettings) {
13
18
  return cachedSettings;
14
19
  }
15
20
  if (!(await deps.fs.pathExists(settingsPath))) {
16
- cachedSettings = normalizeSettings({}, now());
21
+ cachedSettings = normalizeSettings({}, now(), normalizeOptions);
17
22
  await saveSettings(cachedSettings);
18
23
  return cachedSettings;
19
24
  }
20
- cachedSettings = normalizeSettings(await deps.fs.readJson(settingsPath), now());
25
+ cachedSettings = normalizeSettings(await deps.fs.readJson(settingsPath), now(), normalizeOptions);
21
26
  return cachedSettings;
22
27
  }
23
28
  async function saveSettings(settings) {
24
- cachedSettings = normalizeSettings(settings, now());
29
+ cachedSettings = normalizeSettings(settings, now(), normalizeOptions);
25
30
  await deps.fs.writeJsonAtomic(settingsPath, cachedSettings);
26
31
  return cachedSettings;
27
32
  }
@@ -32,9 +37,19 @@ export function createGatewaySettingsService(deps) {
32
37
  return saveSettings({
33
38
  ...current,
34
39
  enabled: input.enabled ?? current.enabled,
40
+ channel: input.channel ?? current.channel,
35
41
  translationEnabled: input.translationEnabled ?? current.translationEnabled,
36
42
  currentProjectId: input.currentProjectId !== undefined ? normalizeNullableString(input.currentProjectId) : current.currentProjectId,
37
43
  currentTaskSlug: input.currentTaskSlug !== undefined ? normalizeNullableString(input.currentTaskSlug) : current.currentTaskSlug,
44
+ binding: {
45
+ ...current.binding,
46
+ baseUrl: input.baseUrl !== undefined
47
+ ? normalizeBaseUrl(input.baseUrl, normalizeOptions.defaultBaseUrl)
48
+ : current.binding.baseUrl,
49
+ appId: input.larkAppId !== undefined ? normalizeNullableString(input.larkAppId) : current.binding.appId,
50
+ appSecret: input.larkAppSecret !== undefined ? normalizeNullableString(input.larkAppSecret) : current.binding.appSecret,
51
+ homeChatId: input.larkHomeChatId !== undefined ? normalizeNullableString(input.larkHomeChatId) : current.binding.homeChatId
52
+ },
38
53
  updatedAt: now()
39
54
  });
40
55
  },
@@ -44,7 +59,13 @@ export function createGatewaySettingsService(deps) {
44
59
  return saveSettings({
45
60
  ...current,
46
61
  enabled: false,
47
- binding: createDefaultBinding(),
62
+ binding: {
63
+ ...createDefaultBinding(normalizeOptions.defaultBaseUrl),
64
+ baseUrl: current.binding.baseUrl || normalizeOptions.defaultBaseUrl,
65
+ appId: current.binding.appId,
66
+ appSecret: current.binding.appSecret,
67
+ homeChatId: current.binding.homeChatId
68
+ },
48
69
  dedupe: {
49
70
  recentInboundMessageIds: []
50
71
  },
@@ -71,7 +92,12 @@ export function createGatewaySettingsService(deps) {
71
92
  baseUrl: settings.binding.baseUrl,
72
93
  boundUserId: settings.binding.boundUserId,
73
94
  loginUserId: settings.binding.loginUserId,
74
- tokenConfigured: Boolean(settings.binding.token)
95
+ tokenConfigured: Boolean(settings.binding.token),
96
+ appId: settings.binding.appId,
97
+ appIdConfigured: Boolean(settings.binding.appId),
98
+ appSecretConfigured: Boolean(settings.binding.appSecret),
99
+ homeChatId: settings.binding.homeChatId,
100
+ pairingCodeExpiresAt: settings.binding.pairingCodeExpiresAt
75
101
  },
76
102
  pendingConfirmations: settings.pendingConfirmations,
77
103
  lastPollStatus: settings.lastPollStatus,
@@ -87,7 +113,10 @@ export function createGatewaySettingsService(deps) {
87
113
  }
88
114
  };
89
115
  }
90
- export function normalizeSettings(input, timestamp) {
116
+ export function normalizeSettings(input, timestamp, options = {
117
+ defaultChannel: DEFAULT_GATEWAY_CHANNEL,
118
+ defaultBaseUrl: DEFAULT_BASE_URL
119
+ }) {
91
120
  const bindingInput = isObject(input.binding) ? input.binding : {};
92
121
  const dedupeInput = isObject(input.dedupe) ? input.dedupe : {};
93
122
  const pendingInput = isObject(input.pendingConfirmations)
@@ -100,20 +129,28 @@ export function normalizeSettings(input, timestamp) {
100
129
  return {
101
130
  version: 1,
102
131
  enabled: input.enabled === true,
103
- channel: "weixin-ilink",
132
+ channel: normalizeGatewayChannel(input.channel, options.defaultChannel),
104
133
  translationEnabled: input.translationEnabled !== false,
105
134
  currentProjectId: normalizeNullableString(input.currentProjectId),
106
135
  currentTaskSlug: normalizeNullableString(input.currentTaskSlug),
107
136
  binding: {
108
- ...createDefaultBinding(),
137
+ ...createDefaultBinding(options.defaultBaseUrl),
109
138
  accountId: normalizeNullableString(bindingInput.accountId),
110
- baseUrl: normalizeBaseUrl(bindingInput.baseUrl),
139
+ baseUrl: normalizeBaseUrl(bindingInput.baseUrl, options.defaultBaseUrl),
111
140
  boundUserId: normalizeNullableString(bindingInput.boundUserId),
112
141
  loginUserId: normalizeNullableString(bindingInput.loginUserId),
113
142
  token: normalizeNullableString(bindingInput.token),
143
+ appId: normalizeNullableString(bindingInput.appId),
144
+ appSecret: normalizeNullableString(bindingInput.appSecret),
145
+ homeChatId: normalizeNullableString(bindingInput.homeChatId),
146
+ pairingCode: normalizeNullableString(bindingInput.pairingCode),
147
+ pairingCodeExpiresAt: normalizeNullableString(bindingInput.pairingCodeExpiresAt),
114
148
  getUpdatesBuf: typeof bindingInput.getUpdatesBuf === "string" ? bindingInput.getUpdatesBuf : "",
115
149
  contextTokens: isObject(bindingInput.contextTokens)
116
150
  ? normalizeStringRecord(bindingInput.contextTokens)
151
+ : {},
152
+ chatIds: isObject(bindingInput.chatIds)
153
+ ? normalizeStringRecord(bindingInput.chatIds)
117
154
  : {}
118
155
  },
119
156
  dedupe: {
@@ -131,17 +168,26 @@ export function normalizeSettings(input, timestamp) {
131
168
  updatedAt: typeof input.updatedAt === "string" ? input.updatedAt : timestamp
132
169
  };
133
170
  }
134
- function createDefaultBinding() {
171
+ function createDefaultBinding(defaultBaseUrl = DEFAULT_BASE_URL) {
135
172
  return {
136
173
  accountId: null,
137
- baseUrl: DEFAULT_BASE_URL,
174
+ baseUrl: defaultBaseUrl,
138
175
  boundUserId: null,
139
176
  loginUserId: null,
140
177
  token: null,
178
+ appId: null,
179
+ appSecret: null,
180
+ homeChatId: null,
181
+ pairingCode: null,
182
+ pairingCodeExpiresAt: null,
141
183
  getUpdatesBuf: "",
142
- contextTokens: {}
184
+ contextTokens: {},
185
+ chatIds: {}
143
186
  };
144
187
  }
188
+ function normalizeGatewayChannel(input, fallback) {
189
+ return input === "weixin-ilink" || input === "lark" ? input : fallback;
190
+ }
145
191
  function normalizeCloseTaskConfirmation(input) {
146
192
  if (!isObject(input)) {
147
193
  return null;
@@ -222,10 +268,10 @@ function normalizeMessageStatus(input) {
222
268
  error: normalizeNullableString(input.error) ?? undefined
223
269
  };
224
270
  }
225
- function normalizeBaseUrl(input) {
271
+ function normalizeBaseUrl(input, fallback = DEFAULT_BASE_URL) {
226
272
  const raw = typeof input === "string" ? input.trim() : "";
227
273
  if (!raw) {
228
- return DEFAULT_BASE_URL;
274
+ return fallback;
229
275
  }
230
276
  if (raw.startsWith("http://") || raw.startsWith("https://")) {
231
277
  return raw.replace(/\/+$/, "");
@@ -18,8 +18,10 @@ import { createNodeFileSystemAdapter } from "./adapters/filesystem.js";
18
18
  import { createNodePtyTerminalRuntime } from "./runtime/node-pty-runtime.js";
19
19
  import { registerGatewayRoutes } from "./api/gateway-routes.js";
20
20
  import { registerDiagnosticsRoutes } from "./api/diagnostics-routes.js";
21
+ import { createLarkChannel } from "./gateway/channels/lark-channel.js";
21
22
  import { createWeixinIlinkChannel } from "./gateway/channels/weixin-ilink-channel.js";
22
23
  import { createGatewayAuditLog } from "./gateway/gateway-audit-log.js";
24
+ import { createGatewayChannelRegistry } from "./gateway/gateway-channel.js";
23
25
  import { createGatewayService } from "./gateway/gateway-service.js";
24
26
  import { createGatewaySettingsService } from "./gateway/gateway-settings-service.js";
25
27
  import { createJobGuardService } from "./services/job-guard-service.js";
@@ -261,7 +263,15 @@ export function createDefaultServerDeps(options = {}) {
261
263
  projectService,
262
264
  appSettings
263
265
  });
264
- const gatewaySettings = createGatewaySettingsService({ fs });
266
+ const gatewayChannels = createGatewayChannelRegistry([
267
+ createWeixinIlinkChannel(),
268
+ createLarkChannel()
269
+ ]);
270
+ const gatewaySettings = createGatewaySettingsService({
271
+ fs,
272
+ defaultChannel: gatewayChannels.defaultChannel.id,
273
+ defaultBaseUrl: gatewayChannels.defaultChannel.defaultBaseUrl
274
+ });
265
275
  const gatewayAudit = createGatewayAuditLog({
266
276
  fs,
267
277
  auditPath: gatewaySettings.getAuditPath()
@@ -270,7 +280,7 @@ export function createDefaultServerDeps(options = {}) {
270
280
  fs,
271
281
  settings: gatewaySettings,
272
282
  audit: gatewayAudit,
273
- channel: createWeixinIlinkChannel(),
283
+ channels: gatewayChannels,
274
284
  projectService,
275
285
  taskService,
276
286
  sessionService,
@@ -266,6 +266,9 @@ export function createHarnessService(deps) {
266
266
  async getRepositoryDiff(repoRoot, input = {}) {
267
267
  return getRepositoryDiffReport(requireRepositoryDiffGit(deps.git), repoRoot, input, now());
268
268
  },
269
+ async getRepositoryFileDiff(repoRoot, input) {
270
+ return getRepositoryFileDiffReport(requireRepositoryDiffGit(deps.git), repoRoot, input, now());
271
+ },
269
272
  async mergeRepositoryDiffToCurrentBranch(baseRepoRoot, input) {
270
273
  return mergeRepositoryDiffToCurrentBranch(requireRepositoryMergeGit(deps.git), baseRepoRoot, input, now());
271
274
  },
@@ -466,6 +469,7 @@ function requireRepositoryDiffGit(git) {
466
469
  !git.getCommitInfo ||
467
470
  !git.getCommitList ||
468
471
  !git.getCurrentBranch ||
472
+ !git.getDiff ||
469
473
  !git.getHeadCommit ||
470
474
  !git.getMergeBase) {
471
475
  throw new VcmError({
@@ -550,6 +554,34 @@ async function getRepositoryDiffReport(git, repoRoot, input, generatedAt) {
550
554
  warnings
551
555
  };
552
556
  }
557
+ async function getRepositoryFileDiffReport(git, repoRoot, input, generatedAt) {
558
+ const filePath = normalizeHarnessGitPath(input.path);
559
+ const baseRepoRoot = input.baseRepoRoot;
560
+ const sourceBranch = await git.getCurrentBranch(repoRoot);
561
+ const targetBranch = await git.getCurrentBranch(baseRepoRoot);
562
+ const baseSha = await git.getHeadCommit(baseRepoRoot);
563
+ const headSha = await git.getHeadCommit(repoRoot);
564
+ const rawDiff = await git.getDiff(repoRoot, baseSha, null, [filePath]);
565
+ const files = parseCommitDiffFiles(rawDiff);
566
+ const file = files.find((entry) => entry.path === filePath || entry.oldPath === filePath) ?? files[0] ?? null;
567
+ const warnings = [];
568
+ if (files.length > 1) {
569
+ warnings.push(`Multiple diff entries matched ${filePath}; showing the first entry.`);
570
+ }
571
+ return {
572
+ version: 1,
573
+ repoRoot,
574
+ baseRepoRoot,
575
+ sourceBranch,
576
+ targetBranch,
577
+ baseSha,
578
+ headSha,
579
+ path: filePath,
580
+ generatedAt,
581
+ file,
582
+ warnings
583
+ };
584
+ }
553
585
  async function mergeRepositoryDiffToCurrentBranch(git, baseRepoRoot, input, mergedAt) {
554
586
  const taskBranch = input.taskBranch.trim();
555
587
  if (!taskBranch) {