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,433 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import {
|
|
3
|
+
CallToolRequestSchema,
|
|
4
|
+
ListToolsRequestSchema,
|
|
5
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
6
|
+
import { searchDocs, readDoc, docCategories } from "../docs/client.js";
|
|
7
|
+
import { searchTruth, readSlice, saveVerified } from "../truth/search.js";
|
|
8
|
+
import { authDispatch } from "../auth/service.js";
|
|
9
|
+
import { consoleDispatch } from "../console/dispatch.js";
|
|
10
|
+
import { probeOpenApi } from "../probe/openapi.js";
|
|
11
|
+
import { probeClientProtocol } from "../probe/client.js";
|
|
12
|
+
|
|
13
|
+
const TOOLS = [
|
|
14
|
+
{
|
|
15
|
+
name: "lark_doc",
|
|
16
|
+
description:
|
|
17
|
+
"Feishu official documentation (4800+ docs, zero config). action=search lists docs by category or keyword; action=read reads a doc path and returns saved markdown + line-bounded content; action=categories lists taxonomy.",
|
|
18
|
+
inputSchema: {
|
|
19
|
+
type: "object",
|
|
20
|
+
additionalProperties: false,
|
|
21
|
+
properties: {
|
|
22
|
+
action: {
|
|
23
|
+
type: "string",
|
|
24
|
+
enum: ["search", "read", "categories"],
|
|
25
|
+
description: "search | read | categories",
|
|
26
|
+
},
|
|
27
|
+
query: { type: "string", description: "Title/path keyword for search" },
|
|
28
|
+
category: {
|
|
29
|
+
type: "string",
|
|
30
|
+
description: "Taxonomy key, e.g. 服务端 API_即时通讯",
|
|
31
|
+
},
|
|
32
|
+
path: { type: "string", description: "Document path from search" },
|
|
33
|
+
offset: {
|
|
34
|
+
type: "integer",
|
|
35
|
+
minimum: 0,
|
|
36
|
+
description: "Read start line (0-based) for large docs",
|
|
37
|
+
},
|
|
38
|
+
limit: {
|
|
39
|
+
type: "integer",
|
|
40
|
+
minimum: 0,
|
|
41
|
+
maximum: 400,
|
|
42
|
+
description: "Read line count / search result cap",
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
required: ["action"],
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "lark_truth_search",
|
|
50
|
+
description:
|
|
51
|
+
"Search the local truth engine: verified_truths, 2020 client protocol commands/schemas, official protocol notes, and formatted web/electron code anchors. Returns compact hits with exact file:line coordinates. Use scope=protocol for commands, web/electron for code, verified for prior conclusions.",
|
|
52
|
+
inputSchema: {
|
|
53
|
+
type: "object",
|
|
54
|
+
additionalProperties: false,
|
|
55
|
+
properties: {
|
|
56
|
+
query: {
|
|
57
|
+
type: "string",
|
|
58
|
+
description:
|
|
59
|
+
"Keyword, API path, card field (chart_spec/card_link), error code, IPC channel, or client command",
|
|
60
|
+
},
|
|
61
|
+
scope: {
|
|
62
|
+
type: "string",
|
|
63
|
+
enum: ["all", "verified", "protocol", "web", "electron"],
|
|
64
|
+
description: "Search domain (default all)",
|
|
65
|
+
},
|
|
66
|
+
limit: {
|
|
67
|
+
type: "integer",
|
|
68
|
+
minimum: 1,
|
|
69
|
+
maximum: 50,
|
|
70
|
+
description: "Max results (default 10)",
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
required: ["query"],
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: "lark_truth_inspect",
|
|
78
|
+
description:
|
|
79
|
+
"Read a bounded slice of formatted code by file/line (action=read_slice), or persist a verified hidden field/protocol/endpoint into verified_truths and the SQLite index (action=save_verified). Prefer max_lines<=120 to save tokens.",
|
|
80
|
+
inputSchema: {
|
|
81
|
+
type: "object",
|
|
82
|
+
additionalProperties: false,
|
|
83
|
+
properties: {
|
|
84
|
+
action: {
|
|
85
|
+
type: "string",
|
|
86
|
+
enum: ["read_slice", "save_verified"],
|
|
87
|
+
description: "read_slice | save_verified",
|
|
88
|
+
},
|
|
89
|
+
file_path: {
|
|
90
|
+
type: "string",
|
|
91
|
+
description: "Formatted file path from lark_truth_search, e.g. web/debugger/x.js",
|
|
92
|
+
},
|
|
93
|
+
line_number: {
|
|
94
|
+
type: "integer",
|
|
95
|
+
minimum: 1,
|
|
96
|
+
description: "1-based line number",
|
|
97
|
+
},
|
|
98
|
+
context: {
|
|
99
|
+
type: "integer",
|
|
100
|
+
minimum: 1,
|
|
101
|
+
maximum: 60,
|
|
102
|
+
description: "Context radius (default 12)",
|
|
103
|
+
},
|
|
104
|
+
mode: {
|
|
105
|
+
type: "string",
|
|
106
|
+
enum: ["context", "function"],
|
|
107
|
+
description: "context slice or nearest function boundary",
|
|
108
|
+
},
|
|
109
|
+
max_lines: {
|
|
110
|
+
type: "integer",
|
|
111
|
+
minimum: 10,
|
|
112
|
+
maximum: 200,
|
|
113
|
+
description: "Hard cap (default 120)",
|
|
114
|
+
},
|
|
115
|
+
topic: { type: "string", description: "Verified-truth topic/title" },
|
|
116
|
+
content: {
|
|
117
|
+
type: "string",
|
|
118
|
+
description: "Markdown conclusion + reusable code",
|
|
119
|
+
},
|
|
120
|
+
evidence: {
|
|
121
|
+
type: "string",
|
|
122
|
+
description: "Probe output, URL, file:line, log id",
|
|
123
|
+
},
|
|
124
|
+
source: {
|
|
125
|
+
type: "string",
|
|
126
|
+
enum: ["probe", "reverse", "doc", "console", "other"],
|
|
127
|
+
description: "Evidence source",
|
|
128
|
+
},
|
|
129
|
+
confidence: {
|
|
130
|
+
type: "string",
|
|
131
|
+
enum: ["high", "medium", "low"],
|
|
132
|
+
description: "Confidence",
|
|
133
|
+
},
|
|
134
|
+
tags: {
|
|
135
|
+
type: "array",
|
|
136
|
+
items: { type: "string" },
|
|
137
|
+
description: "Optional tags",
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
required: ["action"],
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: "lark_auth",
|
|
145
|
+
description:
|
|
146
|
+
"larkit auth: whoami/check/list/use/cookie/login/login_status/import/logout. Profiles live in ~/.larkit. login starts a detached QR worker and returns a status_file (qr_path appears there); import accepts a cookie string or a JSON file.",
|
|
147
|
+
inputSchema: {
|
|
148
|
+
type: "object",
|
|
149
|
+
additionalProperties: false,
|
|
150
|
+
properties: {
|
|
151
|
+
action: {
|
|
152
|
+
type: "string",
|
|
153
|
+
enum: [
|
|
154
|
+
"whoami",
|
|
155
|
+
"check",
|
|
156
|
+
"list",
|
|
157
|
+
"use",
|
|
158
|
+
"cookie",
|
|
159
|
+
"login",
|
|
160
|
+
"login_status",
|
|
161
|
+
"import",
|
|
162
|
+
"logout",
|
|
163
|
+
],
|
|
164
|
+
},
|
|
165
|
+
profile: { type: "string", description: "Profile name" },
|
|
166
|
+
name: { type: "string", description: "Alias for profile on use/login/import/logout" },
|
|
167
|
+
file: {
|
|
168
|
+
type: "string",
|
|
169
|
+
description: "JSON credentials file or plain cookie file for import",
|
|
170
|
+
},
|
|
171
|
+
cookie: {
|
|
172
|
+
type: "string",
|
|
173
|
+
description: "Raw Cookie header string for import",
|
|
174
|
+
},
|
|
175
|
+
wait: {
|
|
176
|
+
type: "boolean",
|
|
177
|
+
description: "Wait for QR scan instead of detaching the login worker",
|
|
178
|
+
},
|
|
179
|
+
timeout_ms: {
|
|
180
|
+
type: "integer",
|
|
181
|
+
minimum: 5000,
|
|
182
|
+
maximum: 300000,
|
|
183
|
+
description: "QR login timeout (default 120000)",
|
|
184
|
+
},
|
|
185
|
+
headless: {
|
|
186
|
+
type: "boolean",
|
|
187
|
+
description: "Headless Chrome for login (default true)",
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
required: ["action"],
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
name: "lark_console",
|
|
195
|
+
description:
|
|
196
|
+
"Feishu developer console lifecycle via cookies: list/create apps, get app_id+secret, update info, grant scopes, enable bot + event/card WebSocket long connection, publish version, or browser_fallback (CDP, records real admin APIs). Mutating actions support dry_run.",
|
|
197
|
+
inputSchema: {
|
|
198
|
+
type: "object",
|
|
199
|
+
additionalProperties: false,
|
|
200
|
+
properties: {
|
|
201
|
+
action: {
|
|
202
|
+
type: "string",
|
|
203
|
+
enum: [
|
|
204
|
+
"list_apps",
|
|
205
|
+
"create_app",
|
|
206
|
+
"get_app",
|
|
207
|
+
"get_credentials",
|
|
208
|
+
"update_app_info",
|
|
209
|
+
"list_scopes",
|
|
210
|
+
"applied_scopes",
|
|
211
|
+
"grant_scopes",
|
|
212
|
+
"list_versions",
|
|
213
|
+
"enable_bot_ws",
|
|
214
|
+
"publish",
|
|
215
|
+
"browser_fallback",
|
|
216
|
+
],
|
|
217
|
+
},
|
|
218
|
+
profile: { type: "string", description: "larkit profile (default active)" },
|
|
219
|
+
app_id: { type: "string", description: "cli_xxx app id" },
|
|
220
|
+
name: { type: "string", description: "App name / login profile alias" },
|
|
221
|
+
desc: { type: "string", description: "App description" },
|
|
222
|
+
avatar: { type: "string", description: "Avatar image key/URL if known" },
|
|
223
|
+
i18n: { type: "object", description: "i18n name/description map" },
|
|
224
|
+
languages: {
|
|
225
|
+
type: "array",
|
|
226
|
+
items: { type: "string" },
|
|
227
|
+
description: "Language codes, e.g. [zh_cn,en_us]",
|
|
228
|
+
},
|
|
229
|
+
scopes: {
|
|
230
|
+
type: "array",
|
|
231
|
+
items: { type: "string" },
|
|
232
|
+
description: "Scope names, e.g. [im:message, im:chat:readonly]",
|
|
233
|
+
},
|
|
234
|
+
scope_type: {
|
|
235
|
+
type: "string",
|
|
236
|
+
enum: ["app", "user", "both"],
|
|
237
|
+
description: "Scope identity (default app)",
|
|
238
|
+
},
|
|
239
|
+
operation: {
|
|
240
|
+
type: "string",
|
|
241
|
+
enum: ["add", "del"],
|
|
242
|
+
description: "Scope operation (default add)",
|
|
243
|
+
},
|
|
244
|
+
version: { type: "string", description: "App version, e.g. 1.0.1" },
|
|
245
|
+
remark: { type: "string", description: "Version remark" },
|
|
246
|
+
full_release: { type: "boolean", description: "Full release (default false)" },
|
|
247
|
+
create_if_missing: {
|
|
248
|
+
type: "boolean",
|
|
249
|
+
description: "Create a version when no draft exists (default true)",
|
|
250
|
+
},
|
|
251
|
+
release: { type: "boolean", description: "Also call release endpoint" },
|
|
252
|
+
dry_run: { type: "boolean", description: "Plan only, do not mutate" },
|
|
253
|
+
enable_bot: { type: "boolean", description: "Enable bot ability" },
|
|
254
|
+
enable_events: { type: "boolean", description: "Enable event WS mode" },
|
|
255
|
+
enable_callbacks: { type: "boolean", description: "Enable card callback WS mode" },
|
|
256
|
+
query: { type: "string", description: "list_apps filter" },
|
|
257
|
+
limit: { type: "integer", minimum: 1, maximum: 200 },
|
|
258
|
+
url: { type: "string", description: "browser_fallback URL" },
|
|
259
|
+
wait_ms: { type: "integer", minimum: 0, maximum: 60000 },
|
|
260
|
+
actions: {
|
|
261
|
+
type: "array",
|
|
262
|
+
description:
|
|
263
|
+
"browser_fallback actions, e.g. [{type:'click_text',text:'创建应用'},{type:'fill',fields:[{placeholder:'应用名称',value:'x'}]}]",
|
|
264
|
+
items: { type: "object" },
|
|
265
|
+
},
|
|
266
|
+
screenshot: { type: "boolean" },
|
|
267
|
+
record_as: { type: "string", description: "Recipe action name to record" },
|
|
268
|
+
app_scene_type: { type: "integer", description: "0 self-built, 1 ISV" },
|
|
269
|
+
primary_lang: { type: "string", description: "Default zh_cn" },
|
|
270
|
+
},
|
|
271
|
+
required: ["action"],
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
name: "lark_probe",
|
|
276
|
+
description:
|
|
277
|
+
"Live probes. mode=openapi gets tenant_access_token (cached) and calls /open-apis/{method,path,query,body}, returning status/code/msg/log_id. mode=client sends a client private-protocol command over WS or im/gateway and decodes protobuf to JSON.",
|
|
278
|
+
inputSchema: {
|
|
279
|
+
type: "object",
|
|
280
|
+
additionalProperties: false,
|
|
281
|
+
properties: {
|
|
282
|
+
mode: { type: "string", enum: ["openapi", "client"] },
|
|
283
|
+
app_id: { type: "string" },
|
|
284
|
+
app_secret: { type: "string" },
|
|
285
|
+
method: { type: "string", description: "HTTP method (default GET)" },
|
|
286
|
+
path: { type: "string", description: "e.g. /im/v1/messages or full URL" },
|
|
287
|
+
query: { type: "object", description: "Query params" },
|
|
288
|
+
body: { description: "JSON object or string body" },
|
|
289
|
+
user_access_token: { type: "string" },
|
|
290
|
+
tenant_access_token: { type: "string" },
|
|
291
|
+
force_token: { type: "boolean" },
|
|
292
|
+
headers: { type: "object", description: "Extra headers" },
|
|
293
|
+
command: {
|
|
294
|
+
type: "string",
|
|
295
|
+
description: "client command name or number, e.g. PUT_MESSAGE / 5",
|
|
296
|
+
},
|
|
297
|
+
payload: { type: "object", description: "Protobuf payload object" },
|
|
298
|
+
transport: {
|
|
299
|
+
type: "string",
|
|
300
|
+
enum: ["ws", "gateway"],
|
|
301
|
+
description: "client transport (default ws)",
|
|
302
|
+
},
|
|
303
|
+
profile: { type: "string", description: "larkit profile for client cookie" },
|
|
304
|
+
timeout: { type: "integer", minimum: 1000, maximum: 120000 },
|
|
305
|
+
wait_push: { type: "boolean", description: "Listen for pushes instead of sending" },
|
|
306
|
+
max_events: { type: "integer", minimum: 1, maximum: 50 },
|
|
307
|
+
listen_ms: { type: "integer", minimum: 0, maximum: 60000 },
|
|
308
|
+
},
|
|
309
|
+
required: ["mode"],
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
];
|
|
313
|
+
|
|
314
|
+
function ok(text) {
|
|
315
|
+
return { content: [{ type: "text", text }] };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function fail(error) {
|
|
319
|
+
return {
|
|
320
|
+
isError: true,
|
|
321
|
+
content: [
|
|
322
|
+
{
|
|
323
|
+
type: "text",
|
|
324
|
+
text: JSON.stringify({
|
|
325
|
+
ok: false,
|
|
326
|
+
error: error instanceof Error ? error.message : String(error),
|
|
327
|
+
}),
|
|
328
|
+
},
|
|
329
|
+
],
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function callTool(name, args) {
|
|
334
|
+
switch (name) {
|
|
335
|
+
case "lark_doc": {
|
|
336
|
+
if (args.action === "search") return searchDocs(args);
|
|
337
|
+
if (args.action === "read") return readDoc({ docPath: args.path, offset: args.offset, limit: args.limit });
|
|
338
|
+
if (args.action === "categories") return docCategories();
|
|
339
|
+
throw new Error(`unknown lark_doc action: ${args.action}`);
|
|
340
|
+
}
|
|
341
|
+
case "lark_truth_search":
|
|
342
|
+
return searchTruth({
|
|
343
|
+
query: args.query,
|
|
344
|
+
scope: args.scope || "all",
|
|
345
|
+
limit: args.limit || 10,
|
|
346
|
+
});
|
|
347
|
+
case "lark_truth_inspect": {
|
|
348
|
+
if (args.action === "read_slice") {
|
|
349
|
+
return readSlice({
|
|
350
|
+
filePath: args.file_path,
|
|
351
|
+
lineNumber: args.line_number,
|
|
352
|
+
context: args.context,
|
|
353
|
+
mode: args.mode,
|
|
354
|
+
maxLines: args.max_lines,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
if (args.action === "save_verified") {
|
|
358
|
+
return saveVerified({
|
|
359
|
+
topic: args.topic,
|
|
360
|
+
content: args.content,
|
|
361
|
+
evidence: args.evidence,
|
|
362
|
+
source: args.source,
|
|
363
|
+
confidence: args.confidence,
|
|
364
|
+
tags: args.tags,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
throw new Error(`unknown lark_truth_inspect action: ${args.action}`);
|
|
368
|
+
}
|
|
369
|
+
case "lark_auth":
|
|
370
|
+
return authDispatch(args);
|
|
371
|
+
case "lark_console":
|
|
372
|
+
return consoleDispatch(args);
|
|
373
|
+
case "lark_probe": {
|
|
374
|
+
if (args.mode === "openapi") {
|
|
375
|
+
return probeOpenApi({
|
|
376
|
+
appId: args.app_id,
|
|
377
|
+
appSecret: args.app_secret,
|
|
378
|
+
method: args.method,
|
|
379
|
+
path: args.path,
|
|
380
|
+
query: args.query,
|
|
381
|
+
body: args.body,
|
|
382
|
+
headers: args.headers,
|
|
383
|
+
userAccessToken: args.user_access_token,
|
|
384
|
+
tenantAccessToken: args.tenant_access_token,
|
|
385
|
+
forceToken: args.force_token,
|
|
386
|
+
timeout: args.timeout,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
if (args.mode === "client") {
|
|
390
|
+
return probeClientProtocol({
|
|
391
|
+
command: args.command,
|
|
392
|
+
payload: args.payload,
|
|
393
|
+
transport: args.transport,
|
|
394
|
+
profile: args.profile,
|
|
395
|
+
timeout: args.timeout,
|
|
396
|
+
waitPush: args.wait_push,
|
|
397
|
+
maxEvents: args.max_events,
|
|
398
|
+
listenMs: args.listen_ms,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
throw new Error(`unknown lark_probe mode: ${args.mode}`);
|
|
402
|
+
}
|
|
403
|
+
default:
|
|
404
|
+
throw new Error(`unknown tool: ${name}`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export function createLarkitServer() {
|
|
409
|
+
const server = new Server(
|
|
410
|
+
{ name: "larkit", version: "0.1.0" },
|
|
411
|
+
{
|
|
412
|
+
capabilities: { tools: {} },
|
|
413
|
+
instructions:
|
|
414
|
+
"Feishu full-stack truth engine. Start with lark_truth_search(scope=verified), then lark_doc, then lark_truth_search(scope=all), then lark_probe; persist confirmed findings with lark_truth_inspect(action=save_verified). Use lark_console for developer-console lifecycle and lark_auth for cookies.",
|
|
415
|
+
},
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
419
|
+
|
|
420
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
421
|
+
const { name, arguments: args } = request.params;
|
|
422
|
+
try {
|
|
423
|
+
const result = await callTool(name, args || {});
|
|
424
|
+
return ok(JSON.stringify(result, null, 2));
|
|
425
|
+
} catch (error) {
|
|
426
|
+
return fail(error);
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
return server;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export { TOOLS };
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { getProfileCookies } from "../auth/store.js";
|
|
3
|
+
import { resolveApi } from "../protocol/catalog.js";
|
|
4
|
+
import { createRegistry } from "../protocol/schema.js";
|
|
5
|
+
import {
|
|
6
|
+
connectFrontier,
|
|
7
|
+
postCommand,
|
|
8
|
+
} from "../protocol/transport.js";
|
|
9
|
+
|
|
10
|
+
function summarizePacket(packet) {
|
|
11
|
+
if (!packet) return null;
|
|
12
|
+
return {
|
|
13
|
+
cmd: packet.cmd,
|
|
14
|
+
status: packet.status,
|
|
15
|
+
sid: packet.sid,
|
|
16
|
+
cid: packet.cid,
|
|
17
|
+
payload_type: packet.payloadType,
|
|
18
|
+
payload_bytes: Array.isArray(packet.payload) ? packet.payload.length : 0,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function summarizeDecoded(decoded) {
|
|
23
|
+
if (!decoded) return null;
|
|
24
|
+
return {
|
|
25
|
+
packet: summarizePacket(decoded.packet),
|
|
26
|
+
payload_type_name: decoded.payloadTypeName,
|
|
27
|
+
layers: {
|
|
28
|
+
frame: Boolean(decoded.frame),
|
|
29
|
+
packet: Boolean(decoded.packet),
|
|
30
|
+
payload: decoded.payload !== undefined,
|
|
31
|
+
},
|
|
32
|
+
payload: decoded.payload,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function probeClientProtocol({
|
|
37
|
+
command,
|
|
38
|
+
payload = {},
|
|
39
|
+
transport = "ws",
|
|
40
|
+
profile,
|
|
41
|
+
timeout = 15_000,
|
|
42
|
+
waitPush = false,
|
|
43
|
+
maxEvents = 5,
|
|
44
|
+
listenMs = 0,
|
|
45
|
+
} = {}) {
|
|
46
|
+
if (!command) throw new Error("command is required");
|
|
47
|
+
const { name: profileName, cookies } = getProfileCookies(profile);
|
|
48
|
+
const registry = createRegistry();
|
|
49
|
+
const api = resolveApi(command);
|
|
50
|
+
const cmd =
|
|
51
|
+
typeof command === "number" ? command : api?.cmd;
|
|
52
|
+
if (cmd === undefined) {
|
|
53
|
+
return {
|
|
54
|
+
ok: false,
|
|
55
|
+
mode: "client",
|
|
56
|
+
transport,
|
|
57
|
+
error: `unknown client command: ${command}`,
|
|
58
|
+
hint: "Use lark_truth_search scope=protocol to find the command name/number.",
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const t0 = Date.now();
|
|
62
|
+
|
|
63
|
+
if (transport === "gateway" || transport === "rest") {
|
|
64
|
+
try {
|
|
65
|
+
const result = await postCommand({
|
|
66
|
+
cmd,
|
|
67
|
+
payload,
|
|
68
|
+
cookie: cookies,
|
|
69
|
+
registry,
|
|
70
|
+
baseDomain: "feishu.cn",
|
|
71
|
+
timeout,
|
|
72
|
+
headers: process.env.LARKIT_GATEWAY_HEADERS
|
|
73
|
+
? JSON.parse(process.env.LARKIT_GATEWAY_HEADERS)
|
|
74
|
+
: {},
|
|
75
|
+
});
|
|
76
|
+
return {
|
|
77
|
+
ok: result.status >= 200 && result.status < 300,
|
|
78
|
+
mode: "client",
|
|
79
|
+
transport: "gateway",
|
|
80
|
+
profile: profileName,
|
|
81
|
+
command: {
|
|
82
|
+
cmd,
|
|
83
|
+
name: api?.name,
|
|
84
|
+
request_type: api?.requestType,
|
|
85
|
+
response_type: api?.responseType,
|
|
86
|
+
},
|
|
87
|
+
status: result.status,
|
|
88
|
+
request_id: result.requestId,
|
|
89
|
+
response: {
|
|
90
|
+
packet: summarizePacket(result.packet),
|
|
91
|
+
payload_type_name: api?.responseType,
|
|
92
|
+
layers: {
|
|
93
|
+
frame: false,
|
|
94
|
+
packet: Boolean(result.packet),
|
|
95
|
+
payload: result.payload !== undefined,
|
|
96
|
+
},
|
|
97
|
+
payload: result.payload,
|
|
98
|
+
},
|
|
99
|
+
payload: result.payload,
|
|
100
|
+
raw_bytes: result.raw.length,
|
|
101
|
+
elapsed_ms: Date.now() - t0,
|
|
102
|
+
};
|
|
103
|
+
} catch (error) {
|
|
104
|
+
return {
|
|
105
|
+
ok: false,
|
|
106
|
+
mode: "client",
|
|
107
|
+
transport: "gateway",
|
|
108
|
+
profile: profileName,
|
|
109
|
+
command: { cmd, name: api?.name },
|
|
110
|
+
error: error instanceof Error ? error.message : String(error),
|
|
111
|
+
elapsed_ms: Date.now() - t0,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let connection;
|
|
117
|
+
const events = [];
|
|
118
|
+
const eventLimit = Math.max(1, Math.min(Number(maxEvents) || 5, 50));
|
|
119
|
+
try {
|
|
120
|
+
connection = await connectFrontier({
|
|
121
|
+
cookie: cookies,
|
|
122
|
+
registry,
|
|
123
|
+
timeout,
|
|
124
|
+
});
|
|
125
|
+
const cid = randomUUID();
|
|
126
|
+
let payloadBytes = new Uint8Array(0);
|
|
127
|
+
if (payload instanceof Uint8Array) {
|
|
128
|
+
payloadBytes = payload;
|
|
129
|
+
} else if (api?.requestType && payload && Object.keys(payload).length > 0) {
|
|
130
|
+
payloadBytes = registry.encode(api.requestType, payload);
|
|
131
|
+
}
|
|
132
|
+
let resolveResponse;
|
|
133
|
+
let rejectResponse;
|
|
134
|
+
const responsePromise = new Promise((resolve, reject) => {
|
|
135
|
+
resolveResponse = resolve;
|
|
136
|
+
rejectResponse = reject;
|
|
137
|
+
});
|
|
138
|
+
const timer = setTimeout(() => {
|
|
139
|
+
rejectResponse(
|
|
140
|
+
new Error(`timeout after ${timeout}ms waiting for cmd=${cmd}`),
|
|
141
|
+
);
|
|
142
|
+
}, timeout);
|
|
143
|
+
const offEvent = connection.onEvent((decoded) => {
|
|
144
|
+
events.push(summarizeDecoded(decoded));
|
|
145
|
+
if (events.length > eventLimit) events.shift();
|
|
146
|
+
if (decoded.packet?.cmd === cmd) {
|
|
147
|
+
clearTimeout(timer);
|
|
148
|
+
resolveResponse(decoded);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
const offPush = connection.onPush((pushPayload, decoded) => {
|
|
152
|
+
events.push(summarizeDecoded(decoded));
|
|
153
|
+
if (events.length > eventLimit) events.shift();
|
|
154
|
+
if (waitPush) {
|
|
155
|
+
clearTimeout(timer);
|
|
156
|
+
resolveResponse({ ...decoded, payload: pushPayload });
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const pushOnly = Boolean(waitPush) || api?.kind === "push";
|
|
161
|
+
if (!pushOnly) {
|
|
162
|
+
connection.send({
|
|
163
|
+
cmd,
|
|
164
|
+
cid,
|
|
165
|
+
payloadType: 1,
|
|
166
|
+
payload: payloadBytes,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let response = null;
|
|
171
|
+
let error = null;
|
|
172
|
+
try {
|
|
173
|
+
response = await responsePromise;
|
|
174
|
+
} catch (err) {
|
|
175
|
+
error = err instanceof Error ? err.message : String(err);
|
|
176
|
+
}
|
|
177
|
+
clearTimeout(timer);
|
|
178
|
+
offEvent();
|
|
179
|
+
offPush();
|
|
180
|
+
if (listenMs > 0) {
|
|
181
|
+
await new Promise((resolve) => setTimeout(resolve, listenMs));
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
ok: Boolean(response) && !error,
|
|
185
|
+
handshake: true,
|
|
186
|
+
mode: "client",
|
|
187
|
+
transport: "ws",
|
|
188
|
+
profile: profileName,
|
|
189
|
+
command: {
|
|
190
|
+
cmd,
|
|
191
|
+
name: api?.name,
|
|
192
|
+
request_type: api?.requestType,
|
|
193
|
+
response_type: api?.responseType,
|
|
194
|
+
},
|
|
195
|
+
request_id: cid,
|
|
196
|
+
push_only: pushOnly,
|
|
197
|
+
request_payload_bytes: pushOnly ? 0 : payloadBytes.length,
|
|
198
|
+
response: summarizeDecoded(response),
|
|
199
|
+
events,
|
|
200
|
+
error: error || undefined,
|
|
201
|
+
elapsed_ms: Date.now() - t0,
|
|
202
|
+
hint: error
|
|
203
|
+
? "Cookie 可能过期;先 lark_auth check/login。读消息命令通常不走这个 WS,可试 transport=gateway 或 lark_probe mode=openapi。"
|
|
204
|
+
: undefined,
|
|
205
|
+
};
|
|
206
|
+
} catch (error) {
|
|
207
|
+
return {
|
|
208
|
+
ok: false,
|
|
209
|
+
mode: "client",
|
|
210
|
+
transport: "ws",
|
|
211
|
+
profile: profileName,
|
|
212
|
+
command: { cmd, name: api?.name },
|
|
213
|
+
error: error instanceof Error ? error.message : String(error),
|
|
214
|
+
events,
|
|
215
|
+
elapsed_ms: Date.now() - t0,
|
|
216
|
+
hint: "WS 握手失败;检查 Cookie(passport_web_did)与 msg-frontier.feishu.cn 网络。",
|
|
217
|
+
};
|
|
218
|
+
} finally {
|
|
219
|
+
try {
|
|
220
|
+
connection?.stop();
|
|
221
|
+
} catch {
|
|
222
|
+
// ignore
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|