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 CHANGED
@@ -4,114 +4,158 @@
4
4
  [![npm version](https://img.shields.io/npm/v/wimotek-chromium-sync.svg?style=flat-square)](https://www.npmjs.com/package/wimotek-chromium-sync)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg?style=flat-square)](LICENSE)
6
6
 
7
- 专为 Chromium / Chrome 设计的现代化会话与 Cookie 集群同步调度工具。
7
+ 专为 Chromium / Chrome 设计的通用会话与 Cookie 集群同步调度工具。支持 1对1 与 1对多集群、本地直连免密、SSH 私钥内容/文件与密码认证、Cookie 有效期智能融合(Merge)以及轻量静态探针。
8
8
 
9
9
  ---
10
10
 
11
- ## 🌟 核心特性
11
+ ## 🌟 核心工程特性
12
12
 
13
- * **统一节点模型 (1对1 / 1对多集群)**:采用纯粹现代的节点架构,同时支持 1对1 穿透与任意规模的 1对多 节点集群分发。
14
- * **本地免密直连与全能 SSH 认证**:
15
- * **本地节点**:指定本地端口即可直接直连 CDP,免登录、免配置、零 SSH 开销。
16
- * **远程节点**:支持自定义 SSH 用户名 (`ssh_user`)、私钥认证 (`ssh_key_path` / `ssh_key_data`) 与密码认证 (`ssh_password`)。
17
- * **端口自愈防撞**:自动为各远程节点分配互不冲突的本地隧道映射端口,已存在的活跃隧道自动复用。
18
- * **两类高效同步策略**:
19
- * **`merge` (全节点凭证智能融合)**:汇总所有在线节点的 Cookie,若存在同名/同域/同路径冲突,**无条件保留有效期最新(expires 更长)的记录**,同时融合 LocalStorage 状态并向集群节点广播热刷新。
20
- * **`sync` (单源广播同步)**:将指定源节点 (`source`) 的全部会话凭据镜像推送至一个或多个目标节点。
21
- * **极简调用,免建数据库表**:
22
- * 代码层面提供 `get_cookies()`、`inspect()` 与 `get_summary_table()`,**仅需 1 行代码即可获取任何节点或全集群的 Cookie 列表与状态**。
23
- * 命令行提供 `--status` (ASCII 表格) 与 `--json` (标准 JSON 格式),随取随用。
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 环境 (PyPI)
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 环境 (npm)
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
- #### (1) 1 行代码获取节点状态与 Cookie 详情 (无需数据库)
53
+ ### 1. 轻量静态探针工具(无需实例化集群)
54
+ 单点探测某个节点时,直接 1 行静态调用:
47
55
  ```python
48
56
  from chromium_sync import ChromiumSync
49
57
 
50
- syncer = ChromiumSync(nodes=[
51
- {"id": "local", "name": "本机容器", "cdp_port": 9222},
52
- {"id": "kr2", "name": "韩国节点", "ssh_host": "kr2.wimotek.com", "ssh_user": "root", "ssh_key_path": "~/.ssh/id_rsa", "cdp_port": 9222}
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
- print(syncer.get_summary_table())
63
+ # 2. 单点直接提取 Cookie 列表
64
+ cookies = ChromiumSync.get_cookies(port=9222)
57
65
 
58
- # 获取指定节点或全集群的 Cookie 明细列表 (无需创建外部数据库表)
59
- all_cookies = syncer.get_cookies() # {"local": [...], "kr2": [...]}
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
- #### (2) 全节点 Cookie 智能 Merge (合并最新有效期)
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
- {"id": "kr2", "ssh_host": "kr2.wimotek.com", "ssh_user": "root", "ssh_key_path": "~/.ssh/id_rsa", "cdp_port": 9222},
72
- {"id": "sg1", "ssh_host": "sg1.wimotek.com", "ssh_user": "ubuntu", "ssh_password": "YourPassword", "cdp_port": 9222}
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
- syncer.run()
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
- #### (3) 单源广播同步 (例如将本机状态推送到所有云端节点)
122
+ #### (2) FastAPI / aiohttp 异步协程原生驱动
80
123
  ```python
124
+ from fastapi import FastAPI
81
125
  from chromium_sync import ChromiumSync
82
126
 
83
- syncer = ChromiumSync(
84
- nodes=[
85
- {"id": "local", "cdp_port": 9222},
86
- {"id": "node_a", "ssh_host": "10.0.0.1", "cdp_port": 9222},
87
- {"id": "node_b", "ssh_host": "10.0.0.2", "cdp_port": 9222}
88
- ],
89
- mode="sync",
90
- source="local"
91
- )
92
- syncer.run()
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
- ### 2. 命令行 (CLI) 快速执行
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=SecretPassword \
152
+ --node id=sg1,ssh=ubuntu@sg1.wimotek.com,port=9222,pass=SecretPass123 \
106
153
  --mode merge
107
154
  ```
108
155
 
109
- #### (2) 仅查看状态与 Cookie 统计 (ASCII 报表)
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 详情为 JSON (便于程序与定时脚本直接提取)
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 --mode merge
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 sync --source local
175
+ # 2. 执行同步并将结构化指标以 JSON 打印 (方便 CI/CD 或流水线捕获)
176
+ chromium-sync --config cluster.json --mode merge --json
147
177
  ```
148
178
 
149
179
  ---
150
180
 
151
- ## ⚙️ 节点配置字段规范 (NodeConfig)
181
+ ## ⚙️ 节点参数配置规范 (NodeConfig)
152
182
 
153
183
  | 字段 | 类型 | 默认值 | 详细说明 |
154
184
  | :--- | :--- | :--- | :--- |
155
- | `id` | str | 必填 | 节点唯一标识符 (例如 `local`, `sg1`, `kr2`) |
185
+ | `id` | str | 必填 | 节点唯一标识符 (例如 `local`, `kr2`) |
156
186
  | `name` | str | `id` | 节点显示名称 |
157
- | `is_local` | bool | 自动识别 | 是否为本地节点 (不填 `ssh_host` 或填回环地址自动为 True,免密直连) |
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
- | `ssh_key_path` | str | `None` | SSH 私钥文件路径 (例如 `~/.ssh/id_rsa`) |
164
- | `ssh_key_data` | str | `None` | SSH 私钥内容字符串 |
165
- | `ssh_password` | str | `None` | SSH 登录密码 (无外部依赖纯原生环境自动适配) |
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
 
@@ -4,5 +4,5 @@ Universal Chromium Session & Cookie Synchronizer with SSH Tunnel and Cluster Sup
4
4
  """
5
5
  from .core import ChromiumSync, NodeConfig, merge_cookies
6
6
 
7
- __version__ = "1.1.0"
7
+ __version__ = "1.2.0"
8
8
  __all__ = ["ChromiumSync", "NodeConfig", "merge_cookies"]
@@ -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["ssh_key_path"] = v
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 格式输出节点完整数据与 Cookie 列表")
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__":
@@ -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
- - 零额外数据库依赖:内置一行代码获取节点在线状态、Cookie 总数与完整详情
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,可选 ssh_user, ssh_key_path, ssh_key_data, ssh_password
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 get_cookies(self, node_id: Optional[str] = None) -> Union[List[dict], Dict[str, List[dict]]]:
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 _run_async(
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
- raise RuntimeError("[-] 未安装 playwright 模块,请先运行: pip install playwright && playwright install chromium")
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
- logging.error("[-] 无可用在线节点,退出同步。")
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
- logging.info("[+] 同步任务已成功完成!")
557
-
558
- for b in browsers.values():
559
- await b.close()
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
- asyncio.run(self._run_async(mode=mode, source=source, targets=targets))
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))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wimotek-chromium-sync",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
4
4
  "description": "Universal Chromium Session & Cookie Synchronizer with SSH Tunnel",
5
5
  "main": "chromium_sync/__init__.py",
6
6
  "bin": {