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.
- 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 +65 -4
- package/scripts/.tmp-npm-webauth.mjs +49 -0
- 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,60 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "../src/shared/quiet.js";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { checkAuth } from "../src/auth/service.js";
|
|
5
|
+
import {
|
|
6
|
+
enableBotWs,
|
|
7
|
+
getCredentials,
|
|
8
|
+
grantScopes,
|
|
9
|
+
listApps,
|
|
10
|
+
publishVersion,
|
|
11
|
+
} from "../src/console/client.js";
|
|
12
|
+
import { probeOpenApi } from "../src/probe/openapi.js";
|
|
13
|
+
|
|
14
|
+
const profileArg = process.argv.find((arg) => arg.startsWith("--profile="));
|
|
15
|
+
const profile = profileArg ? profileArg.slice(10) : "geren";
|
|
16
|
+
|
|
17
|
+
const auth = await checkAuth({ profile });
|
|
18
|
+
console.log("auth:", auth.ok, auth.profile, auth.user?.name || auth.hint || auth.error);
|
|
19
|
+
if (!auth.ok) process.exit(1);
|
|
20
|
+
|
|
21
|
+
const apps = await listApps({ profile, limit: 30 });
|
|
22
|
+
console.log("apps:", apps.ok, apps.total);
|
|
23
|
+
if (!apps.ok || !apps.apps.length) process.exit(1);
|
|
24
|
+
|
|
25
|
+
const app =
|
|
26
|
+
apps.apps.find((item) => item.status === 2 && item.abilities?.includes("bot")) ||
|
|
27
|
+
apps.apps[0];
|
|
28
|
+
console.log("selected app:", app.app_id, app.name, "status=" + app.status);
|
|
29
|
+
|
|
30
|
+
const cred = await getCredentials({ appId: app.app_id, profile });
|
|
31
|
+
console.log("credentials:", cred.ok, "secret_len=" + (cred.app_secret?.length || 0));
|
|
32
|
+
if (!cred.ok) process.exit(1);
|
|
33
|
+
|
|
34
|
+
const probe = await probeOpenApi({
|
|
35
|
+
appId: cred.app_id,
|
|
36
|
+
appSecret: cred.app_secret,
|
|
37
|
+
method: "GET",
|
|
38
|
+
path: "/im/v1/chats",
|
|
39
|
+
query: { page_size: 1 },
|
|
40
|
+
});
|
|
41
|
+
console.log("openapi probe:", probe.status, probe.code, probe.msg, probe.log_id || "");
|
|
42
|
+
|
|
43
|
+
const wsPlan = await enableBotWs({ appId: app.app_id, profile, dryRun: true });
|
|
44
|
+
console.log("bot ws dry-run:", wsPlan.ok, wsPlan.calls?.length || 0, "calls");
|
|
45
|
+
|
|
46
|
+
const scopePlan = await grantScopes({
|
|
47
|
+
appId: app.app_id,
|
|
48
|
+
scopes: ["im:message"],
|
|
49
|
+
profile,
|
|
50
|
+
dryRun: true,
|
|
51
|
+
});
|
|
52
|
+
console.log(
|
|
53
|
+
"scope dry-run:",
|
|
54
|
+
scopePlan.ok,
|
|
55
|
+
"resolved=" + (scopePlan.plan?.resolved?.map((s) => s.name).join(",") || ""),
|
|
56
|
+
"missing=" + (scopePlan.plan?.missing?.join(",") || ""),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const publishPlan = await publishVersion({ appId: app.app_id, profile, dryRun: true });
|
|
60
|
+
console.log("publish dry-run:", publishPlan.ok, publishPlan.plan?.steps?.length || 0, "steps");
|
|
@@ -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.
|