bb-browser 0.1.2 → 0.2.1
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/README.md +113 -160
- package/dist/{chunk-YAVLEXUJ.js → chunk-TUO443YI.js} +2 -1
- package/dist/cli.js +1 -1
- package/dist/daemon.js +14 -3
- package/dist/daemon.js.map +1 -1
- package/extension/background.js +166 -90
- package/extension/background.js.map +1 -1
- package/extension/dist/background.js +27 -5
- package/extension/dist/background.js.map +1 -1
- package/extension/dist/manifest.json +1 -0
- package/extension/dist/options.html +26 -0
- package/extension/dist/options.js +19 -0
- package/extension/dist/options.js.map +1 -0
- package/extension/manifest.json +2 -1
- package/extension/options.html +26 -0
- package/extension/options.js +19 -0
- package/extension/options.js.map +1 -0
- package/package.json +1 -1
- /package/dist/{chunk-YAVLEXUJ.js.map → chunk-TUO443YI.js.map} +0 -0
package/README.md
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
1
3
|
# bb-browser
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
**让 AI Agent 用你的真实浏览器**
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/bb-browser)
|
|
8
|
+
[](https://nodejs.org)
|
|
9
|
+
[](LICENSE)
|
|
4
10
|
|
|
5
|
-
|
|
11
|
+
</div>
|
|
6
12
|
|
|
7
|
-
|
|
8
|
-
- **AI Agent 友好** - 简洁的 CLI 接口,支持 JSON 输出,Ref 系统方便元素引用
|
|
9
|
-
- **反爬绕过** - 使用 chrome.debugger API,避免 Playwright 等工具的自动化指纹检测
|
|
13
|
+
---
|
|
10
14
|
|
|
11
|
-
|
|
15
|
+
你已经登录的 Gmail、Twitter、内部系统——Agent 直接能用。通过 `chrome.debugger` API 操作,绕过自动化指纹检测。
|
|
12
16
|
|
|
13
17
|
```
|
|
14
18
|
AI Agent (Claude, GPT, etc.)
|
|
15
|
-
│ CLI 命令
|
|
19
|
+
│ CLI 命令 / Epiral gRPC
|
|
16
20
|
▼
|
|
17
21
|
bb-browser CLI ──HTTP──▶ Daemon ──SSE──▶ Chrome Extension
|
|
18
22
|
│
|
|
@@ -21,48 +25,73 @@ bb-browser CLI ──HTTP──▶ Daemon ──SSE──▶ Chrome Extension
|
|
|
21
25
|
(已登录的网站、Cookies)
|
|
22
26
|
```
|
|
23
27
|
|
|
28
|
+
## 为什么不用 Playwright / Selenium
|
|
29
|
+
|
|
30
|
+
| | Playwright / Selenium | bb-browser |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| 浏览器环境 | 独立的无头浏览器 | 用户的真实浏览器 |
|
|
33
|
+
| 登录态 | 没有,需要手动登录 | 复用已有的 Cookies 和会话 |
|
|
34
|
+
| 自动化检测 | 容易被识别和拦截 | `chrome.debugger` API,无指纹 |
|
|
35
|
+
| 内部系统 | 需要额外配置 VPN/代理 | 用户能访问的,它都能访问 |
|
|
36
|
+
|
|
37
|
+
## 两种使用方式
|
|
38
|
+
|
|
39
|
+
### 独立使用
|
|
40
|
+
|
|
41
|
+
作为 CLI 工具,任何 AI Agent 都可以直接调用:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
bb-browser open https://example.com
|
|
45
|
+
bb-browser snapshot -i
|
|
46
|
+
bb-browser click @0
|
|
47
|
+
bb-browser fill @2 "search query"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 接入 Epiral Agent
|
|
51
|
+
|
|
52
|
+
通过 [Epiral CLI](https://github.com/epiral/cli) 的 Browser Bridge 接入 [Epiral Agent](https://github.com/epiral/agent),让 Agent 远程控制浏览器:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
Epiral Agent → gRPC → Epiral CLI (Browser Bridge) → SSE → Chrome 扩展 → 浏览器
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
只需在 Chrome 扩展设置中将上游 URL 指向 Epiral CLI 的 SSE 端口即可。Agent 可以同时接入多个浏览器。
|
|
59
|
+
|
|
24
60
|
## 安装
|
|
25
61
|
|
|
26
|
-
###
|
|
62
|
+
### npm 安装(推荐)
|
|
27
63
|
|
|
28
64
|
```bash
|
|
29
65
|
npm install -g bb-browser
|
|
30
66
|
```
|
|
31
67
|
|
|
32
|
-
###
|
|
68
|
+
### 从源码构建
|
|
33
69
|
|
|
34
70
|
```bash
|
|
35
71
|
git clone https://github.com/yan5xu/bb-browser.git
|
|
36
72
|
cd bb-browser
|
|
37
|
-
pnpm install
|
|
38
|
-
pnpm build
|
|
73
|
+
pnpm install && pnpm build
|
|
39
74
|
```
|
|
40
75
|
|
|
41
|
-
### 加载 Chrome
|
|
76
|
+
### 加载 Chrome 扩展(必须)
|
|
42
77
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
1. 打开 Chrome,访问 `chrome://extensions/`
|
|
46
|
-
2. 开启「开发者模式」(右上角开关)
|
|
78
|
+
1. 打开 Chrome → `chrome://extensions/`
|
|
79
|
+
2. 开启「开发者模式」
|
|
47
80
|
3. 点击「加载已解压的扩展程序」
|
|
48
81
|
4. 选择扩展目录:
|
|
49
82
|
- npm 安装:`node_modules/bb-browser/extension/`
|
|
50
83
|
- 源码构建:`packages/extension/dist/`
|
|
51
|
-
5. 确认扩展已启用
|
|
52
84
|
|
|
53
85
|
## 使用
|
|
54
86
|
|
|
55
|
-
###
|
|
87
|
+
### 启动 Daemon
|
|
56
88
|
|
|
57
89
|
```bash
|
|
58
|
-
#
|
|
59
|
-
bb-browser
|
|
60
|
-
|
|
61
|
-
# 或使用别名
|
|
62
|
-
bb-browser start
|
|
90
|
+
bb-browser daemon # 前台启动
|
|
91
|
+
bb-browser start # 别名
|
|
63
92
|
```
|
|
64
93
|
|
|
65
|
-
###
|
|
94
|
+
### 基本操作
|
|
66
95
|
|
|
67
96
|
```bash
|
|
68
97
|
# 打开网页
|
|
@@ -70,150 +99,70 @@ bb-browser open https://example.com
|
|
|
70
99
|
|
|
71
100
|
# 获取页面快照(可交互元素)
|
|
72
101
|
bb-browser snapshot -i
|
|
73
|
-
|
|
74
|
-
# 输出示例:
|
|
102
|
+
# 输出:
|
|
75
103
|
# - link "Learn more" [ref=0]
|
|
76
104
|
# - button "Submit" [ref=1]
|
|
77
105
|
# - textbox "Search" [ref=2]
|
|
78
106
|
|
|
79
|
-
#
|
|
107
|
+
# 通过 ref 操作元素
|
|
80
108
|
bb-browser click @0
|
|
81
|
-
|
|
82
|
-
# 填充输入框
|
|
83
|
-
bb-browser fill @2 "search query"
|
|
84
|
-
|
|
85
|
-
# 按键
|
|
109
|
+
bb-browser fill @2 "hello world"
|
|
86
110
|
bb-browser press Enter
|
|
87
111
|
```
|
|
88
112
|
|
|
89
|
-
###
|
|
113
|
+
### 命令速查
|
|
90
114
|
|
|
91
|
-
|
|
|
115
|
+
| 类别 | 命令 | 说明 |
|
|
92
116
|
|------|------|------|
|
|
93
|
-
| `open <url>` | 打开 URL |
|
|
94
|
-
| `
|
|
95
|
-
|
|
|
96
|
-
|
|
|
97
|
-
|
|
|
98
|
-
| `
|
|
99
|
-
| `
|
|
100
|
-
| `
|
|
101
|
-
| `
|
|
102
|
-
| `
|
|
103
|
-
| `
|
|
104
|
-
|
|
|
105
|
-
|
|
|
106
|
-
|
|
|
107
|
-
| `
|
|
108
|
-
|
|
|
109
|
-
| `
|
|
110
|
-
|
|
|
111
|
-
| `
|
|
112
|
-
| `
|
|
113
|
-
| `close` | 关闭标签页 |
|
|
114
|
-
|
|
115
|
-
|
|
117
|
+
| **导航** | `open <url>` | 打开 URL |
|
|
118
|
+
| | `back` / `forward` / `refresh` | 导航操作 |
|
|
119
|
+
| | `close` | 关闭标签页 |
|
|
120
|
+
| **快照** | `snapshot` | 完整 DOM 树 |
|
|
121
|
+
| | `snapshot -i` | 只看可交互元素 |
|
|
122
|
+
| **交互** | `click <ref>` | 点击 |
|
|
123
|
+
| | `fill <ref> <text>` | 清空后填入 |
|
|
124
|
+
| | `type <ref> <text>` | 逐字符追加 |
|
|
125
|
+
| | `hover <ref>` | 悬停 |
|
|
126
|
+
| | `press <key>` | 按键 |
|
|
127
|
+
| | `scroll <dir> [px]` | 滚动 |
|
|
128
|
+
| | `check` / `uncheck <ref>` | 复选框 |
|
|
129
|
+
| | `select <ref> <val>` | 下拉框 |
|
|
130
|
+
| **信息** | `get text <ref>` | 元素文本 |
|
|
131
|
+
| | `get url` / `get title` | 页面信息 |
|
|
132
|
+
| | `screenshot [path]` | 截图 |
|
|
133
|
+
| | `eval "<js>"` | 执行 JavaScript |
|
|
134
|
+
| **Tab** | `tab` | 列出标签页 |
|
|
135
|
+
| | `tab new <url>` | 新标签页 |
|
|
136
|
+
| | `tab <n>` | 切换标签页 |
|
|
137
|
+
| | `tab close` | 关闭标签页 |
|
|
138
|
+
| **Frame** | `frame "<selector>"` | 进入 iframe |
|
|
139
|
+
| | `frame main` | 回到主 frame |
|
|
140
|
+
| **对话框** | `dialog accept [text]` | 接受 |
|
|
141
|
+
| | `dialog dismiss` | 拒绝 |
|
|
142
|
+
| **网络** | `network requests [filter]` | 查看请求 |
|
|
143
|
+
| | `network route "<pattern>" --abort` | 拦截 |
|
|
144
|
+
| | `network unroute` | 取消拦截 |
|
|
145
|
+
| **调试** | `console` / `errors` | 控制台/错误 |
|
|
146
|
+
| **Daemon** | `daemon` / `start` / `stop` / `status` | 管理 |
|
|
147
|
+
|
|
148
|
+
### JSON 输出
|
|
149
|
+
|
|
150
|
+
所有命令支持 `--json` 参数:
|
|
116
151
|
|
|
117
152
|
```bash
|
|
118
|
-
# 列出所有标签页
|
|
119
|
-
bb-browser tab
|
|
120
|
-
|
|
121
|
-
# 新建标签页
|
|
122
|
-
bb-browser tab new https://google.com
|
|
123
|
-
|
|
124
|
-
# 切换到第 2 个标签页
|
|
125
|
-
bb-browser tab 2
|
|
126
|
-
|
|
127
|
-
# 关闭当前标签页
|
|
128
|
-
bb-browser tab close
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
### 5. iframe 支持
|
|
132
|
-
|
|
133
|
-
```bash
|
|
134
|
-
# 切换到 iframe(通过选择器)
|
|
135
|
-
bb-browser frame "#iframe-id"
|
|
136
|
-
bb-browser frame "[name='content']"
|
|
137
|
-
|
|
138
|
-
# 返回主 frame
|
|
139
|
-
bb-browser frame main
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
### 6. 对话框处理
|
|
143
|
-
|
|
144
|
-
```bash
|
|
145
|
-
# 接受 alert/confirm
|
|
146
|
-
bb-browser dialog accept
|
|
147
|
-
|
|
148
|
-
# 接受 prompt 并输入文本
|
|
149
|
-
bb-browser dialog accept "input text"
|
|
150
|
-
|
|
151
|
-
# 拒绝对话框
|
|
152
|
-
bb-browser dialog dismiss
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
### 7. 网络监控
|
|
156
|
-
|
|
157
|
-
```bash
|
|
158
|
-
# 查看网络请求
|
|
159
|
-
bb-browser network requests
|
|
160
|
-
|
|
161
|
-
# 按关键词过滤
|
|
162
|
-
bb-browser network requests api
|
|
163
|
-
|
|
164
|
-
# 拦截并阻止请求
|
|
165
|
-
bb-browser network route "*ads*" --abort
|
|
166
|
-
|
|
167
|
-
# Mock 响应
|
|
168
|
-
bb-browser network route "/api/user" --body '{"name":"test"}'
|
|
169
|
-
|
|
170
|
-
# 移除拦截规则
|
|
171
|
-
bb-browser network unroute
|
|
172
|
-
|
|
173
|
-
# 清空请求记录
|
|
174
|
-
bb-browser network clear
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
### 8. 调试
|
|
178
|
-
|
|
179
|
-
```bash
|
|
180
|
-
# 查看控制台消息
|
|
181
|
-
bb-browser console
|
|
182
|
-
|
|
183
|
-
# 清空控制台
|
|
184
|
-
bb-browser console --clear
|
|
185
|
-
|
|
186
|
-
# 查看 JS 错误
|
|
187
|
-
bb-browser errors
|
|
188
|
-
|
|
189
|
-
# 清空错误记录
|
|
190
|
-
bb-browser errors --clear
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
### 9. JSON 输出
|
|
194
|
-
|
|
195
|
-
所有命令支持 `--json` 参数,方便程序解析:
|
|
196
|
-
|
|
197
|
-
```bash
|
|
198
|
-
bb-browser snapshot -i --json
|
|
199
|
-
# {"success":true,"data":{"snapshot":"...","refs":{...}}}
|
|
200
|
-
|
|
201
153
|
bb-browser get url --json
|
|
202
154
|
# {"success":true,"data":"https://example.com"}
|
|
203
155
|
```
|
|
204
156
|
|
|
205
|
-
|
|
157
|
+
### 多 Tab 并发
|
|
206
158
|
|
|
207
|
-
|
|
159
|
+
每次 `open` 返回独立的 tabId,通过 `--tab` 参数隔离操作:
|
|
208
160
|
|
|
209
161
|
```bash
|
|
210
|
-
#
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
# 然后加载扩展,之后可以热重载
|
|
216
|
-
bb-browser reload
|
|
162
|
+
bb-browser open https://site-a.com # → tabId: 123
|
|
163
|
+
bb-browser open https://site-b.com # → tabId: 456
|
|
164
|
+
bb-browser snapshot -i --tab 123 # 操作 site-a
|
|
165
|
+
bb-browser click @0 --tab 456 # 操作 site-b
|
|
217
166
|
```
|
|
218
167
|
|
|
219
168
|
## 项目结构
|
|
@@ -221,20 +170,24 @@ bb-browser reload
|
|
|
221
170
|
```
|
|
222
171
|
bb-browser/
|
|
223
172
|
├── packages/
|
|
224
|
-
│ ├── cli/ # CLI
|
|
225
|
-
│ ├── daemon/ # HTTP Daemon(
|
|
226
|
-
│ ├── extension/ # Chrome
|
|
227
|
-
│ └── shared/ #
|
|
228
|
-
|
|
173
|
+
│ ├── cli/ # CLI 工具(参数解析、HTTP 客户端、Daemon 管理)
|
|
174
|
+
│ ├── daemon/ # HTTP Daemon(SSE 推送、请求-响应匹配)
|
|
175
|
+
│ ├── extension/ # Chrome 扩展(Manifest V3、chrome.debugger)
|
|
176
|
+
│ └── shared/ # 共享类型和协议定义
|
|
177
|
+
├── skills/ # AI Agent Skill 文档
|
|
178
|
+
├── dist/ # 构建产物(npm 发布)
|
|
179
|
+
└── extension/ # 构建好的扩展(npm 发布)
|
|
229
180
|
```
|
|
230
181
|
|
|
231
182
|
## 技术栈
|
|
232
183
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
184
|
+
| 层 | 技术 |
|
|
185
|
+
|----|------|
|
|
186
|
+
| CLI | TypeScript,手写参数解析 |
|
|
187
|
+
| Daemon | Node.js HTTP Server + SSE |
|
|
188
|
+
| Extension | Chrome Manifest V3 + `chrome.debugger` API |
|
|
189
|
+
| 构建 | pnpm monorepo + Turborepo + tsup + Vite |
|
|
237
190
|
|
|
238
|
-
##
|
|
191
|
+
## 许可证
|
|
239
192
|
|
|
240
|
-
MIT
|
|
193
|
+
[MIT](LICENSE)
|
package/dist/cli.js
CHANGED
package/dist/daemon.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
COMMAND_TIMEOUT,
|
|
4
|
+
DAEMON_HOST,
|
|
4
5
|
DAEMON_PORT,
|
|
5
6
|
SSE_HEARTBEAT_INTERVAL
|
|
6
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-TUO443YI.js";
|
|
7
8
|
|
|
8
9
|
// packages/daemon/src/index.ts
|
|
9
10
|
import { parseArgs } from "util";
|
|
@@ -164,12 +165,14 @@ var RequestManager = class {
|
|
|
164
165
|
// packages/daemon/src/http-server.ts
|
|
165
166
|
var HttpServer = class {
|
|
166
167
|
server = null;
|
|
168
|
+
host;
|
|
167
169
|
port;
|
|
168
170
|
startTime = 0;
|
|
169
171
|
onShutdown;
|
|
170
172
|
sseManager = new SSEManager();
|
|
171
173
|
requestManager = new RequestManager();
|
|
172
174
|
constructor(options = {}) {
|
|
175
|
+
this.host = options.host ?? "127.0.0.1";
|
|
173
176
|
this.port = options.port ?? DAEMON_PORT;
|
|
174
177
|
this.onShutdown = options.onShutdown;
|
|
175
178
|
}
|
|
@@ -184,7 +187,7 @@ var HttpServer = class {
|
|
|
184
187
|
this.server.on("error", (error) => {
|
|
185
188
|
reject(error);
|
|
186
189
|
});
|
|
187
|
-
this.server.listen(this.port,
|
|
190
|
+
this.server.listen(this.port, this.host, () => {
|
|
188
191
|
this.startTime = Date.now();
|
|
189
192
|
resolve();
|
|
190
193
|
});
|
|
@@ -368,6 +371,11 @@ var PID_FILE_PATH = "/tmp/bb-browser.pid";
|
|
|
368
371
|
function parseOptions() {
|
|
369
372
|
const { values } = parseArgs({
|
|
370
373
|
options: {
|
|
374
|
+
host: {
|
|
375
|
+
type: "string",
|
|
376
|
+
short: "H",
|
|
377
|
+
default: DAEMON_HOST
|
|
378
|
+
},
|
|
371
379
|
port: {
|
|
372
380
|
type: "string",
|
|
373
381
|
short: "p",
|
|
@@ -388,6 +396,7 @@ Usage:
|
|
|
388
396
|
bb-browser-daemon [options]
|
|
389
397
|
|
|
390
398
|
Options:
|
|
399
|
+
-H, --host <host> HTTP server host (default: ${DAEMON_HOST})
|
|
391
400
|
-p, --port <port> HTTP server port (default: ${DAEMON_PORT})
|
|
392
401
|
-h, --help Show this help message
|
|
393
402
|
|
|
@@ -400,6 +409,7 @@ Endpoints:
|
|
|
400
409
|
process.exit(0);
|
|
401
410
|
}
|
|
402
411
|
return {
|
|
412
|
+
host: values.host ?? DAEMON_HOST,
|
|
403
413
|
port: parseInt(values.port ?? String(DAEMON_PORT), 10)
|
|
404
414
|
};
|
|
405
415
|
}
|
|
@@ -423,6 +433,7 @@ async function main() {
|
|
|
423
433
|
process.exit(0);
|
|
424
434
|
};
|
|
425
435
|
const httpServer = new HttpServer({
|
|
436
|
+
host: options.host,
|
|
426
437
|
port: options.port,
|
|
427
438
|
onShutdown: shutdown
|
|
428
439
|
});
|
|
@@ -430,7 +441,7 @@ async function main() {
|
|
|
430
441
|
process.on("SIGTERM", shutdown);
|
|
431
442
|
await httpServer.start();
|
|
432
443
|
writePidFile();
|
|
433
|
-
console.error(`[Daemon] HTTP server listening on http
|
|
444
|
+
console.error(`[Daemon] HTTP server listening on http://${options.host}:${options.port}`);
|
|
434
445
|
console.error("[Daemon] Waiting for extension connection...");
|
|
435
446
|
}
|
|
436
447
|
main().catch((error) => {
|
package/dist/daemon.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../packages/daemon/src/index.ts","../packages/daemon/src/http-server.ts","../packages/daemon/src/sse-manager.ts","../packages/daemon/src/request-manager.ts"],"sourcesContent":["/**\n * bb-browser Daemon 主入口\n *\n * HTTP Server + SSE 推送架构\n *\n * 职责:\n * 1. 启动 HTTP 服务器监听 localhost:19824\n * 2. 处理 CLI 命令请求 (POST /command)\n * 3. 管理扩展 SSE 连接 (GET /sse)\n * 4. 接收扩展结果回传 (POST /result)\n */\n\nimport { parseArgs } from \"node:util\";\nimport { writeFileSync, unlinkSync, existsSync } from \"node:fs\";\nimport { DAEMON_PORT } from \"@bb-browser/shared\";\nimport { HttpServer } from \"./http-server.js\";\n\nconst PID_FILE_PATH = \"/tmp/bb-browser.pid\";\n\ninterface DaemonOptions {\n port: number;\n}\n\n/**\n * 解析命令行参数\n */\nfunction parseOptions(): DaemonOptions {\n const { values } = parseArgs({\n options: {\n port: {\n type: \"string\",\n short: \"p\",\n default: String(DAEMON_PORT),\n },\n help: {\n type: \"boolean\",\n short: \"h\",\n default: false,\n },\n },\n });\n\n if (values.help) {\n console.error(`\nbb-browser-daemon - HTTP Server Daemon for bb-browser\n\nUsage:\n bb-browser-daemon [options]\n\nOptions:\n -p, --port <port> HTTP server port (default: ${DAEMON_PORT})\n -h, --help Show this help message\n\nEndpoints:\n POST /command Send command and wait for result (CLI)\n GET /sse Subscribe to command stream (Extension)\n POST /result Report command result (Extension)\n GET /status Query daemon status\n`);\n process.exit(0);\n }\n\n return {\n port: parseInt(values.port ?? String(DAEMON_PORT), 10),\n };\n}\n\n/**\n * 写入 PID 文件\n */\nfunction writePidFile(): void {\n writeFileSync(PID_FILE_PATH, String(process.pid), \"utf-8\");\n}\n\n/**\n * 清理 PID 文件\n */\nfunction cleanupPidFile(): void {\n if (existsSync(PID_FILE_PATH)) {\n try {\n unlinkSync(PID_FILE_PATH);\n } catch {\n // 忽略清理失败\n }\n }\n}\n\n/**\n * 主函数\n */\nasync function main(): Promise<void> {\n const options = parseOptions();\n\n // 优雅关闭\n const shutdown = async () => {\n console.error(\"[Daemon] Shutting down...\");\n await httpServer.stop();\n cleanupPidFile();\n process.exit(0);\n };\n\n // 创建 HTTP 服务器\n const httpServer = new HttpServer({ \n port: options.port,\n onShutdown: shutdown,\n });\n\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\n\n // 启动服务器\n await httpServer.start();\n\n // 写入 PID 文件\n writePidFile();\n\n console.error(`[Daemon] HTTP server listening on http://127.0.0.1:${options.port}`);\n console.error(\"[Daemon] Waiting for extension connection...\");\n}\n\n// 启动 Daemon\nmain().catch((error) => {\n console.error(\"[Daemon] Fatal error:\", error);\n cleanupPidFile();\n process.exit(1);\n});\n","/**\n * HTTP 服务器\n *\n * 提供 REST API 端点:\n * - POST /command: CLI 发送命令\n * - GET /sse: 扩展订阅命令流\n * - POST /result: 扩展回传结果\n * - GET /status: 查询状态\n */\n\nimport { createServer, type Server, type IncomingMessage, type ServerResponse } from \"node:http\";\nimport type { Request, Response } from \"@bb-browser/shared\";\nimport { DAEMON_PORT } from \"@bb-browser/shared\";\nimport { SSEManager } from \"./sse-manager.js\";\nimport { RequestManager } from \"./request-manager.js\";\n\nexport interface HttpServerOptions {\n port?: number;\n onShutdown?: () => void;\n}\n\n/**\n * HTTP 服务器\n */\nexport class HttpServer {\n private server: Server | null = null;\n private port: number;\n private startTime: number = 0;\n private onShutdown?: () => void;\n\n readonly sseManager = new SSEManager();\n readonly requestManager = new RequestManager();\n\n constructor(options: HttpServerOptions = {}) {\n this.port = options.port ?? DAEMON_PORT;\n this.onShutdown = options.onShutdown;\n }\n\n /**\n * 启动服务器\n */\n async start(): Promise<void> {\n return new Promise((resolve, reject) => {\n this.server = createServer((req, res) => {\n this.handleRequest(req, res);\n });\n\n this.server.on(\"error\", (error) => {\n reject(error);\n });\n\n this.server.listen(this.port, \"127.0.0.1\", () => {\n this.startTime = Date.now();\n resolve();\n });\n });\n }\n\n /**\n * 停止服务器\n */\n async stop(): Promise<void> {\n // 清理 pending 请求\n this.requestManager.clear();\n\n // 断开 SSE 连接\n this.sseManager.disconnect();\n\n // 关闭服务器\n if (this.server) {\n return new Promise((resolve) => {\n this.server!.close(() => {\n resolve();\n });\n });\n }\n }\n\n /**\n * 获取运行时间(秒)\n */\n get uptime(): number {\n if (this.startTime === 0) {\n return 0;\n }\n return Math.floor((Date.now() - this.startTime) / 1000);\n }\n\n /**\n * 路由请求\n */\n private handleRequest(req: IncomingMessage, res: ServerResponse): void {\n // CORS 支持\n res.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n res.setHeader(\"Access-Control-Allow-Methods\", \"GET, POST, OPTIONS\");\n res.setHeader(\"Access-Control-Allow-Headers\", \"Content-Type\");\n\n if (req.method === \"OPTIONS\") {\n res.writeHead(204);\n res.end();\n return;\n }\n\n const url = req.url ?? \"/\";\n\n if (req.method === \"POST\" && url === \"/command\") {\n this.handleCommand(req, res);\n } else if (req.method === \"GET\" && url === \"/sse\") {\n this.handleSSE(req, res);\n } else if (req.method === \"POST\" && url === \"/result\") {\n this.handleResult(req, res);\n } else if (req.method === \"GET\" && url === \"/status\") {\n this.handleStatus(req, res);\n } else if (req.method === \"POST\" && url === \"/shutdown\") {\n this.handleShutdown(req, res);\n } else {\n this.sendJson(res, 404, { error: \"Not found\" });\n }\n }\n\n /**\n * POST /command - CLI 发送命令\n */\n private async handleCommand(req: IncomingMessage, res: ServerResponse): Promise<void> {\n try {\n const body = await this.readBody(req);\n const request = JSON.parse(body) as Request;\n\n // 检查扩展是否连接\n if (!this.sseManager.isConnected) {\n this.sendJson(res, 503, {\n id: request.id,\n success: false,\n error: \"Extension not connected\",\n });\n return;\n }\n\n // 创建 Promise 等待响应\n const responsePromise = new Promise<Response>((resolve, reject) => {\n this.requestManager.add(request.id, resolve, reject);\n });\n\n // 推送命令给扩展\n const sent = this.sseManager.sendCommand(request);\n if (!sent) {\n // 移除 pending 请求\n this.requestManager.resolve(request.id, {\n id: request.id,\n success: false,\n error: \"Failed to send command to extension\",\n });\n this.sendJson(res, 503, {\n id: request.id,\n success: false,\n error: \"Failed to send command to extension\",\n });\n return;\n }\n\n // 等待响应\n try {\n const response = await responsePromise;\n this.sendJson(res, 200, response);\n } catch (error) {\n // 超时或其他错误\n this.sendJson(res, 408, {\n id: request.id,\n success: false,\n error: error instanceof Error ? error.message : \"Unknown error\",\n });\n }\n } catch (error) {\n this.sendJson(res, 400, {\n success: false,\n error: error instanceof Error ? error.message : \"Invalid request\",\n });\n }\n }\n\n /**\n * GET /sse - 扩展订阅命令流\n */\n private handleSSE(_req: IncomingMessage, res: ServerResponse): void {\n this.sseManager.connect(res);\n }\n\n /**\n * POST /result - 扩展回传结果\n */\n private async handleResult(req: IncomingMessage, res: ServerResponse): Promise<void> {\n try {\n const body = await this.readBody(req);\n const result = JSON.parse(body) as Response;\n\n // 匹配 pending 请求\n const resolved = this.requestManager.resolve(result.id, result);\n\n if (resolved) {\n this.sendJson(res, 200, { code: 0, message: \"ok\" });\n } else {\n // 找不到对应请求(可能已超时)\n this.sendJson(res, 200, { code: 1, message: \"Request not found or already expired\" });\n }\n } catch (error) {\n this.sendJson(res, 400, {\n code: -1,\n message: error instanceof Error ? error.message : \"Invalid request\",\n });\n }\n }\n\n /**\n * GET /status - 查询状态\n */\n private handleStatus(_req: IncomingMessage, res: ServerResponse): void {\n this.sendJson(res, 200, {\n running: true,\n extensionConnected: this.sseManager.isConnected,\n pendingRequests: this.requestManager.pendingCount,\n uptime: this.uptime,\n });\n }\n\n /**\n * POST /shutdown - 关闭服务器\n */\n private handleShutdown(_req: IncomingMessage, res: ServerResponse): void {\n this.sendJson(res, 200, { code: 0, message: \"Shutting down\" });\n \n // 延迟关闭,确保响应发送完成\n setTimeout(() => {\n if (this.onShutdown) {\n this.onShutdown();\n }\n }, 100);\n }\n\n /**\n * 读取请求体\n */\n private readBody(req: IncomingMessage): Promise<string> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n\n req.on(\"data\", (chunk: Buffer) => {\n chunks.push(chunk);\n });\n\n req.on(\"end\", () => {\n resolve(Buffer.concat(chunks).toString(\"utf-8\"));\n });\n\n req.on(\"error\", (error) => {\n reject(error);\n });\n });\n }\n\n /**\n * 发送 JSON 响应\n */\n private sendJson(res: ServerResponse, status: number, data: unknown): void {\n res.writeHead(status, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(data));\n }\n}\n","/**\n * SSE 连接管理\n *\n * 职责:\n * - 管理与扩展的 SSE 长连接\n * - 发送心跳保活\n * - 推送命令事件\n */\n\nimport type { ServerResponse } from \"node:http\";\nimport type { Request } from \"@bb-browser/shared\";\nimport { SSE_HEARTBEAT_INTERVAL } from \"@bb-browser/shared\";\n\n/**\n * SSE 连接管理器\n */\nexport class SSEManager {\n private connection: ServerResponse | null = null;\n private heartbeatTimer: NodeJS.Timeout | null = null;\n\n /**\n * 检查是否有活跃连接\n */\n get isConnected(): boolean {\n return this.connection !== null && !this.connection.writableEnded;\n }\n\n /**\n * 建立 SSE 连接\n */\n connect(res: ServerResponse): void {\n // 关闭旧连接\n if (this.connection) {\n this.disconnect();\n }\n\n // 设置 SSE 响应头\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n Connection: \"keep-alive\",\n \"Access-Control-Allow-Origin\": \"*\",\n });\n\n this.connection = res;\n\n // 发送 connected 事件\n this.sendEvent(\"connected\", { time: Date.now() });\n\n // 启动心跳\n this.startHeartbeat();\n\n // 监听连接关闭\n res.on(\"close\", () => {\n this.cleanupConnection();\n });\n }\n\n /**\n * 断开连接\n */\n disconnect(): void {\n this.stopHeartbeat();\n\n if (this.connection && !this.connection.writableEnded) {\n this.connection.end();\n }\n\n this.connection = null;\n }\n\n /**\n * 发送命令给扩展\n */\n sendCommand(request: Request): boolean {\n return this.sendEvent(\"command\", request);\n }\n\n /**\n * 发送 SSE 事件\n */\n private sendEvent(eventType: string, data: unknown): boolean {\n if (!this.connection || this.connection.writableEnded) {\n return false;\n }\n\n try {\n this.connection.write(`event: ${eventType}\\n`);\n this.connection.write(`data: ${JSON.stringify(data)}\\n\\n`);\n return true;\n } catch {\n return false;\n }\n }\n\n /**\n * 启动心跳定时器\n */\n private startHeartbeat(): void {\n this.stopHeartbeat();\n\n this.heartbeatTimer = setInterval(() => {\n const sent = this.sendEvent(\"heartbeat\", { time: Date.now() });\n if (!sent) {\n this.cleanupConnection();\n }\n }, SSE_HEARTBEAT_INTERVAL);\n }\n\n /**\n * 停止心跳定时器\n */\n private stopHeartbeat(): void {\n if (this.heartbeatTimer) {\n clearInterval(this.heartbeatTimer);\n this.heartbeatTimer = null;\n }\n }\n\n /**\n * 清理连接\n */\n private cleanupConnection(): void {\n this.stopHeartbeat();\n this.connection = null;\n }\n}\n","/**\n * Pending 请求管理\n *\n * 职责:\n * - 管理等待响应的请求\n * - 超时处理\n * - 匹配请求和响应\n */\n\nimport type { Response } from \"@bb-browser/shared\";\nimport { COMMAND_TIMEOUT } from \"@bb-browser/shared\";\n\ninterface PendingRequest {\n resolve: (response: Response) => void;\n reject: (error: Error) => void;\n timeout: NodeJS.Timeout;\n}\n\n/**\n * 请求管理器\n */\nexport class RequestManager {\n private pending = new Map<string, PendingRequest>();\n\n /**\n * 获取等待中的请求数量\n */\n get pendingCount(): number {\n return this.pending.size;\n }\n\n /**\n * 添加一个 pending 请求\n */\n add(\n requestId: string,\n resolve: (response: Response) => void,\n reject: (error: Error) => void\n ): void {\n // 设置超时\n const timeout = setTimeout(() => {\n this.timeout(requestId);\n }, COMMAND_TIMEOUT);\n\n this.pending.set(requestId, { resolve, reject, timeout });\n }\n\n /**\n * 解决一个 pending 请求\n * @returns 是否找到并解决了请求\n */\n resolve(requestId: string, response: Response): boolean {\n const pendingRequest = this.pending.get(requestId);\n\n if (!pendingRequest) {\n return false;\n }\n\n // 清理\n clearTimeout(pendingRequest.timeout);\n this.pending.delete(requestId);\n\n // 解决 Promise\n pendingRequest.resolve(response);\n return true;\n }\n\n /**\n * 请求超时处理\n */\n private timeout(requestId: string): void {\n const pendingRequest = this.pending.get(requestId);\n\n if (!pendingRequest) {\n return;\n }\n\n this.pending.delete(requestId);\n pendingRequest.reject(new Error(\"Command timeout\"));\n }\n\n /**\n * 清理所有 pending 请求\n */\n clear(): void {\n for (const [id, request] of this.pending) {\n clearTimeout(request.timeout);\n request.reject(new Error(\"Daemon shutting down\"));\n }\n this.pending.clear();\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../packages/daemon/src/index.ts","../packages/daemon/src/http-server.ts","../packages/daemon/src/sse-manager.ts","../packages/daemon/src/request-manager.ts"],"sourcesContent":["/**\n * bb-browser Daemon 主入口\n *\n * HTTP Server + SSE 推送架构\n *\n * 职责:\n * 1. 启动 HTTP 服务器监听 localhost:19824\n * 2. 处理 CLI 命令请求 (POST /command)\n * 3. 管理扩展 SSE 连接 (GET /sse)\n * 4. 接收扩展结果回传 (POST /result)\n */\n\nimport { parseArgs } from \"node:util\";\nimport { writeFileSync, unlinkSync, existsSync } from \"node:fs\";\nimport { DAEMON_PORT, DAEMON_HOST } from \"@bb-browser/shared\";\nimport { HttpServer } from \"./http-server.js\";\n\nconst PID_FILE_PATH = \"/tmp/bb-browser.pid\";\n\ninterface DaemonOptions {\n host: string;\n port: number;\n}\n\n/**\n * 解析命令行参数\n */\nfunction parseOptions(): DaemonOptions {\n const { values } = parseArgs({\n options: {\n host: {\n type: \"string\",\n short: \"H\",\n default: DAEMON_HOST,\n },\n port: {\n type: \"string\",\n short: \"p\",\n default: String(DAEMON_PORT),\n },\n help: {\n type: \"boolean\",\n short: \"h\",\n default: false,\n },\n },\n });\n\n if (values.help) {\n console.error(`\nbb-browser-daemon - HTTP Server Daemon for bb-browser\n\nUsage:\n bb-browser-daemon [options]\n\nOptions:\n -H, --host <host> HTTP server host (default: ${DAEMON_HOST})\n -p, --port <port> HTTP server port (default: ${DAEMON_PORT})\n -h, --help Show this help message\n\nEndpoints:\n POST /command Send command and wait for result (CLI)\n GET /sse Subscribe to command stream (Extension)\n POST /result Report command result (Extension)\n GET /status Query daemon status\n`);\n process.exit(0);\n }\n\n return {\n host: values.host ?? DAEMON_HOST,\n port: parseInt(values.port ?? String(DAEMON_PORT), 10),\n };\n}\n\n/**\n * 写入 PID 文件\n */\nfunction writePidFile(): void {\n writeFileSync(PID_FILE_PATH, String(process.pid), \"utf-8\");\n}\n\n/**\n * 清理 PID 文件\n */\nfunction cleanupPidFile(): void {\n if (existsSync(PID_FILE_PATH)) {\n try {\n unlinkSync(PID_FILE_PATH);\n } catch {\n // 忽略清理失败\n }\n }\n}\n\n/**\n * 主函数\n */\nasync function main(): Promise<void> {\n const options = parseOptions();\n\n // 优雅关闭\n const shutdown = async () => {\n console.error(\"[Daemon] Shutting down...\");\n await httpServer.stop();\n cleanupPidFile();\n process.exit(0);\n };\n\n // 创建 HTTP 服务器\n const httpServer = new HttpServer({\n host: options.host,\n port: options.port,\n onShutdown: shutdown,\n });\n\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\n\n // 启动服务器\n await httpServer.start();\n\n // 写入 PID 文件\n writePidFile();\n\n console.error(`[Daemon] HTTP server listening on http://${options.host}:${options.port}`);\n console.error(\"[Daemon] Waiting for extension connection...\");\n}\n\n// 启动 Daemon\nmain().catch((error) => {\n console.error(\"[Daemon] Fatal error:\", error);\n cleanupPidFile();\n process.exit(1);\n});\n","/**\n * HTTP 服务器\n *\n * 提供 REST API 端点:\n * - POST /command: CLI 发送命令\n * - GET /sse: 扩展订阅命令流\n * - POST /result: 扩展回传结果\n * - GET /status: 查询状态\n */\n\nimport { createServer, type Server, type IncomingMessage, type ServerResponse } from \"node:http\";\nimport type { Request, Response } from \"@bb-browser/shared\";\nimport { DAEMON_PORT } from \"@bb-browser/shared\";\nimport { SSEManager } from \"./sse-manager.js\";\nimport { RequestManager } from \"./request-manager.js\";\n\nexport interface HttpServerOptions {\n host?: string;\n port?: number;\n onShutdown?: () => void;\n}\n\n/**\n * HTTP 服务器\n */\nexport class HttpServer {\n private server: Server | null = null;\n private host: string;\n private port: number;\n private startTime: number = 0;\n private onShutdown?: () => void;\n\n readonly sseManager = new SSEManager();\n readonly requestManager = new RequestManager();\n\n constructor(options: HttpServerOptions = {}) {\n this.host = options.host ?? \"127.0.0.1\";\n this.port = options.port ?? DAEMON_PORT;\n this.onShutdown = options.onShutdown;\n }\n\n /**\n * 启动服务器\n */\n async start(): Promise<void> {\n return new Promise((resolve, reject) => {\n this.server = createServer((req, res) => {\n this.handleRequest(req, res);\n });\n\n this.server.on(\"error\", (error) => {\n reject(error);\n });\n\n this.server.listen(this.port, this.host, () => {\n this.startTime = Date.now();\n resolve();\n });\n });\n }\n\n /**\n * 停止服务器\n */\n async stop(): Promise<void> {\n // 清理 pending 请求\n this.requestManager.clear();\n\n // 断开 SSE 连接\n this.sseManager.disconnect();\n\n // 关闭服务器\n if (this.server) {\n return new Promise((resolve) => {\n this.server!.close(() => {\n resolve();\n });\n });\n }\n }\n\n /**\n * 获取运行时间(秒)\n */\n get uptime(): number {\n if (this.startTime === 0) {\n return 0;\n }\n return Math.floor((Date.now() - this.startTime) / 1000);\n }\n\n /**\n * 路由请求\n */\n private handleRequest(req: IncomingMessage, res: ServerResponse): void {\n // CORS 支持\n res.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n res.setHeader(\"Access-Control-Allow-Methods\", \"GET, POST, OPTIONS\");\n res.setHeader(\"Access-Control-Allow-Headers\", \"Content-Type\");\n\n if (req.method === \"OPTIONS\") {\n res.writeHead(204);\n res.end();\n return;\n }\n\n const url = req.url ?? \"/\";\n\n if (req.method === \"POST\" && url === \"/command\") {\n this.handleCommand(req, res);\n } else if (req.method === \"GET\" && url === \"/sse\") {\n this.handleSSE(req, res);\n } else if (req.method === \"POST\" && url === \"/result\") {\n this.handleResult(req, res);\n } else if (req.method === \"GET\" && url === \"/status\") {\n this.handleStatus(req, res);\n } else if (req.method === \"POST\" && url === \"/shutdown\") {\n this.handleShutdown(req, res);\n } else {\n this.sendJson(res, 404, { error: \"Not found\" });\n }\n }\n\n /**\n * POST /command - CLI 发送命令\n */\n private async handleCommand(req: IncomingMessage, res: ServerResponse): Promise<void> {\n try {\n const body = await this.readBody(req);\n const request = JSON.parse(body) as Request;\n\n // 检查扩展是否连接\n if (!this.sseManager.isConnected) {\n this.sendJson(res, 503, {\n id: request.id,\n success: false,\n error: \"Extension not connected\",\n });\n return;\n }\n\n // 创建 Promise 等待响应\n const responsePromise = new Promise<Response>((resolve, reject) => {\n this.requestManager.add(request.id, resolve, reject);\n });\n\n // 推送命令给扩展\n const sent = this.sseManager.sendCommand(request);\n if (!sent) {\n // 移除 pending 请求\n this.requestManager.resolve(request.id, {\n id: request.id,\n success: false,\n error: \"Failed to send command to extension\",\n });\n this.sendJson(res, 503, {\n id: request.id,\n success: false,\n error: \"Failed to send command to extension\",\n });\n return;\n }\n\n // 等待响应\n try {\n const response = await responsePromise;\n this.sendJson(res, 200, response);\n } catch (error) {\n // 超时或其他错误\n this.sendJson(res, 408, {\n id: request.id,\n success: false,\n error: error instanceof Error ? error.message : \"Unknown error\",\n });\n }\n } catch (error) {\n this.sendJson(res, 400, {\n success: false,\n error: error instanceof Error ? error.message : \"Invalid request\",\n });\n }\n }\n\n /**\n * GET /sse - 扩展订阅命令流\n */\n private handleSSE(_req: IncomingMessage, res: ServerResponse): void {\n this.sseManager.connect(res);\n }\n\n /**\n * POST /result - 扩展回传结果\n */\n private async handleResult(req: IncomingMessage, res: ServerResponse): Promise<void> {\n try {\n const body = await this.readBody(req);\n const result = JSON.parse(body) as Response;\n\n // 匹配 pending 请求\n const resolved = this.requestManager.resolve(result.id, result);\n\n if (resolved) {\n this.sendJson(res, 200, { code: 0, message: \"ok\" });\n } else {\n // 找不到对应请求(可能已超时)\n this.sendJson(res, 200, { code: 1, message: \"Request not found or already expired\" });\n }\n } catch (error) {\n this.sendJson(res, 400, {\n code: -1,\n message: error instanceof Error ? error.message : \"Invalid request\",\n });\n }\n }\n\n /**\n * GET /status - 查询状态\n */\n private handleStatus(_req: IncomingMessage, res: ServerResponse): void {\n this.sendJson(res, 200, {\n running: true,\n extensionConnected: this.sseManager.isConnected,\n pendingRequests: this.requestManager.pendingCount,\n uptime: this.uptime,\n });\n }\n\n /**\n * POST /shutdown - 关闭服务器\n */\n private handleShutdown(_req: IncomingMessage, res: ServerResponse): void {\n this.sendJson(res, 200, { code: 0, message: \"Shutting down\" });\n \n // 延迟关闭,确保响应发送完成\n setTimeout(() => {\n if (this.onShutdown) {\n this.onShutdown();\n }\n }, 100);\n }\n\n /**\n * 读取请求体\n */\n private readBody(req: IncomingMessage): Promise<string> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n\n req.on(\"data\", (chunk: Buffer) => {\n chunks.push(chunk);\n });\n\n req.on(\"end\", () => {\n resolve(Buffer.concat(chunks).toString(\"utf-8\"));\n });\n\n req.on(\"error\", (error) => {\n reject(error);\n });\n });\n }\n\n /**\n * 发送 JSON 响应\n */\n private sendJson(res: ServerResponse, status: number, data: unknown): void {\n res.writeHead(status, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(data));\n }\n}\n","/**\n * SSE 连接管理\n *\n * 职责:\n * - 管理与扩展的 SSE 长连接\n * - 发送心跳保活\n * - 推送命令事件\n */\n\nimport type { ServerResponse } from \"node:http\";\nimport type { Request } from \"@bb-browser/shared\";\nimport { SSE_HEARTBEAT_INTERVAL } from \"@bb-browser/shared\";\n\n/**\n * SSE 连接管理器\n */\nexport class SSEManager {\n private connection: ServerResponse | null = null;\n private heartbeatTimer: NodeJS.Timeout | null = null;\n\n /**\n * 检查是否有活跃连接\n */\n get isConnected(): boolean {\n return this.connection !== null && !this.connection.writableEnded;\n }\n\n /**\n * 建立 SSE 连接\n */\n connect(res: ServerResponse): void {\n // 关闭旧连接\n if (this.connection) {\n this.disconnect();\n }\n\n // 设置 SSE 响应头\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n Connection: \"keep-alive\",\n \"Access-Control-Allow-Origin\": \"*\",\n });\n\n this.connection = res;\n\n // 发送 connected 事件\n this.sendEvent(\"connected\", { time: Date.now() });\n\n // 启动心跳\n this.startHeartbeat();\n\n // 监听连接关闭\n res.on(\"close\", () => {\n this.cleanupConnection();\n });\n }\n\n /**\n * 断开连接\n */\n disconnect(): void {\n this.stopHeartbeat();\n\n if (this.connection && !this.connection.writableEnded) {\n this.connection.end();\n }\n\n this.connection = null;\n }\n\n /**\n * 发送命令给扩展\n */\n sendCommand(request: Request): boolean {\n return this.sendEvent(\"command\", request);\n }\n\n /**\n * 发送 SSE 事件\n */\n private sendEvent(eventType: string, data: unknown): boolean {\n if (!this.connection || this.connection.writableEnded) {\n return false;\n }\n\n try {\n this.connection.write(`event: ${eventType}\\n`);\n this.connection.write(`data: ${JSON.stringify(data)}\\n\\n`);\n return true;\n } catch {\n return false;\n }\n }\n\n /**\n * 启动心跳定时器\n */\n private startHeartbeat(): void {\n this.stopHeartbeat();\n\n this.heartbeatTimer = setInterval(() => {\n const sent = this.sendEvent(\"heartbeat\", { time: Date.now() });\n if (!sent) {\n this.cleanupConnection();\n }\n }, SSE_HEARTBEAT_INTERVAL);\n }\n\n /**\n * 停止心跳定时器\n */\n private stopHeartbeat(): void {\n if (this.heartbeatTimer) {\n clearInterval(this.heartbeatTimer);\n this.heartbeatTimer = null;\n }\n }\n\n /**\n * 清理连接\n */\n private cleanupConnection(): void {\n this.stopHeartbeat();\n this.connection = null;\n }\n}\n","/**\n * Pending 请求管理\n *\n * 职责:\n * - 管理等待响应的请求\n * - 超时处理\n * - 匹配请求和响应\n */\n\nimport type { Response } from \"@bb-browser/shared\";\nimport { COMMAND_TIMEOUT } from \"@bb-browser/shared\";\n\ninterface PendingRequest {\n resolve: (response: Response) => void;\n reject: (error: Error) => void;\n timeout: NodeJS.Timeout;\n}\n\n/**\n * 请求管理器\n */\nexport class RequestManager {\n private pending = new Map<string, PendingRequest>();\n\n /**\n * 获取等待中的请求数量\n */\n get pendingCount(): number {\n return this.pending.size;\n }\n\n /**\n * 添加一个 pending 请求\n */\n add(\n requestId: string,\n resolve: (response: Response) => void,\n reject: (error: Error) => void\n ): void {\n // 设置超时\n const timeout = setTimeout(() => {\n this.timeout(requestId);\n }, COMMAND_TIMEOUT);\n\n this.pending.set(requestId, { resolve, reject, timeout });\n }\n\n /**\n * 解决一个 pending 请求\n * @returns 是否找到并解决了请求\n */\n resolve(requestId: string, response: Response): boolean {\n const pendingRequest = this.pending.get(requestId);\n\n if (!pendingRequest) {\n return false;\n }\n\n // 清理\n clearTimeout(pendingRequest.timeout);\n this.pending.delete(requestId);\n\n // 解决 Promise\n pendingRequest.resolve(response);\n return true;\n }\n\n /**\n * 请求超时处理\n */\n private timeout(requestId: string): void {\n const pendingRequest = this.pending.get(requestId);\n\n if (!pendingRequest) {\n return;\n }\n\n this.pending.delete(requestId);\n pendingRequest.reject(new Error(\"Command timeout\"));\n }\n\n /**\n * 清理所有 pending 请求\n */\n clear(): void {\n for (const [id, request] of this.pending) {\n clearTimeout(request.timeout);\n request.reject(new Error(\"Daemon shutting down\"));\n }\n this.pending.clear();\n }\n}\n"],"mappings":"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","names":[]}
|