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,194 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { CACHE_DIR } from "../shared/paths.js";
|
|
4
|
+
import { getAppCredentials, loadConfig } from "../shared/config.js";
|
|
5
|
+
import { ensureDir, readJson, writeJson } from "../shared/util.js";
|
|
6
|
+
|
|
7
|
+
const TOKEN_FILE = path.join(CACHE_DIR, "tokens.json");
|
|
8
|
+
const OPENAPI_BASE = "https://open.feishu.cn";
|
|
9
|
+
|
|
10
|
+
function loadTokens() {
|
|
11
|
+
return readJson(TOKEN_FILE, {});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function saveTokens(tokens) {
|
|
15
|
+
ensureDir(CACHE_DIR);
|
|
16
|
+
writeJson(TOKEN_FILE, tokens);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function getTenantToken({ appId, appSecret, force = false } = {}) {
|
|
20
|
+
let resolved = appId && appSecret ? { app_id: appId, app_secret: appSecret, source: "args" } : getAppCredentials(appId);
|
|
21
|
+
if (!resolved) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
"app_id/app_secret required (args, larkit.config.json apps, or FEISHU_APP_ID/FEISHU_APP_SECRET)",
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
const tokens = loadTokens();
|
|
27
|
+
const cached = tokens[resolved.app_id];
|
|
28
|
+
if (!force && cached && cached.expires_at > Date.now() + 60_000) {
|
|
29
|
+
return { ...cached, cached: true };
|
|
30
|
+
}
|
|
31
|
+
const response = await fetch(
|
|
32
|
+
`${OPENAPI_BASE}/open-apis/auth/v3/tenant_access_token/internal`,
|
|
33
|
+
{
|
|
34
|
+
method: "POST",
|
|
35
|
+
headers: { "Content-Type": "application/json; charset=utf-8" },
|
|
36
|
+
body: JSON.stringify({
|
|
37
|
+
app_id: resolved.app_id,
|
|
38
|
+
app_secret: resolved.app_secret,
|
|
39
|
+
}),
|
|
40
|
+
signal: AbortSignal.timeout(20_000),
|
|
41
|
+
},
|
|
42
|
+
);
|
|
43
|
+
const data = await response.json();
|
|
44
|
+
if (data.code !== 0 || !data.tenant_access_token) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`tenant_access_token failed: code=${data.code} msg=${data.msg || "unknown"}`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
const expiresAt = Date.now() + Math.max(60, Number(data.expire) || 7200) * 1000;
|
|
50
|
+
const token = {
|
|
51
|
+
app_id: resolved.app_id,
|
|
52
|
+
tenant_access_token: data.tenant_access_token,
|
|
53
|
+
expires_at: expiresAt,
|
|
54
|
+
obtained_at: Date.now(),
|
|
55
|
+
source: resolved.source,
|
|
56
|
+
};
|
|
57
|
+
tokens[resolved.app_id] = token;
|
|
58
|
+
saveTokens(tokens);
|
|
59
|
+
return { ...token, cached: false };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function clearToken(appId) {
|
|
63
|
+
const tokens = loadTokens();
|
|
64
|
+
if (appId) delete tokens[appId];
|
|
65
|
+
else for (const key of Object.keys(tokens)) delete tokens[key];
|
|
66
|
+
saveTokens(tokens);
|
|
67
|
+
return { ok: true, cleared: appId || "all" };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function normalizePath(input) {
|
|
71
|
+
let p = String(input || "").trim();
|
|
72
|
+
if (!p) throw new Error("path is required");
|
|
73
|
+
if (/^https?:\/\//i.test(p)) return p;
|
|
74
|
+
if (!p.startsWith("/")) p = `/${p}`;
|
|
75
|
+
if (!p.startsWith("/open-apis/")) p = `/open-apis${p}`;
|
|
76
|
+
return `${OPENAPI_BASE}${p}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function truncateDeep(value, maxChars = 12_000) {
|
|
80
|
+
const text = JSON.stringify(value);
|
|
81
|
+
if (text.length <= maxChars) return value;
|
|
82
|
+
return {
|
|
83
|
+
_truncated: true,
|
|
84
|
+
_original_chars: text.length,
|
|
85
|
+
preview: text.slice(0, maxChars),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function probeOpenApi({
|
|
90
|
+
appId,
|
|
91
|
+
appSecret,
|
|
92
|
+
method = "GET",
|
|
93
|
+
path: apiPath,
|
|
94
|
+
query,
|
|
95
|
+
body,
|
|
96
|
+
headers = {},
|
|
97
|
+
userAccessToken,
|
|
98
|
+
tenantAccessToken,
|
|
99
|
+
forceToken = false,
|
|
100
|
+
timeout = 30_000,
|
|
101
|
+
} = {}) {
|
|
102
|
+
if (!apiPath) throw new Error("path is required");
|
|
103
|
+
let token = tenantAccessToken || "";
|
|
104
|
+
let tokenSource = tenantAccessToken ? "arg" : "";
|
|
105
|
+
if (!token && !userAccessToken) {
|
|
106
|
+
const t = await getTenantToken({ appId, appSecret, force: forceToken });
|
|
107
|
+
token = t.tenant_access_token;
|
|
108
|
+
tokenSource = t.cached ? "cache" : "fresh";
|
|
109
|
+
}
|
|
110
|
+
const url = new URL(normalizePath(apiPath));
|
|
111
|
+
if (query && typeof query === "object") {
|
|
112
|
+
for (const [key, value] of Object.entries(query)) {
|
|
113
|
+
if (value === undefined || value === null) continue;
|
|
114
|
+
if (Array.isArray(value)) {
|
|
115
|
+
for (const item of value) url.searchParams.append(key, String(item));
|
|
116
|
+
} else {
|
|
117
|
+
url.searchParams.set(key, String(value));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const requestHeaders = {
|
|
122
|
+
Accept: "application/json, text/plain, */*",
|
|
123
|
+
"User-Agent":
|
|
124
|
+
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/125 Safari/537.36",
|
|
125
|
+
...headers,
|
|
126
|
+
};
|
|
127
|
+
if (token) requestHeaders.Authorization = `Bearer ${token}`;
|
|
128
|
+
else if (userAccessToken) requestHeaders.Authorization = `Bearer ${userAccessToken}`;
|
|
129
|
+
const hasBody = body !== undefined && body !== null && method !== "GET" && method !== "HEAD";
|
|
130
|
+
if (hasBody) requestHeaders["Content-Type"] = "application/json; charset=utf-8";
|
|
131
|
+
const t0 = Date.now();
|
|
132
|
+
let response;
|
|
133
|
+
try {
|
|
134
|
+
response = await fetch(url, {
|
|
135
|
+
method: method.toUpperCase(),
|
|
136
|
+
headers: requestHeaders,
|
|
137
|
+
body: hasBody
|
|
138
|
+
? typeof body === "string"
|
|
139
|
+
? body
|
|
140
|
+
: JSON.stringify(body)
|
|
141
|
+
: undefined,
|
|
142
|
+
signal: AbortSignal.timeout(timeout),
|
|
143
|
+
});
|
|
144
|
+
} catch (error) {
|
|
145
|
+
return {
|
|
146
|
+
ok: false,
|
|
147
|
+
mode: "openapi",
|
|
148
|
+
error: error instanceof Error ? error.message : String(error),
|
|
149
|
+
request: { method, url: url.toString() },
|
|
150
|
+
elapsed_ms: Date.now() - t0,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const raw = await response.text();
|
|
154
|
+
let json = null;
|
|
155
|
+
try {
|
|
156
|
+
json = JSON.parse(raw);
|
|
157
|
+
} catch {
|
|
158
|
+
// non-JSON response
|
|
159
|
+
}
|
|
160
|
+
const code = json?.code ?? null;
|
|
161
|
+
const msg = json?.msg ?? json?.message ?? (json ? "" : raw.slice(0, 500));
|
|
162
|
+
return {
|
|
163
|
+
ok: response.ok && (code === null || code === 0),
|
|
164
|
+
mode: "openapi",
|
|
165
|
+
status: response.status,
|
|
166
|
+
code,
|
|
167
|
+
msg,
|
|
168
|
+
log_id:
|
|
169
|
+
response.headers.get("x-tt-logid") ||
|
|
170
|
+
response.headers.get("x-request-id") ||
|
|
171
|
+
json?.log_id ||
|
|
172
|
+
json?.data?.log_id,
|
|
173
|
+
token_source: tokenSource,
|
|
174
|
+
request: {
|
|
175
|
+
method: method.toUpperCase(),
|
|
176
|
+
url: url.toString(),
|
|
177
|
+
body: hasBody ? truncateDeep(body, 4000) : undefined,
|
|
178
|
+
},
|
|
179
|
+
data: json?.data !== undefined ? truncateDeep(json.data) : undefined,
|
|
180
|
+
body: json ? truncateDeep(json) : raw.slice(0, 12_000),
|
|
181
|
+
raw_bytes: Buffer.byteLength(raw),
|
|
182
|
+
elapsed_ms: Date.now() - t0,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function tokenStatus() {
|
|
187
|
+
const tokens = loadTokens();
|
|
188
|
+
return Object.values(tokens).map((token) => ({
|
|
189
|
+
app_id: token.app_id,
|
|
190
|
+
expires_at: new Date(token.expires_at).toISOString(),
|
|
191
|
+
valid: token.expires_at > Date.now(),
|
|
192
|
+
source: token.source,
|
|
193
|
+
}));
|
|
194
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { createRegistry } from "./schema.js";
|
|
3
|
+
|
|
4
|
+
const COMMANDS_FILE = new URL("./data/commands.json", import.meta.url);
|
|
5
|
+
|
|
6
|
+
const CONTROL = new Set([
|
|
7
|
+
"ACK",
|
|
8
|
+
"PING",
|
|
9
|
+
"CONNECT",
|
|
10
|
+
"DISCONNECT",
|
|
11
|
+
"SERVER_DISCONNET",
|
|
12
|
+
"SYNC_PACKETS",
|
|
13
|
+
"SYNC_REQUEST",
|
|
14
|
+
"SYNC_RESPONSE",
|
|
15
|
+
"PROCESS_MULTI_PACKETS",
|
|
16
|
+
"NO_TRACE_REMOVE_MESSAGE_BY_ID",
|
|
17
|
+
"NOOP",
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
let cachedCatalog;
|
|
21
|
+
|
|
22
|
+
function classify(entry, enumName) {
|
|
23
|
+
const name = (enumName || entry.name || "").toUpperCase();
|
|
24
|
+
const requestType = (entry.requestType || "").toLowerCase();
|
|
25
|
+
const responseType = (entry.responseType || "").toLowerCase();
|
|
26
|
+
if (
|
|
27
|
+
name.startsWith("PUSH_") ||
|
|
28
|
+
name.includes("PUSH") ||
|
|
29
|
+
requestType.includes("push") ||
|
|
30
|
+
responseType.includes("push")
|
|
31
|
+
) {
|
|
32
|
+
return "push";
|
|
33
|
+
}
|
|
34
|
+
if (CONTROL.has(name) || name.startsWith("SYNC_")) return "control";
|
|
35
|
+
if (!entry.requestType) {
|
|
36
|
+
if (responseType.includes("ack") || name.endsWith("_ACK")) return "control";
|
|
37
|
+
return "push";
|
|
38
|
+
}
|
|
39
|
+
if (
|
|
40
|
+
entry.requestType &&
|
|
41
|
+
entry.responseType &&
|
|
42
|
+
requestType !== responseType
|
|
43
|
+
) {
|
|
44
|
+
return "request";
|
|
45
|
+
}
|
|
46
|
+
return "push";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function loadCatalog() {
|
|
50
|
+
if (cachedCatalog) return cachedCatalog;
|
|
51
|
+
const raw = JSON.parse(readFileSync(COMMANDS_FILE, "utf8"));
|
|
52
|
+
const registry = createRegistry();
|
|
53
|
+
const commandEnum = registry.lookup("improto.Command");
|
|
54
|
+
const enumNames = new Map();
|
|
55
|
+
if (commandEnum?.values) {
|
|
56
|
+
for (const [name, value] of Object.entries(commandEnum.values)) {
|
|
57
|
+
enumNames.set(value, name);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const entries = raw.map((command) => {
|
|
61
|
+
const enumName = enumNames.get(command.value);
|
|
62
|
+
const name =
|
|
63
|
+
command.version !== undefined
|
|
64
|
+
? command.name || enumName
|
|
65
|
+
: enumName || command.name;
|
|
66
|
+
const kind = classify(
|
|
67
|
+
command,
|
|
68
|
+
command.version !== undefined ? undefined : enumName,
|
|
69
|
+
);
|
|
70
|
+
const probeType = command.requestType || command.responseType;
|
|
71
|
+
return {
|
|
72
|
+
cmd: command.value,
|
|
73
|
+
name: name || "",
|
|
74
|
+
requestType: command.requestType || "",
|
|
75
|
+
responseType: command.responseType || "",
|
|
76
|
+
version: command.version,
|
|
77
|
+
kind,
|
|
78
|
+
schemaAvailable: probeType ? registry.has(probeType) : false,
|
|
79
|
+
source: command.source || "",
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
entries.sort(
|
|
83
|
+
(a, b) => a.cmd - b.cmd || a.name.localeCompare(b.name),
|
|
84
|
+
);
|
|
85
|
+
cachedCatalog = {
|
|
86
|
+
entries,
|
|
87
|
+
byValue: new Map(entries.map((entry) => [entry.cmd, entry])),
|
|
88
|
+
byName: new Map(),
|
|
89
|
+
byType: new Map(),
|
|
90
|
+
};
|
|
91
|
+
for (const entry of entries) {
|
|
92
|
+
if (entry.name) {
|
|
93
|
+
const list = cachedCatalog.byName.get(entry.name) || [];
|
|
94
|
+
list.push(entry);
|
|
95
|
+
cachedCatalog.byName.set(entry.name, list);
|
|
96
|
+
}
|
|
97
|
+
if (entry.requestType) {
|
|
98
|
+
const list = cachedCatalog.byType.get(entry.requestType) || [];
|
|
99
|
+
list.push(entry);
|
|
100
|
+
cachedCatalog.byType.set(entry.requestType, list);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return cachedCatalog;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function listApis(kind) {
|
|
107
|
+
const { entries } = loadCatalog();
|
|
108
|
+
return kind ? entries.filter((entry) => entry.kind === kind) : [...entries];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function findApiByValue(cmd) {
|
|
112
|
+
return loadCatalog().byValue.get(Number(cmd));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function findApisByName(name) {
|
|
116
|
+
return loadCatalog().byName.get(name) || [];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function findApisByType(typeName) {
|
|
120
|
+
return loadCatalog().byType.get(typeName) || [];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function resolveApi(cmdOrName) {
|
|
124
|
+
if (typeof cmdOrName === "number" || /^\d+$/.test(String(cmdOrName))) {
|
|
125
|
+
return findApiByValue(Number(cmdOrName));
|
|
126
|
+
}
|
|
127
|
+
return (
|
|
128
|
+
findApisByName(String(cmdOrName))[0] ||
|
|
129
|
+
findApisByType(String(cmdOrName))[0]
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function summarize() {
|
|
134
|
+
const entries = listApis();
|
|
135
|
+
const byKind = {};
|
|
136
|
+
let schemaAvailable = 0;
|
|
137
|
+
for (const entry of entries) {
|
|
138
|
+
byKind[entry.kind] = (byKind[entry.kind] || 0) + 1;
|
|
139
|
+
if (entry.schemaAvailable) schemaAvailable++;
|
|
140
|
+
}
|
|
141
|
+
return { total: entries.length, byKind, schemaAvailable };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function catalogDataFile() {
|
|
145
|
+
return COMMANDS_FILE.pathname;
|
|
146
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Protocol data snapshots
|
|
2
|
+
|
|
3
|
+
These are **data**, not executable code from the reference projects:
|
|
4
|
+
|
|
5
|
+
| 文件 | 来源 | 用途 |
|
|
6
|
+
| --- | --- | --- |
|
|
7
|
+
| `commands.json` | 客户端 jssdk worklet 命令注册表(2020 条) | cmd / requestType / responseType / version / name |
|
|
8
|
+
| `bundle.json` | 客户端 protobuf bundle(protobufjs JSON root) | 补充原生 desc 缺失的类型 |
|
|
9
|
+
| `schema.json.gz` | 由 `bundle.json` + 客户端 `pb.desc` / `pb_server.desc` 合并生成 | 无 deb 运行时的完整 schema 兜底 |
|
|
10
|
+
| `constants.json` | 网页版 WS 握手公开常量 | appKey / salt / URL / SDK version |
|
|
11
|
+
| `reference/PROTOCOL.md` | 逆向协议笔记 | 协议文档索引 |
|
|
12
|
+
| `reference/endpoints.json` | 客户端网关/卡片端点笔记 | 端点与默认请求头 |
|
|
13
|
+
|
|
14
|
+
`src/protocol/descriptor.js` 是 larkit 自己的 FileDescriptorSet parser;
|
|
15
|
+
`src/protocol/schema.js` 负责把上述 JSON 合并成 protobufjs registry。
|