bb-browser-api 0.11.5

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.
@@ -0,0 +1,392 @@
1
+ <div align="center">
2
+
3
+ # bb-browser
4
+
5
+ ### 坏男孩浏览器 BadBoy Browser
6
+
7
+ **你的浏览器就是 API。不需要密钥,不需要爬虫,不需要模拟。**
8
+
9
+ [![npm](https://img.shields.io/npm/v/bb-browser?color=CB3837&logo=npm&logoColor=white)](https://www.npmjs.com/package/bb-browser)
10
+ [![Node.js](https://img.shields.io/badge/Node.js-18+-339933?logo=node.js&logoColor=white)](https://nodejs.org)
11
+ [![License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
12
+
13
+ [English](README.md) · [中文](README.zh-CN.md)
14
+
15
+ </div>
16
+
17
+ ---
18
+
19
+ > **Fork 说明 (skVPN/bb-browser-api):** 本 fork 在 daemon 中新增了三个 HTTP API 接口,并将默认端口改为 **18888**。详见 [本 Fork 的改动](#本-fork-的改动)。
20
+
21
+ ---
22
+
23
+ 你已经登录了微博、知乎、B站、小红书、Twitter、GitHub、LinkedIn — bb-browser 让 AI Agent **直接用你的登录态**。
24
+
25
+ ```bash
26
+ bb-browser site twitter/search "AI agent" # 搜索推文
27
+ bb-browser site zhihu/hot # 知乎热榜
28
+ bb-browser site arxiv/search "transformer" # 搜论文
29
+ bb-browser site eastmoney/stock "茅台" # 实时股票行情
30
+ bb-browser site boss/search "AI 工程师" # 搜职位
31
+ bb-browser site wikipedia/summary "Python" # 维基百科摘要
32
+ bb-browser site youtube/transcript VIDEO_ID # YouTube 字幕全文
33
+ bb-browser site stackoverflow/search "async" # 搜 StackOverflow
34
+ ```
35
+
36
+ **36 个平台,103 个命令,全部用你真实浏览器的登录态。** [完整列表 →](https://github.com/epiral/bb-sites)
37
+
38
+ ## 核心理念
39
+
40
+ 互联网是为浏览器构建的。AI Agent 一直试图通过 API 访问它 — 但 99% 的网站不提供 API。
41
+
42
+ bb-browser 翻转了这一逻辑:**不是让网站适配机器,而是让机器使用人的界面。** adapter 在你的浏览器 tab 里跑 `eval`,用你的 Cookie 调 `fetch()`,或者直接调用页面的 webpack 模块。网站以为是你在操作。因为**就是你**。
43
+
44
+ | | Playwright / Selenium | 爬虫库 | bb-browser |
45
+ |---|---|---|---|
46
+ | 浏览器 | 无头、隔离环境 | 没有浏览器 | 你的真实 Chrome |
47
+ | 登录态 | 没有,需重新登录 | 偷 Cookie | 已经在了 |
48
+ | 反爬检测 | 容易被识别 | 猫鼠游戏 | 无法检测 — 它就是用户 |
49
+ | 复杂鉴权 | 无法复制 | 需要逆向 | 页面自己处理 |
50
+
51
+ ## 快速开始
52
+
53
+ ### 安装
54
+
55
+ ```bash
56
+ npm install -g bb-browser
57
+ ```
58
+
59
+ ### 使用
60
+
61
+ ```bash
62
+ bb-browser site update # 拉取社区适配器
63
+ bb-browser site recommend # 看看哪些和你的浏览习惯匹配
64
+ bb-browser site zhihu/hot # 开搜
65
+ ```
66
+
67
+ ### Docker 部署
68
+
69
+ 如果需要在服务器上部署(带 VNC 网页访问),参见:
70
+
71
+ - **[快速部署指南](DEPLOY.md)** - 5 分钟快速上手
72
+ - **[完整部署文档](docs/docker-deployment.md)** - 详细配置和故障排查
73
+
74
+ ```bash
75
+ # 快速启动
76
+ git clone https://github.com/skVPN/bb-browser-api.git
77
+ cd bb-browser-api
78
+ docker compose up -d
79
+
80
+ # 访问 http://<服务器IP>:6080/vnc.html 查看 Chrome 画面
81
+ # API: http://<服务器IP>:18888
82
+ ```
83
+
84
+ ### OpenClaw(无需安装扩展)
85
+
86
+ 如果你使用 [OpenClaw](https://openclaw.ai),bb-browser 可以直接通过 OpenClaw 内置浏览器运行,不需要额外安装 Chrome 扩展或 daemon:
87
+
88
+ ```bash
89
+ bb-browser site reddit/hot --openclaw
90
+ bb-browser site xueqiu/hot-stock 5 --openclaw --jq '.items[] | {name, changePercent}'
91
+ ```
92
+
93
+ ClawHub Skill: [bb-browser-openclaw](https://clawhub.ai/yan5xu/bb-browser)
94
+
95
+ ### MCP 接入(Claude Code / Cursor)
96
+
97
+ ```json
98
+ {
99
+ "mcpServers": {
100
+ "bb-browser": {
101
+ "command": "npx",
102
+ "args": ["-y", "bb-browser", "--mcp"]
103
+ }
104
+ }
105
+ }
106
+ ```
107
+
108
+ ## 36 个平台,103 个命令
109
+
110
+ 社区驱动,通过 [bb-sites](https://github.com/epiral/bb-sites) 维护。每个命令一个 JS 文件。
111
+
112
+ | 类别 | 平台 | 命令 |
113
+ |------|------|------|
114
+ | **搜索引擎** | Google、百度、Bing、DuckDuckGo、搜狗微信 | search |
115
+ | **社交媒体** | Twitter/X、Reddit、微博、小红书、即刻、LinkedIn、虎扑 | search、feed、thread、user、notifications、hot |
116
+ | **新闻资讯** | BBC、Reuters、36氪、今日头条、东方财富 | headlines、search、newsflash、hot |
117
+ | **技术开发** | GitHub、StackOverflow、HackerNews、CSDN、博客园、V2EX、Dev.to、npm、PyPI、arXiv | search、issues、repo、top、thread、package |
118
+ | **视频平台** | YouTube、B站 | search、video、transcript、popular、comments、feed |
119
+ | **影音娱乐** | 豆瓣、IMDb、Genius、起点中文网 | movie、search、top250 |
120
+ | **财经股票** | 雪球、东方财富、Yahoo Finance | stock、hot-stock、feed、watchlist、search |
121
+ | **求职招聘** | BOSS直聘、LinkedIn | search、detail、profile |
122
+ | **知识百科** | Wikipedia、知乎、Open Library | search、summary、hot、question |
123
+ | **消费购物** | 什么值得买 | search |
124
+ | **实用工具** | 有道翻译、GSMArena、Product Hunt、携程 | translate、手机参数、热门产品 |
125
+
126
+ ## 10 分钟,CLI 化任何网站
127
+
128
+ ```bash
129
+ bb-browser guide # 完整教程
130
+ ```
131
+
132
+ 跟你的 AI Agent 说:*"帮我把 XX 网站 CLI 化"*。它会读 guide,用 `network --with-body` 抓包逆向,写 adapter,测试,然后提 PR 到社区仓库。全程自动。
133
+
134
+ 三种 adapter 复杂度:
135
+
136
+ | 层级 | 认证方式 | 代表 | 耗时 |
137
+ |------|----------|------|------|
138
+ | **Tier 1** | Cookie(直接 fetch) | Reddit、GitHub、V2EX | ~1 分钟 |
139
+ | **Tier 2** | Bearer + CSRF token | Twitter、知乎 | ~3 分钟 |
140
+ | **Tier 3** | Webpack 注入 / Pinia store | Twitter 搜索、小红书 | ~10 分钟 |
141
+
142
+ 实测:**20 个 AI Agent 并发运行,每个独立逆向一个网站并产出可用的 adapter。** 将一个新网站纳入 Agent 可访问范围的边际成本趋近于零。
143
+
144
+ ## 对 AI Agent 意味着什么
145
+
146
+ 没有 bb-browser,AI Agent 的世界是:**文件系统 + 终端 + 少数有 API key 的服务。**
147
+
148
+ 有了 bb-browser:**文件系统 + 终端 + 整个互联网。**
149
+
150
+ 一个 Agent 现在可以在一分钟内:
151
+
152
+ ```bash
153
+ # 跨平台调研任何话题
154
+ bb-browser site arxiv/search "retrieval augmented generation"
155
+ bb-browser site twitter/search "RAG"
156
+ bb-browser site github search rag-framework
157
+ bb-browser site stackoverflow/search "RAG implementation"
158
+ bb-browser site zhihu/search "RAG"
159
+ bb-browser site 36kr/newsflash
160
+ ```
161
+
162
+ 六个平台,六个维度,结构化 JSON。比任何人类研究员都快、都广。
163
+
164
+ ## 同时也是完整的浏览器自动化工具
165
+
166
+ ```bash
167
+ bb-browser open https://example.com
168
+ bb-browser snapshot -i # 可访问性树
169
+ bb-browser click @3 # 点击元素
170
+ bb-browser fill @5 "hello" # 填写输入框
171
+ bb-browser eval "document.title" # 执行 JS
172
+ bb-browser fetch URL --json # 带登录态的 fetch
173
+ bb-browser network requests --with-body --json # 抓包
174
+ bb-browser screenshot # 截图
175
+ ```
176
+
177
+ 所有命令支持 `--json` 输出、`--jq <expr>` 内联过滤、和 `--tab <id>` 多标签页并发操作。
178
+
179
+ ```bash
180
+ bb-browser site xueqiu/hot-stock 5 --jq '.items[] | {name, changePercent}'
181
+ # {"name":"云天化","changePercent":"2.08%"}
182
+ # {"name":"东吴股份","changePercent":"-7.60%"}
183
+
184
+ bb-browser site info xueqiu/stock # 查看 adapter 参数、示例、域名
185
+ ```
186
+
187
+ ## HTTP API 编程接入
188
+
189
+ Daemon 暴露 HTTP API 供直接集成。**默认端口:`18888`**(相比上游的 `19824` 已修改)。
190
+
191
+ ```bash
192
+ # 启动 daemon
193
+ bb-browser daemon start
194
+
195
+ # Fetch API — 在浏览器上下文中执行请求
196
+ curl -X POST http://localhost:18888/api/fetch \
197
+ -H "Content-Type: application/json" \
198
+ -d '{
199
+ "url": "https://api.github.com/users/octocat",
200
+ "method": "GET",
201
+ "credentials": "include"
202
+ }'
203
+
204
+ # Capture API — 访问页面并捕获匹配的网络请求
205
+ curl "http://localhost:18888/api/capture?url=https://example.com&pattern=api"
206
+
207
+ # Storage API — 读取指定域名的 Cookie / localStorage / sessionStorage
208
+ curl "http://localhost:18888/api/storage?domain=example.com"
209
+ ```
210
+
211
+ **核心优势:** 在你的真实浏览器上下文中执行,自动携带 Cookie 和登录态。
212
+
213
+ ### Node.js 示例
214
+
215
+ ```javascript
216
+ const response = await fetch('http://localhost:18888/api/fetch', {
217
+ method: 'POST',
218
+ headers: { 'Content-Type': 'application/json' },
219
+ body: JSON.stringify({
220
+ url: 'https://www.reddit.com/api/me.json',
221
+ credentials: 'include', // 发送 Cookie
222
+ }),
223
+ });
224
+ const result = await response.json();
225
+ console.log(result.body); // 你的 Reddit 用户数据
226
+ ```
227
+
228
+ ### Python 示例
229
+
230
+ ```python
231
+ import requests
232
+
233
+ response = requests.post('http://localhost:18888/api/fetch', json={
234
+ 'url': 'https://api.github.com/users/octocat',
235
+ })
236
+ result = response.json()
237
+ print(result['body'])
238
+ ```
239
+
240
+ **文档:** [API Fetch 指南](docs/api-fetch.md) · [抓包与存储指南](docs/api-capture-storage.md)
241
+
242
+ ## Daemon 配置
243
+
244
+ Daemon 默认绑定 `127.0.0.1:18888`,可通过 `--host` 自定义监听地址:
245
+
246
+ ```bash
247
+ bb-browser daemon --host 127.0.0.1 # 仅 IPv4(解决 macOS IPv6 问题)
248
+ bb-browser daemon --host 0.0.0.0 # 监听所有网卡(用于 Tailscale / ZeroTier 跨机器访问)
249
+ ```
250
+
251
+ ## 架构
252
+
253
+ ```
254
+ AI Agent (Claude Code, Codex, Cursor 等)
255
+ ┆ CLI 或 MCP (stdio)
256
+
257
+ bb-browser CLI ──HTTP──▶ Daemon ──CDP WebSocket──▶ 你的真实浏览器
258
+
259
+ ┌──────┴──────┐
260
+ │ Per-tab │
261
+ │ 事件缓存 │
262
+ │ (network, │
263
+ │ console, │
264
+ │ errors) │
265
+ └─────────────┘
266
+ ```
267
+
268
+ ---
269
+
270
+ ## 本 Fork 的改动
271
+
272
+ 本 fork ([skVPN/bb-browser-api](https://github.com/skVPN/bb-browser-api)) 在上游 [epiral/bb-browser](https://github.com/epiral/bb-browser) 基础上做了以下改动:
273
+
274
+ ### 1. 默认端口修改:`19824` → `18888`
275
+
276
+ **文件:** `packages/shared/src/constants.ts`
277
+
278
+ ```diff
279
+ - export const DAEMON_PORT = 19824;
280
+ + export const DAEMON_PORT = 18888;
281
+ ```
282
+
283
+ ### 2. 新增 HTTP API:`POST /api/fetch`
284
+
285
+ 在浏览器上下文中执行 HTTP 请求(携带 Cookie、登录态等)。
286
+
287
+ **文件:** `packages/daemon/src/http-server.ts`、`packages/daemon/src/command-dispatch.ts`
288
+
289
+ **请求:**
290
+ ```json
291
+ POST http://localhost:18888/api/fetch
292
+ {
293
+ "url": "https://api.example.com/data",
294
+ "method": "GET",
295
+ "headers": { "Accept": "application/json" },
296
+ "credentials": "include",
297
+ "body": ""
298
+ }
299
+ ```
300
+
301
+ | 字段 | 类型 | 必填 | 说明 |
302
+ |------|------|------|------|
303
+ | `url` | string | ✅ | 目标 URL |
304
+ | `method` | string | — | HTTP 方法,默认 `GET` |
305
+ | `headers` | object | — | 自定义请求头 |
306
+ | `credentials` | `omit` \| `same-origin` \| `include` | — | Cookie 发送策略,默认 `omit` |
307
+ | `body` | string | — | 请求体(POST/PUT 时使用) |
308
+ | `tabId` | string \| number | — | 指定使用的标签页 |
309
+
310
+ **响应:**
311
+ ```json
312
+ {
313
+ "status": 200,
314
+ "contentType": "application/json",
315
+ "body": { ... }
316
+ }
317
+ ```
318
+
319
+ > **关于 `credentials`:** `Sec-Fetch-*` 等安全 headers 由浏览器自动设置,JavaScript 无法覆盖(这是浏览器安全规范)。`credentials` 字段控制是否发送 Cookie。
320
+
321
+ ### 3. 新增 HTTP API:`GET /api/capture`
322
+
323
+ 访问指定 URL 并捕获匹配的网络请求。
324
+
325
+ **文件:** `packages/daemon/src/http-server.ts`
326
+
327
+ ```
328
+ GET http://localhost:18888/api/capture?url=https://example.com&pattern=api\.&timeout=5000
329
+ ```
330
+
331
+ | 参数 | 必填 | 说明 |
332
+ |------|------|------|
333
+ | `url` | ✅ | 要访问的页面 URL |
334
+ | `pattern` | — | 过滤请求的正则表达式 |
335
+ | `timeout` | — | 等待时间(毫秒),默认 `5000` |
336
+
337
+ **响应:**
338
+ ```json
339
+ {
340
+ "requests": [
341
+ {
342
+ "url": "https://example.com/api/data",
343
+ "method": "GET",
344
+ "status": 200,
345
+ "responseBody": "..."
346
+ }
347
+ ]
348
+ }
349
+ ```
350
+
351
+ ### 4. 新增 HTTP API:`GET /api/storage`
352
+
353
+ 读取指定域名的 Cookie、localStorage、sessionStorage。
354
+
355
+ **文件:** `packages/daemon/src/http-server.ts`
356
+
357
+ ```
358
+ GET http://localhost:18888/api/storage?domain=example.com
359
+ ```
360
+
361
+ | 参数 | 必填 | 说明 |
362
+ |------|------|------|
363
+ | `domain` | ✅ | 要读取存储的域名 |
364
+
365
+ **响应:**
366
+ ```json
367
+ {
368
+ "cookies": [ { "name": "session", "value": "...", "domain": "example.com" } ],
369
+ "localStorage": { "key": "value" },
370
+ "sessionStorage": { "key": "value" }
371
+ }
372
+ ```
373
+
374
+ ### 5. `Request` 协议新增 `credentials` 字段
375
+
376
+ **文件:** `packages/shared/src/protocol.ts`
377
+
378
+ ```typescript
379
+ export interface Request {
380
+ // ...已有字段...
381
+ /** fetch credentials 选项:omit | same-origin | include(默认:omit) */
382
+ credentials?: "omit" | "same-origin" | "include";
383
+ }
384
+ ```
385
+
386
+ 之前的 fetch 实现硬编码了 `credentials: 'include'`,现在默认为 `'omit'`,并尊重调用方的选择。
387
+
388
+ ---
389
+
390
+ ## 许可证
391
+
392
+ MIT
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env node
2
+
3
+ // packages/cli/src/jq.ts
4
+ function splitTopLevel(input, separator) {
5
+ const parts = [];
6
+ let current = "";
7
+ let depth = 0;
8
+ let inString = false;
9
+ for (let i = 0; i < input.length; i++) {
10
+ const char = input[i];
11
+ const prev = input[i - 1];
12
+ if (char === '"' && prev !== "\\") inString = !inString;
13
+ if (!inString) {
14
+ if (char === "{" || char === "(" || char === "[") depth++;
15
+ if (char === "}" || char === ")" || char === "]") depth--;
16
+ if (depth === 0 && input.slice(i, i + separator.length) === separator) {
17
+ parts.push(current.trim());
18
+ current = "";
19
+ i += separator.length - 1;
20
+ continue;
21
+ }
22
+ }
23
+ current += char;
24
+ }
25
+ if (current.trim()) parts.push(current.trim());
26
+ return parts;
27
+ }
28
+ function parseLiteral(value) {
29
+ const trimmed = value.trim();
30
+ if (trimmed.startsWith('"') && trimmed.endsWith('"')) return JSON.parse(trimmed);
31
+ if (trimmed === "true") return true;
32
+ if (trimmed === "false") return false;
33
+ if (trimmed === "null") return null;
34
+ return Number(trimmed);
35
+ }
36
+ function getField(value, field) {
37
+ return value !== null && typeof value === "object" ? value[field] : void 0;
38
+ }
39
+ function applySegment(inputs, expr) {
40
+ if (expr === ".") return inputs;
41
+ if (expr.startsWith("select(")) {
42
+ const match = expr.match(/^select\((.+?)\s*(==|>)\s*(.+)\)$/);
43
+ if (!match) throw new Error(`\u4E0D\u652F\u6301\u7684 jq \u8868\u8FBE\u5F0F: ${expr}`);
44
+ const [, leftExpr, op, rightExpr] = match;
45
+ const expected = parseLiteral(rightExpr);
46
+ return inputs.filter((item) => {
47
+ const left = applyExpression([item], leftExpr)[0];
48
+ return op === "==" ? left === expected : Number(left) > Number(expected);
49
+ });
50
+ }
51
+ if (expr.startsWith("{") && expr.endsWith("}")) {
52
+ const body = expr.slice(1, -1).trim();
53
+ if (!body) return inputs.map(() => ({}));
54
+ const entries = splitTopLevel(body, ",");
55
+ return inputs.map((item) => {
56
+ const obj = {};
57
+ for (const entry of entries) {
58
+ const colon = entry.indexOf(":");
59
+ if (colon === -1) {
60
+ const key = entry.trim().replace(/^\./, "");
61
+ obj[key] = applyExpression([item], `.${key}`)[0];
62
+ } else {
63
+ const key = entry.slice(0, colon).trim();
64
+ const valueExpr = entry.slice(colon + 1).trim();
65
+ obj[key] = applyExpression([item], valueExpr)[0];
66
+ }
67
+ }
68
+ return obj;
69
+ });
70
+ }
71
+ if (!expr.startsWith(".")) throw new Error(`\u4E0D\u652F\u6301\u7684 jq \u8868\u8FBE\u5F0F: ${expr}`);
72
+ let current = inputs;
73
+ let remaining = expr.slice(1);
74
+ while (remaining.length > 0) {
75
+ if (remaining.startsWith("[]")) {
76
+ current = current.flatMap((item) => Array.isArray(item) ? item : []);
77
+ remaining = remaining.slice(2);
78
+ } else if (remaining.startsWith("[")) {
79
+ const match = remaining.match(/^\[(-?\d+)\]/);
80
+ if (!match) throw new Error(`\u4E0D\u652F\u6301\u7684 jq \u8868\u8FBE\u5F0F: .${remaining}`);
81
+ const index = Number(match[1]);
82
+ current = current.map((item) => {
83
+ if (!Array.isArray(item)) return void 0;
84
+ return item[index >= 0 ? index : item.length + index];
85
+ });
86
+ remaining = remaining.slice(match[0].length);
87
+ } else if (remaining.startsWith(".")) {
88
+ remaining = remaining.slice(1);
89
+ } else {
90
+ const match = remaining.match(/^([A-Za-z_][A-Za-z0-9_]*)/);
91
+ if (!match) throw new Error(`\u4E0D\u652F\u6301\u7684 jq \u8868\u8FBE\u5F0F: .${remaining}`);
92
+ const field = match[1];
93
+ current = current.map((item) => getField(item, field));
94
+ remaining = remaining.slice(field.length);
95
+ }
96
+ }
97
+ return current;
98
+ }
99
+ function applyExpression(inputs, expression) {
100
+ const segments = splitTopLevel(expression.trim(), "|");
101
+ return segments.reduce((current, segment) => applySegment(current, segment.trim()), inputs);
102
+ }
103
+ function applyJq(data, expression) {
104
+ return applyExpression([data], expression).filter((item) => item !== void 0);
105
+ }
106
+
107
+ export {
108
+ applyJq
109
+ };
110
+ //# sourceMappingURL=chunk-3H3RKS2K.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../packages/cli/src/jq.ts"],"sourcesContent":["function splitTopLevel(input: string, separator: string): string[] {\r\n const parts: string[] = [];\r\n let current = \"\";\r\n let depth = 0;\r\n let inString = false;\r\n\r\n for (let i = 0; i < input.length; i++) {\r\n const char = input[i];\r\n const prev = input[i - 1];\r\n if (char === '\"' && prev !== '\\\\') inString = !inString;\r\n if (!inString) {\r\n if (char === '{' || char === '(' || char === '[') depth++;\r\n if (char === '}' || char === ')' || char === ']') depth--;\r\n if (depth === 0 && input.slice(i, i + separator.length) === separator) {\r\n parts.push(current.trim());\r\n current = \"\";\r\n i += separator.length - 1;\r\n continue;\r\n }\r\n }\r\n current += char;\r\n }\r\n\r\n if (current.trim()) parts.push(current.trim());\r\n return parts;\r\n}\r\n\r\nfunction parseLiteral(value: string): unknown {\r\n const trimmed = value.trim();\r\n if (trimmed.startsWith('\"') && trimmed.endsWith('\"')) return JSON.parse(trimmed);\r\n if (trimmed === \"true\") return true;\r\n if (trimmed === \"false\") return false;\r\n if (trimmed === \"null\") return null;\r\n return Number(trimmed);\r\n}\r\n\r\nfunction getField(value: unknown, field: string): unknown {\r\n return value !== null && typeof value === \"object\" ? (value as Record<string, unknown>)[field] : undefined;\r\n}\r\n\r\nfunction applySegment(inputs: unknown[], expr: string): unknown[] {\r\n if (expr === \".\") return inputs;\r\n if (expr.startsWith(\"select(\")) {\r\n const match = expr.match(/^select\\((.+?)\\s*(==|>)\\s*(.+)\\)$/);\r\n if (!match) throw new Error(`不支持的 jq 表达式: ${expr}`);\r\n const [, leftExpr, op, rightExpr] = match;\r\n const expected = parseLiteral(rightExpr);\r\n return inputs.filter((item) => {\r\n const left = applyExpression([item], leftExpr)[0];\r\n return op === \"==\" ? left === expected : Number(left) > Number(expected);\r\n });\r\n }\r\n if (expr.startsWith(\"{\") && expr.endsWith(\"}\")) {\r\n const body = expr.slice(1, -1).trim();\r\n if (!body) return inputs.map(() => ({}));\r\n const entries = splitTopLevel(body, \",\");\r\n return inputs.map((item) => {\r\n const obj: Record<string, unknown> = {};\r\n for (const entry of entries) {\r\n const colon = entry.indexOf(\":\");\r\n if (colon === -1) {\r\n const key = entry.trim().replace(/^\\./, \"\");\r\n obj[key] = applyExpression([item], `.${key}`)[0];\r\n } else {\r\n const key = entry.slice(0, colon).trim();\r\n const valueExpr = entry.slice(colon + 1).trim();\r\n obj[key] = applyExpression([item], valueExpr)[0];\r\n }\r\n }\r\n return obj;\r\n });\r\n }\r\n if (!expr.startsWith(\".\")) throw new Error(`不支持的 jq 表达式: ${expr}`);\r\n\r\n let current = inputs;\r\n let remaining = expr.slice(1);\r\n while (remaining.length > 0) {\r\n if (remaining.startsWith(\"[]\")) {\r\n current = current.flatMap((item) => Array.isArray(item) ? item : []);\r\n remaining = remaining.slice(2);\r\n } else if (remaining.startsWith(\"[\")) {\r\n const match = remaining.match(/^\\[(-?\\d+)\\]/);\r\n if (!match) throw new Error(`不支持的 jq 表达式: .${remaining}`);\r\n const index = Number(match[1]);\r\n current = current.map((item) => {\r\n if (!Array.isArray(item)) return undefined;\r\n return item[index >= 0 ? index : item.length + index];\r\n });\r\n remaining = remaining.slice(match[0].length);\r\n } else if (remaining.startsWith(\".\")) {\r\n remaining = remaining.slice(1);\r\n } else {\r\n const match = remaining.match(/^([A-Za-z_][A-Za-z0-9_]*)/);\r\n if (!match) throw new Error(`不支持的 jq 表达式: .${remaining}`);\r\n const field = match[1];\r\n current = current.map((item) => getField(item, field));\r\n remaining = remaining.slice(field.length);\r\n }\r\n }\r\n return current;\r\n}\r\n\r\nfunction applyExpression(inputs: unknown[], expression: string): unknown[] {\r\n const segments = splitTopLevel(expression.trim(), \"|\");\r\n return segments.reduce((current, segment) => applySegment(current, segment.trim()), inputs);\r\n}\r\n\r\nexport function applyJq(data: unknown, expression: string): unknown[] {\r\n return applyExpression([data], expression).filter((item) => item !== undefined);\r\n}\r\n"],"mappings":"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","names":[]}