vibe-coding-master 0.4.35 → 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.
|
@@ -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:
|
|
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:
|
|
44
|
+
domain: sdkDomain,
|
|
44
45
|
source: "vcm-gateway",
|
|
45
46
|
autoReconnect: true,
|
|
46
47
|
loggerLevel: options.loggerLevel,
|
|
@@ -1,115 +1,146 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"
|
|
19
|
-
}
|
|
20
|
-
|
|
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
|
|
24
|
+
return registerAppPromise;
|
|
23
25
|
}
|
|
24
26
|
return {
|
|
25
|
-
async init(
|
|
26
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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:
|
|
56
|
-
intervalSeconds:
|
|
57
|
-
expiresInSeconds:
|
|
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
|
-
|
|
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: "
|
|
80
|
-
|
|
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
|
-
|
|
89
|
-
|
|
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 (
|
|
93
|
-
|
|
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:
|
|
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(
|
|
190
|
-
stringOrNull(
|
|
191
|
-
stringOrNull(
|
|
192
|
-
stringOrNull(
|
|
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
|
|
199
|
-
|
|
200
|
-
|
|
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
|
|
204
|
-
|
|
205
|
-
url.searchParams.set("tp", "vcm");
|
|
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
|
|
264
|
+
return null;
|
|
210
265
|
}
|
|
211
266
|
}
|
|
212
267
|
function positiveNumberOr(input, fallback) {
|