larkit 0.0.0-stage → 0.1.1

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 (72) 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 +73 -4
  7. package/scripts/build-index.mjs +66 -0
  8. package/scripts/check-syntax.mjs +28 -0
  9. package/scripts/login-worker.mjs +50 -0
  10. package/scripts/publish-pack.mjs +277 -0
  11. package/scripts/verify-bootstrap.mjs +238 -0
  12. package/scripts/verify-console.mjs +60 -0
  13. package/scripts/verify-index.mjs +59 -0
  14. package/scripts/verify-mcp.mjs +215 -0
  15. package/skills/larkit/SKILL.md +120 -0
  16. package/skills/larkit/references/console.md +86 -0
  17. package/skills/larkit/references/playbooks.md +177 -0
  18. package/skills/larkit/references/tools.md +115 -0
  19. package/skills/larkit/references/truth-model.md +117 -0
  20. package/src/auth/login.js +171 -0
  21. package/src/auth/service.js +213 -0
  22. package/src/auth/store.js +213 -0
  23. package/src/bootstrap/channel.js +201 -0
  24. package/src/bootstrap/cli-install.js +184 -0
  25. package/src/bootstrap/download.js +182 -0
  26. package/src/bootstrap/init.js +672 -0
  27. package/src/bootstrap/pack-cli.js +97 -0
  28. package/src/bootstrap/pack.js +541 -0
  29. package/src/bootstrap/report.js +62 -0
  30. package/src/bootstrap/skills.js +223 -0
  31. package/src/bootstrap/state.js +66 -0
  32. package/src/bootstrap/tar.js +417 -0
  33. package/src/console/browser.js +338 -0
  34. package/src/console/client.js +827 -0
  35. package/src/console/dispatch.js +101 -0
  36. package/src/docs/client.js +284 -0
  37. package/src/indexer/anchors.js +366 -0
  38. package/src/indexer/db.js +449 -0
  39. package/src/indexer/extract-assets.js +285 -0
  40. package/src/indexer/extract-deb.js +56 -0
  41. package/src/indexer/format-pool.js +64 -0
  42. package/src/indexer/format-worker.js +132 -0
  43. package/src/indexer/format.js +239 -0
  44. package/src/indexer/protocol.js +123 -0
  45. package/src/mcp/server.js +433 -0
  46. package/src/probe/client.js +225 -0
  47. package/src/probe/openapi.js +194 -0
  48. package/src/protocol/catalog.js +146 -0
  49. package/src/protocol/data/README.md +15 -0
  50. package/src/protocol/data/bundle.json +1 -0
  51. package/src/protocol/data/commands.json +12896 -0
  52. package/src/protocol/data/constants.json +8 -0
  53. package/src/protocol/data/reference/PROTOCOL.md +66 -0
  54. package/src/protocol/data/reference/endpoints.json +46 -0
  55. package/src/protocol/data/schema.json.gz +0 -0
  56. package/src/protocol/descriptor.js +358 -0
  57. package/src/protocol/schema.js +235 -0
  58. package/src/protocol/transport.js +387 -0
  59. package/src/shared/browser.js +70 -0
  60. package/src/shared/config.js +90 -0
  61. package/src/shared/paths.js +151 -0
  62. package/src/shared/quiet.js +12 -0
  63. package/src/shared/util.js +124 -0
  64. package/src/truth/fts.js +24 -0
  65. package/src/truth/search.js +554 -0
  66. package/src/truth/verified.js +107 -0
  67. package/verified_truths/README.md +28 -0
  68. package/verified_truths/card-json-2.0-official.md +55 -0
  69. package/verified_truths/client-ws-gateway-boundaries.md +33 -0
  70. package/verified_truths/feishu-developer-console-apis.md +104 -0
  71. package/workers/truth-api/src/index.js +180 -0
  72. package/workers/truth-api/wrangler.toml +31 -0
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+ import "../src/shared/quiet.js";
3
+ import process from "node:process";
4
+ import { searchTruth, getProtocolEntry, readSlice, truthStats } from "../src/truth/search.js";
5
+ import { TRUTH_DB, FORMATTED_DIR } from "../src/shared/paths.js";
6
+
7
+ const queries = process.argv.slice(2).filter((x) => !x.startsWith("--"));
8
+ const list = queries.length
9
+ ? queries
10
+ : ["chart_spec", "card_link", "PUT_MESSAGE", "PUSH_MESSAGES"];
11
+
12
+ let failed = 0;
13
+ const stats = truthStats();
14
+ console.log(JSON.stringify({ truth_db: TRUTH_DB, formatted_dir: FORMATTED_DIR, stats }, null, 2));
15
+
16
+ for (const query of list) {
17
+ const result = searchTruth({ query, scope: "all", limit: 5 });
18
+ const ok = result.results.length > 0;
19
+ const fast = result.elapsed_ms < 50;
20
+ if (!ok || !fast) failed++;
21
+ console.log(
22
+ `\n${ok ? "PASS" : "FAIL"} ${fast ? "fast" : "SLOW"} ${result.elapsed_ms}ms query=${JSON.stringify(query)} hits=${result.results.length}`,
23
+ );
24
+ for (const hit of result.results.slice(0, 3)) {
25
+ if (hit.kind === "protocol") {
26
+ console.log(
27
+ ` protocol cmd=${hit.cmd} ${hit.name} ${hit.request_type || ""} schema=${(hit.schema || "").slice(0, 100)}`,
28
+ );
29
+ } else {
30
+ console.log(
31
+ ` ${hit.kind} ${hit.file_path}:${hit.line_number ?? hit.line_start} ${hit.anchor_type || hit.section || hit.title || ""} ${(hit.literal || "").slice(0, 90)}`,
32
+ );
33
+ }
34
+ }
35
+ if (query === list[0]) {
36
+ const first = result.results[0];
37
+ if (first && !first.kind.includes("protocol")) {
38
+ const slice = readSlice({
39
+ filePath: first.file_path,
40
+ lineNumber: first.line_number || first.line_start || 1,
41
+ context: 3,
42
+ maxLines: 12,
43
+ });
44
+ console.log(
45
+ ` slice ${slice.file_path}:${slice.range.start}-${slice.range.end} (${slice.lines.length} lines)`,
46
+ );
47
+ }
48
+ }
49
+ }
50
+
51
+ const protocol = getProtocolEntry("PUT_MESSAGE");
52
+ console.log(`\nprotocol lookup PUT_MESSAGE: ${protocol.length} entr${protocol.length === 1 ? "y" : "ies"}`);
53
+ if (!protocol.length) failed++;
54
+
55
+ if (failed) {
56
+ console.error(`\n${failed} verification check(s) failed`);
57
+ process.exit(1);
58
+ }
59
+ console.log("\nall index checks passed");
@@ -0,0 +1,215 @@
1
+ #!/usr/bin/env node
2
+ import "../src/shared/quiet.js";
3
+ import { spawn } from "node:child_process";
4
+ import process from "node:process";
5
+
6
+ const child = spawn(process.execPath, ["bin/larkit-mcp.js"], {
7
+ cwd: process.cwd(),
8
+ stdio: ["pipe", "pipe", "pipe"],
9
+ env: { ...process.env, LARKIT_MCP_TEST: "1" },
10
+ });
11
+
12
+ let buffer = "";
13
+ const pending = new Map();
14
+ let nextId = 1;
15
+ let stderr = "";
16
+ const received = [];
17
+
18
+ child.stderr.on("data", (chunk) => {
19
+ stderr += chunk.toString();
20
+ if (stderr.length > 8000) stderr = stderr.slice(-8000);
21
+ });
22
+
23
+ child.stdout.on("data", (chunk) => {
24
+ buffer += chunk.toString();
25
+ let index;
26
+ while ((index = buffer.indexOf("\n")) >= 0) {
27
+ const line = buffer.slice(0, index).trim();
28
+ buffer = buffer.slice(index + 1);
29
+ if (!line) continue;
30
+ let message;
31
+ try {
32
+ message = JSON.parse(line);
33
+ } catch {
34
+ console.error("non-JSON stdout:", line.slice(0, 300));
35
+ continue;
36
+ }
37
+ received.push(message);
38
+ if (message.id && pending.has(message.id)) {
39
+ pending.get(message.id)(message);
40
+ pending.delete(message.id);
41
+ }
42
+ }
43
+ });
44
+
45
+ function send(method, params, timeout = 60_000) {
46
+ const id = nextId++;
47
+ const payload = { jsonrpc: "2.0", id, method, params };
48
+ child.stdin.write(`${JSON.stringify(payload)}\n`);
49
+ return new Promise((resolve, reject) => {
50
+ const timer = setTimeout(() => {
51
+ pending.delete(id);
52
+ reject(new Error(`timeout waiting for ${method}`));
53
+ }, timeout);
54
+ pending.set(id, (message) => {
55
+ clearTimeout(timer);
56
+ if (message.error) reject(new Error(JSON.stringify(message.error)));
57
+ else resolve(message.result);
58
+ });
59
+ });
60
+ }
61
+
62
+ function notify(method, params = {}) {
63
+ child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}\n`);
64
+ }
65
+
66
+ function textOf(result) {
67
+ return result?.content?.map((item) => item.text).join("\n") || "";
68
+ }
69
+
70
+ async function main() {
71
+ const init = await send("initialize", {
72
+ protocolVersion: "2024-11-05",
73
+ capabilities: {},
74
+ clientInfo: { name: "larkit-verify", version: "1.0.0" },
75
+ });
76
+ console.log("initialize:", init.serverInfo?.name, init.serverInfo?.version);
77
+ notify("notifications/initialized");
78
+
79
+ const list = await send("tools/list", {});
80
+ console.log(
81
+ "tools:",
82
+ list.tools.map((tool) => tool.name).join(", "),
83
+ );
84
+ if (list.tools.length !== 6) {
85
+ throw new Error(`expected 6 tools, got ${list.tools.length}`);
86
+ }
87
+
88
+ const search = await send("tools/call", {
89
+ name: "lark_truth_search",
90
+ arguments: { query: "chart_spec", scope: "all", limit: 3 },
91
+ });
92
+ const searchJson = JSON.parse(textOf(search));
93
+ console.log(
94
+ "truth_search:",
95
+ `${searchJson.results.length} hits, ${searchJson.elapsed_ms}ms, first=${searchJson.results[0]?.file_path}:${searchJson.results[0]?.line_start || searchJson.results[0]?.line_number}`,
96
+ );
97
+ if (!searchJson.results.length) throw new Error("truth search returned no hits");
98
+
99
+ const firstCode = searchJson.results.find((hit) => hit.kind === "code") || {
100
+ file_path: "verified_truths/card-json-2.0-official.md",
101
+ line_number: 10,
102
+ };
103
+ const slice = await send("tools/call", {
104
+ name: "lark_truth_inspect",
105
+ arguments: {
106
+ action: "read_slice",
107
+ file_path: firstCode.file_path,
108
+ line_number: firstCode.line_number || 1,
109
+ context: 3,
110
+ max_lines: 20,
111
+ },
112
+ });
113
+ const sliceJson = JSON.parse(textOf(slice));
114
+ console.log(
115
+ "truth_inspect:",
116
+ `${sliceJson.file_path}:${sliceJson.range?.start}-${sliceJson.range?.end}, ${sliceJson.lines?.length} lines`,
117
+ );
118
+ if (!sliceJson.lines?.length) throw new Error("read_slice returned no lines");
119
+
120
+ const who = await send("tools/call", {
121
+ name: "lark_auth",
122
+ arguments: { action: "whoami", profile: "geren" },
123
+ });
124
+ const whoJson = JSON.parse(textOf(who));
125
+ console.log("auth whoami:", whoJson.ok, whoJson.profile, whoJson.user?.name || "");
126
+
127
+ const doc = await send("tools/call", {
128
+ name: "lark_doc",
129
+ arguments: { action: "search", query: "图表", limit: 3 },
130
+ });
131
+ const docJson = JSON.parse(textOf(doc));
132
+ console.log("doc search:", docJson.ok, docJson.total, docJson.docs?.[0]?.title || "");
133
+ if (!docJson.docs?.length) throw new Error("doc search returned no docs");
134
+ const docRead = await send("tools/call", {
135
+ name: "lark_doc",
136
+ arguments: { action: "read", path: docJson.docs[0].path, limit: 20 },
137
+ });
138
+ const docReadJson = JSON.parse(textOf(docRead));
139
+ console.log("doc read:", docReadJson.ok, docReadJson.total_lines, "lines");
140
+
141
+ const consoleList = await send("tools/call", {
142
+ name: "lark_console",
143
+ arguments: { action: "list_apps", profile: "geren", limit: 50 },
144
+ });
145
+ const consoleJson = JSON.parse(textOf(consoleList));
146
+ console.log("console list_apps:", consoleJson.ok, consoleJson.total, consoleJson.apps?.[0]?.app_id || "");
147
+ if (!consoleJson.apps?.length) throw new Error("console list_apps returned no apps");
148
+ const testApp = consoleJson.apps[0].app_id;
149
+ const probeApp =
150
+ (process.env.LARKIT_VERIFY_APP_ID &&
151
+ consoleJson.apps.find(
152
+ (app) => app.app_id === process.env.LARKIT_VERIFY_APP_ID,
153
+ )) ||
154
+ consoleJson.apps.find((app) => app.status === 2) ||
155
+ consoleJson.apps[0];
156
+
157
+ const consoleDry = await send("tools/call", {
158
+ name: "lark_console",
159
+ arguments: { action: "enable_bot_ws", app_id: testApp, profile: "geren", dry_run: true },
160
+ });
161
+ const consoleDryJson = JSON.parse(textOf(consoleDry));
162
+ console.log("console dry-run ws:", consoleDryJson.ok, consoleDryJson.calls?.length || 0, "calls");
163
+
164
+ const credResult = await send("tools/call", {
165
+ name: "lark_console",
166
+ arguments: { action: "get_credentials", app_id: probeApp.app_id, profile: "geren" },
167
+ });
168
+ const cred = JSON.parse(textOf(credResult));
169
+ console.log("console credentials:", cred.ok, cred.app_id, "secret_len=" + (cred.app_secret?.length || 0));
170
+
171
+ const openapi = await send("tools/call", {
172
+ name: "lark_probe",
173
+ arguments: {
174
+ mode: "openapi",
175
+ app_id: cred.app_id,
176
+ app_secret: cred.app_secret,
177
+ method: "GET",
178
+ path: "/im/v1/chats",
179
+ query: { page_size: 1 },
180
+ },
181
+ }, 60_000);
182
+ const openapiJson = JSON.parse(textOf(openapi));
183
+ console.log("openapi probe:", openapiJson.ok, openapiJson.status, openapiJson.code, openapiJson.ms || openapiJson.elapsed_ms);
184
+
185
+ const probe = await send("tools/call", {
186
+ name: "lark_probe",
187
+ arguments: {
188
+ mode: "client",
189
+ command: "PUSH_MESSAGES",
190
+ profile: "geren",
191
+ transport: "ws",
192
+ wait_push: true,
193
+ timeout: 5000,
194
+ },
195
+ }, 30_000);
196
+ const probeJson = JSON.parse(textOf(probe));
197
+ console.log(
198
+ "client probe:",
199
+ `handshake=${probeJson.handshake} ok=${probeJson.ok} events=${probeJson.events?.length || 0}`,
200
+ );
201
+
202
+ console.log("\nMCP verification passed");
203
+ if (stderr.trim()) console.log("server stderr tail:\n", stderr.trim().split("\n").slice(-8).join("\n"));
204
+ }
205
+
206
+ try {
207
+ await main();
208
+ } catch (error) {
209
+ console.error("MCP verification failed:", error);
210
+ if (stderr.trim()) console.error("stderr:\n", stderr.trim().slice(-3000));
211
+ process.exitCode = 1;
212
+ } finally {
213
+ child.kill("SIGTERM");
214
+ setTimeout(() => process.exit(process.exitCode || 0), 200);
215
+ }
@@ -0,0 +1,120 @@
1
+ ---
2
+ name: larkit
3
+ description: >
4
+ Feishu/Lark full-stack development and reverse-engineering with a local truth engine. Use when the
5
+ task involves Feishu/Lark official docs, cards (卡片/JSON 2.0/chart_spec/card_link), messages,
6
+ bots or event subscriptions, WebSocket long connections, app creation and publishing in the
7
+ developer console (open.feishu.cn/app), permission scopes, tenant_access_token, OpenAPI probing,
8
+ client private protocol commands (PUT_MESSAGE/PUSH_MESSAGES/2020 commands), protobuf schemas,
9
+ hidden fields, error codes (99991672/9499), or reading reverse-engineered web/Electron client code.
10
+ Provides MCP tools `lark_doc`, `lark_truth_search`, `lark_truth_inspect`, `lark_auth`,
11
+ `lark_console`, `lark_probe` and the equivalent `larkit` CLI.
12
+ ---
13
+
14
+ # Larkit — Feishu full-stack truth engine
15
+
16
+ Larkit answers Feishu questions from **evidence**, not guesses: 4800+ official docs, 2020 client
17
+ protocol commands, 237k code anchors from the web/Electron client, and a `verified_truths`
18
+ knowledge base that grows as you verify things. It can also create/configure/publish real apps in
19
+ the developer console and send real API/protocol probes.
20
+
21
+ ## 1. Preflight (do this once per session)
22
+
23
+ ```bash
24
+ larkit status --json # fastest: truth.db? code corpus? skill? MCP config?
25
+ larkit doctor # auth profiles, tokens, Chrome, install state
26
+ ```
27
+
28
+ - `truth.installed=false` → have the user run `npx larkit` (or `larkit init`).
29
+ - `code.installed=false` → the truth pack is half-extracted; rerun `larkit init` (the code corpus
30
+ ships in the same archive, there is nothing optional to add).
31
+ - If the MCP tools are not mounted, use the CLI equivalents in `references/tools.md`; they return
32
+ the same JSON.
33
+
34
+ Never invent Feishu behavior when the truth engine or the official docs can answer it.
35
+
36
+ ## 2. Route the question
37
+
38
+ | User asks | Start with | Then |
39
+ | --- | --- | --- |
40
+ | "Is card field X supported?" / card JSON 2.0 | `lark_truth_search(scope="verified")` | `lark_doc search` → `read` → code anchors |
41
+ | "What does this error code mean?" | `lark_truth_search(scope="all")` | doc error-code pages → `lark_probe` |
42
+ | "Create a bot that receives events over WebSocket" | `references/playbooks.md` §B | `lark_auth` → `lark_console` → `lark_probe` |
43
+ | "Why does this OpenAPI call fail?" | `lark_probe(mode="openapi")` | read `code/msg/log_id`, then scope/CSRF checks |
44
+ | "What is command N / PUT_MESSAGE?" | `lark_truth_search(scope="protocol")` | `read_slice` on protocol docs or client code |
45
+ | "Show me the client implementation" | search `scope="web"`/`"electron"` | `read_slice` with `mode="function"` |
46
+ | "Configure/publish an app" | `lark_console` (`dry_run` first) | `references/console.md` |
47
+ | "Log me in / switch account" | `lark_auth(action="list")` | `check` → QR login → `check` |
48
+ | "Set up larkit / install the skill" | §6 below | |
49
+
50
+ Full playbooks: `references/playbooks.md`. Console API recipes and failure matrix:
51
+ `references/console.md`. Tool/CLI/env reference: `references/tools.md`.
52
+
53
+ ## 3. Token discipline (hard rules)
54
+
55
+ 1. **Always try `scope="verified"` first.** If a prior conclusion exists, stop there.
56
+ 2. `limit <= 10`; results contain coordinates + a 320-char snippet, not whole files.
57
+ 3. Never read a formatted client file end-to-end. `search` → `read_slice`.
58
+ 4. `read_slice`: prefer `mode="function"`, `max_lines <= 120`.
59
+ 5. Docs: `lark_doc search` → `read` with `offset/limit`; use returned `headings` to jump.
60
+ 6. Probes: `GET` before `POST`; `dry_run=true` before every console write; `wait_push=true` for
61
+ listener-only protocol commands.
62
+ 7. `save_verified` only with evidence (probe output/log_id, doc URL, `file:line`). No secrets,
63
+ cookies or tokens in the knowledge base.
64
+ 8. Never echo credentials or auth headers back to the user.
65
+
66
+ ## 4. Standard loop
67
+
68
+ ```
69
+ lark_auth check
70
+ → lark_truth_search(scope=verified)
71
+ → lark_doc search/read
72
+ → lark_truth_search(scope=protocol|web|electron)
73
+ → lark_truth_inspect(read_slice)
74
+ → lark_probe(openapi|client)
75
+ → lark_truth_inspect(save_verified)
76
+ → lark_console(dry_run → apply)
77
+ → lark_probe re-verify
78
+ ```
79
+
80
+ Stop as soon as the answer is certain. Each step exists only to cover the previous step's gap.
81
+
82
+ ## 5. Evidence boundaries
83
+
84
+ - Desktop client main process is a native binary (`feishu`/`libiron`); the corpus covers renderer,
85
+ preload, JSSDK, worker and protocol assets, not native main-process logic.
86
+ - `read_slice` paths are relative to `formatted/` (web/electron) or `verified_truths/`.
87
+ - i18n bundles are not indexed; search specific keys if needed.
88
+ - Feishu internal console APIs and private protocol routes drift between client versions. Record
89
+ what you verified, with the client/doc version, in `verified_truths`.
90
+ - Official doc endpoints and console APIs are rate-limited; back off instead of retrying blindly.
91
+
92
+ ## 6. Installing / repairing larkit (when the user asks)
93
+
94
+ ```bash
95
+ npx larkit # init on first run, status afterwards
96
+ npx larkit init --channel # (re)install from the official truth channel
97
+ npx larkit init --pack=<manifest.json|dir|.tar.gz> # offline / pinned install
98
+ npx larkit init --from-assets --deb=./feishu-linux.deb --web-dump=./feishu-web-dump
99
+ # ↑ 构建资产不随包分发,需自备 deb + web dump;常规安装用上面两条命令即可
100
+ npx larkit status # verify truth.db / skill / CLI / MCP
101
+ npx larkit update # refresh outdated pieces
102
+ npx larkit skills install # reinstall the agent skill
103
+ npx larkit pack verify <manifest.json> # maintainers: verify a truth pack
104
+ ```
105
+
106
+ Layout: `~/.larkit/{data/truth.db, verified_truths, cli, install.json, mcp.json}`. The MCP client
107
+ config is written to `~/.larkit/mcp.json`; the skill lands in existing agent skill directories
108
+ (`~/.agents/skills/larkit`, `~/.claude/skills/larkit`) or `--target=`/`--project`.
109
+
110
+ ## 7. When a tool fails
111
+
112
+ | Symptom | Action |
113
+ | --- | --- |
114
+ | `truth.db not found` | `npx larkit` (or `larkit init`) |
115
+ | `file not found: formatted/...` | `larkit init` (corpus ships with the truth pack) |
116
+ | `needs_login` / `9499` | `lark_auth(action="login")` then poll `login_status`; or `import` cookies |
117
+ | `99991672` | missing scope → `lark_console grant_scopes` → re-probe |
118
+ | `command: N not found` | re-check cmd/kind with `lark_truth_search(scope="protocol")` |
119
+ | `PullFeedGroups invalid request` | read operations belong on OpenAPI, not the private gateway |
120
+ | MCP tool missing | use the CLI (`larkit search/inspect/doc/console/probe`) |
@@ -0,0 +1,86 @@
1
+ # Developer console (`open.feishu.cn/app`) — recipes and failure matrix
2
+
3
+ ## Session / CSRF
4
+
5
+ - Read the active profile from `~/.larkit/credentials.json`.
6
+ - Open `https://open.feishu.cn/app`, extract `window.csrfToken`, and merge response cookies
7
+ (`lark_oapi_csrf_token`, `sl_session`).
8
+ - Headers: `Cookie`, `x-csrf-token`, `Origin`, `Referer`, `User-Agent`, `X-Timezone-Offset`.
9
+ - `swp_csrf_token` alone → `9499 lark_oapi_csrf_token not exist in cookie`.
10
+ - Mismatched header token → `9499 csrf token check fail`.
11
+
12
+ ## Internal HTTP recipes
13
+
14
+ | Capability | Method / path | Key body |
15
+ | --- | --- | --- |
16
+ | App list | `POST /developers/v1/app/list` | `{Count,Cursor}` |
17
+ | Create app | `POST /developers/v1/app/create` | `{name,desc,avatar,primaryLang,i18n,appSceneType}` |
18
+ | App detail | `POST /developers/v1/app/:clientId` | `{}` |
19
+ | Credentials | `POST /developers/v1/secret/:clientId` | `{}` → `data.secret` |
20
+ | Update info | `POST /developers/v1/base_info/:clientId` | `{name,desc,i18n,avatar,languages}` |
21
+ | All scopes | `POST /developers/v1/scope/all/:clientId` | `{clientId}` |
22
+ | Apply scopes | `POST /developers/v1/scope/update/:clientId` | `{operation:"add",scopeIds:[],appScopeIDs:[...]}` |
23
+ | Robot state | `POST /developers/v1/robot/:clientId` | `{clientId}` |
24
+ | Enable robot | `POST /developers/v1/robot/switch/:clientId` | `{enable:true}` |
25
+ | Event mode | `POST /developers/v1/event/switch/:clientId` | `{eventMode:4}` |
26
+ | Card callback mode | `POST /developers/v1/callback/switch/:clientId` | `{callbackMode:4}` |
27
+ | Version list | `POST /developers/v1/app_version/list/:clientId` | `{clientId}` |
28
+ | Create version | `POST /developers/v1/app_version/create/:clientId` | `{appVersion,updateRemark,visibleSuggest,changeLog}` |
29
+ | Commit release | `POST /developers/v1/publish/commit/:clientId/:versionId` | `{is_full_release}` |
30
+ | Release | `POST /developers/v1/publish/release/:clientId/:versionId` | `{is_full_release}` |
31
+
32
+ `eventMode=4` and `callbackMode=4` mean WebSocket long connection. `robot.cardCallbackMode` is the
33
+ card request-address mode (1=Webhook), not the long-connection switch. Sending `callbackMode:4` to
34
+ `/v2/robot/push_mode/switch` returns `ParamInvalid`.
35
+
36
+ ## HTTP first, browser fallback second
37
+
38
+ Use plain HTTP when `lark_console` already has a recipe: milliseconds, dry-runnable, reproducible.
39
+ Use `browser_fallback` when:
40
+
41
+ - the internal endpoint is not covered (avatar upload, complex forms, review flows),
42
+ - HTTP returns 9499/403 while the session is known-good,
43
+ - you need to capture a new console API shape.
44
+
45
+ Example:
46
+
47
+ ```
48
+ lark_console({
49
+ action:"browser_fallback",
50
+ profile:"geren",
51
+ url:"https://open.feishu.cn/app",
52
+ wait_ms:3000,
53
+ actions:[
54
+ {type:"click_text", text:"创建应用"},
55
+ {type:"fill", fields:[{placeholder:"应用名称", value:"my-app"}]}
56
+ ],
57
+ screenshot:true,
58
+ record_as:"create_app"
59
+ })
60
+ ```
61
+
62
+ It reuses system Chrome (`CHROME_PATH`) with the larkit cookie and records `/developers/` +
63
+ `/open-apis/` requests (sanitized headers, body shape, status) into
64
+ `<data>/console-api-recipes.json`. Promote stable paths into `src/console/client.js`
65
+ `DEFAULT_RECIPES`, then reproduce via `lark_console` HTTP.
66
+
67
+ ## Failure matrix
68
+
69
+ | Symptom | Cause | Fix |
70
+ | --- | --- | --- |
71
+ | `9499 lark_oapi_csrf_token not exist in cookie` | only `swp_csrf_token` sent | re-resolve session (`lark_auth check`), merge Set-Cookie |
72
+ | `9499 csrf token check fail` | header/page/cookie token mismatch | use page `window.csrfToken` + `lark_oapi_csrf_token` |
73
+ | `99991672` | app lacks scope | `grant_scopes` then re-probe |
74
+ | WS `timeout waiting for cmd=6` | push command was sent instead of listened | use `wait_push:true` |
75
+ | Gateway `PullFeedGroups invalid request` | read command not routed on the gateway | use OpenAPI for reads |
76
+ | `command: N not found` | wrong command id/name or wrong transport | re-check via `lark_truth_search(scope=protocol)` |
77
+ | `ParamInvalid` on `/v2/robot/push_mode/switch` | `callbackMode:4` misuse | use `/v1/callback/switch` |
78
+ | `truth.db not found` | no truth source installed | `npx larkit` / `larkit init` |
79
+ | `file not found` in `read_slice` | code corpus missing | `larkit init` |
80
+ | `needs_login` from browser fallback | cookie expired | `lark_auth login` |
81
+
82
+ ## Evidence rules for console work
83
+
84
+ - Writes have real side effects (apps, versions, releases). Always dry-run first.
85
+ - Record `app_id`, action, `code`, `msg`, and verification fields in `save_verified`.
86
+ - Never persist credentials, tokens, or full auth headers.
@@ -0,0 +1,177 @@
1
+ # Playbooks
2
+
3
+ Each playbook is a closed loop: evidence → action → verification → `save_verified`.
4
+ Load only the one that matches the task.
5
+
6
+ ## A. Card field / hidden property support (e.g. `chart_spec`, `card_link`, `streaming_mode`)
7
+
8
+ 1. `lark_truth_search({query:"chart_spec", scope:"verified"})`
9
+ - Hit in `verified_truths/card-json-2.0-official.md` → read that section and stop.
10
+ 2. Miss → official docs:
11
+ - `lark_doc({action:"search", query:"卡片 JSON 2.0"})`
12
+ - `lark_doc({action:"read", path, limit:120})`, jump with `offset` from `headings`.
13
+ 3. Docs ambiguous → client code:
14
+ - `lark_truth_search({query:"chart_spec", scope:"all"})`
15
+ - `lark_truth_inspect({action:"read_slice", file_path, line_number, mode:"function", max_lines:80})`
16
+ 4. Need server truth → `lark_probe({mode:"openapi", method:"POST", path:"/im/v1/messages", body:{...}})`
17
+ or `PATCH /open-apis/im/v1/messages/:message_id` to update a card; inspect `code/msg`.
18
+ 5. Persist:
19
+ ```
20
+ lark_truth_inspect({
21
+ action:"save_verified",
22
+ topic:"卡片 chart_spec 的 media 行为",
23
+ content:"## 结论 ...\n## 最小 JSON ...",
24
+ evidence:"doc URL + probe log_id/response",
25
+ source:"doc|probe",
26
+ confidence:"high"
27
+ })
28
+ ```
29
+
30
+ Note: `chart_spec` is **not** present in the current desktop/Web code; only the official docs
31
+ describe it. Do not claim a code location you have not seen.
32
+
33
+ ## B. Create a bot that receives events over WebSocket (from zero)
34
+
35
+ ### B1. Auth
36
+
37
+ ```
38
+ lark_auth({action:"check", profile:"geren"})
39
+ ```
40
+
41
+ - `needs_login` → `lark_auth({action:"login", name:"geren"})`, then poll
42
+ `lark_auth({action:"login_status", name:"geren"})` and show the user `qr_path`.
43
+ - Headless/import path: `lark_auth({action:"import", name:"geren", cookie:"..."})`.
44
+ - `lark_auth list` to find a non-expired profile. Do not use stale `tapeless-*` profiles.
45
+
46
+ ### B2. Create the app (dry-run first)
47
+
48
+ ```
49
+ lark_console({action:"create_app", profile:"geren", name:"my-agent-app", desc:"...", dry_run:true})
50
+ ```
51
+
52
+ Review the plan, then run without `dry_run`. Capture `app_id` (`cli_xxx`).
53
+
54
+ ### B3. Credentials
55
+
56
+ ```
57
+ lark_console({action:"get_credentials", app_id:"cli_xxx", profile:"geren"})
58
+ ```
59
+
60
+ Store in `larkit.config.json` / env vars, never in `verified_truths`.
61
+
62
+ ### B4. Scopes
63
+
64
+ ```
65
+ lark_console({action:"list_scopes", app_id:"cli_xxx", profile:"geren"})
66
+ lark_console({action:"grant_scopes", app_id:"cli_xxx",
67
+ scopes:["im:message","im:chat:readonly","im:message:send_as_bot"],
68
+ scope_type:"app", dry_run:true})
69
+ ```
70
+
71
+ Check `resolved.missing` is empty before applying.
72
+
73
+ ### B5. Robot + event/card WebSocket
74
+
75
+ ```
76
+ lark_console({action:"enable_bot_ws", app_id:"cli_xxx", profile:"geren", dry_run:true})
77
+ lark_console({action:"enable_bot_ws", app_id:"cli_xxx", profile:"geren"})
78
+ ```
79
+
80
+ It performs three writes:
81
+
82
+ - `/developers/v1/robot/switch/:id` → `{enable:true}`
83
+ - `/developers/v1/event/switch/:id` → `{eventMode:4}`
84
+ - `/developers/v1/callback/switch/:id` → `{callbackMode:4}`
85
+
86
+ Verification must show `bot_enabled=true, event_mode=4, callback_mode=4`.
87
+ `robot.cardCallbackMode` is the card request-address mode (1=Webhook), **not** the long-connection
88
+ switch. Do not send `callbackMode:4` to `/v2/robot/push_mode/switch` (returns `ParamInvalid`).
89
+
90
+ ### B6. Publish
91
+
92
+ ```
93
+ lark_console({action:"publish", app_id:"cli_xxx", version:"1.0.0",
94
+ remark:"agent bootstrap", full_release:false, dry_run:true})
95
+ ```
96
+
97
+ Apply after reviewing. `commit.code=0` means submitted; `list_versions` confirms status.
98
+
99
+ ### B7. Probe
100
+
101
+ ```
102
+ lark_probe({mode:"openapi", app_id:"cli_xxx", app_secret:"...",
103
+ method:"GET", path:"/im/v1/chats", query:{page_size:1}})
104
+ ```
105
+
106
+ `code:0` = credentials + scope OK. `99991672` = missing scope (back to B4). `9499` = CSRF/login
107
+ (back to B1).
108
+
109
+ ### B8. Persist
110
+
111
+ ```
112
+ lark_truth_inspect({action:"save_verified", topic:"my-agent-app 的长连接配置",
113
+ content:"## 结论\napp_id=... eventMode=4 / callbackMode=4 ...",
114
+ evidence:"enable_bot_ws verification=...; probe log_id=...",
115
+ source:"console", confidence:"high"})
116
+ ```
117
+
118
+ ## C. Verify an undocumented API / error code
119
+
120
+ 1. `lark_truth_search({query:"99991672", scope:"all"})` (or the path/field).
121
+ 2. `lark_doc` for permission/error-code pages.
122
+ 3. Send the minimal request:
123
+ ```
124
+ lark_probe({mode:"openapi", app_id, app_secret, method:"POST",
125
+ path:"/open-apis/...", query:{...}, body:{...}})
126
+ ```
127
+ Keep `status/code/msg/log_id/body` verbatim. Do not retry dozens of times; read `msg` to
128
+ distinguish permission vs parameter vs route errors.
129
+ 4. Not an OpenAPI capability → search the private protocol:
130
+ `lark_truth_search({query:"COMMAND_OR_ENDPOINT", scope:"protocol"})`.
131
+ 5. `lark_probe({mode:"client", command:"...", transport:"gateway"|"ws", profile})`.
132
+ 6. Save conclusion + evidence.
133
+
134
+ ## D. Reverse-engineer the private client protocol
135
+
136
+ 1. `lark_truth_search({query:"PUT_MESSAGE", scope:"protocol"})`
137
+ → `cmd=5`, `messages.PutMessageRequest`, schema summary.
138
+ 2. Field details:
139
+ - `lark_truth_search({query:"PutMessageRequest", scope:"protocol", limit:5})`
140
+ - or read `src/protocol/data/reference/PROTOCOL.md`.
141
+ 3. Client implementation evidence:
142
+ ```
143
+ lark_truth_search({query:"PUT_MESSAGE", scope:"electron"})
144
+ lark_truth_inspect({action:"read_slice", file_path, line_number, mode:"function", max_lines:100})
145
+ ```
146
+ 4. Send:
147
+ ```
148
+ lark_probe({mode:"client", command:"PUT_MESSAGE", transport:"gateway",
149
+ profile:"geren", payload:{...}})
150
+ ```
151
+ - Writes go through `gateway`; pushes/signals through `ws`.
152
+ - `PUSH_MESSAGES` is listen-only: `wait_push:true`, never send it.
153
+ - `GET_CHAT_MESSAGES` over the gateway returns `PullFeedGroups invalid request`; use OpenAPI for reads.
154
+ 5. Save new commands/fields/boundaries.
155
+
156
+ ## E. Inspect the developer console / capture a new internal API
157
+
158
+ Use `lark_console action=browser_fallback` only when the HTTP recipes are insufficient:
159
+
160
+ ```
161
+ lark_console({
162
+ action:"browser_fallback",
163
+ profile:"geren",
164
+ url:"https://open.feishu.cn/app",
165
+ wait_ms:3000,
166
+ actions:[
167
+ {type:"click_text", text:"创建应用"},
168
+ {type:"fill", fields:[{placeholder:"应用名称", value:"my-app"}]}
169
+ ],
170
+ screenshot:true,
171
+ record_as:"create_app"
172
+ })
173
+ ```
174
+
175
+ Returns `network[]` (sanitized headers/body shape), `recipes_recorded`, `needs_login`,
176
+ `screenshot_path`. Add stable paths to `src/console/client.js` `DEFAULT_RECIPES`, then reproduce
177
+ over plain HTTP.