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.
- package/AGENT_SKILL.md +12 -0
- package/README.md +765 -2
- package/bin/larkit-mcp.js +8 -0
- package/bin/larkit.js +532 -0
- package/larkit.config.example.json +12 -0
- package/package.json +73 -4
- package/scripts/build-index.mjs +66 -0
- package/scripts/check-syntax.mjs +28 -0
- package/scripts/login-worker.mjs +50 -0
- package/scripts/publish-pack.mjs +277 -0
- package/scripts/verify-bootstrap.mjs +238 -0
- package/scripts/verify-console.mjs +60 -0
- package/scripts/verify-index.mjs +59 -0
- package/scripts/verify-mcp.mjs +215 -0
- package/skills/larkit/SKILL.md +120 -0
- package/skills/larkit/references/console.md +86 -0
- package/skills/larkit/references/playbooks.md +177 -0
- package/skills/larkit/references/tools.md +115 -0
- package/skills/larkit/references/truth-model.md +117 -0
- package/src/auth/login.js +171 -0
- package/src/auth/service.js +213 -0
- package/src/auth/store.js +213 -0
- package/src/bootstrap/channel.js +201 -0
- package/src/bootstrap/cli-install.js +184 -0
- package/src/bootstrap/download.js +182 -0
- package/src/bootstrap/init.js +672 -0
- package/src/bootstrap/pack-cli.js +97 -0
- package/src/bootstrap/pack.js +541 -0
- package/src/bootstrap/report.js +62 -0
- package/src/bootstrap/skills.js +223 -0
- package/src/bootstrap/state.js +66 -0
- package/src/bootstrap/tar.js +417 -0
- package/src/console/browser.js +338 -0
- package/src/console/client.js +827 -0
- package/src/console/dispatch.js +101 -0
- package/src/docs/client.js +284 -0
- package/src/indexer/anchors.js +366 -0
- package/src/indexer/db.js +449 -0
- package/src/indexer/extract-assets.js +285 -0
- package/src/indexer/extract-deb.js +56 -0
- package/src/indexer/format-pool.js +64 -0
- package/src/indexer/format-worker.js +132 -0
- package/src/indexer/format.js +239 -0
- package/src/indexer/protocol.js +123 -0
- package/src/mcp/server.js +433 -0
- package/src/probe/client.js +225 -0
- package/src/probe/openapi.js +194 -0
- package/src/protocol/catalog.js +146 -0
- package/src/protocol/data/README.md +15 -0
- package/src/protocol/data/bundle.json +1 -0
- package/src/protocol/data/commands.json +12896 -0
- package/src/protocol/data/constants.json +8 -0
- package/src/protocol/data/reference/PROTOCOL.md +66 -0
- package/src/protocol/data/reference/endpoints.json +46 -0
- package/src/protocol/data/schema.json.gz +0 -0
- package/src/protocol/descriptor.js +358 -0
- package/src/protocol/schema.js +235 -0
- package/src/protocol/transport.js +387 -0
- package/src/shared/browser.js +70 -0
- package/src/shared/config.js +90 -0
- package/src/shared/paths.js +151 -0
- package/src/shared/quiet.js +12 -0
- package/src/shared/util.js +124 -0
- package/src/truth/fts.js +24 -0
- package/src/truth/search.js +554 -0
- package/src/truth/verified.js +107 -0
- package/verified_truths/README.md +28 -0
- package/verified_truths/card-json-2.0-official.md +55 -0
- package/verified_truths/client-ws-gateway-boundaries.md +33 -0
- package/verified_truths/feishu-developer-console-apis.md +104 -0
- package/workers/truth-api/src/index.js +180 -0
- 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.
|