wimotek-chromium-sync 1.0.8 → 1.0.9
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 +110 -92
- package/chromium_sync/__init__.py +1 -1
- package/chromium_sync/cli.py +13 -5
- package/chromium_sync/core.py +269 -25
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,114 +4,158 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/wimotek-chromium-sync)
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
|
|
7
|
-
专为 Chromium / Chrome
|
|
7
|
+
专为 Chromium / Chrome 设计的通用会话与 Cookie 集群同步调度工具。支持 1对1 与 1对多集群、本地直连免密、SSH 私钥内容/文件与密码认证、Cookie 有效期智能融合(Merge)以及轻量静态探针。
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
-
## 🌟
|
|
11
|
+
## 🌟 核心工程特性
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
*
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
*
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
13
|
+
1. **统一现代节点模型 (1对1 / 1对多集群)**:
|
|
14
|
+
* 原生支持任意规模的节点集群管理与多隧道并发映射。
|
|
15
|
+
* **本地节点免密直连**:配置本地端口即可直接直连 CDP 探针,跳过 SSH 隧道拉起。
|
|
16
|
+
|
|
17
|
+
2. **原生私钥内容 (PEM String) / 路径与密码认证**:
|
|
18
|
+
* `ssh_key`: 原生支持**私钥文本内容字符串**(如以 `-----BEGIN` 开头的 PEM 文本)或**私钥文件绝对路径**(如 `~/.ssh/id_rsa`),调用方无需配置繁琐的环境变量中转。
|
|
19
|
+
* `ssh_password`: 直接支持 SSH 密码认证,支持集群级默认凭证与单节点重载。
|
|
20
|
+
|
|
21
|
+
3. **结构化执行指标返回 (Structured Return Dict)**:
|
|
22
|
+
* `syncer.run()` 与 `await syncer.run_async()` 执行后返回标准指标字典,上层 Web 控制台可直接捕获迁移量、源端/目标端存量、耗时及明细,无需额外发送 CDP 探针度量。
|
|
23
|
+
|
|
24
|
+
4. **双驱动入口:原生异步协程 + 同步阻塞**:
|
|
25
|
+
* 原生提供 `await syncer.run_async()` 与 `await syncer.inspect_async()`,适配 FastAPI、Tornado、aiohttp 等现代异步服务。
|
|
26
|
+
* 同步环境直接调用 `syncer.run()`,开箱即用。
|
|
27
|
+
|
|
28
|
+
5. **暴露轻量静态探针工具方法 (无需实例化集群)**:
|
|
29
|
+
* 提供单点工具函数:`ChromiumSync.get_cookies(port=9222)`、`ChromiumSync.set_cookies(cookies, port=9222)`、`ChromiumSync.probe(port=9222)`,单点探测仅需 1 行代码。
|
|
30
|
+
|
|
31
|
+
6. **全节点智能 Merge 算法**:
|
|
32
|
+
* 汇总所有节点 Cookie,针对冲突项**优先保留有效期最新(expires 更长 / 最新持久凭证)的 Cookie**,并写回热刷新。
|
|
24
33
|
|
|
25
34
|
---
|
|
26
35
|
|
|
27
36
|
## 📦 安装
|
|
28
37
|
|
|
29
|
-
### Python
|
|
38
|
+
### Python (PyPI)
|
|
30
39
|
```bash
|
|
31
40
|
pip install wimotek-chromium-sync
|
|
32
41
|
playwright install chromium
|
|
33
42
|
```
|
|
34
43
|
|
|
35
|
-
### Node.js
|
|
44
|
+
### Node.js (npm)
|
|
36
45
|
```bash
|
|
37
46
|
npm install -g wimotek-chromium-sync
|
|
38
47
|
```
|
|
39
48
|
|
|
40
49
|
---
|
|
41
50
|
|
|
42
|
-
## 🚀
|
|
43
|
-
|
|
44
|
-
### 1. Python 代码调用
|
|
51
|
+
## 🚀 极简开发示例
|
|
45
52
|
|
|
46
|
-
|
|
53
|
+
### 1. 轻量静态探针工具(无需实例化集群)
|
|
54
|
+
单点探测某个节点时,直接 1 行静态调用:
|
|
47
55
|
```python
|
|
48
56
|
from chromium_sync import ChromiumSync
|
|
49
57
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
58
|
+
# 1. 单点探测节点在线状态与页面信息
|
|
59
|
+
info = ChromiumSync.probe(port=9222)
|
|
60
|
+
print(info)
|
|
61
|
+
# {'online': True, 'cookie_count': 42, 'page_title': 'GitHub', 'page_url': 'https://github.com', ...}
|
|
54
62
|
|
|
55
|
-
#
|
|
56
|
-
|
|
63
|
+
# 2. 单点直接提取 Cookie 列表
|
|
64
|
+
cookies = ChromiumSync.get_cookies(port=9222)
|
|
57
65
|
|
|
58
|
-
#
|
|
59
|
-
|
|
60
|
-
local_cookies = syncer.get_cookies("local") # 获取单个节点的 Cookie 列表
|
|
61
|
-
print(f"本地有效 Cookie 数: {len(local_cookies)}")
|
|
66
|
+
# 3. 单点直接批量写入 Cookie 并热刷新页面
|
|
67
|
+
ChromiumSync.set_cookies(cookies, port=9223, reload=True)
|
|
62
68
|
```
|
|
63
69
|
|
|
64
|
-
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
### 2. 现代多节点集群同步与结构化返回
|
|
73
|
+
|
|
74
|
+
#### (1) Python 同步调用 (同步脚本 / Celery 任务)
|
|
65
75
|
```python
|
|
66
76
|
from chromium_sync import ChromiumSync
|
|
67
77
|
|
|
68
78
|
syncer = ChromiumSync(
|
|
69
79
|
nodes=[
|
|
70
|
-
{"id": "local", "cdp_port": 9223},
|
|
71
|
-
{
|
|
72
|
-
|
|
80
|
+
{"id": "local", "name": "本地容器", "cdp_port": 9223},
|
|
81
|
+
{
|
|
82
|
+
"id": "kr2",
|
|
83
|
+
"name": "韩国生产服",
|
|
84
|
+
"ssh_host": "kr2.wimotek.com",
|
|
85
|
+
"ssh_user": "root",
|
|
86
|
+
# 直接支持私钥文本内容 (PEM String) 或文件路径
|
|
87
|
+
"ssh_key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIB...\n-----END RSA PRIVATE KEY-----",
|
|
88
|
+
"cdp_port": 9222
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"id": "sg1",
|
|
92
|
+
"name": "新加坡服",
|
|
93
|
+
"ssh_host": "sg1.wimotek.com",
|
|
94
|
+
"ssh_user": "ubuntu",
|
|
95
|
+
# 直接支持密码认证
|
|
96
|
+
"ssh_password": "YourStrongPassword123",
|
|
97
|
+
"cdp_port": 9222
|
|
98
|
+
}
|
|
73
99
|
],
|
|
74
|
-
mode="merge"
|
|
100
|
+
mode="merge" # "merge" (合并最新凭据) 或 "sync" (单源广播)
|
|
75
101
|
)
|
|
76
|
-
|
|
102
|
+
|
|
103
|
+
# 执行同步并获取度量指标字典
|
|
104
|
+
result = syncer.run()
|
|
105
|
+
print(result)
|
|
106
|
+
```
|
|
107
|
+
**结构化返回值示例**:
|
|
108
|
+
```python
|
|
109
|
+
{
|
|
110
|
+
"status": "success", # "success" | "failed"
|
|
111
|
+
"mode": "merge", # 执行模式
|
|
112
|
+
"transferred_count": 45, # 实际迁移/融合生成的 Cookie 数量
|
|
113
|
+
"source_total": 80, # 源端/融合前凭证总数
|
|
114
|
+
"target_total": 45, # 目标端/融合后凭证总数
|
|
115
|
+
"nodes_synced": ["local", "kr2", "sg1"], # 成功同步的节点列表
|
|
116
|
+
"duration": 1.25, # 耗时(秒)
|
|
117
|
+
"details": "全节点 Merge 成功: 融合 45 条最新凭证至 ['本地容器', '韩国生产服', '新加坡服']",
|
|
118
|
+
"error": None
|
|
119
|
+
}
|
|
77
120
|
```
|
|
78
121
|
|
|
79
|
-
#### (
|
|
122
|
+
#### (2) FastAPI / aiohttp 异步协程原生驱动
|
|
80
123
|
```python
|
|
124
|
+
from fastapi import FastAPI
|
|
81
125
|
from chromium_sync import ChromiumSync
|
|
82
126
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
127
|
+
app = FastAPI()
|
|
128
|
+
|
|
129
|
+
@app.post("/api/sync")
|
|
130
|
+
async def trigger_sync():
|
|
131
|
+
syncer = ChromiumSync(
|
|
132
|
+
nodes=[
|
|
133
|
+
{"id": "local", "cdp_port": 9222},
|
|
134
|
+
{"id": "cloud", "ssh_host": "remote.host.com", "ssh_key": "~/.ssh/id_rsa", "cdp_port": 9222}
|
|
135
|
+
],
|
|
136
|
+
mode="merge"
|
|
137
|
+
)
|
|
138
|
+
# 原生 asyncio 协程,零线程切换开销,高并发性能极佳
|
|
139
|
+
result = await syncer.run_async()
|
|
140
|
+
return result
|
|
93
141
|
```
|
|
94
142
|
|
|
95
143
|
---
|
|
96
144
|
|
|
97
|
-
###
|
|
145
|
+
### 3. 命令行 CLI 执行
|
|
98
146
|
|
|
99
|
-
#### (1)
|
|
147
|
+
#### (1) 内联语法直接启动多节点 Merge
|
|
100
148
|
```bash
|
|
101
|
-
# 智能合并多节点最新 Cookie
|
|
102
149
|
chromium-sync \
|
|
103
150
|
--node id=local,port=9223 \
|
|
104
151
|
--node id=kr2,ssh=root@kr2.wimotek.com,port=9222,key=~/.ssh/id_rsa \
|
|
105
|
-
--node id=sg1,ssh=ubuntu@sg1.wimotek.com,port=9222,pass=
|
|
152
|
+
--node id=sg1,ssh=ubuntu@sg1.wimotek.com,port=9222,pass=SecretPass123 \
|
|
106
153
|
--mode merge
|
|
107
154
|
```
|
|
108
155
|
|
|
109
|
-
#### (2)
|
|
156
|
+
#### (2) 仅查看节点状态与 Cookie 数量 (ASCII 表格)
|
|
110
157
|
```bash
|
|
111
|
-
chromium-sync
|
|
112
|
-
--node id=local,port=9223 \
|
|
113
|
-
--node id=kr2,ssh=root@kr2.wimotek.com,port=9222,key=~/.ssh/id_rsa \
|
|
114
|
-
--status
|
|
158
|
+
chromium-sync --config cluster.json --status
|
|
115
159
|
```
|
|
116
160
|
输出样例:
|
|
117
161
|
```text
|
|
@@ -123,58 +167,32 @@ kr2 | 韩国节点 | SSH远程 | 19222 | 38 条 | [G
|
|
|
123
167
|
==========================================================================================
|
|
124
168
|
```
|
|
125
169
|
|
|
126
|
-
#### (3) 导出 Cookie
|
|
127
|
-
```bash
|
|
128
|
-
chromium-sync --config cluster.json --status --json > /tmp/cluster_cookies.json
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
#### (4) 使用 JSON 配置文件运行集群同步
|
|
132
|
-
准备配置文件 `cluster.json`:
|
|
133
|
-
```json
|
|
134
|
-
[
|
|
135
|
-
{ "id": "local", "name": "本地开发机", "cdp_port": 9222 },
|
|
136
|
-
{ "id": "sg1", "name": "新加坡服", "ssh_host": "sg.host.com", "ssh_user": "ubuntu", "ssh_key_path": "~/.ssh/id_rsa", "cdp_port": 9222 },
|
|
137
|
-
{ "id": "kr2", "name": "韩国服", "ssh_host": "kr.host.com", "ssh_user": "root", "ssh_password": "SafePassword123", "cdp_port": 9222 }
|
|
138
|
-
]
|
|
139
|
-
```
|
|
140
|
-
单源广播或合并:
|
|
170
|
+
#### (3) 导出 Cookie 详情或执行结果为 JSON
|
|
141
171
|
```bash
|
|
142
|
-
#
|
|
143
|
-
chromium-sync --config cluster.json --
|
|
172
|
+
# 1. 导出所有节点及 Cookie 列表为 JSON
|
|
173
|
+
chromium-sync --config cluster.json --status --json > /tmp/cluster_data.json
|
|
144
174
|
|
|
145
|
-
#
|
|
146
|
-
chromium-sync --config cluster.json --mode
|
|
175
|
+
# 2. 执行同步并将结构化指标以 JSON 打印 (方便 CI/CD 或流水线捕获)
|
|
176
|
+
chromium-sync --config cluster.json --mode merge --json
|
|
147
177
|
```
|
|
148
178
|
|
|
149
179
|
---
|
|
150
180
|
|
|
151
|
-
## ⚙️
|
|
181
|
+
## ⚙️ 节点参数配置规范 (NodeConfig)
|
|
152
182
|
|
|
153
183
|
| 字段 | 类型 | 默认值 | 详细说明 |
|
|
154
184
|
| :--- | :--- | :--- | :--- |
|
|
155
|
-
| `id` | str | 必填 | 节点唯一标识符 (例如 `local`, `
|
|
185
|
+
| `id` | str | 必填 | 节点唯一标识符 (例如 `local`, `kr2`) |
|
|
156
186
|
| `name` | str | `id` | 节点显示名称 |
|
|
157
|
-
| `is_local` | bool | 自动识别 | 是否为本地节点 (不填 `ssh_host`
|
|
187
|
+
| `is_local` | bool | 自动识别 | 是否为本地节点 (不填 `ssh_host` 或填回环地址自动判定为本地,免密直连) |
|
|
158
188
|
| `cdp_host` | str | `127.0.0.1` | 节点 CDP 服务监听的主机地址 |
|
|
159
189
|
| `cdp_port` | int | `9222` | 节点暴露的 CDP 端口 |
|
|
160
190
|
| `ssh_host` | str | `None` | 远程服务器 IP 或主机名 (提供该字段时识别为远程 SSH 节点) |
|
|
161
191
|
| `ssh_port` | int | `22` | 远程服务器 SSH 端口 |
|
|
162
192
|
| `ssh_user` | str | `root` | 远程 SSH 用户名 |
|
|
163
|
-
| `
|
|
164
|
-
| `
|
|
165
|
-
| `
|
|
166
|
-
| `tunnel_port` | int | 自动分配 | 本地用于映射隧道的端口 (集群环境下自动递增分配防冲突) |
|
|
167
|
-
|
|
168
|
-
---
|
|
169
|
-
|
|
170
|
-
## 🤖 定时任务配置示例 (Crontab)
|
|
171
|
-
|
|
172
|
-
配合 Linux Crontab,每天凌晨定时执行全节点凭证智能融合:
|
|
173
|
-
```bash
|
|
174
|
-
crontab -e
|
|
175
|
-
# 每天凌晨 3:00 自动融合多节点最新有效凭证并释放隧道
|
|
176
|
-
0 3 * * * /usr/local/bin/chromium-sync --config /etc/chromium/cluster.json --mode merge --auto-close >> /var/log/chromium_sync.log 2>&1
|
|
177
|
-
```
|
|
193
|
+
| `ssh_key` | str | `None` | **SSH 私钥 (同时支持文件绝对路径 或 私钥 PEM 文本字符串)** |
|
|
194
|
+
| `ssh_password` | str | `None` | SSH 登录密码 (自动适配 `sshpass` 与原生 `SSH_ASKPASS`) |
|
|
195
|
+
| `tunnel_port` | int | 自动分配 | 本地用于映射隧道的端口 (多节点自动分配防冲突) |
|
|
178
196
|
|
|
179
197
|
---
|
|
180
198
|
|
package/chromium_sync/cli.py
CHANGED
|
@@ -53,7 +53,7 @@ def parse_node_str(val: str) -> dict:
|
|
|
53
53
|
elif k in ("user", "ssh_user"):
|
|
54
54
|
res["ssh_user"] = v
|
|
55
55
|
elif k in ("key", "ssh_key", "ssh_key_path"):
|
|
56
|
-
res["
|
|
56
|
+
res["ssh_key"] = v
|
|
57
57
|
elif k in ("pass", "password", "ssh_pass", "ssh_password"):
|
|
58
58
|
res["ssh_password"] = v
|
|
59
59
|
elif k in ("id", "name"):
|
|
@@ -81,8 +81,8 @@ def main():
|
|
|
81
81
|
# 3. 全节点 Cookie 智能 Merge (合并所有节点并保留最新有效期)
|
|
82
82
|
chromium-sync --config cluster.json --mode merge
|
|
83
83
|
|
|
84
|
-
# 4.
|
|
85
|
-
chromium-sync --config cluster.json --mode sync --source local
|
|
84
|
+
# 4. 指定单源节点广播同步并输出结构化指标 JSON
|
|
85
|
+
chromium-sync --config cluster.json --mode sync --source local --json
|
|
86
86
|
|
|
87
87
|
# 5. 极简命令行多节点直接同步
|
|
88
88
|
chromium-sync \\
|
|
@@ -97,6 +97,10 @@ def main():
|
|
|
97
97
|
parser.add_argument("--node", action="append", default=[], help="定义节点 (可多次传入,如 id=local,port=9222)")
|
|
98
98
|
parser.add_argument("--nodes", help="JSON 数组格式的节点配置字符串")
|
|
99
99
|
|
|
100
|
+
# 全局默认 SSH 凭据
|
|
101
|
+
parser.add_argument("--ssh-key", help="集群默认 SSH 私钥 (支持私钥文件绝对路径 或 私钥 PEM 文本内容)")
|
|
102
|
+
parser.add_argument("--ssh-password", help="集群默认 SSH 连接密码")
|
|
103
|
+
|
|
100
104
|
# 同步模式
|
|
101
105
|
parser.add_argument("--mode", choices=["sync", "merge"], default="sync",
|
|
102
106
|
help="同步方式: 'merge'(合并所有节点最新Cookie), 'sync'(单源广播同步,默认)")
|
|
@@ -105,7 +109,7 @@ def main():
|
|
|
105
109
|
|
|
106
110
|
# 探测与数据查询
|
|
107
111
|
parser.add_argument("--status", action="store_true", help="仅探测并输出各节点的 Cookie 数与详情表格,不执行写入")
|
|
108
|
-
parser.add_argument("--json", action="store_true", help="以 JSON
|
|
112
|
+
parser.add_argument("--json", action="store_true", help="以 JSON 格式输出节点数据或执行结果指标")
|
|
109
113
|
|
|
110
114
|
# 隧道与连接
|
|
111
115
|
parser.add_argument("--auto-close", action="store_true", help="任务完成后关闭拉起的 SSH 隧道")
|
|
@@ -139,6 +143,8 @@ def main():
|
|
|
139
143
|
mode=args.mode,
|
|
140
144
|
source=args.source,
|
|
141
145
|
targets=args.targets,
|
|
146
|
+
ssh_key=args.ssh_key,
|
|
147
|
+
ssh_password=args.ssh_password,
|
|
142
148
|
auto_close_tunnel=args.auto_close,
|
|
143
149
|
verbose=args.verbose
|
|
144
150
|
)
|
|
@@ -151,7 +157,9 @@ def main():
|
|
|
151
157
|
print(syncer.get_summary_table(report))
|
|
152
158
|
return
|
|
153
159
|
|
|
154
|
-
syncer.run()
|
|
160
|
+
result = syncer.run()
|
|
161
|
+
if args.json:
|
|
162
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
155
163
|
|
|
156
164
|
|
|
157
165
|
if __name__ == "__main__":
|
package/chromium_sync/core.py
CHANGED
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
Chromium Cluster Session & Cookie Synchronizer
|
|
5
5
|
现代多节点 Chromium 会话同步调度器
|
|
6
6
|
- 支持 1对1 与 1对多集群拓扑
|
|
7
|
-
- 本地节点直接直连免密免隧道,远程节点支持 SSH
|
|
7
|
+
- 本地节点直接直连免密免隧道,远程节点支持 SSH 密钥内容(PEM)/路径与密码
|
|
8
8
|
- 同步策略: merge (全节点 Cookie/Storage 智能融合并保留最新有效期) 与 sync (单源节点向多目标广播)
|
|
9
|
-
-
|
|
9
|
+
- 零外部数据库依赖:内置轻量静态探针工具与 1 行代码快速读写 Cookie
|
|
10
|
+
- 结构化执行结果返回 (包含 status, transferred_count, duration, details 等指标)
|
|
11
|
+
- 原生支持 async/await 异步协程 (run_async) 与同步阻塞 (run)
|
|
10
12
|
"""
|
|
11
13
|
|
|
12
14
|
import asyncio
|
|
@@ -33,7 +35,10 @@ class NodeConfig:
|
|
|
33
35
|
"""
|
|
34
36
|
统一节点配置
|
|
35
37
|
- 本地节点:只需提供 cdp_port (无需提供 ssh_host,自动识别为本地免密直连)
|
|
36
|
-
- 远程节点:提供 ssh_host, cdp_port
|
|
38
|
+
- 远程节点:提供 ssh_host, cdp_port
|
|
39
|
+
- 认证方式:
|
|
40
|
+
* ssh_key: 原生支持私钥文件绝对路径 或 私钥 PEM 文本内容字符串
|
|
41
|
+
* ssh_password: SSH 密码认证
|
|
37
42
|
"""
|
|
38
43
|
id: str
|
|
39
44
|
name: str = ""
|
|
@@ -43,6 +48,7 @@ class NodeConfig:
|
|
|
43
48
|
ssh_host: Optional[str] = None
|
|
44
49
|
ssh_port: int = 22
|
|
45
50
|
ssh_user: str = "root"
|
|
51
|
+
ssh_key: Optional[str] = None
|
|
46
52
|
ssh_key_path: Optional[str] = None
|
|
47
53
|
ssh_key_data: Optional[str] = None
|
|
48
54
|
ssh_password: Optional[str] = None
|
|
@@ -60,6 +66,21 @@ class NodeConfig:
|
|
|
60
66
|
if self.is_local:
|
|
61
67
|
self.tunnel_port = self.cdp_port
|
|
62
68
|
|
|
69
|
+
# 智能解析 ssh_key:支持文件路径或 PEM 字符串内容
|
|
70
|
+
key_input = self.ssh_key or self.ssh_key_path or self.ssh_key_data
|
|
71
|
+
if key_input:
|
|
72
|
+
key_str = key_input.strip()
|
|
73
|
+
# 判定为私钥文本内容 (包含 BEGIN 标头或多行密钥)
|
|
74
|
+
if "-----BEGIN" in key_str or "\n" in key_str:
|
|
75
|
+
self.ssh_key_data = key_str
|
|
76
|
+
else:
|
|
77
|
+
expanded = os.path.expanduser(key_str)
|
|
78
|
+
if os.path.exists(expanded):
|
|
79
|
+
self.ssh_key_path = expanded
|
|
80
|
+
else:
|
|
81
|
+
# 如果传入的是不存在的单行字符,兼容作为潜在的 key_path 或 key_data
|
|
82
|
+
self.ssh_key_path = expanded
|
|
83
|
+
|
|
63
84
|
@property
|
|
64
85
|
def cdp_url(self) -> str:
|
|
65
86
|
if self.is_local:
|
|
@@ -114,7 +135,8 @@ def merge_cookies(cookies_lists: List[List[dict]]) -> List[dict]:
|
|
|
114
135
|
class SshTunnelSession:
|
|
115
136
|
"""
|
|
116
137
|
SSH 隧道生命周期管理
|
|
117
|
-
-
|
|
138
|
+
- 支持密钥文本 (PEM String) 自动写入安全受限临时文件
|
|
139
|
+
- 支持密钥路径与密码认证
|
|
118
140
|
- 支持端口复用检测与防僵尸连接自愈
|
|
119
141
|
"""
|
|
120
142
|
def __init__(self, node: NodeConfig):
|
|
@@ -170,7 +192,7 @@ class SshTunnelSession:
|
|
|
170
192
|
|
|
171
193
|
env = os.environ.copy()
|
|
172
194
|
|
|
173
|
-
#
|
|
195
|
+
# 1. 私钥内容 (PEM String) 处理
|
|
174
196
|
if self.node.ssh_key_data:
|
|
175
197
|
fd, tmp_path = tempfile.mkstemp(prefix="ssh_key_")
|
|
176
198
|
with open(tmp_path, "w") as f:
|
|
@@ -184,7 +206,7 @@ class SshTunnelSession:
|
|
|
184
206
|
if os.path.exists(expanded_key):
|
|
185
207
|
ssh_cmd.extend(["-i", expanded_key])
|
|
186
208
|
|
|
187
|
-
#
|
|
209
|
+
# 2. 密码认证处理
|
|
188
210
|
if self.node.ssh_password:
|
|
189
211
|
if shutil.which("sshpass"):
|
|
190
212
|
ssh_cmd = ["sshpass", "-p", self.node.ssh_password] + ssh_cmd
|
|
@@ -259,6 +281,8 @@ class ChromiumSync:
|
|
|
259
281
|
mode: str = "sync", # "sync" (单源广播) 或 "merge" (全节点合并最新)
|
|
260
282
|
source: Optional[str] = None, # sync 模式下的源节点 ID (默认第一个节点)
|
|
261
283
|
targets: Optional[List[str]] = None, # sync 模式下的目标节点 ID 列表 (默认其余全部在线节点)
|
|
284
|
+
ssh_key: Optional[str] = None, # 集群通用私钥 (路径 或 PEM String)
|
|
285
|
+
ssh_password: Optional[str] = None, # 集群通用 SSH 密码
|
|
262
286
|
auto_close_tunnel: bool = False,
|
|
263
287
|
verbose: bool = True
|
|
264
288
|
):
|
|
@@ -283,7 +307,14 @@ class ChromiumSync:
|
|
|
283
307
|
|
|
284
308
|
for item in nodes:
|
|
285
309
|
cfg = item if isinstance(item, NodeConfig) else NodeConfig(**item)
|
|
310
|
+
# 若节点未单独配置私钥或密码,继承集群通用设置
|
|
286
311
|
if not cfg.is_local:
|
|
312
|
+
if not cfg.ssh_key and not cfg.ssh_key_path and not cfg.ssh_key_data and ssh_key:
|
|
313
|
+
cfg.ssh_key = ssh_key
|
|
314
|
+
cfg.__post_init__()
|
|
315
|
+
if not cfg.ssh_password and ssh_password:
|
|
316
|
+
cfg.ssh_password = ssh_password
|
|
317
|
+
|
|
287
318
|
if not cfg.tunnel_port:
|
|
288
319
|
while auto_port in used_tunnel_ports:
|
|
289
320
|
auto_port += 1
|
|
@@ -296,6 +327,123 @@ class ChromiumSync:
|
|
|
296
327
|
|
|
297
328
|
self.tunnels: Dict[str, SshTunnelSession] = {}
|
|
298
329
|
|
|
330
|
+
# =========================================================================
|
|
331
|
+
# 静态探针与工具方法 (轻量级无需实例化 ChromiumSync)
|
|
332
|
+
# =========================================================================
|
|
333
|
+
|
|
334
|
+
@staticmethod
|
|
335
|
+
async def get_cookies_async(port: int = 9222, host: str = "127.0.0.1") -> List[dict]:
|
|
336
|
+
"""[静态异步工具] 直接连通指定 CDP 端口获取 Cookie 列表"""
|
|
337
|
+
try:
|
|
338
|
+
from playwright.async_api import async_playwright
|
|
339
|
+
except ImportError:
|
|
340
|
+
raise RuntimeError("[-] 未安装 playwright 模块,请先运行: pip install playwright && playwright install chromium")
|
|
341
|
+
|
|
342
|
+
cdp_url = f"http://{host}:{port}"
|
|
343
|
+
async with async_playwright() as p:
|
|
344
|
+
browser = await p.chromium.connect_over_cdp(cdp_url)
|
|
345
|
+
ctx = browser.contexts[0] if browser.contexts else await browser.new_context()
|
|
346
|
+
cookies = await ctx.cookies()
|
|
347
|
+
await browser.close()
|
|
348
|
+
return cookies
|
|
349
|
+
|
|
350
|
+
@staticmethod
|
|
351
|
+
def get_cookies_from_host(port: int = 9222, host: str = "127.0.0.1") -> List[dict]:
|
|
352
|
+
"""[静态同步工具] 直接连通指定 CDP 端口获取 Cookie 列表"""
|
|
353
|
+
return asyncio.run(ChromiumSync.get_cookies_async(port=port, host=host))
|
|
354
|
+
|
|
355
|
+
@staticmethod
|
|
356
|
+
async def set_cookies_async(
|
|
357
|
+
cookies: List[dict],
|
|
358
|
+
port: int = 9222,
|
|
359
|
+
host: str = "127.0.0.1",
|
|
360
|
+
reload: bool = True
|
|
361
|
+
) -> bool:
|
|
362
|
+
"""[静态异步工具] 向指定 CDP 端口写入 Cookie 并可选择热重载"""
|
|
363
|
+
try:
|
|
364
|
+
from playwright.async_api import async_playwright
|
|
365
|
+
except ImportError:
|
|
366
|
+
raise RuntimeError("[-] 未安装 playwright 模块,请先运行: pip install playwright && playwright install chromium")
|
|
367
|
+
|
|
368
|
+
cdp_url = f"http://{host}:{port}"
|
|
369
|
+
async with async_playwright() as p:
|
|
370
|
+
browser = await p.chromium.connect_over_cdp(cdp_url)
|
|
371
|
+
ctx = browser.contexts[0] if browser.contexts else await browser.new_context()
|
|
372
|
+
if cookies:
|
|
373
|
+
await ctx.add_cookies(cookies)
|
|
374
|
+
if reload and ctx.pages:
|
|
375
|
+
try:
|
|
376
|
+
await ctx.pages[0].reload()
|
|
377
|
+
except Exception:
|
|
378
|
+
pass
|
|
379
|
+
await browser.close()
|
|
380
|
+
return True
|
|
381
|
+
|
|
382
|
+
@staticmethod
|
|
383
|
+
def set_cookies(
|
|
384
|
+
cookies: List[dict],
|
|
385
|
+
port: int = 9222,
|
|
386
|
+
host: str = "127.0.0.1",
|
|
387
|
+
reload: bool = True
|
|
388
|
+
) -> bool:
|
|
389
|
+
"""[静态同步工具] 向指定 CDP 端口写入 Cookie 并热重载"""
|
|
390
|
+
return asyncio.run(ChromiumSync.set_cookies_async(cookies, port=port, host=host, reload=reload))
|
|
391
|
+
|
|
392
|
+
@staticmethod
|
|
393
|
+
async def probe_async(port: int = 9222, host: str = "127.0.0.1") -> dict:
|
|
394
|
+
"""[静态异步工具] 单点探测 CDP 节点状态 (包含在线状态、Cookie 数与页面标题)"""
|
|
395
|
+
try:
|
|
396
|
+
from playwright.async_api import async_playwright
|
|
397
|
+
except ImportError:
|
|
398
|
+
raise RuntimeError("[-] 未安装 playwright 模块,请先运行: pip install playwright && playwright install chromium")
|
|
399
|
+
|
|
400
|
+
cdp_url = f"http://{host}:{port}"
|
|
401
|
+
try:
|
|
402
|
+
async with async_playwright() as p:
|
|
403
|
+
browser = await p.chromium.connect_over_cdp(cdp_url)
|
|
404
|
+
ctx = browser.contexts[0] if browser.contexts else await browser.new_context()
|
|
405
|
+
cookies = await ctx.cookies()
|
|
406
|
+
page_title = ""
|
|
407
|
+
page_url = ""
|
|
408
|
+
if ctx.pages:
|
|
409
|
+
p_obj = ctx.pages[0]
|
|
410
|
+
page_url = p_obj.url
|
|
411
|
+
try:
|
|
412
|
+
page_title = await p_obj.title()
|
|
413
|
+
except Exception:
|
|
414
|
+
pass
|
|
415
|
+
await browser.close()
|
|
416
|
+
return {
|
|
417
|
+
"online": True,
|
|
418
|
+
"host": host,
|
|
419
|
+
"port": port,
|
|
420
|
+
"cdp_url": cdp_url,
|
|
421
|
+
"cookie_count": len(cookies),
|
|
422
|
+
"page_title": page_title,
|
|
423
|
+
"page_url": page_url,
|
|
424
|
+
"error": None
|
|
425
|
+
}
|
|
426
|
+
except Exception as e:
|
|
427
|
+
return {
|
|
428
|
+
"online": False,
|
|
429
|
+
"host": host,
|
|
430
|
+
"port": port,
|
|
431
|
+
"cdp_url": cdp_url,
|
|
432
|
+
"cookie_count": 0,
|
|
433
|
+
"page_title": "",
|
|
434
|
+
"page_url": "",
|
|
435
|
+
"error": str(e)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
@staticmethod
|
|
439
|
+
def probe(port: int = 9222, host: str = "127.0.0.1") -> dict:
|
|
440
|
+
"""[静态同步工具] 单点探测 CDP 节点状态"""
|
|
441
|
+
return asyncio.run(ChromiumSync.probe_async(port=port, host=host))
|
|
442
|
+
|
|
443
|
+
# =========================================================================
|
|
444
|
+
# 集群管理与同步
|
|
445
|
+
# =========================================================================
|
|
446
|
+
|
|
299
447
|
def _start_all_tunnels(self):
|
|
300
448
|
for node in self.nodes:
|
|
301
449
|
if not node.is_local:
|
|
@@ -386,17 +534,27 @@ class ChromiumSync:
|
|
|
386
534
|
"""同步获取所有节点完整状态与 Cookie 数据 (供代码直接调用)"""
|
|
387
535
|
return asyncio.run(self.inspect_async())
|
|
388
536
|
|
|
389
|
-
def
|
|
390
|
-
"""
|
|
391
|
-
极简 Cookie 查询接口:
|
|
392
|
-
- 传入 node_id: 返回指定节点的 List[dict] Cookie 列表
|
|
393
|
-
- 不传 node_id: 返回全节点字典 { node_id: List[dict] }
|
|
394
|
-
"""
|
|
537
|
+
def _get_instance_cookies(self, node_id: Optional[str] = None) -> Union[List[dict], Dict[str, List[dict]]]:
|
|
395
538
|
report = self.inspect()
|
|
396
539
|
if node_id:
|
|
397
540
|
return report.get(node_id, {}).get("cookies", [])
|
|
398
541
|
return {nid: data.get("cookies", []) for nid, data in report.items()}
|
|
399
542
|
|
|
543
|
+
def get_cookies(self, *args, **kwargs) -> Any:
|
|
544
|
+
"""
|
|
545
|
+
双模式 Cookie 获取接口:
|
|
546
|
+
1. 实例调用: syncer.get_cookies(node_id="local") -> 返回集群节点 Cookie
|
|
547
|
+
2. 静态/类调用: ChromiumSync.get_cookies(port=9222, host="127.0.0.1") -> 单点直接获取
|
|
548
|
+
"""
|
|
549
|
+
if isinstance(self, ChromiumSync):
|
|
550
|
+
node_id = kwargs.get("node_id") if "node_id" in kwargs else (args[0] if args else None)
|
|
551
|
+
return self._get_instance_cookies(node_id)
|
|
552
|
+
else:
|
|
553
|
+
# 作为类静态方法调用时
|
|
554
|
+
port = args[0] if args else kwargs.get("port", 9222)
|
|
555
|
+
host = args[1] if len(args) > 1 else kwargs.get("host", "127.0.0.1")
|
|
556
|
+
return ChromiumSync.get_cookies_from_host(port=port, host=host)
|
|
557
|
+
|
|
400
558
|
def get_summary_table(self, report: Optional[Dict[str, dict]] = None) -> str:
|
|
401
559
|
"""生成格式化节点状态 ASCII 摘要表"""
|
|
402
560
|
if report is None:
|
|
@@ -458,22 +616,39 @@ class ChromiumSync:
|
|
|
458
616
|
except Exception:
|
|
459
617
|
pass
|
|
460
618
|
|
|
461
|
-
async def
|
|
619
|
+
async def run_async(
|
|
462
620
|
self,
|
|
463
621
|
mode: Optional[str] = None,
|
|
464
622
|
source: Optional[str] = None,
|
|
465
623
|
targets: Optional[List[str]] = None
|
|
466
|
-
):
|
|
624
|
+
) -> Dict[str, Any]:
|
|
625
|
+
"""
|
|
626
|
+
[原生异步执行入口]
|
|
627
|
+
返回结构化执行结果字典 (包含 status, transferred_count, duration, details 等指标),
|
|
628
|
+
上层 Web 控制台可直接捕获使用。
|
|
629
|
+
"""
|
|
630
|
+
start_time = time.time()
|
|
467
631
|
active_mode = (mode or self.mode).lower()
|
|
468
632
|
active_source = source or self.source
|
|
469
633
|
active_targets = targets or self.targets
|
|
470
634
|
|
|
471
|
-
self._start_all_tunnels()
|
|
472
635
|
try:
|
|
473
636
|
from playwright.async_api import async_playwright
|
|
474
637
|
except ImportError:
|
|
475
|
-
|
|
638
|
+
elapsed = round(time.time() - start_time, 2)
|
|
639
|
+
return {
|
|
640
|
+
"status": "failed",
|
|
641
|
+
"mode": active_mode,
|
|
642
|
+
"transferred_count": 0,
|
|
643
|
+
"source_total": 0,
|
|
644
|
+
"target_total": 0,
|
|
645
|
+
"nodes_synced": [],
|
|
646
|
+
"duration": elapsed,
|
|
647
|
+
"details": "未安装 playwright 模块,请先运行: pip install playwright && playwright install chromium",
|
|
648
|
+
"error": "playwright not installed"
|
|
649
|
+
}
|
|
476
650
|
|
|
651
|
+
self._start_all_tunnels()
|
|
477
652
|
try:
|
|
478
653
|
async with async_playwright() as p:
|
|
479
654
|
browsers: Dict[str, Any] = {}
|
|
@@ -492,14 +667,25 @@ class ChromiumSync:
|
|
|
492
667
|
logging.error(f"[-] 连接节点 [{node.name}] 失败 ({cdp_url}): {e}")
|
|
493
668
|
|
|
494
669
|
if not contexts:
|
|
495
|
-
|
|
496
|
-
return
|
|
670
|
+
elapsed = round(time.time() - start_time, 2)
|
|
671
|
+
return {
|
|
672
|
+
"status": "failed",
|
|
673
|
+
"mode": active_mode,
|
|
674
|
+
"transferred_count": 0,
|
|
675
|
+
"source_total": 0,
|
|
676
|
+
"target_total": 0,
|
|
677
|
+
"nodes_synced": [],
|
|
678
|
+
"duration": elapsed,
|
|
679
|
+
"details": "无可用在线节点,同步终止",
|
|
680
|
+
"error": "No available online nodes"
|
|
681
|
+
}
|
|
497
682
|
|
|
498
683
|
print(self.get_summary_table(nodes_data))
|
|
499
684
|
|
|
500
685
|
if active_mode == "merge":
|
|
501
686
|
logging.info("[*] 正在执行全节点 Cookie 智能 Merge (合并最新有效凭据)...")
|
|
502
687
|
all_cookies = [data["cookies"] for data in nodes_data.values() if data.get("cookies")]
|
|
688
|
+
source_total = sum(len(c) for c in all_cookies)
|
|
503
689
|
merged = merge_cookies(all_cookies)
|
|
504
690
|
logging.info(f" -> 汇总融合完成: 共生成 {len(merged)} 条最新有效 Cookie")
|
|
505
691
|
|
|
@@ -527,6 +713,22 @@ class ChromiumSync:
|
|
|
527
713
|
combined_storage
|
|
528
714
|
)
|
|
529
715
|
|
|
716
|
+
for b in browsers.values():
|
|
717
|
+
await b.close()
|
|
718
|
+
|
|
719
|
+
elapsed = round(time.time() - start_time, 2)
|
|
720
|
+
return {
|
|
721
|
+
"status": "success",
|
|
722
|
+
"mode": "merge",
|
|
723
|
+
"transferred_count": len(merged),
|
|
724
|
+
"source_total": source_total,
|
|
725
|
+
"target_total": len(merged),
|
|
726
|
+
"nodes_synced": [n.id for n in dest_nodes],
|
|
727
|
+
"duration": elapsed,
|
|
728
|
+
"details": f"全节点 Merge 成功: 融合 {len(merged)} 条最新凭证至 {[n.name for n in dest_nodes]}",
|
|
729
|
+
"error": None
|
|
730
|
+
}
|
|
731
|
+
|
|
530
732
|
elif active_mode == "sync":
|
|
531
733
|
if not active_source:
|
|
532
734
|
active_source = self.nodes[0].id
|
|
@@ -553,10 +755,36 @@ class ChromiumSync:
|
|
|
553
755
|
src_storage
|
|
554
756
|
)
|
|
555
757
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
758
|
+
for b in browsers.values():
|
|
759
|
+
await b.close()
|
|
760
|
+
|
|
761
|
+
elapsed = round(time.time() - start_time, 2)
|
|
762
|
+
return {
|
|
763
|
+
"status": "success",
|
|
764
|
+
"mode": "sync",
|
|
765
|
+
"transferred_count": len(src_cookies),
|
|
766
|
+
"source_total": len(src_cookies),
|
|
767
|
+
"target_total": len(src_cookies),
|
|
768
|
+
"nodes_synced": [n.id for n in dest_nodes],
|
|
769
|
+
"duration": elapsed,
|
|
770
|
+
"details": f"单源同步成功: [{src_data['name']}] 推送 {len(src_cookies)} 条凭据至 {[n.name for n in dest_nodes]}",
|
|
771
|
+
"error": None
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
except Exception as e:
|
|
775
|
+
elapsed = round(time.time() - start_time, 2)
|
|
776
|
+
logging.error(f"[-] 同步发生异常: {e}")
|
|
777
|
+
return {
|
|
778
|
+
"status": "failed",
|
|
779
|
+
"mode": active_mode,
|
|
780
|
+
"transferred_count": 0,
|
|
781
|
+
"source_total": 0,
|
|
782
|
+
"target_total": 0,
|
|
783
|
+
"nodes_synced": [],
|
|
784
|
+
"duration": elapsed,
|
|
785
|
+
"details": f"同步发生异常: {e}",
|
|
786
|
+
"error": str(e)
|
|
787
|
+
}
|
|
560
788
|
finally:
|
|
561
789
|
self._stop_all_tunnels()
|
|
562
790
|
|
|
@@ -565,6 +793,22 @@ class ChromiumSync:
|
|
|
565
793
|
mode: Optional[str] = None,
|
|
566
794
|
source: Optional[str] = None,
|
|
567
795
|
targets: Optional[List[str]] = None
|
|
568
|
-
):
|
|
569
|
-
"""
|
|
570
|
-
|
|
796
|
+
) -> Dict[str, Any]:
|
|
797
|
+
"""
|
|
798
|
+
[同步运行入口 (开箱即用)]
|
|
799
|
+
返回执行结果字典,兼容同步后端与脚本自动化。
|
|
800
|
+
"""
|
|
801
|
+
try:
|
|
802
|
+
loop = asyncio.get_running_loop()
|
|
803
|
+
except RuntimeError:
|
|
804
|
+
loop = None
|
|
805
|
+
|
|
806
|
+
if loop and loop.is_running():
|
|
807
|
+
import concurrent.futures
|
|
808
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
|
|
809
|
+
return pool.submit(
|
|
810
|
+
asyncio.run,
|
|
811
|
+
self.run_async(mode=mode, source=source, targets=targets)
|
|
812
|
+
).result()
|
|
813
|
+
else:
|
|
814
|
+
return asyncio.run(self.run_async(mode=mode, source=source, targets=targets))
|