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,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"webAppKey": "5f45da0e6c7a17dcba80494ef0ab9b21",
|
|
3
|
+
"accessKeySalt": "f8a69f1719916z",
|
|
4
|
+
"baseUrl": "wss://msg-frontier.feishu.cn/ws/v2",
|
|
5
|
+
"ticketUrl": "https://login.feishu.cn/suite/passport/frontier_ticket/",
|
|
6
|
+
"sdkVersion": "7.68.5",
|
|
7
|
+
"origin": "https://www.feishu.cn"
|
|
8
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# larkit 协议笔记(逆向自飞书客户端)
|
|
2
|
+
|
|
3
|
+
> 从 Linux 客户端 `/opt/bytedance/feishu/webcontent/` 提取。用于 headless 客户端 SDK 开发。
|
|
4
|
+
|
|
5
|
+
## 1. 传输面总览
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
业务请求 ──┬─ HTTP pb 网关(写操作)── internal-api-lark-api.feishu.cn/im/gateway/
|
|
9
|
+
├─ WS frontier 长连接(信令/推送)── cookie 认证 connect
|
|
10
|
+
└─ 开放 API(JSON)── open.feishu.cn/open-apis/...(应用/用户 token)
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## 2. frame / packet 编解码
|
|
14
|
+
|
|
15
|
+
- **frame**:WS 字节流外层帧(详见 `src/codec/frame.ts`)。
|
|
16
|
+
- **packet**:`improto.Packet`
|
|
17
|
+
- 字段:`cmd`(命令号)、`cid`(requestId,网关必填,否则 "extract packet fail")、`payloadType`、`payload`、`status`
|
|
18
|
+
- `payloadType=1(PB2)` 时 `payload` 是业务 protobuf 字节
|
|
19
|
+
- 网关响应是**裸 packet**(不是 frame 包 packet)
|
|
20
|
+
|
|
21
|
+
## 3. HTTP pb 网关
|
|
22
|
+
|
|
23
|
+
- `POST https://internal-api-lark-api.feishu.cn/im/gateway/`
|
|
24
|
+
- `Content-Type: application/x-protobuf`
|
|
25
|
+
- body = `encodePacket({cmd, cid=requestId, payloadType: PB2, payload})`
|
|
26
|
+
- 头:`Cookie`(larkit 登录 cookie) + `gatewayHeaders()` 全套(见 endpoints.json)
|
|
27
|
+
- 命令级版本:`x-command-version` / `x-web-version`(见 endpoints.json.commandWebVersions)
|
|
28
|
+
|
|
29
|
+
### 已验证路由
|
|
30
|
+
| cmd | 名称 | 结果 |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| 5 | PUT_MESSAGE | ✅ 发文本/richText 卡片 |
|
|
33
|
+
| 13 | PUT_CHAT | ✅ 建群 |
|
|
34
|
+
| 7004/7025/7026 | 卡片相关 | ⚠️ 网关接受,wire 组错时报 "PB payload unmarshal failed" |
|
|
35
|
+
|
|
36
|
+
### 不路由(读操作)
|
|
37
|
+
- 1 MGET_MESSAGES / 7 PULL_CHAT_MESSAGES / 33 PULL_RECENT_CHATS / 1020 GET_CHAT_MESSAGES
|
|
38
|
+
- 报 `command: N not found` 或 fallback 到 `PullFeedGroups invalid request`
|
|
39
|
+
- **结论:读消息走开放 API 或网页版另一个 HTTP 通道,不在此 pb 网关**
|
|
40
|
+
|
|
41
|
+
## 4. WS frontier 长连接
|
|
42
|
+
|
|
43
|
+
- 实现见 `src/connect.ts`(逆向移植网页版 cookie 认证建连)
|
|
44
|
+
- cookie 登录后 connect,绑定 `LarkClient`
|
|
45
|
+
- **当前只收信令/设备状态**(7001/7017/6000/1105079),**不收 PUSH_MESSAGES(6)/5053**
|
|
46
|
+
- 网页版消息推送走另一条通道(待挖)
|
|
47
|
+
|
|
48
|
+
## 5. 消息 / 卡片
|
|
49
|
+
|
|
50
|
+
- `entities.Message.Type`:TEXT=4,CARD=14
|
|
51
|
+
- `entities.CardContent.Type`:UNKNOWN=0 / VOTE=1 / TEXT=2 / VCHAT=3 / **OPEN_CARD=4**
|
|
52
|
+
- 个人账号 PUT_MESSAGE:type=14 + `Content{cardType, richText}` 可发 richText 元素卡片(BUTTON/MARKDOWN/DIV/...);open-card(4) 发不了(系统错误)
|
|
53
|
+
- 开放 API 发 open-card(schema 2.0)成功;PATCH `/open-apis/im/v1/messages/{om_xxx}` 更新卡片成功
|
|
54
|
+
|
|
55
|
+
## 6. 卡片交互 actionv2
|
|
56
|
+
|
|
57
|
+
- 端点:`POST https://internal-api.feishu.cn/lark/card/api/actionv2`(v2_2,cookie 认证)
|
|
58
|
+
- body 见 `endpoints.json.actionCardActionV2Body`
|
|
59
|
+
- **三个关键参数 `card_id / action_id / refresh_token` 由服务端在卡片消息推送的 `cardEntity` 里下发**
|
|
60
|
+
- 推送通道(PUSH_MESSAGES 6)当前 SDK 收不到 → 端到端点击验证待打通读消息链路
|
|
61
|
+
|
|
62
|
+
## 7. 凭证
|
|
63
|
+
|
|
64
|
+
- 用户 cookie:`larkit auth cookie`(`~/.larkit/credentials.json`)
|
|
65
|
+
- 应用:`cli_aa69ba6ab1389ce2`,tenant_access_token 走 `/open-apis/auth/v3/tenant_access_token/internal`
|
|
66
|
+
- 开放 API 读群消息需 scope `im:message.group_msg`(当前未授权)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_source": "飞书 Linux 客户端 webcontent/jssdk(worklet 信令层)运行时配置提取 + 实测验证",
|
|
3
|
+
"gateway": {
|
|
4
|
+
"fetchURL": "https://internal-api-lark-api.feishu.cn/im/gateway/",
|
|
5
|
+
"transport": "POST application/x-protobuf",
|
|
6
|
+
"body": "improto.Packet{cmd, cid=requestId, payloadType=PB2(=1), payload=业务 proto 字节}",
|
|
7
|
+
"note": "只路由 PUT 类写命令(5/13/7004/7025/7026 等);PULL/GET/MGET 读命令(1/7/33/1020)不路由或 fallback 到 PullFeedGroups"
|
|
8
|
+
},
|
|
9
|
+
"internalApi": "https://internal-api.feishu.cn",
|
|
10
|
+
"openApi": "https://open.feishu.cn",
|
|
11
|
+
"ticketURL": "https://{passportHost}/suite/passport/frontier_ticket/",
|
|
12
|
+
"webClient": "https://www.feishu.cn/messenger",
|
|
13
|
+
"actionCardApis": {
|
|
14
|
+
"v2_0": "https://{openHost}/open-apis/api/v2/message/action",
|
|
15
|
+
"v2_1": "https://{openHost}/open-apis/card/api/actionv2",
|
|
16
|
+
"v2_2": "https://internal-api.feishu.cn/lark/card/api/actionv2",
|
|
17
|
+
"v3": "https://{openHost}/open-apis/message/v3/action",
|
|
18
|
+
"vote": "https://{openHost}/lark-vote/Vote"
|
|
19
|
+
},
|
|
20
|
+
"actionCardActionV2Body": {
|
|
21
|
+
"common": { "uid": "<用户数字uid>", "mid": "<消息数字id>", "card_id": "<卡片数字id>" },
|
|
22
|
+
"action": {
|
|
23
|
+
"creator_tenant_id": "<卡片创建者租户数字id>",
|
|
24
|
+
"action_id": "act_v1_<...>",
|
|
25
|
+
"refresh_token": "<uuid>",
|
|
26
|
+
"card_schema": "1"
|
|
27
|
+
},
|
|
28
|
+
"note": "card_id/action_id/refresh_token 由服务端在卡片消息推送(PUSH_MESSAGES 6)的 content.cardEntity 里下发"
|
|
29
|
+
},
|
|
30
|
+
"commandWebVersions": {
|
|
31
|
+
"_note": "x-command-version 命令级覆盖;默认 DEFAULT_WEB_VERSION=7.61.0",
|
|
32
|
+
"5": "7.67.0",
|
|
33
|
+
"13": "2.7.0",
|
|
34
|
+
"105": "7.69.0",
|
|
35
|
+
"11021": "2.7.0",
|
|
36
|
+
"11106": "7.60.0"
|
|
37
|
+
},
|
|
38
|
+
"defaultGatewayHeaders": {
|
|
39
|
+
"x-appid": "161471",
|
|
40
|
+
"x-lgw-os-type": "3",
|
|
41
|
+
"x-lgw-terminal-type": "2",
|
|
42
|
+
"x-lgw-req-sdk-type": "220",
|
|
43
|
+
"x-lsc-bizid": "1",
|
|
44
|
+
"x-lsc-version": "1"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FileDescriptorSet -> protobufjs-compatible JSON root.
|
|
3
|
+
*
|
|
4
|
+
* This is larkit's own parser. It only reads descriptor bytes that the Feishu
|
|
5
|
+
* client ships (pb.desc / pb_server.desc); it does not import any SDK code.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const TYPE_MESSAGE = 11;
|
|
9
|
+
const TYPE_ENUM = 14;
|
|
10
|
+
|
|
11
|
+
const SCALAR = {
|
|
12
|
+
1: "double",
|
|
13
|
+
2: "float",
|
|
14
|
+
3: "int64",
|
|
15
|
+
4: "uint64",
|
|
16
|
+
5: "int32",
|
|
17
|
+
6: "fixed64",
|
|
18
|
+
7: "fixed32",
|
|
19
|
+
8: "bool",
|
|
20
|
+
9: "string",
|
|
21
|
+
10: "group",
|
|
22
|
+
11: "message",
|
|
23
|
+
12: "bytes",
|
|
24
|
+
13: "uint32",
|
|
25
|
+
14: "enum",
|
|
26
|
+
15: "sfixed32",
|
|
27
|
+
16: "sfixed64",
|
|
28
|
+
17: "sint32",
|
|
29
|
+
18: "sint64",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
class Reader {
|
|
33
|
+
constructor(input) {
|
|
34
|
+
this.buf =
|
|
35
|
+
input instanceof Uint8Array ? input : new Uint8Array(input);
|
|
36
|
+
this.pos = 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get eof() {
|
|
40
|
+
return this.pos >= this.buf.length;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
readVarint() {
|
|
44
|
+
let result = 0n;
|
|
45
|
+
let shift = 0n;
|
|
46
|
+
while (this.pos < this.buf.length) {
|
|
47
|
+
const byte = this.buf[this.pos++];
|
|
48
|
+
result |= BigInt(byte & 0x7f) << shift;
|
|
49
|
+
if ((byte & 0x80) === 0) return result;
|
|
50
|
+
shift += 7n;
|
|
51
|
+
if (shift > 70n) throw new Error("varint too long");
|
|
52
|
+
}
|
|
53
|
+
throw new Error("truncated varint");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
readTag() {
|
|
57
|
+
if (this.eof) return null;
|
|
58
|
+
const value = Number(this.readVarint());
|
|
59
|
+
return { field: value >>> 3, wireType: value & 7 };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
readBytes() {
|
|
63
|
+
const length = Number(this.readVarint());
|
|
64
|
+
if (length < 0 || this.pos + length > this.buf.length) {
|
|
65
|
+
throw new Error("truncated length-delimited field");
|
|
66
|
+
}
|
|
67
|
+
const out = this.buf.subarray(this.pos, this.pos + length);
|
|
68
|
+
this.pos += length;
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
readString() {
|
|
73
|
+
return new TextDecoder().decode(this.readBytes());
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
skip(wireType) {
|
|
77
|
+
if (wireType === 0) {
|
|
78
|
+
this.readVarint();
|
|
79
|
+
} else if (wireType === 1) {
|
|
80
|
+
this.pos += 8;
|
|
81
|
+
} else if (wireType === 2) {
|
|
82
|
+
const length = Number(this.readVarint());
|
|
83
|
+
this.pos += length;
|
|
84
|
+
} else if (wireType === 5) {
|
|
85
|
+
this.pos += 4;
|
|
86
|
+
} else if (wireType === 3) {
|
|
87
|
+
// group: skip until matching end-group
|
|
88
|
+
while (!this.eof) {
|
|
89
|
+
const tag = this.readTag();
|
|
90
|
+
if (!tag) break;
|
|
91
|
+
if (tag.wireType === 4) break;
|
|
92
|
+
this.skip(tag.wireType);
|
|
93
|
+
}
|
|
94
|
+
} else if (wireType === 4) {
|
|
95
|
+
// end-group marker; caller handles
|
|
96
|
+
} else {
|
|
97
|
+
throw new Error(`unsupported wire type ${wireType}`);
|
|
98
|
+
}
|
|
99
|
+
if (this.pos > this.buf.length) throw new Error("truncated field");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function lowerCamel(name) {
|
|
104
|
+
return String(name).replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function parseFieldProto(bytes) {
|
|
108
|
+
const reader = new Reader(bytes);
|
|
109
|
+
const field = { name: "", number: 0, label: 0, type: 0 };
|
|
110
|
+
let tag;
|
|
111
|
+
while ((tag = reader.readTag()) !== null) {
|
|
112
|
+
if (tag.field === 1 && tag.wireType === 2) field.name = reader.readString();
|
|
113
|
+
else if (tag.field === 3 && tag.wireType === 0)
|
|
114
|
+
field.number = Number(reader.readVarint());
|
|
115
|
+
else if (tag.field === 4 && tag.wireType === 0)
|
|
116
|
+
field.label = Number(reader.readVarint());
|
|
117
|
+
else if (tag.field === 5 && tag.wireType === 0)
|
|
118
|
+
field.type = Number(reader.readVarint());
|
|
119
|
+
else if (tag.field === 6 && tag.wireType === 2)
|
|
120
|
+
field.typeName = reader.readString();
|
|
121
|
+
else if (tag.field === 7 && tag.wireType === 2)
|
|
122
|
+
field.defaultValue = reader.readString();
|
|
123
|
+
else if (tag.field === 8 && tag.wireType === 2) {
|
|
124
|
+
const options = new Reader(reader.readBytes());
|
|
125
|
+
let optionTag;
|
|
126
|
+
while ((optionTag = options.readTag()) !== null) {
|
|
127
|
+
if (optionTag.field === 1 && optionTag.wireType === 0) {
|
|
128
|
+
field.packed = Number(options.readVarint()) === 1;
|
|
129
|
+
} else {
|
|
130
|
+
options.skip(optionTag.wireType);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
} else if (tag.field === 10 && tag.wireType === 2)
|
|
134
|
+
field.jsonName = reader.readString();
|
|
135
|
+
else reader.skip(tag.wireType);
|
|
136
|
+
}
|
|
137
|
+
return field;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function parseEnumProto(bytes) {
|
|
141
|
+
const reader = new Reader(bytes);
|
|
142
|
+
const out = { name: "", values: [] };
|
|
143
|
+
let tag;
|
|
144
|
+
while ((tag = reader.readTag()) !== null) {
|
|
145
|
+
if (tag.field === 1 && tag.wireType === 2) {
|
|
146
|
+
out.name = reader.readString();
|
|
147
|
+
} else if (tag.field === 2 && tag.wireType === 2) {
|
|
148
|
+
const valueReader = new Reader(reader.readBytes());
|
|
149
|
+
let name;
|
|
150
|
+
let number = 0;
|
|
151
|
+
let valueTag;
|
|
152
|
+
while ((valueTag = valueReader.readTag()) !== null) {
|
|
153
|
+
if (valueTag.field === 1 && valueTag.wireType === 2) {
|
|
154
|
+
name = valueReader.readString();
|
|
155
|
+
} else if (valueTag.field === 2 && valueTag.wireType === 0) {
|
|
156
|
+
number = Number(valueReader.readVarint());
|
|
157
|
+
} else {
|
|
158
|
+
valueReader.skip(valueTag.wireType);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (name) out.values.push({ name, number });
|
|
162
|
+
} else {
|
|
163
|
+
reader.skip(tag.wireType);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function parseDescriptorProto(bytes) {
|
|
170
|
+
const reader = new Reader(bytes);
|
|
171
|
+
const out = {
|
|
172
|
+
name: "",
|
|
173
|
+
fields: [],
|
|
174
|
+
nested: [],
|
|
175
|
+
enums: [],
|
|
176
|
+
mapEntry: false,
|
|
177
|
+
};
|
|
178
|
+
let tag;
|
|
179
|
+
while ((tag = reader.readTag()) !== null) {
|
|
180
|
+
if (tag.field === 1 && tag.wireType === 2) {
|
|
181
|
+
out.name = reader.readString();
|
|
182
|
+
} else if (tag.field === 2 && tag.wireType === 2) {
|
|
183
|
+
out.fields.push(parseFieldProto(reader.readBytes()));
|
|
184
|
+
} else if (tag.field === 3 && tag.wireType === 2) {
|
|
185
|
+
out.nested.push(parseDescriptorProto(reader.readBytes()));
|
|
186
|
+
} else if (tag.field === 4 && tag.wireType === 2) {
|
|
187
|
+
out.enums.push(parseEnumProto(reader.readBytes()));
|
|
188
|
+
} else if (tag.field === 7 && tag.wireType === 2) {
|
|
189
|
+
const options = new Reader(reader.readBytes());
|
|
190
|
+
let optionTag;
|
|
191
|
+
while ((optionTag = options.readTag()) !== null) {
|
|
192
|
+
if (optionTag.field === 7 && optionTag.wireType === 0) {
|
|
193
|
+
out.mapEntry = Number(options.readVarint()) === 1;
|
|
194
|
+
} else {
|
|
195
|
+
options.skip(optionTag.wireType);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
reader.skip(tag.wireType);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return out;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function parseFileDescriptorProto(bytes) {
|
|
206
|
+
const reader = new Reader(bytes);
|
|
207
|
+
const file = { name: "", package: "", messages: [], enums: [] };
|
|
208
|
+
let tag;
|
|
209
|
+
while ((tag = reader.readTag()) !== null) {
|
|
210
|
+
if (tag.field === 1 && tag.wireType === 2) file.name = reader.readString();
|
|
211
|
+
else if (tag.field === 2 && tag.wireType === 2)
|
|
212
|
+
file.package = reader.readString();
|
|
213
|
+
else if (tag.field === 3 && tag.wireType === 2) reader.readString();
|
|
214
|
+
else if (tag.field === 4 && tag.wireType === 2)
|
|
215
|
+
file.messages.push(parseDescriptorProto(reader.readBytes()));
|
|
216
|
+
else if (tag.field === 5 && tag.wireType === 2)
|
|
217
|
+
file.enums.push(parseEnumProto(reader.readBytes()));
|
|
218
|
+
else reader.skip(tag.wireType);
|
|
219
|
+
}
|
|
220
|
+
return file;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function fieldTypeName(field) {
|
|
224
|
+
if (field.type === TYPE_MESSAGE || field.type === TYPE_ENUM) {
|
|
225
|
+
return (field.typeName || "").replace(/^\./, "") || "bytes";
|
|
226
|
+
}
|
|
227
|
+
return SCALAR[field.type] || "bytes";
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function attach(root, fq, definition) {
|
|
231
|
+
const parts = fq.split(".").filter(Boolean);
|
|
232
|
+
let node = root;
|
|
233
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
234
|
+
const key = parts[i];
|
|
235
|
+
const nested = node.nested || (node.nested = {});
|
|
236
|
+
const child = nested[key];
|
|
237
|
+
if (child && typeof child === "object" && "nested" in child) {
|
|
238
|
+
node = child;
|
|
239
|
+
} else if (child && typeof child === "object") {
|
|
240
|
+
child.nested = child.nested || {};
|
|
241
|
+
node = child;
|
|
242
|
+
} else {
|
|
243
|
+
const next = { nested: {} };
|
|
244
|
+
nested[key] = next;
|
|
245
|
+
node = next;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const last = parts[parts.length - 1];
|
|
249
|
+
const nested = node.nested || (node.nested = {});
|
|
250
|
+
nested[last] = definition;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function collectMessage(root, scope, descriptor) {
|
|
254
|
+
if (descriptor.mapEntry) return;
|
|
255
|
+
const fq = scope ? `${scope}.${descriptor.name}` : descriptor.name;
|
|
256
|
+
const node = { fields: {} };
|
|
257
|
+
attach(root, fq, node);
|
|
258
|
+
|
|
259
|
+
for (const field of descriptor.fields) {
|
|
260
|
+
const key = field.jsonName || lowerCamel(field.name);
|
|
261
|
+
const def = { type: fieldTypeName(field), id: field.number };
|
|
262
|
+
if (field.label === 3) def.rule = "repeated";
|
|
263
|
+
else if (field.label === 2) def.rule = "required";
|
|
264
|
+
if (field.packed || field.defaultValue !== undefined) {
|
|
265
|
+
def.options = {};
|
|
266
|
+
if (field.packed) def.options.packed = true;
|
|
267
|
+
if (field.defaultValue !== undefined)
|
|
268
|
+
def.options.default = field.defaultValue;
|
|
269
|
+
}
|
|
270
|
+
node.fields[key] = def;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Map entries are represented as nested messages whose parent field points
|
|
274
|
+
// at the entry type. Convert them to protobufjs map fields (keyType/type).
|
|
275
|
+
for (const sub of descriptor.nested) {
|
|
276
|
+
if (!sub.mapEntry) continue;
|
|
277
|
+
const keyField = sub.fields.find((field) => field.number === 1);
|
|
278
|
+
const valueField = sub.fields.find((field) => field.number === 2);
|
|
279
|
+
if (!keyField || !valueField) continue;
|
|
280
|
+
const entryFq = `${fq}.${sub.name}`;
|
|
281
|
+
for (const field of descriptor.fields) {
|
|
282
|
+
const raw = (field.typeName || "").replace(/^\./, "");
|
|
283
|
+
if (raw !== entryFq) continue;
|
|
284
|
+
const key = field.jsonName || lowerCamel(field.name);
|
|
285
|
+
node.fields[key] = {
|
|
286
|
+
keyType: fieldTypeName(keyField),
|
|
287
|
+
type: fieldTypeName(valueField),
|
|
288
|
+
id: field.number,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
for (const sub of descriptor.nested) {
|
|
294
|
+
if (!sub.mapEntry) collectMessage(root, fq, sub);
|
|
295
|
+
}
|
|
296
|
+
for (const enumDescriptor of descriptor.enums) {
|
|
297
|
+
const values = {};
|
|
298
|
+
for (const value of enumDescriptor.values) {
|
|
299
|
+
values[value.name] = value.number;
|
|
300
|
+
}
|
|
301
|
+
attach(root, `${fq}.${enumDescriptor.name}`, { values });
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function parseFileDescriptorSet(input) {
|
|
306
|
+
const reader = new Reader(input);
|
|
307
|
+
const root = { nested: {} };
|
|
308
|
+
let tag;
|
|
309
|
+
while ((tag = reader.readTag()) !== null) {
|
|
310
|
+
if (tag.field === 1 && tag.wireType === 2) {
|
|
311
|
+
const file = parseFileDescriptorProto(reader.readBytes());
|
|
312
|
+
for (const message of file.messages) {
|
|
313
|
+
collectMessage(root, file.package, message);
|
|
314
|
+
}
|
|
315
|
+
for (const enumDescriptor of file.enums) {
|
|
316
|
+
const values = {};
|
|
317
|
+
for (const value of enumDescriptor.values) {
|
|
318
|
+
values[value.name] = value.number;
|
|
319
|
+
}
|
|
320
|
+
attach(
|
|
321
|
+
root,
|
|
322
|
+
file.package
|
|
323
|
+
? `${file.package}.${enumDescriptor.name}`
|
|
324
|
+
: enumDescriptor.name,
|
|
325
|
+
{ values },
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
} else {
|
|
329
|
+
reader.skip(tag.wireType);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return root;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function mergeRoots(...roots) {
|
|
336
|
+
const out = { nested: {} };
|
|
337
|
+
for (const root of roots) mergeInto(out, root);
|
|
338
|
+
return out;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function mergeInto(target, source) {
|
|
342
|
+
const targetNested = target.nested || (target.nested = {});
|
|
343
|
+
const sourceNested = source?.nested || {};
|
|
344
|
+
for (const [key, value] of Object.entries(sourceNested)) {
|
|
345
|
+
const existing = targetNested[key];
|
|
346
|
+
if (
|
|
347
|
+
existing &&
|
|
348
|
+
typeof existing === "object" &&
|
|
349
|
+
typeof value === "object" &&
|
|
350
|
+
"nested" in existing &&
|
|
351
|
+
"nested" in value
|
|
352
|
+
) {
|
|
353
|
+
mergeInto(existing, value);
|
|
354
|
+
} else {
|
|
355
|
+
targetNested[key] = value;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|