vibe-coding-master 0.4.33 → 0.4.35
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 +16 -16
- package/dist/backend/api/gateway-routes.js +0 -3
- package/dist/backend/gateway/channels/lark-registration.js +39 -13
- package/dist/backend/gateway/gateway-service.js +32 -91
- package/dist/backend/gateway/gateway-settings-service.js +5 -7
- package/dist-frontend/assets/{index-B0hNcYoB.js → index-XvzXPPEy.js} +42 -42
- package/dist-frontend/index.html +1 -1
- package/docs/gateway-design.md +41 -39
- package/docs/product-design.md +20 -17
- package/package.json +1 -1
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
|
|
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
|
|
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
|
-
-
|
|
247
|
-
-
|
|
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
|
|
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.
|
|
283
|
-
6. Send
|
|
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.
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
382
|
-
- If `/start` or read-only commands do not receive replies, check that the selected channel is connected and
|
|
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
|
|
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
|
});
|
|
@@ -24,8 +24,9 @@ export function createLarkRegistrationClient(deps = {}) {
|
|
|
24
24
|
return {
|
|
25
25
|
async init(domain) {
|
|
26
26
|
const payload = await postRegistration(domain, { action: "init" });
|
|
27
|
-
const
|
|
28
|
-
|
|
27
|
+
const data = responseData(payload);
|
|
28
|
+
const methods = Array.isArray(data.supported_auth_methods)
|
|
29
|
+
? data.supported_auth_methods.filter((value) => typeof value === "string")
|
|
29
30
|
: [];
|
|
30
31
|
if (!methods.includes("client_secret")) {
|
|
31
32
|
throw new Error(`Lark QR setup does not support client_secret auth. Supported methods: ${methods.join(", ") || "none"}.`);
|
|
@@ -38,11 +39,12 @@ export function createLarkRegistrationClient(deps = {}) {
|
|
|
38
39
|
auth_method: "client_secret",
|
|
39
40
|
request_user_info: "open_id"
|
|
40
41
|
});
|
|
41
|
-
const
|
|
42
|
+
const data = responseData(payload);
|
|
43
|
+
const deviceCode = stringOrNull(data.device_code);
|
|
42
44
|
if (!deviceCode) {
|
|
43
45
|
throw new Error("Lark QR setup did not return a device_code.");
|
|
44
46
|
}
|
|
45
|
-
const qrUrl = appendQrTrackingParams(stringOrNull(
|
|
47
|
+
const qrUrl = appendQrTrackingParams(stringOrNull(data.verification_uri_complete) ?? "");
|
|
46
48
|
if (!qrUrl) {
|
|
47
49
|
throw new Error("Lark QR setup did not return a QR URL.");
|
|
48
50
|
}
|
|
@@ -50,9 +52,9 @@ export function createLarkRegistrationClient(deps = {}) {
|
|
|
50
52
|
domain,
|
|
51
53
|
deviceCode,
|
|
52
54
|
qrUrl,
|
|
53
|
-
userCode: stringOrNull(
|
|
54
|
-
intervalSeconds: positiveNumberOr(
|
|
55
|
-
expiresInSeconds: positiveNumberOr(
|
|
55
|
+
userCode: stringOrNull(data.user_code),
|
|
56
|
+
intervalSeconds: positiveNumberOr(data.interval, 5),
|
|
57
|
+
expiresInSeconds: positiveNumberOr(data.expire_in, 600)
|
|
56
58
|
};
|
|
57
59
|
},
|
|
58
60
|
async poll(input) {
|
|
@@ -61,11 +63,12 @@ export function createLarkRegistrationClient(deps = {}) {
|
|
|
61
63
|
device_code: input.deviceCode,
|
|
62
64
|
tp: "ob_app"
|
|
63
65
|
});
|
|
64
|
-
const
|
|
66
|
+
const data = responseData(payload);
|
|
67
|
+
const userInfo = isObject(data.user_info) ? data.user_info : {};
|
|
65
68
|
const tenantBrand = stringOrNull(userInfo.tenant_brand);
|
|
66
69
|
const domain = tenantBrand === "lark" ? "lark" : input.domain;
|
|
67
|
-
const appId = stringOrNull(
|
|
68
|
-
const appSecret = stringOrNull(
|
|
70
|
+
const appId = stringOrNull(data.client_id);
|
|
71
|
+
const appSecret = stringOrNull(data.client_secret);
|
|
69
72
|
if (appId && appSecret) {
|
|
70
73
|
const bot = await probeBot(fetchImpl, {
|
|
71
74
|
appId,
|
|
@@ -82,20 +85,31 @@ export function createLarkRegistrationClient(deps = {}) {
|
|
|
82
85
|
botOpenId: bot.botOpenId
|
|
83
86
|
};
|
|
84
87
|
}
|
|
85
|
-
const error = stringOrNull(payload.error);
|
|
88
|
+
const error = stringOrNull(data.error) ?? stringOrNull(payload.error);
|
|
86
89
|
if (error === "expired_token") {
|
|
87
90
|
return { status: "expired", message: "Lark QR setup expired. Start a new setup." };
|
|
88
91
|
}
|
|
89
92
|
if (error === "access_denied") {
|
|
90
93
|
return { status: "failed", message: "Lark QR setup was denied." };
|
|
91
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);
|
|
92
101
|
return {
|
|
93
102
|
status: "wait",
|
|
94
|
-
message: error && error !== "authorization_pending"
|
|
103
|
+
message: error && error !== "authorization_pending"
|
|
104
|
+
? [error, errorDescription].filter(Boolean).join(": ")
|
|
105
|
+
: undefined
|
|
95
106
|
};
|
|
96
107
|
}
|
|
97
108
|
};
|
|
98
109
|
}
|
|
110
|
+
function responseData(payload) {
|
|
111
|
+
return isObject(payload.data) ? payload.data : payload;
|
|
112
|
+
}
|
|
99
113
|
async function probeBot(fetchImpl, input) {
|
|
100
114
|
try {
|
|
101
115
|
const tokenPayload = await fetchJson(fetchImpl, `${OPEN_BASE_URLS[input.domain]}/open-apis/auth/v3/tenant_access_token/internal`, {
|
|
@@ -155,7 +169,7 @@ async function fetchJson(fetchImpl, url, init = {}) {
|
|
|
155
169
|
throw new Error("Lark setup response was not a JSON object.");
|
|
156
170
|
}
|
|
157
171
|
if (!response.ok && !payload.error) {
|
|
158
|
-
throw new Error(
|
|
172
|
+
throw new Error(`Lark QR setup request failed: ${response.status} ${response.statusText}${formatPayloadError(payload)}`);
|
|
159
173
|
}
|
|
160
174
|
return payload;
|
|
161
175
|
}
|
|
@@ -169,6 +183,18 @@ async function fetchJson(fetchImpl, url, init = {}) {
|
|
|
169
183
|
clearTimeout(timeout);
|
|
170
184
|
}
|
|
171
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
|
+
}
|
|
172
198
|
function appendQrTrackingParams(value) {
|
|
173
199
|
if (!value) {
|
|
174
200
|
return "";
|
|
@@ -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,12 +7,12 @@ 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;
|
|
15
13
|
const MAX_FAILURES_BEFORE_LONG_BACKOFF = 3;
|
|
16
14
|
const DEFAULT_POLL_TIMEOUT_MS = 35_000;
|
|
15
|
+
const LARK_REGISTRATION_CONFIRM_TIMEOUT_MS = 15_000;
|
|
17
16
|
const MAX_LATEST_PM_REPLY_CHARS = 8_000;
|
|
18
17
|
const GATEWAY_TRANSLATION_FAILURE_TEXT = "PM 回复已收到,但翻译失败。\n发送 /retry 重新翻译。";
|
|
19
18
|
const COMMANDS_ALLOWED_WHEN_DISABLED = new Set([
|
|
@@ -202,37 +201,10 @@ export function createGatewayService(deps) {
|
|
|
202
201
|
});
|
|
203
202
|
return;
|
|
204
203
|
}
|
|
205
|
-
|
|
206
|
-
settings
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
await reply(settings, update.fromUserId, "Lark Gateway is not paired. Generate a pairing code in desktop VCM, then send /bind CODE here.");
|
|
210
|
-
await recordMessageStatus("inbound", "ignored", update.text, "lark gateway not paired");
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
await deps.settings.saveSettings({
|
|
214
|
-
...settings,
|
|
215
|
-
binding: {
|
|
216
|
-
...settings.binding,
|
|
217
|
-
boundUserId: update.fromUserId,
|
|
218
|
-
loginUserId: update.fromUserId,
|
|
219
|
-
pairingCode: null,
|
|
220
|
-
pairingCodeExpiresAt: null,
|
|
221
|
-
chatIds: update.chatId
|
|
222
|
-
? {
|
|
223
|
-
...settings.binding.chatIds,
|
|
224
|
-
[update.fromUserId]: update.chatId
|
|
225
|
-
}
|
|
226
|
-
: settings.binding.chatIds
|
|
227
|
-
},
|
|
228
|
-
updatedAt: now()
|
|
229
|
-
});
|
|
230
|
-
await reply(await deps.settings.loadSettings(), update.fromUserId, "Lark Gateway bound. Send /help for available commands.");
|
|
231
|
-
await recordMessageStatus("inbound", "ok", update.text, undefined, "bind");
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
settings = await saveInboundMetadata(settings, update, { bindIfMissing: true });
|
|
235
|
-
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) {
|
|
236
208
|
await reply(settings, update.fromUserId, "This VCM gateway is already bound to another user.");
|
|
237
209
|
await recordMessageStatus("inbound", "ignored", update.text, "unbound user");
|
|
238
210
|
return;
|
|
@@ -267,11 +239,19 @@ export function createGatewayService(deps) {
|
|
|
267
239
|
}
|
|
268
240
|
}
|
|
269
241
|
async function saveInboundMetadata(settings, update, options = {}) {
|
|
242
|
+
const bindMode = options.bind ?? "never";
|
|
270
243
|
return deps.settings.saveSettings({
|
|
271
244
|
...settings,
|
|
272
245
|
binding: {
|
|
273
246
|
...settings.binding,
|
|
274
|
-
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,
|
|
275
255
|
contextTokens: update.contextToken
|
|
276
256
|
? {
|
|
277
257
|
...settings.binding.contextTokens,
|
|
@@ -731,24 +711,18 @@ export function createGatewayService(deps) {
|
|
|
731
711
|
async function statusText(settings) {
|
|
732
712
|
const synced = await syncDesktopContext(settings);
|
|
733
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"}`;
|
|
734
717
|
return [
|
|
735
718
|
`Gateway: ${synced.enabled ? "on" : "off"}${isRunning() ? " / polling" : ""}`,
|
|
736
|
-
|
|
719
|
+
bindingLine,
|
|
737
720
|
`Translation: ${synced.translationEnabled ? "on" : "off"}`,
|
|
738
721
|
`Project: ${project?.repoRoot ?? synced.currentProjectId ?? "none"}`,
|
|
739
722
|
`Task: ${synced.currentTaskSlug ?? "none"}`,
|
|
740
723
|
`Last poll: ${synced.lastPollStatus.state}${synced.lastPollStatus.error ? ` (${synced.lastPollStatus.error})` : ""}`
|
|
741
724
|
].join("\n");
|
|
742
725
|
}
|
|
743
|
-
function parseLarkPairingCommand(text) {
|
|
744
|
-
const match = text.trim().match(/^\/bind\s+([A-Z0-9]{6,12})$/i);
|
|
745
|
-
return match?.[1]?.toUpperCase() ?? null;
|
|
746
|
-
}
|
|
747
|
-
function isValidPairingCode(settings, code) {
|
|
748
|
-
const expected = settings.binding.pairingCode?.toUpperCase();
|
|
749
|
-
const expiresAt = settings.binding.pairingCodeExpiresAt;
|
|
750
|
-
return Boolean(expected && expected === code.toUpperCase() && expiresAt && Date.parse(expiresAt) > Date.now());
|
|
751
|
-
}
|
|
752
726
|
return {
|
|
753
727
|
async start() {
|
|
754
728
|
await ensurePolling();
|
|
@@ -788,40 +762,6 @@ export function createGatewayService(deps) {
|
|
|
788
762
|
const settings = await deps.settings.resetBinding();
|
|
789
763
|
return deps.settings.expose(settings, isRunning());
|
|
790
764
|
},
|
|
791
|
-
async createPairingCode() {
|
|
792
|
-
const settings = await deps.settings.loadSettings();
|
|
793
|
-
if (settings.channel !== "lark") {
|
|
794
|
-
throw new VcmError({
|
|
795
|
-
code: "GATEWAY_PAIRING_UNSUPPORTED",
|
|
796
|
-
message: "Pairing codes are only available for Lark Gateway.",
|
|
797
|
-
statusCode: 409
|
|
798
|
-
});
|
|
799
|
-
}
|
|
800
|
-
if (!settings.binding.appId || !settings.binding.appSecret) {
|
|
801
|
-
throw new VcmError({
|
|
802
|
-
code: "GATEWAY_LARK_CONFIG_MISSING",
|
|
803
|
-
message: "Complete Lark QR Setup before creating a pairing code.",
|
|
804
|
-
statusCode: 409
|
|
805
|
-
});
|
|
806
|
-
}
|
|
807
|
-
const code = randomPairingCode();
|
|
808
|
-
const expiresAt = new Date(Date.now() + PAIRING_CODE_TTL_MS).toISOString();
|
|
809
|
-
const nextSettings = await deps.settings.saveSettings({
|
|
810
|
-
...settings,
|
|
811
|
-
binding: {
|
|
812
|
-
...settings.binding,
|
|
813
|
-
pairingCode: code,
|
|
814
|
-
pairingCodeExpiresAt: expiresAt
|
|
815
|
-
},
|
|
816
|
-
updatedAt: now()
|
|
817
|
-
});
|
|
818
|
-
await ensurePolling();
|
|
819
|
-
return {
|
|
820
|
-
code,
|
|
821
|
-
expiresAt,
|
|
822
|
-
status: deps.settings.expose(nextSettings, isRunning())
|
|
823
|
-
};
|
|
824
|
-
},
|
|
825
765
|
async startQrLogin() {
|
|
826
766
|
const settings = await deps.settings.loadSettings();
|
|
827
767
|
const channel = resolveChannel(settings);
|
|
@@ -938,17 +878,25 @@ export function createGatewayService(deps) {
|
|
|
938
878
|
message: "Lark QR setup expired. Start a new setup."
|
|
939
879
|
};
|
|
940
880
|
}
|
|
941
|
-
const
|
|
881
|
+
const confirmDeadline = Date.now() + LARK_REGISTRATION_CONFIRM_TIMEOUT_MS;
|
|
882
|
+
let result = await larkRegistration.poll({
|
|
942
883
|
domain: larkRegistrationState.domain,
|
|
943
884
|
deviceCode: larkRegistrationState.deviceCode
|
|
944
885
|
});
|
|
886
|
+
while (result.status === "wait" && Date.now() < confirmDeadline) {
|
|
887
|
+
await sleep(Math.min(larkRegistrationState.intervalSeconds * 1000, 3_000), new AbortController().signal);
|
|
888
|
+
result = await larkRegistration.poll({
|
|
889
|
+
domain: larkRegistrationState.domain,
|
|
890
|
+
deviceCode: larkRegistrationState.deviceCode
|
|
891
|
+
});
|
|
892
|
+
}
|
|
945
893
|
if (result.status !== "confirmed") {
|
|
946
894
|
if (result.status === "expired" || result.status === "failed") {
|
|
947
895
|
larkRegistrationState = null;
|
|
948
896
|
}
|
|
949
897
|
return {
|
|
950
898
|
status: result.status,
|
|
951
|
-
message: result.message
|
|
899
|
+
message: result.message ?? (result.status === "wait" ? "Lark has not returned app credentials yet. Confirm again in a moment." : undefined)
|
|
952
900
|
};
|
|
953
901
|
}
|
|
954
902
|
if (!result.appId || !result.appSecret || !result.domain) {
|
|
@@ -970,8 +918,6 @@ export function createGatewayService(deps) {
|
|
|
970
918
|
larkOpenId: result.openId ?? null,
|
|
971
919
|
larkBotName: result.botName ?? null,
|
|
972
920
|
larkBotOpenId: result.botOpenId ?? null,
|
|
973
|
-
pairingCode: null,
|
|
974
|
-
pairingCodeExpiresAt: null,
|
|
975
921
|
getUpdatesBuf: ""
|
|
976
922
|
},
|
|
977
923
|
lastPollStatus: {
|
|
@@ -1313,14 +1259,6 @@ function formatAheadBehind(project) {
|
|
|
1313
1259
|
}
|
|
1314
1260
|
return ahead > 0 ? `ahead ${ahead}` : `behind ${behind}`;
|
|
1315
1261
|
}
|
|
1316
|
-
function randomPairingCode() {
|
|
1317
|
-
const alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
1318
|
-
let out = "";
|
|
1319
|
-
for (let index = 0; index < 8; index += 1) {
|
|
1320
|
-
out += alphabet[randomInt(alphabet.length)];
|
|
1321
|
-
}
|
|
1322
|
-
return out;
|
|
1323
|
-
}
|
|
1324
1262
|
function sleep(ms, signal) {
|
|
1325
1263
|
return new Promise((resolve) => {
|
|
1326
1264
|
if (signal.aborted) {
|
|
@@ -1339,7 +1277,10 @@ function normalizeBaseUrl(input, fallback) {
|
|
|
1339
1277
|
if (!trimmed) {
|
|
1340
1278
|
return fallback;
|
|
1341
1279
|
}
|
|
1342
|
-
if (
|
|
1280
|
+
if (/^https:\/\/lark:\/\//i.test(trimmed)) {
|
|
1281
|
+
return trimmed.replace(/^https:\/\//i, "").replace(/\/+$/, "");
|
|
1282
|
+
}
|
|
1283
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)) {
|
|
1343
1284
|
return trimmed.replace(/\/+$/, "");
|
|
1344
1285
|
}
|
|
1345
1286
|
return `https://${trimmed.replace(/\/+$/, "")}`;
|
|
@@ -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: {}
|
|
@@ -289,7 +284,10 @@ function normalizeBaseUrl(input, fallback = DEFAULT_BASE_URL) {
|
|
|
289
284
|
if (!raw) {
|
|
290
285
|
return fallback;
|
|
291
286
|
}
|
|
292
|
-
if (
|
|
287
|
+
if (/^https:\/\/lark:\/\//i.test(raw)) {
|
|
288
|
+
return raw.replace(/^https:\/\//i, "").replace(/\/+$/, "");
|
|
289
|
+
}
|
|
290
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(raw)) {
|
|
293
291
|
return raw.replace(/\/+$/, "");
|
|
294
292
|
}
|
|
295
293
|
return `https://${raw.replace(/\/+$/, "")}`;
|