vibe-coding-master 0.4.34 → 0.4.36

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 CHANGED
@@ -221,7 +221,7 @@ The left sidebar is intentionally compact and collapsible:
221
221
  - `Settings`: `Theme`, `Flow pause alert`, `Try alert`, `Messages`, and `Events`.
222
222
  - `Translation`: global conversation translation, auto-send, target language, output scope, file translation, bootstrap, memory update, session status, and Translator session access.
223
223
  - `Gate Review Gates`: global gate switches for architecture plan, validation adequacy, and final diff.
224
- - `Gateway`: Weixin iLink or Lark binding, Gateway on/off, Gateway translation, QR login/setup, and Lark pairing.
224
+ - `Gateway`: Weixin iLink or Lark setup, Gateway on/off, Gateway translation, and QR login/setup.
225
225
  - `VCM Harness`: fixed-install status, bootstrap completion checks, and the bootstrap terminal when one is running.
226
226
  - `New Task`: one `task name` input.
227
227
  - `Tasks`: task list and task status.
@@ -238,15 +238,16 @@ When VCM is connected to an active task, the bottom of the sidebar shows a task
238
238
 
239
239
  ## Mobile Gateway
240
240
 
241
- VCM Gateway lets one mobile chat identity bind to one desktop VCM instance. It supports Weixin iLink and Lark. It is a mobile control surface for the current desktop VCM, not a remote terminal and not a multi-user collaboration surface.
241
+ VCM Gateway lets mobile chat clients talk to one desktop VCM instance. It supports Weixin iLink and Lark. It is a mobile control surface for the current desktop VCM, not a remote terminal.
242
242
 
243
243
  Gateway rules:
244
244
 
245
245
  - Weixin is DM only; Lark can receive group messages only when the bot is mentioned.
246
- - One phone identity binds to one desktop VCM instance.
247
- - The phone can manage projects and tasks available to that desktop VCM instance.
246
+ - Weixin binds one phone identity to one desktop VCM instance.
247
+ - Lark accepts any DM or @mention from a chat that can reach the bot; the most recent active Lark chat becomes the PM reply target.
248
+ - The active mobile chat can manage projects and tasks available to that desktop VCM instance.
248
249
  - When the desktop UI has an active task selected, Gateway uses that task automatically.
249
- - After binding, VCM keeps a lightweight Gateway connection for `/start` and read-only commands even when the `Gateway` toggle is off.
250
+ - After setup, VCM keeps a lightweight Gateway connection for `/start` and read-only commands even when the `Gateway` toggle is off.
250
251
  - VCM caches the latest PM reply for each task locally, so `/start` can immediately return the current task's latest PM status when available.
251
252
  - Plain text messages go only to the current task's `project-manager`.
252
253
  - Gateway never sends directly to `architect`, `coder`, or `reviewer`.
@@ -279,11 +280,10 @@ The `Gateway` toggle is disabled until a QR login has produced a usable iLink to
279
280
  2. Click `Start QR Setup`.
280
281
  3. Scan the QR code with Lark and approve bot creation.
281
282
  4. Click `Confirm` in the setup dialog.
282
- 5. Click `Create Pairing Code`.
283
- 6. Send `/bind CODE` to the Lark bot before the code expires.
284
- 7. After binding succeeds, turn `Gateway` on in the sidebar.
283
+ 5. Turn `Gateway` on in the sidebar.
284
+ 6. Send a DM to the bot, or @mention it from a group.
285
285
 
286
- Lark QR setup creates/configures the bot app and stores the resulting App ID/App Secret in local VCM state. The App Secret is not shown in the UI. `Reset Binding` clears the bound Lark user/chat state while keeping the saved Lark app credentials.
286
+ Lark QR setup creates/configures the bot app and stores the resulting App ID/App Secret in local VCM state. The App Secret is not shown in the UI. Any Lark chat that can message the bot can control Gateway; VCM always treats the most recent active Lark chat as the PM reply target.
287
287
 
288
288
  When Gateway is turned on, VCM automatically turns off the browser `Flow pause alert` and disables `Try alert`. Gateway becomes the notification path, so the browser should not show blocking flow-pause dialogs while the user is managing the task from the phone.
289
289
 
@@ -296,14 +296,14 @@ When Gateway translation is on:
296
296
  - Mobile input is translated to English before being submitted to PM.
297
297
  - The prompt sent to PM includes only the translated English text with a `[VCM Gateway]` marker.
298
298
  - The original Chinese text is not included in the PM prompt.
299
- - PM replies are translated before VCM sends them to the bound mobile chat.
299
+ - PM replies are translated before VCM sends them to the active mobile chat.
300
300
  - If PM reply translation fails or times out, VCM sends a translation failure notice instead of the English source. The user can send `/retry` to retry the latest failed Gateway output translation.
301
301
 
302
302
  When Gateway translation is off, plain mobile text is sent to PM as-is.
303
303
 
304
304
  ### Commands
305
305
 
306
- After Gateway is bound, send commands in the bound mobile conversation.
306
+ After Gateway is configured, send commands in the active mobile conversation.
307
307
 
308
308
  When `Gateway` is off, only these commands are accepted:
309
309
 
@@ -353,9 +353,9 @@ Typical mobile flow:
353
353
 
354
354
  ### Command Behavior
355
355
 
356
- - `/status`: shows Gateway, binding, translation, current project, current task, and last poll status.
356
+ - `/status`: shows Gateway, active chat, translation, current project, current task, and last poll status.
357
357
  - `/status` also adopts the current desktop project/task when one is selected.
358
- - `/start`: turns Gateway on from the bound mobile conversation so full mobile task operations and PM messages are allowed. If the current task has a cached latest PM reply, `/start` includes it in the response.
358
+ - `/start`: turns Gateway on from the active mobile conversation so full mobile task operations and PM messages are allowed. If the current task has a cached latest PM reply, `/start` includes it in the response.
359
359
  - `/retry`: retries the latest failed Gateway output translation in the current VCM process.
360
360
  - `/projects`: lists the current/recent repositories known by the desktop VCM.
361
361
  - `/use-project <index-or-path>`: selects the Gateway's current project context.
@@ -378,12 +378,12 @@ Typical mobile flow:
378
378
  - If the QR dialog does not appear, refresh the page and click `Start QR Login` again.
379
379
  - If the QR status stays `wait`, confirm the login on the phone and click `Confirm` again.
380
380
  - If the QR code expires, start a new QR login.
381
- - If `Gateway` cannot be enabled, bind Weixin or complete Lark QR setup and pairing first.
382
- - If `/start` or read-only commands do not receive replies, check that the selected channel is connected and the message comes from the bound identity.
381
+ - If `Gateway` cannot be enabled, bind Weixin or complete Lark QR setup first.
382
+ - If `/start` or read-only commands do not receive replies, check that the selected channel is connected and that Lark messages are sent as DM or group @mentions.
383
383
  - If PM messages or task-changing commands are rejected, check that Gateway is on.
384
384
  - If plain text cannot be sent to PM, select a project and task first, and make sure the task's PM session is running and idle.
385
385
  - If PM replies are not pushed, check that Gateway is on and the PM session is producing normal Claude transcript output.
386
- - If PM reply translation fails, send `/retry` from the bound mobile conversation. Retry state is memory-only and is cleared when VCM restarts.
386
+ - If PM reply translation fails, send `/retry` from the active mobile conversation. Retry state is memory-only and is cleared when VCM restarts.
387
387
 
388
388
  ## Translation
389
389
 
@@ -8,9 +8,6 @@ export function registerGatewayRoutes(app, deps) {
8
8
  app.post("/api/gateway/qr/start", async () => {
9
9
  return deps.gatewayService.startQrLogin();
10
10
  });
11
- app.post("/api/gateway/pairing-code", async () => {
12
- return deps.gatewayService.createPairingCode();
13
- });
14
11
  app.post("/api/gateway/qr/check", async (request) => {
15
12
  return deps.gatewayService.checkQrLogin(request.body);
16
13
  });
@@ -21,10 +21,11 @@ export function createLarkChannel(options = {}) {
21
21
  ready: Promise.resolve(),
22
22
  error: null
23
23
  };
24
+ const sdkDomain = input.domain === "lark" ? Lark.Domain.Lark : Lark.Domain.Feishu;
24
25
  const client = new Lark.Client({
25
26
  appId: input.appId,
26
27
  appSecret: input.appSecret,
27
- domain: input.domain
28
+ domain: sdkDomain
28
29
  });
29
30
  const dispatcher = new Lark.EventDispatcher({}).register({
30
31
  "im.message.receive_v1": async (data) => {
@@ -40,7 +41,7 @@ export function createLarkChannel(options = {}) {
40
41
  const wsClient = new Lark.WSClient({
41
42
  appId: input.appId,
42
43
  appSecret: input.appSecret,
43
- domain: input.domain,
44
+ domain: sdkDomain,
44
45
  source: "vcm-gateway",
45
46
  autoReconnect: true,
46
47
  loggerLevel: options.loggerLevel,
@@ -1,115 +1,146 @@
1
- const ACCOUNTS_BASE_URLS = {
2
- feishu: "https://accounts.feishu.cn",
3
- lark: "https://accounts.larksuite.com"
1
+ import { randomUUID } from "node:crypto";
2
+ const ACCOUNTS_HOSTS = {
3
+ feishu: "accounts.feishu.cn",
4
+ lark: "accounts.larksuite.com"
4
5
  };
5
6
  const OPEN_BASE_URLS = {
6
7
  feishu: "https://open.feishu.cn",
7
8
  lark: "https://open.larksuite.com"
8
9
  };
9
- const REGISTRATION_PATH = "/oauth/v1/app/registration";
10
10
  const REQUEST_TIMEOUT_MS = 10_000;
11
+ const DEFAULT_REGISTRATION_EXPIRES_IN_SECONDS = 600;
12
+ const DEFAULT_REGISTRATION_INTERVAL_SECONDS = 5;
11
13
  export function createLarkRegistrationClient(deps = {}) {
12
14
  const fetchImpl = deps.fetch ?? fetch;
13
- async function postRegistration(domain, body) {
14
- const url = `${ACCOUNTS_BASE_URLS[domain]}${REGISTRATION_PATH}`;
15
- const payload = await fetchJson(fetchImpl, url, {
16
- method: "POST",
17
- headers: {
18
- "content-type": "application/x-www-form-urlencoded"
19
- },
20
- body: new URLSearchParams(body)
15
+ let registerAppPromise = deps.registerApp ? Promise.resolve(deps.registerApp) : null;
16
+ let active = null;
17
+ async function getRegisterApp() {
18
+ registerAppPromise ??= import("@larksuiteoapi/node-sdk").then((sdk) => {
19
+ if (typeof sdk.registerApp !== "function") {
20
+ throw new Error("Lark SDK does not expose registerApp.");
21
+ }
22
+ return sdk.registerApp;
21
23
  });
22
- return payload;
24
+ return registerAppPromise;
23
25
  }
24
26
  return {
25
- async init(domain) {
26
- const payload = await postRegistration(domain, { action: "init" });
27
- const data = responseData(payload);
28
- const methods = Array.isArray(data.supported_auth_methods)
29
- ? data.supported_auth_methods.filter((value) => typeof value === "string")
30
- : [];
31
- if (!methods.includes("client_secret")) {
32
- throw new Error(`Lark QR setup does not support client_secret auth. Supported methods: ${methods.join(", ") || "none"}.`);
33
- }
27
+ async init() {
28
+ await getRegisterApp();
34
29
  },
35
30
  async begin(domain) {
36
- const payload = await postRegistration(domain, {
37
- action: "begin",
38
- archetype: "PersonalAgent",
39
- auth_method: "client_secret",
40
- request_user_info: "open_id"
31
+ active?.controller.abort();
32
+ const registerApp = await getRegisterApp();
33
+ const controller = new AbortController();
34
+ const id = `lark-registration-${Date.now()}-${randomUUID()}`;
35
+ const session = {
36
+ id,
37
+ domain,
38
+ controller,
39
+ expiresAtMs: Date.now() + DEFAULT_REGISTRATION_EXPIRES_IN_SECONDS * 1000,
40
+ qrUrl: "",
41
+ intervalSeconds: DEFAULT_REGISTRATION_INTERVAL_SECONDS,
42
+ result: null,
43
+ message: undefined,
44
+ promise: Promise.resolve()
45
+ };
46
+ active = session;
47
+ let qrReadySettled = false;
48
+ let resolveQrReady = () => undefined;
49
+ let rejectQrReady = () => undefined;
50
+ const qrReady = new Promise((resolve, reject) => {
51
+ resolveQrReady = resolve;
52
+ rejectQrReady = reject;
41
53
  });
42
- const data = responseData(payload);
43
- const deviceCode = stringOrNull(data.device_code);
44
- if (!deviceCode) {
45
- throw new Error("Lark QR setup did not return a device_code.");
46
- }
47
- const qrUrl = appendQrTrackingParams(stringOrNull(data.verification_uri_complete) ?? "");
48
- if (!qrUrl) {
49
- throw new Error("Lark QR setup did not return a QR URL.");
50
- }
54
+ session.promise = (async () => {
55
+ try {
56
+ const result = await registerApp({
57
+ domain: ACCOUNTS_HOSTS[domain],
58
+ larkDomain: ACCOUNTS_HOSTS.lark,
59
+ source: "vcm",
60
+ signal: controller.signal,
61
+ onQRCodeReady(info) {
62
+ const qrUrl = info.url;
63
+ const expireIn = positiveNumberOr(info.expireIn, DEFAULT_REGISTRATION_EXPIRES_IN_SECONDS);
64
+ session.qrUrl = qrUrl;
65
+ session.expiresAtMs = Date.now() + expireIn * 1000;
66
+ if (!qrReadySettled) {
67
+ qrReadySettled = true;
68
+ resolveQrReady({ url: qrUrl, expireIn });
69
+ }
70
+ },
71
+ onStatusChange(info) {
72
+ session.message = formatSdkStatus(info);
73
+ }
74
+ });
75
+ const appId = stringOrNull(result.client_id);
76
+ const appSecret = stringOrNull(result.client_secret);
77
+ const resultDomain = result.user_info?.tenant_brand === "lark" ? "lark" : domain;
78
+ if (!appId || !appSecret) {
79
+ session.result = {
80
+ status: "failed",
81
+ message: "Lark QR setup completed without app credentials."
82
+ };
83
+ return;
84
+ }
85
+ const bot = await probeBot(fetchImpl, {
86
+ appId,
87
+ appSecret,
88
+ domain: resultDomain
89
+ });
90
+ session.result = {
91
+ status: "confirmed",
92
+ appId,
93
+ appSecret,
94
+ domain: resultDomain,
95
+ openId: stringOrNull(result.user_info?.open_id),
96
+ botName: bot.botName,
97
+ botOpenId: bot.botOpenId
98
+ };
99
+ }
100
+ catch (error) {
101
+ const mapped = mapSdkError(error);
102
+ session.result = mapped;
103
+ if (!qrReadySettled) {
104
+ qrReadySettled = true;
105
+ rejectQrReady(new Error(mapped.message ?? "Lark QR setup failed."));
106
+ }
107
+ }
108
+ })();
109
+ const qrInfo = await qrReady;
51
110
  return {
52
111
  domain,
53
- deviceCode,
54
- qrUrl,
55
- userCode: stringOrNull(data.user_code),
56
- intervalSeconds: positiveNumberOr(data.interval, 5),
57
- expiresInSeconds: positiveNumberOr(data.expire_in, 600)
112
+ deviceCode: id,
113
+ qrUrl: qrInfo.url,
114
+ userCode: extractUserCode(qrInfo.url),
115
+ intervalSeconds: session.intervalSeconds,
116
+ expiresInSeconds: qrInfo.expireIn
58
117
  };
59
118
  },
60
119
  async poll(input) {
61
- const payload = await postRegistration(input.domain, {
62
- action: "poll",
63
- device_code: input.deviceCode,
64
- tp: "ob_app"
65
- });
66
- const data = responseData(payload);
67
- const userInfo = isObject(data.user_info) ? data.user_info : {};
68
- const tenantBrand = stringOrNull(userInfo.tenant_brand);
69
- const domain = tenantBrand === "lark" ? "lark" : input.domain;
70
- const appId = stringOrNull(data.client_id);
71
- const appSecret = stringOrNull(data.client_secret);
72
- if (appId && appSecret) {
73
- const bot = await probeBot(fetchImpl, {
74
- appId,
75
- appSecret,
76
- domain
77
- });
120
+ if (!active || active.id !== input.deviceCode) {
78
121
  return {
79
- status: "confirmed",
80
- appId,
81
- appSecret,
82
- domain,
83
- openId: stringOrNull(userInfo.open_id),
84
- botName: bot.botName,
85
- botOpenId: bot.botOpenId
122
+ status: "expired",
123
+ message: "Lark QR setup session is no longer active. Start a new setup."
86
124
  };
87
125
  }
88
- const error = stringOrNull(data.error) ?? stringOrNull(payload.error);
89
- if (error === "expired_token") {
90
- return { status: "expired", message: "Lark QR setup expired. Start a new setup." };
126
+ if (active.result) {
127
+ return active.result;
91
128
  }
92
- if (error === "access_denied") {
93
- return { status: "failed", message: "Lark QR setup was denied." };
129
+ if (Date.now() > active.expiresAtMs) {
130
+ active.controller.abort();
131
+ active.result = {
132
+ status: "expired",
133
+ message: "Lark QR setup expired. Start a new setup."
134
+ };
135
+ return active.result;
94
136
  }
95
- const errorDescription = stringOrNull(data.error_description)
96
- ?? stringOrNull(payload.error_description)
97
- ?? stringOrNull(data.message)
98
- ?? stringOrNull(payload.message)
99
- ?? stringOrNull(data.msg)
100
- ?? stringOrNull(payload.msg);
101
137
  return {
102
138
  status: "wait",
103
- message: error && error !== "authorization_pending"
104
- ? [error, errorDescription].filter(Boolean).join(": ")
105
- : undefined
139
+ message: active.message
106
140
  };
107
141
  }
108
142
  };
109
143
  }
110
- function responseData(payload) {
111
- return isObject(payload.data) ? payload.data : payload;
112
- }
113
144
  async function probeBot(fetchImpl, input) {
114
145
  try {
115
146
  const tokenPayload = await fetchJson(fetchImpl, `${OPEN_BASE_URLS[input.domain]}/open-apis/auth/v3/tenant_access_token/internal`, {
@@ -184,29 +215,53 @@ async function fetchJson(fetchImpl, url, init = {}) {
184
215
  }
185
216
  }
186
217
  function formatPayloadError(payload) {
187
- const data = responseData(payload);
188
218
  const message = [
189
- stringOrNull(data.error) ?? stringOrNull(payload.error),
190
- stringOrNull(data.error_description) ?? stringOrNull(payload.error_description),
191
- stringOrNull(data.message) ?? stringOrNull(payload.message),
192
- stringOrNull(data.msg) ?? stringOrNull(payload.msg),
193
- typeof data.code === "number" || typeof data.code === "string" ? `code ${data.code}` : null,
219
+ stringOrNull(payload.error),
220
+ stringOrNull(payload.error_description),
221
+ stringOrNull(payload.message),
222
+ stringOrNull(payload.msg),
194
223
  typeof payload.code === "number" || typeof payload.code === "string" ? `code ${payload.code}` : null
195
224
  ].filter(Boolean).join(": ");
196
225
  return message ? ` (${message})` : "";
197
226
  }
198
- function appendQrTrackingParams(value) {
199
- if (!value) {
200
- return "";
227
+ function mapSdkError(error) {
228
+ const code = isObject(error) ? stringOrNull(error.code) : null;
229
+ const description = isObject(error) ? stringOrNull(error.description) : null;
230
+ if (code === "expired_token") {
231
+ return { status: "expired", message: "Lark QR setup expired. Start a new setup." };
232
+ }
233
+ if (code === "access_denied") {
234
+ return { status: "failed", message: "Lark QR setup was denied." };
201
235
  }
236
+ if (code === "abort") {
237
+ return { status: "failed", message: "Lark QR setup was cancelled." };
238
+ }
239
+ if (error instanceof Error) {
240
+ return { status: "failed", message: error.message };
241
+ }
242
+ return {
243
+ status: "failed",
244
+ message: description ?? (code ? `Lark QR setup failed: ${code}` : "Lark QR setup failed.")
245
+ };
246
+ }
247
+ function formatSdkStatus(info) {
248
+ if (info.status === "domain_switched") {
249
+ return "Detected a Lark tenant; continuing setup on the Lark domain.";
250
+ }
251
+ if (info.status === "slow_down") {
252
+ return info.interval
253
+ ? `Lark requested slower setup polling; retrying every ${info.interval} seconds.`
254
+ : "Lark requested slower setup polling.";
255
+ }
256
+ return undefined;
257
+ }
258
+ function extractUserCode(qrUrl) {
202
259
  try {
203
- const url = new URL(value);
204
- url.searchParams.set("from", "hermes");
205
- url.searchParams.set("tp", "hermes");
206
- return url.toString();
260
+ const value = new URL(qrUrl).searchParams.get("user_code");
261
+ return stringOrNull(value);
207
262
  }
208
263
  catch {
209
- return `${value}${value.includes("?") ? "&" : "?"}from=hermes&tp=hermes`;
264
+ return null;
210
265
  }
211
266
  }
212
267
  function positiveNumberOr(input, fallback) {
@@ -1,4 +1,3 @@
1
- import { randomInt } from "node:crypto";
2
1
  import { readFile } from "node:fs/promises";
3
2
  import { CORE_VCM_ROLE_DEFINITIONS, GATE_REVIEWER_ROLE_DEFINITION, VCM_ROLE_NAMES } from "../../shared/constants.js";
4
3
  import { VcmError } from "../errors.js";
@@ -8,7 +7,6 @@ import { parseAssistantContent, resolveExistingClaudeTranscriptPath } from "../s
8
7
  import { parseGatewayCommand } from "./gateway-command-parser.js";
9
8
  import { createLarkRegistrationClient } from "./channels/lark-registration.js";
10
9
  const QR_LOGIN_TTL_MS = 8 * 60 * 1000;
11
- const PAIRING_CODE_TTL_MS = 10 * 60 * 1000;
12
10
  const CLOSE_CONFIRM_TTL_MS = 10 * 60 * 1000;
13
11
  const POLL_ERROR_BACKOFF_MS = 2_000;
14
12
  const POLL_LONG_BACKOFF_MS = 30_000;
@@ -203,37 +201,10 @@ export function createGatewayService(deps) {
203
201
  });
204
202
  return;
205
203
  }
206
- if (settings.channel === "lark" && !settings.binding.boundUserId) {
207
- settings = await saveInboundMetadata(settings, update);
208
- const pairing = parseLarkPairingCommand(update.text);
209
- if (!pairing || !isValidPairingCode(settings, pairing)) {
210
- await reply(settings, update.fromUserId, "Lark Gateway is not paired. Generate a pairing code in desktop VCM, then send /bind CODE here.");
211
- await recordMessageStatus("inbound", "ignored", update.text, "lark gateway not paired");
212
- return;
213
- }
214
- await deps.settings.saveSettings({
215
- ...settings,
216
- binding: {
217
- ...settings.binding,
218
- boundUserId: update.fromUserId,
219
- loginUserId: update.fromUserId,
220
- pairingCode: null,
221
- pairingCodeExpiresAt: null,
222
- chatIds: update.chatId
223
- ? {
224
- ...settings.binding.chatIds,
225
- [update.fromUserId]: update.chatId
226
- }
227
- : settings.binding.chatIds
228
- },
229
- updatedAt: now()
230
- });
231
- await reply(await deps.settings.loadSettings(), update.fromUserId, "Lark Gateway bound. Send /help for available commands.");
232
- await recordMessageStatus("inbound", "ok", update.text, undefined, "bind");
233
- return;
234
- }
235
- settings = await saveInboundMetadata(settings, update, { bindIfMissing: true });
236
- if (settings.binding.boundUserId !== update.fromUserId) {
204
+ settings = await saveInboundMetadata(settings, update, {
205
+ bind: settings.channel === "lark" ? "always" : "if-missing"
206
+ });
207
+ if (settings.channel !== "lark" && settings.binding.boundUserId !== update.fromUserId) {
237
208
  await reply(settings, update.fromUserId, "This VCM gateway is already bound to another user.");
238
209
  await recordMessageStatus("inbound", "ignored", update.text, "unbound user");
239
210
  return;
@@ -268,11 +239,19 @@ export function createGatewayService(deps) {
268
239
  }
269
240
  }
270
241
  async function saveInboundMetadata(settings, update, options = {}) {
242
+ const bindMode = options.bind ?? "never";
271
243
  return deps.settings.saveSettings({
272
244
  ...settings,
273
245
  binding: {
274
246
  ...settings.binding,
275
- boundUserId: options.bindIfMissing ? settings.binding.boundUserId ?? update.fromUserId : settings.binding.boundUserId,
247
+ boundUserId: bindMode === "always"
248
+ ? update.fromUserId
249
+ : bindMode === "if-missing"
250
+ ? settings.binding.boundUserId ?? update.fromUserId
251
+ : settings.binding.boundUserId,
252
+ loginUserId: bindMode === "always"
253
+ ? update.fromUserId
254
+ : settings.binding.loginUserId,
276
255
  contextTokens: update.contextToken
277
256
  ? {
278
257
  ...settings.binding.contextTokens,
@@ -732,24 +711,18 @@ export function createGatewayService(deps) {
732
711
  async function statusText(settings) {
733
712
  const synced = await syncDesktopContext(settings);
734
713
  const project = await deps.projectService.getCurrentProject();
714
+ const bindingLine = synced.channel === "lark"
715
+ ? `Active Lark chat: ${synced.binding.boundUserId ? "yes" : "none"}`
716
+ : `Binding: ${synced.binding.boundUserId ? "bound" : "not bound"}`;
735
717
  return [
736
718
  `Gateway: ${synced.enabled ? "on" : "off"}${isRunning() ? " / polling" : ""}`,
737
- `Binding: ${synced.binding.boundUserId ? "bound" : "not bound"}`,
719
+ bindingLine,
738
720
  `Translation: ${synced.translationEnabled ? "on" : "off"}`,
739
721
  `Project: ${project?.repoRoot ?? synced.currentProjectId ?? "none"}`,
740
722
  `Task: ${synced.currentTaskSlug ?? "none"}`,
741
723
  `Last poll: ${synced.lastPollStatus.state}${synced.lastPollStatus.error ? ` (${synced.lastPollStatus.error})` : ""}`
742
724
  ].join("\n");
743
725
  }
744
- function parseLarkPairingCommand(text) {
745
- const match = text.trim().match(/^\/bind\s+([A-Z0-9]{6,12})$/i);
746
- return match?.[1]?.toUpperCase() ?? null;
747
- }
748
- function isValidPairingCode(settings, code) {
749
- const expected = settings.binding.pairingCode?.toUpperCase();
750
- const expiresAt = settings.binding.pairingCodeExpiresAt;
751
- return Boolean(expected && expected === code.toUpperCase() && expiresAt && Date.parse(expiresAt) > Date.now());
752
- }
753
726
  return {
754
727
  async start() {
755
728
  await ensurePolling();
@@ -789,40 +762,6 @@ export function createGatewayService(deps) {
789
762
  const settings = await deps.settings.resetBinding();
790
763
  return deps.settings.expose(settings, isRunning());
791
764
  },
792
- async createPairingCode() {
793
- const settings = await deps.settings.loadSettings();
794
- if (settings.channel !== "lark") {
795
- throw new VcmError({
796
- code: "GATEWAY_PAIRING_UNSUPPORTED",
797
- message: "Pairing codes are only available for Lark Gateway.",
798
- statusCode: 409
799
- });
800
- }
801
- if (!settings.binding.appId || !settings.binding.appSecret) {
802
- throw new VcmError({
803
- code: "GATEWAY_LARK_CONFIG_MISSING",
804
- message: "Complete Lark QR Setup before creating a pairing code.",
805
- statusCode: 409
806
- });
807
- }
808
- const code = randomPairingCode();
809
- const expiresAt = new Date(Date.now() + PAIRING_CODE_TTL_MS).toISOString();
810
- const nextSettings = await deps.settings.saveSettings({
811
- ...settings,
812
- binding: {
813
- ...settings.binding,
814
- pairingCode: code,
815
- pairingCodeExpiresAt: expiresAt
816
- },
817
- updatedAt: now()
818
- });
819
- await ensurePolling();
820
- return {
821
- code,
822
- expiresAt,
823
- status: deps.settings.expose(nextSettings, isRunning())
824
- };
825
- },
826
765
  async startQrLogin() {
827
766
  const settings = await deps.settings.loadSettings();
828
767
  const channel = resolveChannel(settings);
@@ -979,8 +918,6 @@ export function createGatewayService(deps) {
979
918
  larkOpenId: result.openId ?? null,
980
919
  larkBotName: result.botName ?? null,
981
920
  larkBotOpenId: result.botOpenId ?? null,
982
- pairingCode: null,
983
- pairingCodeExpiresAt: null,
984
921
  getUpdatesBuf: ""
985
922
  },
986
923
  lastPollStatus: {
@@ -1322,14 +1259,6 @@ function formatAheadBehind(project) {
1322
1259
  }
1323
1260
  return ahead > 0 ? `ahead ${ahead}` : `behind ${behind}`;
1324
1261
  }
1325
- function randomPairingCode() {
1326
- const alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
1327
- let out = "";
1328
- for (let index = 0; index < 8; index += 1) {
1329
- out += alphabet[randomInt(alphabet.length)];
1330
- }
1331
- return out;
1332
- }
1333
1262
  function sleep(ms, signal) {
1334
1263
  return new Promise((resolve) => {
1335
1264
  if (signal.aborted) {
@@ -101,8 +101,7 @@ export function createGatewaySettingsService(deps) {
101
101
  larkOpenId: settings.binding.larkOpenId,
102
102
  larkBotName: settings.binding.larkBotName,
103
103
  larkBotOpenId: settings.binding.larkBotOpenId,
104
- homeChatId: settings.binding.homeChatId,
105
- pairingCodeExpiresAt: settings.binding.pairingCodeExpiresAt
104
+ homeChatId: settings.binding.homeChatId
106
105
  },
107
106
  pendingConfirmations: settings.pendingConfirmations,
108
107
  lastPollStatus: settings.lastPollStatus,
@@ -152,8 +151,6 @@ export function normalizeSettings(input, timestamp, options = {
152
151
  larkBotName: normalizeNullableString(bindingInput.larkBotName),
153
152
  larkBotOpenId: normalizeNullableString(bindingInput.larkBotOpenId),
154
153
  homeChatId: normalizeNullableString(bindingInput.homeChatId),
155
- pairingCode: normalizeNullableString(bindingInput.pairingCode),
156
- pairingCodeExpiresAt: normalizeNullableString(bindingInput.pairingCodeExpiresAt),
157
154
  getUpdatesBuf: typeof bindingInput.getUpdatesBuf === "string" ? bindingInput.getUpdatesBuf : "",
158
155
  contextTokens: isObject(bindingInput.contextTokens)
159
156
  ? normalizeStringRecord(bindingInput.contextTokens)
@@ -191,8 +188,6 @@ function createDefaultBinding(defaultBaseUrl = DEFAULT_BASE_URL) {
191
188
  larkBotName: null,
192
189
  larkBotOpenId: null,
193
190
  homeChatId: null,
194
- pairingCode: null,
195
- pairingCodeExpiresAt: null,
196
191
  getUpdatesBuf: "",
197
192
  contextTokens: {},
198
193
  chatIds: {}