vibe-coding-master 0.4.32 → 0.4.34

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, and Lark pairing.
224
+ - `Gateway`: Weixin iLink or Lark binding, Gateway on/off, Gateway translation, QR login/setup, and Lark pairing.
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.
@@ -275,14 +275,15 @@ The `Gateway` toggle is disabled until a QR login has produced a usable iLink to
275
275
 
276
276
  ### Bind Lark
277
277
 
278
- 1. Create a Lark app with bot capability, message receive events, and WebSocket event delivery.
279
- 2. Open the sidebar `Gateway` section and select `Lark`.
280
- 3. Save the Lark App ID and App Secret.
281
- 4. Click `Create Pairing Code`.
282
- 5. Send `/bind CODE` to the Lark bot before the code expires.
283
- 6. After binding succeeds, turn `Gateway` on in the sidebar.
278
+ 1. Open the sidebar `Gateway` section and select `Lark`.
279
+ 2. Click `Start QR Setup`.
280
+ 3. Scan the QR code with Lark and approve bot creation.
281
+ 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.
284
285
 
285
- Lark credentials stay in local VCM state. The App Secret is not shown again 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. `Reset Binding` clears the bound Lark user/chat state while keeping the saved Lark app credentials.
286
287
 
287
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.
288
289
 
@@ -377,7 +378,7 @@ Typical mobile flow:
377
378
  - If the QR dialog does not appear, refresh the page and click `Start QR Login` again.
378
379
  - If the QR status stays `wait`, confirm the login on the phone and click `Confirm` again.
379
380
  - If the QR code expires, start a new QR login.
380
- - If `Gateway` cannot be enabled, bind Weixin or pair Lark first.
381
+ - If `Gateway` cannot be enabled, bind Weixin or complete Lark QR setup and pairing first.
381
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.
382
383
  - If PM messages or task-changing commands are rejected, check that Gateway is on.
383
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.
@@ -14,6 +14,12 @@ export function registerGatewayRoutes(app, deps) {
14
14
  app.post("/api/gateway/qr/check", async (request) => {
15
15
  return deps.gatewayService.checkQrLogin(request.body);
16
16
  });
17
+ app.post("/api/gateway/lark-registration/start", async () => {
18
+ return deps.gatewayService.startLarkRegistration();
19
+ });
20
+ app.post("/api/gateway/lark-registration/check", async () => {
21
+ return deps.gatewayService.checkLarkRegistration();
22
+ });
17
23
  app.post("/api/gateway/binding/reset", async () => {
18
24
  return deps.gatewayService.resetBinding();
19
25
  });
@@ -4,7 +4,7 @@ const DEFAULT_LONG_POLL_TIMEOUT_MS = 35_000;
4
4
  export function createLarkChannel(options = {}) {
5
5
  let connection = null;
6
6
  async function ensureConnection(input) {
7
- const key = `${input.appId}:${input.appSecret}`;
7
+ const key = `${input.domain}:${input.appId}:${input.appSecret}`;
8
8
  if (connection?.key === key) {
9
9
  return connection;
10
10
  }
@@ -24,7 +24,7 @@ export function createLarkChannel(options = {}) {
24
24
  const client = new Lark.Client({
25
25
  appId: input.appId,
26
26
  appSecret: input.appSecret,
27
- domain: options.domain ?? "lark"
27
+ domain: input.domain
28
28
  });
29
29
  const dispatcher = new Lark.EventDispatcher({}).register({
30
30
  "im.message.receive_v1": async (data) => {
@@ -40,7 +40,7 @@ export function createLarkChannel(options = {}) {
40
40
  const wsClient = new Lark.WSClient({
41
41
  appId: input.appId,
42
42
  appSecret: input.appSecret,
43
- domain: options.domain ?? "lark",
43
+ domain: input.domain,
44
44
  source: "vcm-gateway",
45
45
  autoReconnect: true,
46
46
  loggerLevel: options.loggerLevel,
@@ -75,10 +75,11 @@ export function createLarkChannel(options = {}) {
75
75
  async getUpdates(input) {
76
76
  const appId = input.account.appId?.trim();
77
77
  const appSecret = input.account.appSecret?.trim();
78
+ const domain = input.account.larkDomain ?? options.domain ?? "lark";
78
79
  if (!appId || !appSecret) {
79
80
  throw new Error("Lark App ID and App Secret are required.");
80
81
  }
81
- const state = await ensureConnection({ appId, appSecret });
82
+ const state = await ensureConnection({ appId, appSecret, domain });
82
83
  await state.ready;
83
84
  if (state.error) {
84
85
  throw state.error;
@@ -97,10 +98,11 @@ export function createLarkChannel(options = {}) {
97
98
  async sendText(input) {
98
99
  const appId = input.account.appId?.trim();
99
100
  const appSecret = input.account.appSecret?.trim();
101
+ const domain = input.account.larkDomain ?? options.domain ?? "lark";
100
102
  if (!appId || !appSecret) {
101
103
  throw new Error("Lark App ID and App Secret are required.");
102
104
  }
103
- const state = await ensureConnection({ appId, appSecret });
105
+ const state = await ensureConnection({ appId, appSecret, domain });
104
106
  await state.ready;
105
107
  if (!state.client) {
106
108
  throw new Error("Lark client is not initialized.");
@@ -0,0 +1,220 @@
1
+ const ACCOUNTS_BASE_URLS = {
2
+ feishu: "https://accounts.feishu.cn",
3
+ lark: "https://accounts.larksuite.com"
4
+ };
5
+ const OPEN_BASE_URLS = {
6
+ feishu: "https://open.feishu.cn",
7
+ lark: "https://open.larksuite.com"
8
+ };
9
+ const REGISTRATION_PATH = "/oauth/v1/app/registration";
10
+ const REQUEST_TIMEOUT_MS = 10_000;
11
+ export function createLarkRegistrationClient(deps = {}) {
12
+ 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)
21
+ });
22
+ return payload;
23
+ }
24
+ 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
+ }
34
+ },
35
+ 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"
41
+ });
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
+ }
51
+ return {
52
+ domain,
53
+ deviceCode,
54
+ qrUrl,
55
+ userCode: stringOrNull(data.user_code),
56
+ intervalSeconds: positiveNumberOr(data.interval, 5),
57
+ expiresInSeconds: positiveNumberOr(data.expire_in, 600)
58
+ };
59
+ },
60
+ 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
+ });
78
+ return {
79
+ status: "confirmed",
80
+ appId,
81
+ appSecret,
82
+ domain,
83
+ openId: stringOrNull(userInfo.open_id),
84
+ botName: bot.botName,
85
+ botOpenId: bot.botOpenId
86
+ };
87
+ }
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." };
91
+ }
92
+ if (error === "access_denied") {
93
+ return { status: "failed", message: "Lark QR setup was denied." };
94
+ }
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
+ return {
102
+ status: "wait",
103
+ message: error && error !== "authorization_pending"
104
+ ? [error, errorDescription].filter(Boolean).join(": ")
105
+ : undefined
106
+ };
107
+ }
108
+ };
109
+ }
110
+ function responseData(payload) {
111
+ return isObject(payload.data) ? payload.data : payload;
112
+ }
113
+ async function probeBot(fetchImpl, input) {
114
+ try {
115
+ const tokenPayload = await fetchJson(fetchImpl, `${OPEN_BASE_URLS[input.domain]}/open-apis/auth/v3/tenant_access_token/internal`, {
116
+ method: "POST",
117
+ headers: {
118
+ "content-type": "application/json"
119
+ },
120
+ body: JSON.stringify({
121
+ app_id: input.appId,
122
+ app_secret: input.appSecret
123
+ })
124
+ });
125
+ const accessToken = stringOrNull(tokenPayload.tenant_access_token);
126
+ if (!accessToken) {
127
+ return { botName: null, botOpenId: null };
128
+ }
129
+ const botPayload = await fetchJson(fetchImpl, `${OPEN_BASE_URLS[input.domain]}/open-apis/bot/v3/info`, {
130
+ headers: {
131
+ authorization: `Bearer ${accessToken}`,
132
+ "content-type": "application/json"
133
+ }
134
+ });
135
+ if (botPayload.code !== 0) {
136
+ return { botName: null, botOpenId: null };
137
+ }
138
+ const bot = isObject(botPayload.bot)
139
+ ? botPayload.bot
140
+ : isObject(botPayload.data) && isObject(botPayload.data.bot)
141
+ ? botPayload.data.bot
142
+ : {};
143
+ return {
144
+ botName: stringOrNull(bot.app_name) ?? stringOrNull(bot.bot_name),
145
+ botOpenId: stringOrNull(bot.open_id)
146
+ };
147
+ }
148
+ catch {
149
+ return { botName: null, botOpenId: null };
150
+ }
151
+ }
152
+ async function fetchJson(fetchImpl, url, init = {}) {
153
+ const controller = new AbortController();
154
+ const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
155
+ try {
156
+ const response = await fetchImpl(url, {
157
+ ...init,
158
+ signal: controller.signal
159
+ });
160
+ const text = await response.text();
161
+ if (!text) {
162
+ if (!response.ok) {
163
+ throw new Error(`${response.status} ${response.statusText}`);
164
+ }
165
+ return {};
166
+ }
167
+ const payload = JSON.parse(text);
168
+ if (!isObject(payload)) {
169
+ throw new Error("Lark setup response was not a JSON object.");
170
+ }
171
+ if (!response.ok && !payload.error) {
172
+ throw new Error(`Lark QR setup request failed: ${response.status} ${response.statusText}${formatPayloadError(payload)}`);
173
+ }
174
+ return payload;
175
+ }
176
+ catch (error) {
177
+ if (error instanceof Error && error.name === "AbortError") {
178
+ throw new Error("Lark QR setup request timed out.");
179
+ }
180
+ throw error;
181
+ }
182
+ finally {
183
+ clearTimeout(timeout);
184
+ }
185
+ }
186
+ function formatPayloadError(payload) {
187
+ const data = responseData(payload);
188
+ 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,
194
+ typeof payload.code === "number" || typeof payload.code === "string" ? `code ${payload.code}` : null
195
+ ].filter(Boolean).join(": ");
196
+ return message ? ` (${message})` : "";
197
+ }
198
+ function appendQrTrackingParams(value) {
199
+ if (!value) {
200
+ return "";
201
+ }
202
+ try {
203
+ const url = new URL(value);
204
+ url.searchParams.set("from", "hermes");
205
+ url.searchParams.set("tp", "hermes");
206
+ return url.toString();
207
+ }
208
+ catch {
209
+ return `${value}${value.includes("?") ? "&" : "?"}from=hermes&tp=hermes`;
210
+ }
211
+ }
212
+ function positiveNumberOr(input, fallback) {
213
+ return typeof input === "number" && Number.isFinite(input) && input > 0 ? input : fallback;
214
+ }
215
+ function stringOrNull(input) {
216
+ return typeof input === "string" && input.trim() ? input.trim() : null;
217
+ }
218
+ function isObject(input) {
219
+ return Boolean(input && typeof input === "object" && !Array.isArray(input));
220
+ }
@@ -6,6 +6,7 @@ import { submitTerminalInput } from "../runtime/terminal-submit.js";
6
6
  import { getTaskRuntimeRepoRoot } from "../services/task-service.js";
7
7
  import { parseAssistantContent, resolveExistingClaudeTranscriptPath } from "../services/claude-transcript-service.js";
8
8
  import { parseGatewayCommand } from "./gateway-command-parser.js";
9
+ import { createLarkRegistrationClient } from "./channels/lark-registration.js";
9
10
  const QR_LOGIN_TTL_MS = 8 * 60 * 1000;
10
11
  const PAIRING_CODE_TTL_MS = 10 * 60 * 1000;
11
12
  const CLOSE_CONFIRM_TTL_MS = 10 * 60 * 1000;
@@ -13,6 +14,7 @@ const POLL_ERROR_BACKOFF_MS = 2_000;
13
14
  const POLL_LONG_BACKOFF_MS = 30_000;
14
15
  const MAX_FAILURES_BEFORE_LONG_BACKOFF = 3;
15
16
  const DEFAULT_POLL_TIMEOUT_MS = 35_000;
17
+ const LARK_REGISTRATION_CONFIRM_TIMEOUT_MS = 15_000;
16
18
  const MAX_LATEST_PM_REPLY_CHARS = 8_000;
17
19
  const GATEWAY_TRANSLATION_FAILURE_TEXT = "PM 回复已收到,但翻译失败。\n发送 /retry 重新翻译。";
18
20
  const COMMANDS_ALLOWED_WHEN_DISABLED = new Set([
@@ -24,10 +26,12 @@ const COMMANDS_ALLOWED_WHEN_DISABLED = new Set([
24
26
  ]);
25
27
  export function createGatewayService(deps) {
26
28
  const now = deps.now ?? (() => new Date().toISOString());
29
+ const larkRegistration = deps.larkRegistration ?? createLarkRegistrationClient();
27
30
  let pollAbort = null;
28
31
  let pollLoopPromise = null;
29
32
  let pollStartingPromise = null;
30
33
  let qrLogin = null;
34
+ let larkRegistrationState = null;
31
35
  let lastFailedTranslation = null;
32
36
  function isRunning() {
33
37
  return Boolean(pollAbort && !pollAbort.signal.aborted);
@@ -90,6 +94,7 @@ export function createGatewayService(deps) {
90
94
  token: settings.binding.token,
91
95
  appId: settings.binding.appId,
92
96
  appSecret: settings.binding.appSecret,
97
+ larkDomain: settings.binding.larkDomain,
93
98
  homeChatId: settings.binding.homeChatId
94
99
  };
95
100
  }
@@ -633,7 +638,7 @@ export function createGatewayService(deps) {
633
638
  const settings = await deps.settings.loadSettings();
634
639
  if (!toAccount(settings)) {
635
640
  return settings.channel === "lark"
636
- ? "Gateway is not configured. Save Lark App ID and App Secret in desktop VCM first."
641
+ ? "Gateway is not configured. Complete Lark QR Setup in desktop VCM first."
637
642
  : "Gateway is not bound. Start QR login from desktop VCM first.";
638
643
  }
639
644
  if (settings.enabled) {
@@ -758,7 +763,13 @@ export function createGatewayService(deps) {
758
763
  return deps.settings.expose(settings, isRunning());
759
764
  },
760
765
  async updateSettings(input) {
761
- let settings = await deps.settings.updateSettings(input);
766
+ const updateInput = input.channel && input.baseUrl === undefined
767
+ ? {
768
+ ...input,
769
+ baseUrl: deps.channels.get(input.channel).defaultBaseUrl
770
+ }
771
+ : input;
772
+ let settings = await deps.settings.updateSettings(updateInput);
762
773
  if (settings.enabled) {
763
774
  settings = await syncDesktopContext(settings);
764
775
  }
@@ -773,6 +784,8 @@ export function createGatewayService(deps) {
773
784
  async resetBinding() {
774
785
  await stopPolling();
775
786
  lastFailedTranslation = null;
787
+ qrLogin = null;
788
+ larkRegistrationState = null;
776
789
  const settings = await deps.settings.resetBinding();
777
790
  return deps.settings.expose(settings, isRunning());
778
791
  },
@@ -788,7 +801,7 @@ export function createGatewayService(deps) {
788
801
  if (!settings.binding.appId || !settings.binding.appSecret) {
789
802
  throw new VcmError({
790
803
  code: "GATEWAY_LARK_CONFIG_MISSING",
791
- message: "Save Lark App ID and App Secret before creating a pairing code.",
804
+ message: "Complete Lark QR Setup before creating a pairing code.",
792
805
  statusCode: 409
793
806
  });
794
807
  }
@@ -893,6 +906,103 @@ export function createGatewayService(deps) {
893
906
  loginUserId: result.loginUserId
894
907
  };
895
908
  },
909
+ async startLarkRegistration() {
910
+ await stopPolling();
911
+ const settings = await deps.settings.updateSettings({
912
+ channel: "lark",
913
+ baseUrl: deps.channels.get("lark").defaultBaseUrl
914
+ });
915
+ const domain = "lark";
916
+ await larkRegistration.init(domain);
917
+ const begin = await larkRegistration.begin(domain);
918
+ larkRegistrationState = {
919
+ domain: begin.domain,
920
+ deviceCode: begin.deviceCode,
921
+ qrUrl: begin.qrUrl,
922
+ userCode: begin.userCode,
923
+ intervalSeconds: begin.intervalSeconds,
924
+ expiresAt: new Date(Date.now() + begin.expiresInSeconds * 1000).toISOString()
925
+ };
926
+ return {
927
+ status: "wait",
928
+ qrUrl: begin.qrUrl,
929
+ userCode: begin.userCode,
930
+ intervalSeconds: begin.intervalSeconds,
931
+ expiresAt: larkRegistrationState.expiresAt
932
+ };
933
+ },
934
+ async checkLarkRegistration() {
935
+ if (!larkRegistrationState || Date.parse(larkRegistrationState.expiresAt) < Date.now()) {
936
+ larkRegistrationState = null;
937
+ return {
938
+ status: "expired",
939
+ message: "Lark QR setup expired. Start a new setup."
940
+ };
941
+ }
942
+ const confirmDeadline = Date.now() + LARK_REGISTRATION_CONFIRM_TIMEOUT_MS;
943
+ let result = await larkRegistration.poll({
944
+ domain: larkRegistrationState.domain,
945
+ deviceCode: larkRegistrationState.deviceCode
946
+ });
947
+ while (result.status === "wait" && Date.now() < confirmDeadline) {
948
+ await sleep(Math.min(larkRegistrationState.intervalSeconds * 1000, 3_000), new AbortController().signal);
949
+ result = await larkRegistration.poll({
950
+ domain: larkRegistrationState.domain,
951
+ deviceCode: larkRegistrationState.deviceCode
952
+ });
953
+ }
954
+ if (result.status !== "confirmed") {
955
+ if (result.status === "expired" || result.status === "failed") {
956
+ larkRegistrationState = null;
957
+ }
958
+ return {
959
+ status: result.status,
960
+ message: result.message ?? (result.status === "wait" ? "Lark has not returned app credentials yet. Confirm again in a moment." : undefined)
961
+ };
962
+ }
963
+ if (!result.appId || !result.appSecret || !result.domain) {
964
+ larkRegistrationState = null;
965
+ return {
966
+ status: "failed",
967
+ message: "Lark QR setup completed without app credentials."
968
+ };
969
+ }
970
+ const current = await deps.settings.loadSettings();
971
+ const settings = await deps.settings.saveSettings({
972
+ ...current,
973
+ channel: "lark",
974
+ binding: {
975
+ ...current.binding,
976
+ appId: result.appId,
977
+ appSecret: result.appSecret,
978
+ larkDomain: result.domain,
979
+ larkOpenId: result.openId ?? null,
980
+ larkBotName: result.botName ?? null,
981
+ larkBotOpenId: result.botOpenId ?? null,
982
+ pairingCode: null,
983
+ pairingCodeExpiresAt: null,
984
+ getUpdatesBuf: ""
985
+ },
986
+ lastPollStatus: {
987
+ state: "idle",
988
+ checkedAt: now()
989
+ },
990
+ updatedAt: now()
991
+ });
992
+ larkRegistrationState = null;
993
+ await ensurePolling();
994
+ const status = deps.settings.expose(settings, isRunning());
995
+ return {
996
+ status: "confirmed",
997
+ appIdConfigured: Boolean(result.appId),
998
+ appSecretConfigured: Boolean(result.appSecret),
999
+ larkDomain: result.domain,
1000
+ larkOpenId: result.openId ?? null,
1001
+ larkBotName: result.botName ?? null,
1002
+ larkBotOpenId: result.botOpenId ?? null,
1003
+ gatewayStatus: status
1004
+ };
1005
+ },
896
1006
  async handlePmStop(input) {
897
1007
  if (input.session.role !== "project-manager") {
898
1008
  return;
@@ -1238,7 +1348,10 @@ function normalizeBaseUrl(input, fallback) {
1238
1348
  if (!trimmed) {
1239
1349
  return fallback;
1240
1350
  }
1241
- if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) {
1351
+ if (/^https:\/\/lark:\/\//i.test(trimmed)) {
1352
+ return trimmed.replace(/^https:\/\//i, "").replace(/\/+$/, "");
1353
+ }
1354
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)) {
1242
1355
  return trimmed.replace(/\/+$/, "");
1243
1356
  }
1244
1357
  return `https://${trimmed.replace(/\/+$/, "")}`;
@@ -45,10 +45,7 @@ export function createGatewaySettingsService(deps) {
45
45
  ...current.binding,
46
46
  baseUrl: input.baseUrl !== undefined
47
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
48
+ : current.binding.baseUrl
52
49
  },
53
50
  updatedAt: now()
54
51
  });
@@ -64,6 +61,10 @@ export function createGatewaySettingsService(deps) {
64
61
  baseUrl: current.binding.baseUrl || normalizeOptions.defaultBaseUrl,
65
62
  appId: current.binding.appId,
66
63
  appSecret: current.binding.appSecret,
64
+ larkDomain: current.binding.larkDomain,
65
+ larkOpenId: current.binding.larkOpenId,
66
+ larkBotName: current.binding.larkBotName,
67
+ larkBotOpenId: current.binding.larkBotOpenId,
67
68
  homeChatId: current.binding.homeChatId
68
69
  },
69
70
  dedupe: {
@@ -96,6 +97,10 @@ export function createGatewaySettingsService(deps) {
96
97
  appId: settings.binding.appId,
97
98
  appIdConfigured: Boolean(settings.binding.appId),
98
99
  appSecretConfigured: Boolean(settings.binding.appSecret),
100
+ larkDomain: settings.binding.larkDomain,
101
+ larkOpenId: settings.binding.larkOpenId,
102
+ larkBotName: settings.binding.larkBotName,
103
+ larkBotOpenId: settings.binding.larkBotOpenId,
99
104
  homeChatId: settings.binding.homeChatId,
100
105
  pairingCodeExpiresAt: settings.binding.pairingCodeExpiresAt
101
106
  },
@@ -142,6 +147,10 @@ export function normalizeSettings(input, timestamp, options = {
142
147
  token: normalizeNullableString(bindingInput.token),
143
148
  appId: normalizeNullableString(bindingInput.appId),
144
149
  appSecret: normalizeNullableString(bindingInput.appSecret),
150
+ larkDomain: normalizeLarkDomain(bindingInput.larkDomain),
151
+ larkOpenId: normalizeNullableString(bindingInput.larkOpenId),
152
+ larkBotName: normalizeNullableString(bindingInput.larkBotName),
153
+ larkBotOpenId: normalizeNullableString(bindingInput.larkBotOpenId),
145
154
  homeChatId: normalizeNullableString(bindingInput.homeChatId),
146
155
  pairingCode: normalizeNullableString(bindingInput.pairingCode),
147
156
  pairingCodeExpiresAt: normalizeNullableString(bindingInput.pairingCodeExpiresAt),
@@ -177,6 +186,10 @@ function createDefaultBinding(defaultBaseUrl = DEFAULT_BASE_URL) {
177
186
  token: null,
178
187
  appId: null,
179
188
  appSecret: null,
189
+ larkDomain: null,
190
+ larkOpenId: null,
191
+ larkBotName: null,
192
+ larkBotOpenId: null,
180
193
  homeChatId: null,
181
194
  pairingCode: null,
182
195
  pairingCodeExpiresAt: null,
@@ -188,6 +201,9 @@ function createDefaultBinding(defaultBaseUrl = DEFAULT_BASE_URL) {
188
201
  function normalizeGatewayChannel(input, fallback) {
189
202
  return input === "weixin-ilink" || input === "lark" ? input : fallback;
190
203
  }
204
+ function normalizeLarkDomain(input) {
205
+ return input === "lark" || input === "feishu" ? input : null;
206
+ }
191
207
  function normalizeCloseTaskConfirmation(input) {
192
208
  if (!isObject(input)) {
193
209
  return null;
@@ -273,7 +289,10 @@ function normalizeBaseUrl(input, fallback = DEFAULT_BASE_URL) {
273
289
  if (!raw) {
274
290
  return fallback;
275
291
  }
276
- if (raw.startsWith("http://") || raw.startsWith("https://")) {
292
+ if (/^https:\/\/lark:\/\//i.test(raw)) {
293
+ return raw.replace(/^https:\/\//i, "").replace(/\/+$/, "");
294
+ }
295
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(raw)) {
277
296
  return raw.replace(/\/+$/, "");
278
297
  }
279
298
  return `https://${raw.replace(/\/+$/, "")}`;