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,338 @@
1
+ import path from "node:path";
2
+ import puppeteer from "puppeteer-core";
3
+ import { CACHE_DIR, displayPath } from "../shared/paths.js";
4
+ import { getProfileCookies } from "../auth/store.js";
5
+ import { ensureDir } from "../shared/util.js";
6
+ import { findChrome as findChromeShared } from "../shared/browser.js";
7
+ import { recordRecipe } from "./client.js";
8
+
9
+ async function findChrome() {
10
+ return findChromeShared();
11
+ }
12
+
13
+ function parseCookies(cookie) {
14
+ const out = {};
15
+ for (const part of String(cookie || "").split(";")) {
16
+ const index = part.indexOf("=");
17
+ if (index > 0) out[part.slice(0, index).trim()] = part.slice(index + 1).trim();
18
+ }
19
+ return out;
20
+ }
21
+
22
+ function redactHeaders(headers) {
23
+ const out = {};
24
+ for (const [key, value] of Object.entries(headers || {})) {
25
+ if (/cookie|authorization|csrf|token|secret/i.test(key)) {
26
+ out[key] = "<redacted>";
27
+ } else {
28
+ out[key] = value;
29
+ }
30
+ }
31
+ return out;
32
+ }
33
+
34
+ function shapeOf(value, depth = 0) {
35
+ if (depth > 4) return "<deep>";
36
+ if (value === null) return "null";
37
+ if (Array.isArray(value)) {
38
+ return value.length ? [shapeOf(value[0], depth + 1)] : [];
39
+ }
40
+ if (typeof value === "object") {
41
+ const out = {};
42
+ for (const [key, child] of Object.entries(value)) {
43
+ out[key] = shapeOf(child, depth + 1);
44
+ }
45
+ return out;
46
+ }
47
+ return typeof value;
48
+ }
49
+
50
+ function parseMaybeJson(text) {
51
+ if (!text) return undefined;
52
+ try {
53
+ return JSON.parse(text);
54
+ } catch {
55
+ return undefined;
56
+ }
57
+ }
58
+
59
+ function actionForPath(pathname) {
60
+ if (pathname.includes("/v1/app/list")) return "list_apps";
61
+ if (pathname.includes("/v1/app/create")) return "create_app";
62
+ if (pathname.includes("/v1/secret/")) return "get_credentials";
63
+ if (pathname.includes("/v1/base_info/")) return "update_app_info";
64
+ if (pathname.includes("/v1/scope/update/")) return "grant_scopes";
65
+ if (pathname.includes("/v1/scope/")) return "list_scopes";
66
+ if (pathname.includes("/v1/app_version/list/")) return "list_versions";
67
+ if (pathname.includes("/v1/app_version/create/")) return "create_version";
68
+ if (pathname.includes("/v1/publish/commit/")) return "publish";
69
+ if (pathname.includes("/v1/publish/release/")) return "release";
70
+ if (pathname.includes("/v1/robot/switch/")) return "switch_robot";
71
+ if (pathname.includes("/v1/robot/update")) return "update_robot";
72
+ if (pathname.includes("/v1/robot/")) return "get_robot";
73
+ if (pathname.includes("/v1/event/switch/")) return "switch_event";
74
+ if (pathname.includes("/v1/event/update/")) return "update_event";
75
+ if (pathname.includes("/v1/event/")) return "get_event";
76
+ if (pathname.includes("/v1/callback/switch/")) return "switch_callback";
77
+ if (pathname.includes("/v1/callback/")) return "get_callback";
78
+ if (pathname.includes("/v2/robot/push_mode/switch/")) return "switch_card_callback";
79
+ return null;
80
+ }
81
+
82
+ async function applyAction(page, action) {
83
+ if (!action || typeof action !== "object") return null;
84
+ const result = { type: action.type, ok: false };
85
+ try {
86
+ if (action.type === "click_text") {
87
+ result.clicked = await page.evaluate((text) => {
88
+ const candidates = [
89
+ ...document.querySelectorAll(
90
+ 'button, a, [role="button"], [class*="btn"], [class*="button"]',
91
+ ),
92
+ ];
93
+ const exact = candidates.find(
94
+ (el) => (el.innerText || "").trim() === text,
95
+ );
96
+ const fuzzy = candidates.find((el) =>
97
+ (el.innerText || "").includes(text),
98
+ );
99
+ const target = exact || fuzzy;
100
+ if (!target) return false;
101
+ target.click();
102
+ return true;
103
+ }, action.text);
104
+ result.ok = Boolean(result.clicked);
105
+ } else if (action.type === "fill") {
106
+ const fields = Array.isArray(action.fields) ? action.fields : [action];
107
+ result.fields = [];
108
+ for (const field of fields) {
109
+ const filled = await page.evaluate(({ selector, placeholder, label, value }) => {
110
+ let el = selector ? document.querySelector(selector) : null;
111
+ if (!el && placeholder) {
112
+ el = [...document.querySelectorAll("input, textarea")].find((node) =>
113
+ (node.getAttribute("placeholder") || "").includes(placeholder),
114
+ );
115
+ }
116
+ if (!el && label) {
117
+ const labelNode = [...document.querySelectorAll("label")].find((node) =>
118
+ (node.innerText || "").includes(label),
119
+ );
120
+ if (labelNode) {
121
+ const forId = labelNode.getAttribute("for");
122
+ if (forId) el = document.getElementById(forId);
123
+ if (!el) el = labelNode.parentElement?.querySelector("input, textarea");
124
+ }
125
+ }
126
+ if (!el) return false;
127
+ el.focus();
128
+ const setter = Object.getOwnPropertyDescriptor(
129
+ Object.getPrototypeOf(el),
130
+ "value",
131
+ )?.set;
132
+ if (setter) setter.call(el, value);
133
+ else el.value = value;
134
+ el.dispatchEvent(new Event("input", { bubbles: true }));
135
+ el.dispatchEvent(new Event("change", { bubbles: true }));
136
+ return true;
137
+ }, field);
138
+ result.fields.push({ ...field, filled });
139
+ }
140
+ result.ok = result.fields.every((field) => field.filled);
141
+ } else if (action.type === "wait_for_text") {
142
+ await page.waitForFunction(
143
+ (text) => document.body.innerText.includes(text),
144
+ { timeout: action.timeout || 15_000 },
145
+ action.text,
146
+ );
147
+ result.ok = true;
148
+ } else if (action.type === "evaluate") {
149
+ result.value = await page.evaluate(action.code);
150
+ result.ok = true;
151
+ } else if (action.type === "wait") {
152
+ await new Promise((resolve) => setTimeout(resolve, action.ms || 1000));
153
+ result.ok = true;
154
+ }
155
+ } catch (error) {
156
+ result.error = error instanceof Error ? error.message : String(error);
157
+ }
158
+ return result;
159
+ }
160
+
161
+ export async function browserFallback({
162
+ profile,
163
+ url = "https://open.feishu.cn/app",
164
+ waitMs = 3000,
165
+ actions = [],
166
+ screenshot = false,
167
+ recordAs = null,
168
+ timeout = 45_000,
169
+ } = {}) {
170
+ const executablePath = await findChrome();
171
+ if (!executablePath) {
172
+ return {
173
+ ok: false,
174
+ error: "Chrome not found; install google-chrome or set CHROME_PATH",
175
+ };
176
+ }
177
+ const { name, cookies: rawCookie } = getProfileCookies(profile);
178
+ const cookies = parseCookies(rawCookie);
179
+ const started = Date.now();
180
+ let browser;
181
+ try {
182
+ browser = await puppeteer.launch({
183
+ executablePath,
184
+ headless: true,
185
+ args: [
186
+ "--no-sandbox",
187
+ "--disable-setuid-sandbox",
188
+ "--disable-dev-shm-usage",
189
+ "--disable-gpu",
190
+ "--window-size=1440,1000",
191
+ "--lang=zh-CN",
192
+ ],
193
+ defaultViewport: { width: 1440, height: 1000 },
194
+ });
195
+ const page = await browser.newPage();
196
+ await page.setUserAgent(
197
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
198
+ );
199
+ await page.setCookie(
200
+ ...Object.entries(cookies).map(([cookieName, value]) => ({
201
+ name: cookieName,
202
+ value,
203
+ domain: ".feishu.cn",
204
+ path: "/",
205
+ })),
206
+ ).catch(async () => {
207
+ // Fall back to per-cookie attempts when one malformed cookie rejects the batch.
208
+ for (const [cookieName, value] of Object.entries(cookies)) {
209
+ try {
210
+ await page.setCookie({
211
+ name: cookieName,
212
+ value,
213
+ domain: ".feishu.cn",
214
+ path: "/",
215
+ });
216
+ } catch {
217
+ // ignore individual failures
218
+ }
219
+ }
220
+ });
221
+
222
+ const network = [];
223
+ const byKey = new Map();
224
+ page.on("request", (request) => {
225
+ const requestUrl = request.url();
226
+ if (!/\/developers\/|\/open-apis\/|open\.feishu\.cn/.test(requestUrl)) return;
227
+ if (network.length >= 300) return;
228
+ let parsedUrl;
229
+ try {
230
+ parsedUrl = new URL(requestUrl);
231
+ } catch {
232
+ return;
233
+ }
234
+ const postData = request.postData();
235
+ const body = parseMaybeJson(postData);
236
+ const entry = {
237
+ method: request.method(),
238
+ url: requestUrl,
239
+ path: parsedUrl.pathname,
240
+ resource_type: request.resourceType(),
241
+ headers: redactHeaders(request.headers()),
242
+ body_shape: body !== undefined ? shapeOf(body) : undefined,
243
+ body_preview:
244
+ body !== undefined
245
+ ? JSON.stringify(body).slice(0, 800)
246
+ : postData?.slice(0, 400),
247
+ status: null,
248
+ };
249
+ network.push(entry);
250
+ byKey.set(`${entry.method} ${entry.url}`, entry);
251
+ });
252
+ page.on("response", (response) => {
253
+ const key = `${response.request().method()} ${response.url()}`;
254
+ const entry = byKey.get(key);
255
+ if (entry) entry.status = response.status();
256
+ });
257
+
258
+ await page.goto(url, { waitUntil: "domcontentloaded", timeout });
259
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
260
+
261
+ const actionResults = [];
262
+ for (const action of actions) {
263
+ actionResults.push(await applyAction(page, action));
264
+ }
265
+ if (actions.length) {
266
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
267
+ }
268
+
269
+ const title = await page.title();
270
+ const finalUrl = page.url();
271
+ const domText = await page.evaluate(() => document.body?.innerText || "");
272
+ const needsLogin =
273
+ /accounts\.feishu\.cn/.test(finalUrl) ||
274
+ /扫码登录|Log in to Feishu|登录飞书/.test(domText);
275
+
276
+ let screenshotPath = null;
277
+ if (screenshot) {
278
+ ensureDir(path.join(CACHE_DIR, "browser"));
279
+ screenshotPath = path.join(
280
+ CACHE_DIR,
281
+ "browser",
282
+ `console-${Date.now()}.png`,
283
+ );
284
+ await page.screenshot({ path: screenshotPath, fullPage: false });
285
+ }
286
+
287
+ let recipesRecorded = 0;
288
+ for (const entry of network) {
289
+ if (!entry.path.startsWith("/developers/")) continue;
290
+ if (!entry.body_shape && entry.method === "GET") continue;
291
+ const action = recordAs || actionForPath(entry.path);
292
+ if (!action) continue;
293
+ recordRecipe(action, {
294
+ method: entry.method,
295
+ path: entry.path.replace(/\/cli_[A-Za-z0-9]+/g, "/:app_id"),
296
+ headers: Object.fromEntries(
297
+ Object.entries(entry.headers).filter(
298
+ ([key]) => !/cookie|authorization|csrf|token|secret/i.test(key),
299
+ ),
300
+ ),
301
+ body_shape: entry.body_shape,
302
+ last_status: entry.status,
303
+ source: "browser_fallback",
304
+ });
305
+ recipesRecorded++;
306
+ }
307
+
308
+ return {
309
+ ok: true,
310
+ profile: name,
311
+ final_url: finalUrl,
312
+ title,
313
+ needs_login: needsLogin,
314
+ screenshot_path: screenshotPath
315
+ ? displayPath(screenshotPath)
316
+ : null,
317
+ actions: actionResults,
318
+ network: network.slice(0, 80),
319
+ recipes_recorded: recipesRecorded,
320
+ dom_text: domText.slice(0, 5000),
321
+ elapsed_ms: Date.now() - started,
322
+ };
323
+ } catch (error) {
324
+ return {
325
+ ok: false,
326
+ error: error instanceof Error ? error.message : String(error),
327
+ elapsed_ms: Date.now() - started,
328
+ };
329
+ } finally {
330
+ if (browser) {
331
+ try {
332
+ await browser.close();
333
+ } catch {
334
+ // ignore
335
+ }
336
+ }
337
+ }
338
+ }