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 +10 -9
- package/dist/backend/api/gateway-routes.js +6 -0
- package/dist/backend/gateway/channels/lark-channel.js +7 -5
- package/dist/backend/gateway/channels/lark-registration.js +220 -0
- package/dist/backend/gateway/gateway-service.js +117 -4
- package/dist/backend/gateway/gateway-settings-service.js +24 -5
- package/dist-frontend/assets/index-B0hNcYoB.js +96 -0
- package/dist-frontend/index.html +1 -1
- package/docs/gateway-design.md +12 -7
- package/docs/product-design.md +4 -3
- package/package.json +1 -1
- package/dist-frontend/assets/index-CKX3RbCz.js +0 -96
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.
|
|
279
|
-
2.
|
|
280
|
-
3.
|
|
281
|
-
4. Click `
|
|
282
|
-
5.
|
|
283
|
-
6.
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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.
|
|
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
|
-
|
|
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: "
|
|
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 (
|
|
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 (
|
|
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(/\/+$/, "")}`;
|