larkit 0.0.0-stage → 0.1.0

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.
Files changed (73) hide show
  1. package/AGENT_SKILL.md +12 -0
  2. package/README.md +765 -2
  3. package/bin/larkit-mcp.js +8 -0
  4. package/bin/larkit.js +532 -0
  5. package/larkit.config.example.json +12 -0
  6. package/package.json +65 -4
  7. package/scripts/.tmp-npm-webauth.mjs +49 -0
  8. package/scripts/build-index.mjs +66 -0
  9. package/scripts/check-syntax.mjs +28 -0
  10. package/scripts/login-worker.mjs +50 -0
  11. package/scripts/publish-pack.mjs +277 -0
  12. package/scripts/verify-bootstrap.mjs +238 -0
  13. package/scripts/verify-console.mjs +60 -0
  14. package/scripts/verify-index.mjs +59 -0
  15. package/scripts/verify-mcp.mjs +215 -0
  16. package/skills/larkit/SKILL.md +120 -0
  17. package/skills/larkit/references/console.md +86 -0
  18. package/skills/larkit/references/playbooks.md +177 -0
  19. package/skills/larkit/references/tools.md +115 -0
  20. package/skills/larkit/references/truth-model.md +117 -0
  21. package/src/auth/login.js +171 -0
  22. package/src/auth/service.js +213 -0
  23. package/src/auth/store.js +213 -0
  24. package/src/bootstrap/channel.js +201 -0
  25. package/src/bootstrap/cli-install.js +184 -0
  26. package/src/bootstrap/download.js +182 -0
  27. package/src/bootstrap/init.js +672 -0
  28. package/src/bootstrap/pack-cli.js +97 -0
  29. package/src/bootstrap/pack.js +541 -0
  30. package/src/bootstrap/report.js +62 -0
  31. package/src/bootstrap/skills.js +223 -0
  32. package/src/bootstrap/state.js +66 -0
  33. package/src/bootstrap/tar.js +417 -0
  34. package/src/console/browser.js +338 -0
  35. package/src/console/client.js +827 -0
  36. package/src/console/dispatch.js +101 -0
  37. package/src/docs/client.js +284 -0
  38. package/src/indexer/anchors.js +366 -0
  39. package/src/indexer/db.js +449 -0
  40. package/src/indexer/extract-assets.js +285 -0
  41. package/src/indexer/extract-deb.js +56 -0
  42. package/src/indexer/format-pool.js +64 -0
  43. package/src/indexer/format-worker.js +132 -0
  44. package/src/indexer/format.js +239 -0
  45. package/src/indexer/protocol.js +123 -0
  46. package/src/mcp/server.js +433 -0
  47. package/src/probe/client.js +225 -0
  48. package/src/probe/openapi.js +194 -0
  49. package/src/protocol/catalog.js +146 -0
  50. package/src/protocol/data/README.md +15 -0
  51. package/src/protocol/data/bundle.json +1 -0
  52. package/src/protocol/data/commands.json +12896 -0
  53. package/src/protocol/data/constants.json +8 -0
  54. package/src/protocol/data/reference/PROTOCOL.md +66 -0
  55. package/src/protocol/data/reference/endpoints.json +46 -0
  56. package/src/protocol/data/schema.json.gz +0 -0
  57. package/src/protocol/descriptor.js +358 -0
  58. package/src/protocol/schema.js +235 -0
  59. package/src/protocol/transport.js +387 -0
  60. package/src/shared/browser.js +70 -0
  61. package/src/shared/config.js +90 -0
  62. package/src/shared/paths.js +151 -0
  63. package/src/shared/quiet.js +12 -0
  64. package/src/shared/util.js +124 -0
  65. package/src/truth/fts.js +24 -0
  66. package/src/truth/search.js +554 -0
  67. package/src/truth/verified.js +107 -0
  68. package/verified_truths/README.md +28 -0
  69. package/verified_truths/card-json-2.0-official.md +55 -0
  70. package/verified_truths/client-ws-gateway-boundaries.md +33 -0
  71. package/verified_truths/feishu-developer-console-apis.md +104 -0
  72. package/workers/truth-api/src/index.js +180 -0
  73. package/workers/truth-api/wrangler.toml +31 -0
@@ -0,0 +1,235 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { gunzipSync } from "node:zlib";
4
+ import protobuf from "protobufjs";
5
+ import {
6
+ DEB_ROOT,
7
+ DATA_DIR,
8
+ } from "../shared/paths.js";
9
+ import { ensureDir, readJson, writeJson } from "../shared/util.js";
10
+ import { mergeRoots, parseFileDescriptorSet } from "./descriptor.js";
11
+
12
+ const BUNDLE_FILE = new URL("./data/bundle.json", import.meta.url);
13
+ const BUNDLED_SCHEMA = new URL("./data/schema.json.gz", import.meta.url);
14
+ const SCHEMA_CACHE = path.join(DATA_DIR, "protocol", "schema.json");
15
+ const FEISHU_APP_DIR = path.join(DEB_ROOT, "opt", "bytedance", "feishu");
16
+
17
+ let cachedRoot;
18
+ let cachedRegistry;
19
+
20
+ function readBundle() {
21
+ return readJson(BUNDLE_FILE.pathname, { nested: {} });
22
+ }
23
+
24
+ function descriptorFiles() {
25
+ return {
26
+ pb: path.join(FEISHU_APP_DIR, "pb.desc"),
27
+ pbServer: path.join(FEISHU_APP_DIR, "pb_server.desc"),
28
+ };
29
+ }
30
+
31
+ export function buildSchemaRoot({ force = false } = {}) {
32
+ const { pb, pbServer } = descriptorFiles();
33
+ const cacheMtime = fs.existsSync(SCHEMA_CACHE)
34
+ ? fs.statSync(SCHEMA_CACHE).mtimeMs
35
+ : 0;
36
+ const sourceMtime = Math.max(
37
+ fs.existsSync(pb) ? fs.statSync(pb).mtimeMs : 0,
38
+ fs.existsSync(pbServer) ? fs.statSync(pbServer).mtimeMs : 0,
39
+ );
40
+ if (!force && cacheMtime > 0 && cacheMtime >= sourceMtime) {
41
+ return readJson(SCHEMA_CACHE, { nested: {} });
42
+ }
43
+ const bundle = readBundle();
44
+ const roots = [bundle];
45
+ const hasPb = fs.existsSync(pb);
46
+ const hasPbServer = fs.existsSync(pbServer);
47
+ if (!hasPb || !hasPbServer) {
48
+ // Runtime-only install without the deb: use the bundled merged schema.
49
+ try {
50
+ if (fs.existsSync(BUNDLED_SCHEMA)) {
51
+ return JSON.parse(gunzipSync(fs.readFileSync(BUNDLED_SCHEMA)).toString("utf8"));
52
+ }
53
+ } catch (error) {
54
+ console.error(`[protocol] bundled schema load failed: ${error.message}`);
55
+ }
56
+ }
57
+ for (const file of [pb, pbServer]) {
58
+ if (!fs.existsSync(file)) continue;
59
+ try {
60
+ roots.push(parseFileDescriptorSet(fs.readFileSync(file)));
61
+ } catch (error) {
62
+ console.error(`[protocol] descriptor parse failed ${file}: ${error.message}`);
63
+ }
64
+ }
65
+ const root = mergeRoots(...roots);
66
+ ensureDir(path.dirname(SCHEMA_CACHE));
67
+ writeJson(SCHEMA_CACHE, root);
68
+ return root;
69
+ }
70
+
71
+ export function getSchemaRoot({ force = false } = {}) {
72
+ if (cachedRoot && !force) return cachedRoot;
73
+ cachedRoot = buildSchemaRoot({ force });
74
+ return cachedRoot;
75
+ }
76
+
77
+ export function getProtoRoot({ force = false } = {}) {
78
+ const root = protobuf.Root.fromJSON(getSchemaRoot({ force }));
79
+ root.resolveAll();
80
+ return root;
81
+ }
82
+
83
+ function lowerCamel(value) {
84
+ return String(value).replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase());
85
+ }
86
+
87
+ function snakeCase(value) {
88
+ return String(value)
89
+ .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
90
+ .toLowerCase();
91
+ }
92
+
93
+ function normalizeValue(type, value, depth = 0) {
94
+ if (value === undefined || value === null || depth > 20) return value;
95
+ if (Array.isArray(value)) {
96
+ return value.map((item) => normalizeValue(type, item, depth + 1));
97
+ }
98
+ if (typeof value !== "object" || value instanceof Uint8Array) return value;
99
+ if (!type || !type.fields) return value;
100
+ const out = {};
101
+ for (const [rawKey, rawValue] of Object.entries(value)) {
102
+ const key = lowerCamel(rawKey);
103
+ const snake = snakeCase(rawKey);
104
+ const field =
105
+ type.fields[key] ||
106
+ type.fields[rawKey] ||
107
+ type.fields[snake] ||
108
+ Object.values(type.fields).find(
109
+ (candidate) =>
110
+ candidate.name === rawKey ||
111
+ candidate.name === key ||
112
+ candidate.name === snake,
113
+ );
114
+ if (!field) {
115
+ out[rawKey] = rawValue;
116
+ continue;
117
+ }
118
+ if (field.resolvedType && field.resolvedType.fields) {
119
+ out[field.name] = normalizeValue(field.resolvedType, rawValue, depth + 1);
120
+ } else {
121
+ out[field.name] = rawValue;
122
+ }
123
+ }
124
+ return out;
125
+ }
126
+
127
+ function camelizeOutput(value, depth = 0) {
128
+ if (value === undefined || value === null || depth > 20) return value;
129
+ if (Array.isArray(value)) return value.map((item) => camelizeOutput(item, depth + 1));
130
+ if (typeof value !== "object" || value instanceof Uint8Array) return value;
131
+ const out = {};
132
+ for (const [key, child] of Object.entries(value)) {
133
+ out[lowerCamel(key)] = camelizeOutput(child, depth + 1);
134
+ }
135
+ return out;
136
+ }
137
+
138
+ export function createRegistry() {
139
+ if (cachedRegistry) return cachedRegistry;
140
+ const root = getProtoRoot();
141
+ const registry = {
142
+ root,
143
+ lookup(typeName) {
144
+ try {
145
+ const found = root.lookup(typeName);
146
+ return found || null;
147
+ } catch {
148
+ return null;
149
+ }
150
+ },
151
+ has(typeName) {
152
+ return Boolean(registry.lookup(typeName));
153
+ },
154
+ typeInfo(typeName) {
155
+ const found = registry.lookup(typeName);
156
+ if (!found) return null;
157
+ if (found.values) {
158
+ return {
159
+ fq: found.fullName.replace(/^\./, ""),
160
+ kind: "enum",
161
+ enumValues: Object.entries(found.values).map(([name, value]) => ({
162
+ name,
163
+ value,
164
+ })),
165
+ };
166
+ }
167
+ if (!found.fields) return null;
168
+ return {
169
+ fq: found.fullName.replace(/^\./, ""),
170
+ kind: "message",
171
+ fields: Object.entries(found.fields).map(([name, field]) => ({
172
+ name,
173
+ type:
174
+ field.resolvedType?.fullName?.replace(/^\./, "") || field.type,
175
+ id: field.id,
176
+ rule: field.repeated ? "repeated" : field.required ? "required" : undefined,
177
+ keyType: field.keyType || undefined,
178
+ })),
179
+ };
180
+ },
181
+ encode(typeName, value) {
182
+ const type = root.lookupType(typeName);
183
+ const normalized = normalizeValue(type, value);
184
+ return type.encode(type.fromObject(normalized)).finish();
185
+ },
186
+ decode(typeName, bytes) {
187
+ const type = root.lookupType(typeName);
188
+ const message = type.decode(
189
+ bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes),
190
+ );
191
+ return camelizeOutput(
192
+ type.toObject(message, {
193
+ longs: String,
194
+ enums: Number,
195
+ bytes: String,
196
+ defaults: false,
197
+ arrays: false,
198
+ objects: false,
199
+ }),
200
+ );
201
+ },
202
+ decodeRaw(typeName, bytes) {
203
+ const type = root.lookupType(typeName);
204
+ const message = type.decode(
205
+ bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes),
206
+ );
207
+ return type.toObject(message, {
208
+ longs: String,
209
+ enums: Number,
210
+ bytes: Array,
211
+ defaults: false,
212
+ arrays: false,
213
+ objects: false,
214
+ });
215
+ },
216
+ };
217
+ cachedRegistry = registry;
218
+ return registry;
219
+ }
220
+
221
+ export function schemaStats() {
222
+ const root = getSchemaRoot();
223
+ let messages = 0;
224
+ let enums = 0;
225
+ const walk = (node) => {
226
+ for (const value of Object.values(node.nested || {})) {
227
+ if (!value || typeof value !== "object") continue;
228
+ if (value.fields) messages++;
229
+ else if (value.values) enums++;
230
+ if (value.nested) walk(value);
231
+ }
232
+ };
233
+ walk(root);
234
+ return { messages, enums, cache: SCHEMA_CACHE };
235
+ }
@@ -0,0 +1,387 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { gunzipSync } from "node:zlib";
3
+ import { readFileSync } from "node:fs";
4
+ import { fileURLToPath } from "node:url";
5
+ import WebSocket from "ws";
6
+ import { readJson } from "../shared/util.js";
7
+ import { findApiByValue } from "./catalog.js";
8
+ import { createRegistry } from "./schema.js";
9
+
10
+ const CONSTANTS = readJson(
11
+ fileURLToPath(new URL("./data/constants.json", import.meta.url)),
12
+ {},
13
+ );
14
+ const ENDPOINTS = readJson(
15
+ fileURLToPath(new URL("./data/reference/endpoints.json", import.meta.url)),
16
+ {},
17
+ );
18
+ const GZIP_MAGIC = [0x1f, 0x8b];
19
+ const DEFAULT_WEB_VERSION = "7.61.0";
20
+
21
+ export const DEFAULT_BASE_URL =
22
+ CONSTANTS.baseUrl || "wss://msg-frontier.feishu.cn/ws/v2";
23
+ export const DEFAULT_TICKET_URL =
24
+ CONSTANTS.ticketUrl || "https://login.feishu.cn/suite/passport/frontier_ticket/";
25
+ export const WEB_APP_KEY = CONSTANTS.webAppKey || "";
26
+ const ACCESS_KEY_SALT = CONSTANTS.accessKeySalt || "";
27
+ const SDK_VERSION = CONSTANTS.sdkVersion || "7.68.5";
28
+ const ORIGIN = CONSTANTS.origin || "https://www.feishu.cn";
29
+
30
+ export function parseCookies(cookie) {
31
+ const out = {};
32
+ for (const part of String(cookie || "").split(";")) {
33
+ const index = part.indexOf("=");
34
+ if (index > 0) out[part.slice(0, index).trim()] = part.slice(index + 1).trim();
35
+ }
36
+ return out;
37
+ }
38
+
39
+ export function cookieHeader(cookies) {
40
+ return Object.entries(cookies)
41
+ .map(([key, value]) => `${key}=${value}`)
42
+ .join("; ");
43
+ }
44
+
45
+ export function generateAccessKey(value) {
46
+ return createHash("md5").update(value, "utf8").digest("hex");
47
+ }
48
+
49
+ export function buildWsQuery(cookies, options = {}) {
50
+ const deviceId = cookies.passport_web_did || "";
51
+ const appKey = options.appKey || WEB_APP_KEY;
52
+ const accessKey = generateAccessKey(
53
+ `2${appKey}${deviceId}${ACCESS_KEY_SALT}`,
54
+ );
55
+ return {
56
+ access_key: accessKey,
57
+ aid: "1",
58
+ ticket: options.ticket || "",
59
+ device_id: deviceId,
60
+ fpid: "2",
61
+ accept_encoding: "gzip",
62
+ request_id: randomUUID().toUpperCase(),
63
+ sdk_version: options.sdkVersion || SDK_VERSION,
64
+ };
65
+ }
66
+
67
+ async function fetchTicket(cookies, options = {}) {
68
+ const deviceId = cookies.passport_web_did || "";
69
+ const ticketUrl = options.ticketUrl || DEFAULT_TICKET_URL;
70
+ const url = `${ticketUrl}?${new URLSearchParams({ local_device_id: deviceId })}`;
71
+ try {
72
+ const response = await fetch(url, {
73
+ headers: {
74
+ Accept: "application/json, text/plain, */*",
75
+ Cookie: cookieHeader(cookies),
76
+ "User-Agent":
77
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/125.0.0.0 Safari/537.36",
78
+ },
79
+ signal: AbortSignal.timeout(15_000),
80
+ });
81
+ if (!response.ok) return "";
82
+ const data = await response.json();
83
+ return data?.ticket || "";
84
+ } catch {
85
+ return "";
86
+ }
87
+ }
88
+
89
+ export function encodeFrame(registry, frame) {
90
+ return registry.encode("frame.Frame", frame);
91
+ }
92
+
93
+ export function decodeFrame(registry, bytes) {
94
+ return registry.decodeRaw("frame.Frame", bytes);
95
+ }
96
+
97
+ export function encodePacket(registry, packet) {
98
+ return registry.encode("improto.Packet", packet);
99
+ }
100
+
101
+ export function buildAckBytes(registry, sid) {
102
+ const now = Date.now();
103
+ const packet = encodePacket(registry, {
104
+ cmd: 1,
105
+ sid,
106
+ payloadType: 1,
107
+ });
108
+ return encodeFrame(registry, {
109
+ seqid: String(now),
110
+ logid: String(now),
111
+ service: 1,
112
+ method: 1,
113
+ headers: [{ key: "x-request-time", value: `${now}000` }],
114
+ payloadType: "pb",
115
+ payload: packet,
116
+ });
117
+ }
118
+
119
+ export function decodeWsMessage(registry, input) {
120
+ let buf = Buffer.isBuffer(input) ? input : Buffer.from(input);
121
+ if (buf.length >= 2 && buf[0] === GZIP_MAGIC[0] && buf[1] === GZIP_MAGIC[1]) {
122
+ buf = gunzipSync(buf);
123
+ }
124
+ const frame = decodeFrame(registry, buf);
125
+ const packetBytes = Uint8Array.from(frame.payload || []);
126
+ const packet = registry.decodeRaw("improto.Packet", packetBytes);
127
+ const api = findApiByValue(packet.cmd);
128
+ const payloadTypeName = api?.responseType || api?.requestType || "";
129
+ let payload;
130
+ const businessBytes = packet.payload
131
+ ? Uint8Array.from(packet.payload)
132
+ : new Uint8Array(0);
133
+ if (payloadTypeName && businessBytes.length > 0) {
134
+ try {
135
+ payload = registry.decode(payloadTypeName, businessBytes);
136
+ } catch {
137
+ payload = undefined;
138
+ }
139
+ }
140
+ return { frame, packet, payload, payloadTypeName };
141
+ }
142
+
143
+ export async function connectFrontier({ cookie, registry, timeout = 15_000 } = {}) {
144
+ const cookies = parseCookies(cookie);
145
+ if (!cookies.passport_web_did) {
146
+ throw new Error(
147
+ "cookie 缺少 passport_web_did;需要飞书网页版登录后的 Cookie",
148
+ );
149
+ }
150
+ const reg = registry || createRegistry();
151
+ const ticket = await fetchTicket(cookies);
152
+ const query = buildWsQuery(cookies, { ticket });
153
+ const url = `${DEFAULT_BASE_URL}?${Object.entries(query)
154
+ .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
155
+ .join("&")}`;
156
+ const ws = new WebSocket(url, {
157
+ headers: {
158
+ Origin: ORIGIN,
159
+ "User-Agent":
160
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36",
161
+ },
162
+ });
163
+ const pushHandlers = [];
164
+ const eventHandlers = [];
165
+ const closeHandlers = [];
166
+ let closed = false;
167
+
168
+ const handle = (data) => {
169
+ let decoded;
170
+ try {
171
+ decoded = decodeWsMessage(reg, data);
172
+ } catch {
173
+ return;
174
+ }
175
+ const packet = decoded.packet;
176
+ if (!packet) return;
177
+ if (packet.sid) {
178
+ try {
179
+ ws.send(buildAckBytes(reg, packet.sid));
180
+ } catch {
181
+ // ignore ack failures
182
+ }
183
+ }
184
+ if (packet.cmd === 6) {
185
+ for (const handler of pushHandlers) {
186
+ try {
187
+ handler(decoded.payload, decoded);
188
+ } catch {
189
+ // ignore handler failure
190
+ }
191
+ }
192
+ } else {
193
+ for (const handler of eventHandlers) {
194
+ try {
195
+ handler(decoded);
196
+ } catch {
197
+ // ignore handler failure
198
+ }
199
+ }
200
+ }
201
+ };
202
+
203
+ await new Promise((resolve, reject) => {
204
+ const timer = setTimeout(() => {
205
+ reject(new Error(`WS 握手超时(${timeout}ms)`));
206
+ }, timeout);
207
+ ws.on("open", () => {
208
+ clearTimeout(timer);
209
+ resolve();
210
+ });
211
+ ws.on("error", (error) => {
212
+ clearTimeout(timer);
213
+ reject(error);
214
+ });
215
+ ws.on("message", handle);
216
+ ws.on("close", (code, reason) => {
217
+ closed = true;
218
+ if (!ws.__opened) {
219
+ clearTimeout(timer);
220
+ reject(new Error(`WS 未打开即关闭 code=${code}`));
221
+ }
222
+ for (const handler of closeHandlers) {
223
+ try {
224
+ handler(code, reason?.toString());
225
+ } catch {
226
+ // ignore
227
+ }
228
+ }
229
+ });
230
+ ws.on("open", () => {
231
+ ws.__opened = true;
232
+ });
233
+ });
234
+
235
+ return {
236
+ get open() {
237
+ return ws.readyState === WebSocket.OPEN;
238
+ },
239
+ onPush(handler) {
240
+ pushHandlers.push(handler);
241
+ return () => {
242
+ const index = pushHandlers.indexOf(handler);
243
+ if (index >= 0) pushHandlers.splice(index, 1);
244
+ };
245
+ },
246
+ onEvent(handler) {
247
+ eventHandlers.push(handler);
248
+ return () => {
249
+ const index = eventHandlers.indexOf(handler);
250
+ if (index >= 0) eventHandlers.splice(index, 1);
251
+ };
252
+ },
253
+ onClose(handler) {
254
+ closeHandlers.push(handler);
255
+ return () => {
256
+ const index = closeHandlers.indexOf(handler);
257
+ if (index >= 0) closeHandlers.splice(index, 1);
258
+ };
259
+ },
260
+ send(packet, frame = {}) {
261
+ const now = Date.now();
262
+ const payload = encodePacket(reg, packet);
263
+ const frameBytes = encodeFrame(reg, {
264
+ seqid: String(now),
265
+ logid: String(now),
266
+ service: 1,
267
+ method: 1,
268
+ payloadType: "pb",
269
+ payload,
270
+ ...frame,
271
+ });
272
+ if (ws.readyState === WebSocket.OPEN) ws.send(frameBytes);
273
+ },
274
+ ack(sid) {
275
+ if (ws.readyState === WebSocket.OPEN) ws.send(buildAckBytes(reg, sid));
276
+ },
277
+ stop() {
278
+ closed = true;
279
+ try {
280
+ ws.close();
281
+ } catch {
282
+ // ignore
283
+ }
284
+ },
285
+ get closed() {
286
+ return closed;
287
+ },
288
+ };
289
+ }
290
+
291
+ export function gatewayOrigin(baseDomain = "feishu.cn") {
292
+ let hostname = String(baseDomain || "feishu.cn").trim();
293
+ if (!/^https?:\/\//i.test(hostname)) hostname = `https://${hostname}`;
294
+ try {
295
+ hostname = new URL(hostname).hostname;
296
+ } catch {
297
+ hostname = hostname.replace(/^https?:\/\//i, "").replace(/[/?#].*$/, "");
298
+ }
299
+ const labels = hostname.split(".").filter(Boolean).slice(-2).join(".");
300
+ return `https://internal-api-lark-api.${labels}`;
301
+ }
302
+
303
+ export function gatewayHeaders(cmd, requestId, overrides = {}) {
304
+ const webVersion =
305
+ ENDPOINTS.commandWebVersions?.[String(cmd)] || DEFAULT_WEB_VERSION;
306
+ return {
307
+ accept: "*/*",
308
+ "content-type": "application/x-protobuf",
309
+ locale: "zh-CN",
310
+ "user-agent":
311
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/148.0.0.0 Safari/537.36",
312
+ ...(ENDPOINTS.defaultGatewayHeaders || {}),
313
+ "x-command": String(cmd),
314
+ "x-command-version": webVersion,
315
+ "x-request-id": requestId,
316
+ "x-source": "web",
317
+ "x-web-version": webVersion,
318
+ ...overrides,
319
+ };
320
+ }
321
+
322
+ export async function postCommand({
323
+ cmd,
324
+ payload,
325
+ cookie,
326
+ registry,
327
+ origin,
328
+ baseDomain = "feishu.cn",
329
+ timeout = 15_000,
330
+ headers = {},
331
+ } = {}) {
332
+ const reg = registry || createRegistry();
333
+ const api = findApiByValue(cmd);
334
+ const requestId = randomUUID();
335
+ let payloadBytes;
336
+ if (payload instanceof Uint8Array) payloadBytes = payload;
337
+ else if (api?.requestType && payload && Object.keys(payload).length > 0) {
338
+ payloadBytes = reg.encode(api.requestType, payload);
339
+ } else {
340
+ payloadBytes = new Uint8Array(0);
341
+ }
342
+ const packetBytes = encodePacket(reg, {
343
+ cmd,
344
+ cid: requestId,
345
+ payloadType: 1,
346
+ payload: payloadBytes,
347
+ sid: requestId,
348
+ });
349
+ const base = origin || gatewayOrigin(baseDomain);
350
+ const cookieValue =
351
+ typeof cookie === "string" ? cookie : cookieHeader(cookie || {});
352
+ const response = await fetch(`${base}/im/gateway/`, {
353
+ method: "POST",
354
+ headers: gatewayHeaders(cmd, requestId, {
355
+ ...(cookieValue ? { cookie: cookieValue } : {}),
356
+ ...headers,
357
+ }),
358
+ body: packetBytes,
359
+ signal: AbortSignal.timeout(timeout),
360
+ });
361
+ const raw = new Uint8Array(await response.arrayBuffer());
362
+ let packet;
363
+ let decodedPayload;
364
+ if (raw.length > 0) {
365
+ try {
366
+ packet = reg.decodeRaw("improto.Packet", raw);
367
+ const typeName = api?.responseType || api?.requestType;
368
+ const business = packet.payload
369
+ ? Uint8Array.from(packet.payload)
370
+ : new Uint8Array(0);
371
+ if (typeName && business.length > 0) {
372
+ decodedPayload = reg.decode(typeName, business);
373
+ }
374
+ } catch {
375
+ // gateway may return raw response bytes; keep raw
376
+ }
377
+ }
378
+ return {
379
+ status: response.status,
380
+ requestId,
381
+ packet,
382
+ payload: decodedPayload,
383
+ raw,
384
+ };
385
+ }
386
+
387
+ export { createRegistry };
@@ -0,0 +1,70 @@
1
+ import fs from "node:fs";
2
+ import { execSync } from "node:child_process";
3
+ import puppeteer from "puppeteer-core";
4
+
5
+ const CHROME_CANDIDATES = [
6
+ process.env.CHROME_PATH,
7
+ process.env.PUPPETEER_EXECUTABLE_PATH,
8
+ "/usr/bin/google-chrome",
9
+ "/usr/bin/google-chrome-stable",
10
+ "/usr/bin/chromium",
11
+ "/usr/bin/chromium-browser",
12
+ "/snap/bin/chromium",
13
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
14
+ "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
15
+ ].filter(Boolean);
16
+
17
+ export function findChrome() {
18
+ for (const candidate of CHROME_CANDIDATES) {
19
+ try {
20
+ if (fs.existsSync(candidate)) return candidate;
21
+ } catch {
22
+ // ignore
23
+ }
24
+ }
25
+ try {
26
+ const detected = execSync(
27
+ "which google-chrome || which google-chrome-stable || which chromium || which chromium-browser",
28
+ { encoding: "utf8", timeout: 3000 },
29
+ ).trim();
30
+ if (detected) return detected;
31
+ } catch {
32
+ // ignore
33
+ }
34
+ throw new Error(
35
+ "Chrome/Chromium not found; install google-chrome or set CHROME_PATH",
36
+ );
37
+ }
38
+
39
+ export async function launchBrowser({
40
+ headless = true,
41
+ viewport = { width: 1280, height: 900 },
42
+ } = {}) {
43
+ const executablePath = findChrome();
44
+ return await puppeteer.launch({
45
+ executablePath,
46
+ headless,
47
+ args: [
48
+ "--no-sandbox",
49
+ "--disable-setuid-sandbox",
50
+ "--disable-dev-shm-usage",
51
+ "--disable-gpu",
52
+ ],
53
+ defaultViewport: viewport,
54
+ });
55
+ }
56
+
57
+ export function parseCookieString(cookie) {
58
+ const out = {};
59
+ for (const part of String(cookie || "").split(";")) {
60
+ const index = part.indexOf("=");
61
+ if (index > 0) out[part.slice(0, index).trim()] = part.slice(index + 1).trim();
62
+ }
63
+ return out;
64
+ }
65
+
66
+ export function cookieString(cookies) {
67
+ return Object.entries(cookies)
68
+ .map(([key, value]) => `${key}=${value}`)
69
+ .join("; ");
70
+ }