Great Lotus — Kiến trúc End-to-End

n8n → MQTT → Worker GPM → Profile NAS → IP Pool · cập nhật 2026-08-05 · số liệu khảo sát trực tiếp trên HP1, không suy đoán

Hệ thống này làm gì?

Một nền tảng tự động hoá trình duyệt quy mô fleet: người dùng bấm chạy một workflow trên n8n, lệnh bay qua MQTT tới một máy Windows ở site khác, máy đó mở Chrome bằng GPM Login, rồi n8n điều khiển ngược lại chính trình duyệt đó qua cổng CDP.

Bộ não ra lệnh
n8n · B15 · HP1
Đường truyền
MQTT 128.199.200.17:1883
Tay chân thực thi
NUC1 · 172.16.30.101
Giám sát
B34 · gpmfleet…

Mười hai dòng tóm tắt

  • n8n (container B15 trên HP1) chạy workflow chứa node C10 (Playwright) và/hoặc C12 (PyAutoGUI).
  • Node không tự động hoá gì cả — nó chỉ đóng gói lựa chọn UI thành JSON rồi PUBLISH lên MQTT một topic (VD WP2_PC176).
  • Máy Worker (VD NUC1 ở WP3) chạy Process Manager GUI của APP_5, subscribe đúng topic đó.
  • Process Manager định tuyến bản tin tới đúng worker process — mỗi session trình duyệt là một tiến trình con — theo remote_port / profile_id.
  • Worker gọi GPM Login (http://127.0.0.1:19995/api/v3) để mở profile Chrome, hoặc mở Chrome local.
  • Trình duyệt mở ra kèm cổng CDP. Cổng này chảy ngược về n8n rồi truyền xuôi cho các node sau ⇒ cả workflow dùng chung một session.
  • Kết quả trả về topic RES_<topic>, khớp bằng meta.requestId.
  • Worker gửi heartbeat HTTP mỗi ~2s tới B34 GPM Fleet Monitor để người dùng giám sát ở bất kỳ đâu.
  • Profile Chrome dự kiến lưu tập trung trên NAS2 dạng .tar không nén — CHƯA CÓ CODE
  • IP proxy lấy từ IP Pool Redis (B27) do ScanIpProxy (B30) quản lý — CHƯA NỐI VÀO LUỒNG
  • IP sinh ra từ DCOM 4G ở WP2, vòng đời 03_NEW04_USING05_DONE.
  • ⇒ Hai mắt xích 9 và 10 chính là phần việc còn lại. Xem tab Quy trình 13 bước.

Bản đồ thành phần đã xác minh trên thiết bị

        NGƯỜI DÙNG                                    NGƯỜI DÙNG (giám sát)
            │                                          │              │
            │ https://n8n.lotus1104.synology.me        │              │
            ▼                                          ▼              ▼
 ┌──────────────────────────── HP1 · WP1 · 172.16.10.220 ────────────────────────────┐
 │  B16 lotus_caddy  :80/:443  — reverse proxy HTTPS cho *.lotus1104.synology.me     │
 │      ├── n8n.        → B15 lotus_n8n:5678          (host 20150 = API, 20151)      │
 │      ├── gpmfleet.   → B34 lotus_GPM_Fleet_Monitor:8080     (host 20340)          │
 │      ├── scanIpProxy → B30 lotus_python_proxy_script:8080   (host 20300)          │
 │      ├── web.        → B06 lotus_mqtt:8081   (share file tĩnh — trang bạn đang đọc)│
 │      └── novnc.      → B35 lotus_novnc_chrome:7900  (20350 xem / 20351 Selenium)  │
 │                                                                                   │
 │  B27 lotus_redis (Redis Stack) :6379 → host 20270   ← IP POOL + LOTUS:01_MAN:*    │
 │  B28 lotus_redisinsight → host 20280  (GUI Redis)                                 │
 │  B06 lotus_mqtt (mosquitto) — broker NỘI BỘ (MQTTS 20061 / WSS 20063)             │
 └───────────────────────────────────────────────────────────────────────────────────┘
            │  (1) PUBLISH lệnh                             ▲ (4) heartbeat HTTP mỗi 2s
            ▼                                               │
 ┌───────────────────────────────┐                          │
 │  MQTT BROKER ĐANG DÙNG THẬT   │                          │
 │  128.199.200.17:1883          │◄── KHÔNG phải B06!       │
 └───────────────┬───────────────┘                          │
                 │ (2) topic = WP2_PC176                    │
                 ▼                                          │
 ┌──────────────────── WORKER (Windows) ────────────────────┐│
 │  NUC1 · WP3 · 172.16.30.101   (hostname WP3-PC1)         │┘
 │  APP_5 → 2_GPM_multi_remote_browser_process_manager_gui  │
 │     ├── engine A12 Playwright   (payload great-lotus-v2) │
 │     ├── engine A13 PyAutoGUI    (great-lotus-pyauto-v1)  │
 │     └── GPM Login  http://127.0.0.1:19995/api/v3         │
 │            └── mở Chrome + cổng CDP ──► (3) RES_<topic>  │
 └──────────────────────────────────────────────────────────┘
                 │ cần                          │ cần
                 ▼                              ▼
 ┌──────────────────────────┐   ┌─────────────────────────────────────┐
 │ NAS2 White NAS · WP2     │   │ IP POOL (Redis B27) ← ScanIpProxy    │
 │ 172.16.20.201            │   │  03_NEW → 04_USING → 05_DONE        │
 │ kho profile .tar         │   │       ▲ DCOM 4G ở DELL_Proxy         │
 │ (THIẾT KẾ — chưa có code)│   │       │ 172.16.20.202:6868           │
 └──────────────────────────┘   └─────────────────────────────────────┘

Ba site nối bằng VPN IPSec full-mesh ⇒ mọi máy gọi thẳng IP LAN của site khác. Chi tiết hạ tầng mạng: xem tài liệu Bản đồ hệ thống mạng 3 Workpool.

Ba trang web người dùng cần biết

Trang Dùng để Đi tới đâu
https://n8n.lotus1104.synology.me/ Dựng & chạy workflow B15 lotus_n8n:5678
https://gpmfleet.lotus1104.synology.me/ Giám sát đội máy worker B34 :8080
https://scanipproxy.lotus1104.synology.me/ Giám sát vòng đời IP proxy B30 :8080

Máy giao tiếp n8n B15

Hạng mục Sự thật đã xác minh
URL người dùng https://n8n.lotus1104.synology.me/ (qua Caddy B16 → lotus_n8n:5678)
Container lotus_n8n · IP 114.114.114.15 · host 20150 (=5678, cũng là API), 20151
REST API http://127.0.0.1:20150/api/v1/… · header X-N8N-API-KEY · key ở N8N_API_KEY trong .env cạnh docker-compose.yml
Credential đăng nhập N8N_EMAIL / N8N_PASSWORD trong cùng .env
Workflow mẫu 8SEPoybAbRtA7oe4 = 02_RegHotMail_WEB_PyAutoGUI_TEST_MCP — 77 node, dùng cả C10 lẫn C12 trong cùng workflow, tất cả publish về topic WP2_PC176
Bẫy đăng nhập: n8n bật N8N_SECURE_COOKIEkhông đăng nhập được qua http://lotus_n8n:5678. Muốn mở GUI bằng trình duyệt tự động (container B35) phải đi HTTPS kèm --host-resolver-rules=MAP n8n.lotus1104.synology.me 114.114.114.16.

Hai node phát lệnh — C10 và C12

Cả hai đều là MQTT command emitter thuần: không import Playwright/PyAutoGUI, chỉ mqtt.connect → publish → (tuỳ chọn) chờ RES_.

C10 n8n-nodes-c10_great_lotus C12 n8n-nodes-c12_pyautogui_web
Tên node GREAT LOTUS · nodeVersion 2 GREAT LOTUS PYAUTO WEB · nodeVersion 1
payload.version great-lotus-v2 great-lotus-pyauto-v1
Engine ở worker A12 Playwright — CDP, DOM thật, chạy song song được A13 PyAutoGUI — chuột/bàn phím vật lý, bị mutex tuần tự hoá
Action GPM profile đủ 8 action (list/get/create/open/close/update/delete profile, list_groups) + gpm_connect không có; gpm_connect đã bị xoá
Kích thước mã ~4269 dòng TS ~1819 dòng TS
Quy tắc chọn node. Cần DOM / nhiều tab / chạy song song ⇒ C10. Cần thao tác giống người thật để né bot-detection (di chuột theo quỹ đạo, gõ theo WPM) ⇒ C12, nhưng chấp nhận một máy chỉ làm một việc tại một thời điểm.

Giao thức MQTT — hợp đồng bất biến

Vai trò Topic Ai publish Ai subscribe
Request <topic> người dùng nhập, VD WP2_PC176 node C10/C12 Worker
Response RES_<topic> — tự suy ra, không cấu hình được Worker node (chỉ khi waitForResponse=true)
// REQUEST  node → worker
{
  "version": "great-lotus-v2",                       // hoặc great-lotus-pyauto-v1
  "action":  { "key":"mouse.click", "group":"MouseAction", "function":"click" },
  "inputs":  { "remote_port": {"type":"number","value":8888},
               "x": {"type":"number","value":320} },  // ô để TRỐNG bị loại khỏi payload
  "meta":    { "requestId":"a1b2c3d4e5f60718", "requestTopic":"WP2_PC176",
               "responseTopic":"RES_WP2_PC176", "waitForResponse":true,
               "responseTimeoutSec":60 }
}

// RESPONSE worker → node
{ "return":"OK"|"NG", "result":true, "error":"…", "data":{…},
  "meta":{ "requestId":"a1b2c3d4e5f60718" } }        // PHẢI echo đúng requestId
  • action.function = khoá dispatch phía Python. Đổi nó ⇒ phá worker.
  • return: "NG" ⇒ node ném lỗi, đọc chi tiết theo thứ tự errormessagedata.error.
  • Riêng image.wait_color: result === false cũng là fail dù return không NG.
  • Worker phải echo meta.requestId — nếu không, node sẽ chấp nhận response đầu tiên bất kỳ ⇒ nhận nhầm khi chạy song song trên cùng topic.
⚠ Broker nào đang dùng thật — điểm dễ sai nhất. Hệ thống có broker nội bộ B06 lotus_mqtt (mosquitto, MQTTS 20061 / WSS 20063), nhưng luồng chạy thật lại dùng một broker công cộng bên ngoài: 128.199.200.17:1883 — cấu hình y hệt ở cả worker (*_config.json) lẫn ScanIpProxy (docker-compose.yml). Đã kiểm chứng cổng này mở và đang phục vụ. Đây là rủi ro tập trung: mất VPS đó là mất cả đường điều khiển lẫn đường quản lý proxy. B06 đang chạy nhưng không nằm trên đường điều khiển chính.

Chương trình chạy trên worker

Application/APP_5_GPM_GREAT_LOTUS/App/2_GPM_Multi_Remote_Browser_Process_Manager_GUI/

File Vai trò
2_GPM_…_gui.bat Bootstrap: dò Python → tạo venv trên ổ LOCAL %LocalAppData%\GPM_ProcessManagerGUI\… → cài 12 gói pip → chạy GUI
GPM_multi_remote_browser_process_manager_gui.py
~2900 dòng
GUI Tkinter: cấu hình, log màu, bảng worker, heartbeat fleet
_core/GPM_multi_remote_browser_process_manager.py
~2472 dòng
Bộ định tuyến MQTT → worker process; _parse_route(), mutex desktop, faulthandler
_core/GPM_multi_remote_browser_manager.py
~1942 dòng
Lớp session: gọi GPM HTTP API, chọn engine, dựng addination_args
Ba bài học đã ghi trong chính file .bat — đừng phá: venv phải nằm ổ local (pip qua SMB/UNC cực chậm, nhìn như GUI treo) · tên thư mục venv cố định (map UNC sang chữ ổ khác nhau mỗi phiên không được đẻ venv mới) · không cd vào UNC path (cmd.exe âm thầm rơi về C:\Windows\System32).

Cấu hình worker *_config.json

Khoá Ý nghĩa
broker / port / username / password MQTT
subscribe_topics Topic worker lắng nghe — phải khớp ô Topic của node n8n
response_prefix RES_
gpm_base_url http://127.0.0.1:19995 — GPM Login API, không kèm /api/v3
work_pool / pc_name Cặp định danh trên Fleet Monitor — trùng cặp = đè lên cùng một card
monitor_server_url https://gpmfleet.lotus1104.synology.me/
max_request_workers mặc định 8 — số luồng xử lý request đồng thời
enable_fleet_monitor true mới gửi heartbeat
File config trong repo KHÔNG phải file đang chạy trên NUC1. File ở gốc repo ghi pc_name = "NUC", nhưng NUC1 thật đang báo cáo pc_name = "PC_1", hostname = WP3-PC1, Windows 11. File repo chỉ là bản mẫu — luôn đọc trạng thái thật qua Fleet Monitor API, đừng tin file config trong git.

Định tuyến đa session — _parse_route()

Một máy worker chạy nhiều trình duyệt cùng lúc. Mỗi session = một worker_key = một tiến trình con riêng.

Trường hợp worker_key
manager.open_profile (có profile_id) profile:<profile_id> + restart worker
manager.local_browser_connect local:<session_id> (mặc định local:default)
manager.gpm_connectprofile_id / chỉ có port profile:<id> / remote:<port>
Action thường có profile_id đã map worker đang giữ profile đó
Action thường có remote_port worker đang giữ cổng đó
manager.quit + quit_all = true __quit_all__ — đóng SẠCH mọi session
others.random, General Debug control:utility_random, control:general_debug — không cần session

⭐ Cổng ảo 127001 và chuỗi session

  • remote_port = 127001 nghĩa là "session local" (gợi nhớ 127.0.0.1) — không phải cổng TCP. Chọn số ngoài dải 1..65535 để không bao giờ đụng cổng CDP thật.
  • Giá trị cũ 0 vẫn được chấp nhận nhưng không dùng nữa0falsy → dễ bị coi nhầm là "chưa nhập" khi đi qua expression n8n.
  • Node luôn xuất cổng ra top-level output dưới CẢ HAI tên khoá: remote_debugging_port (tên C10) và remote_port (tên C12), cùng giá trị ⇒ nối chéo C10 ↔ C12 trong một workflow đều chạy.
  • Ô Remote Port của action thường mặc định ={{$json.remote_debugging_port}} ⇒ cổng tự chảy xuôi chuỗi node.
Cạm bẫy sentinel: mọi chỗ dò cổng phải loại 127001 ra, nếu không create_connection(127001) sẽ wrap thành 61465 và dò nhầm sang dịch vụ khác.
[open_profile] ──► Client mở trình duyệt qua GPM, trả về cổng CDP
      │            C10 rút cổng từ response và GẮN vào output:
      ▼            json.remote_debugging_port = <port>
[mouse.click]  ──► ô remote_port mặc định ={{$json.remote_debugging_port}}
      │            → attach đúng phiên → click đúng cửa sổ
      ▼
[element.get_text] ─► tương tự, cùng session…

GPM Login — phần mềm quản lý profile

API local
127.0.0.1:19995/api/v3
Wrapper Python
B05_GPM_Login_Manager
Giấy phép
theo SỐ THIẾT BỊ
Số profile
không giới hạn

Anti-detect browser: mỗi profile = một danh tính số độc lập (fingerprint, cookie, cache riêng), chạy nhiều Chrome song song. Cổng 16813 cũng xuất hiện trong repo — phiên bản GPM khác nhau đổi cổng, luôn lấy từ gpm_base_url.

Action HTTP Endpoint Tham số đáng nhớ
list_profiles GET /profiles page, per_page, group_id, search
get_profile_info GET /profiles/{id}
create_profile POST /profiles/create profile_name*, group_name*, raw_proxy, fingerprint flags
open_profile GET /profiles/start/{id} addination_args, remote_port, win_scale/pos/size
close_profile GET /profiles/close/{id}
update_profile POST /profiles/update/{id} raw_proxy, group, fingerprint…
delete_profile GET /profiles/delete/{id}?mode= mode mặc định 2
list_groups GET /groups
🔴 open_profile không nhận proxy MỚI khi mở. open_profile = GET /profiles/start/<id>, tham số chỉ có addination_args/remote_port/win_*không có ô proxy. Profile vẫn chạy được với proxy, nhưng là proxy đã lưu sẵn trên nó. Chỉ create_profileupdate_profile mới nhận raw_proxy. ⇒ muốn gán/đổi proxy (luồng IP pool) thì phải update_profile(raw_proxy=…) TRƯỚC, rồi mới open_profile. Định dạng GPM: host:port:user:pass (bỏ :user:pass nếu proxy không auth).
Kiểm chứng trong code worker: _handle_open_profile (chỉ query addination_args/remote_port/win_*) vs _handle_create_profile/_handle_update_profile (có raw_proxy).

Giám sát — B34 GPM Fleet Monitor

Hạng mục Sự thật
URL https://gpmfleet.lotus1104.synology.me/
Container lotus_GPM_Fleet_Monitor · 114.114.114.34 · host 20340 → 8080
Image python:3.12-alpine · không dependency ngoài (http.server thuần)
Giao thức HTTP JSON — KHÔNG dùng MQTT cho kênh giám sát
Nhịp client POST /api/heartbeat mỗi 2s · offline nếu > 10s
Định danh 1 máy cặp (work_pool, pc_name) — trùng cặp = đè cùng card
Chiều ngược request_screenshot, kill_workers, restart_service, restart_pc
Trạng thái thật lúc khảo sát (2026-08-05): pool WP3 có 1 máy PC_1 (WP3-PC1, Windows 11, CPU 4/8 nhân, RAM 23.9GB, uptime ~10.5 ngày), MQTT connected, 0 worker đang mở, 120 sự kiện gần đây đều là action tới topic WP2_PC176 với worker_key = local:default. ⇒ Đường n8n → MQTT → worker → B34 đang chạy tốt.

Thiết kế đã chốt — Hybrid Storage & Decentralized Config

Nguồn: báo cáo UID_00001_profile_optimization_report.html — thiết kế cho quy mô 10.000 profile.

Nguyên tắc Nội dung
Kho tập trung Mỗi profile đóng thành <profile_id>.tar KHÔNG NÉN trên NAS
Config phi tập trung Mỗi profile tự mang config.json trong gói — không cần DB trung tâm
Vận hành cục bộ Tải .tar → xả nén ra SSD local (C:\temp_profiles) → đọc config → chạy → đóng gói lại → upload đè → xoá sạch thư mục tạm
Khống chế dung lượng Cấu hình Chromium giữ profile ≤ 50MB thay vì 300MB

Tham số Chromium bắt buộc

Cờ Giá trị Mục đích
--disk-cache-size lấy từ config.json cache thích ứng 30–100MB
--media-cache-size 1 tắt hẳn cache video/audio — chặn YouTube làm phình profile
--disable-pdf-viewer bật PDF về Downloads thay vì nằm trong cache
(khuyến nghị) uBlock Origin giảm thêm 40–60% dữ liệu tải
// config.json nằm ở gốc mỗi profile
{ "profile_id": "profile_101", "status": "active",
  "cache_settings": { "current_cache_limit_mb": 50, "last_actual_cache_mb": 42.5 },
  "audit_logs": { "total_runs": 124, "last_run_time": "2026-07-19T18:12:00Z",
                  "last_run_machine": "CLIENT-PC-04", "last_run_duration_seconds": 320 } }

Sau mỗi lần chạy, tính lại hạn mức cache bằng AIMD (dùng gần chạm trần → +20MB, tối đa 100MB; dùng ít → giảm) rồi ghi đè config.json trước khi đóng gói.

Proxy và Fingerprint CỐ Ý KHÔNG nằm trong config.json — để GPM tự quản, tránh trùng lặp nguồn sự thật. Đây chính là lý do proxy phải đi đường update_profile(raw_proxy).

Vì sao phải đóng gói .tar? — số đo quyết định

Profile 50MB, LAN ~40–50MB/s.

Cách làm Thời gian khởi chạy Ghi chú
TAR không nén (đề xuất) 1.45 s 0% tải CPU local, tải tuần tự nhanh nhất
ZIP/ZSTD có nén 3.12 s mạng nhanh hơn nhưng tốn 1.5–3s CPU giải nén
Chạy thẳng từ NAS qua SMB 166.7 s ❌ random I/O trên SMB rơi còn ~1.5 MB/s
Kết luận cốt lõi: KHÔNG BAO GIỜ mount profile chạy trực tiếp trên network share. Chênh lệch 115 lần. Chỉ copy nguyên file .tar rồi chạy trên SSD local.

🟡 Trạng thái: CƠ CHẾ ĐÃ CÓ CÔNG CỤ THAM CHIẾU — CHƯA NỐI VÀO WORKER

Trong luồng chạy thật của worker, mắt xích "tải profile từ NAS" vẫn chưa được nối: grep toàn repo (APP_5, A12_PlayWright, A13_PyAutoGUI_Web, B05_GPM_Login_Manager) với smb|cifs|webdav|nas_|profile*.tar|temp_profiles không có một dòng nào. Thư viện Library/C02_NAS/nas.py chỉ là SSH tới NAS1 (172.16.10.201:222), không liên quan kho profile. Cái worker đang chạy thật vẫn chỉ là open_profile (mở profile có sẵn trên máy) và sync_master_profile_to_local_test_profile (copy master cục bộ).

MỚI (2026-08-05): đã có công cụ tham chiếu docs/tools/profile_store.py — hiện thực đầy đủ vòng đời thiết kế (init / push / pull / quarantine / list / verify), TAR không nén, upload nguyên tử incoming/ → os.replace → profiles/, config.json + AIMD, sha256 trong manifest, chống path-traversal khi giải nén. Chạy được cả trên Linux (CIFS mount) lẫn Windows worker (UNC \\172.16.20.201\lotus_profiles). Đã kiểm chứng end-to-end TRỰC TIẾP trên NAS2 qua VPN (share đã tạo) — xem thẻ "Kiểm chứng cơ chế" bên dưới.

⇒ Còn thiếu để thành production: (1) share đã tạo — còn tách quyền lotus_worker (rw) / lotus_ro (chỉ đọc) thay cho tài khoản admin; (2) gọi profile_store.pull trước open_profilepush sau khi đóng, ngay trong Process Manager của APP_5; (3) chính sách xử lý xung đột khi 2 máy cùng đụng một profile (xem cột "Đánh giá / rủi ro").

Khảo sát thực tế NAS2 từ HP1, qua VPN IPSec

Port Dịch vụ Trạng thái đo được
22 SSH OPEN
139 / 445 SMB / CIFS OPENtài liệu mạng cũ chưa ghi 2 cổng này
873 rsync OPEN
5000 / 5001 DSM Web + FileStation API OPEN — đã gọi thử SYNO.API.Infosuccess: true
5005 / 5006 WebDAV / WebDAVS ĐÓNG — package chưa bật
6690 Synology Drive OPEN

Trên HP1 có sẵn mount.cifsrsync. Worker là Windows ⇒ SMB là công dân hạng nhất.

⭐ ĐỀ XUẤT: dùng SMB3 (CIFS) làm kênh chính

✅ SMB3 / CIFS CHỌN

Kênh chính cho đường nóng: tải & upload file .tar.

🟡 rsync bổ trợ

Seed hàng loạt lần đầu, mirror NAS1 ↔ NAS2, backup định kỳ. Không ở đường nóng — thuật toán delta vô ích với .tar thay đổi toàn bộ, và Windows không có rsync bản địa.

🟡 DSM FileStation API control-plane

n8n chạy trong container Linux, không mount SMB → muốn liệt kê/kiểm kê profile thì gọi HTTPS API này là gọn nhất. Cũng là đường dự phòng khi worker bị chặn 445. Chậm hơn, cần session token.

❌ WebDAV

Đang tắt trên NAS2 (5005/5006 đóng). Bật lên phải thêm package + cổng + chứng chỉ, rồi vẫn chậm hơn SMB vì gánh thêm tầng HTTP/nginx cho một tác vụ truyền khối.

❌ Synology Drive

Là daemon đồng bộ liên tục hai chiều — chống lại hẳn mô hình "tải theo yêu cầu, xoá sau khi xong", và sẽ đẻ ra conflict-file khi 2 máy đụng cùng profile.

🟡 SSH / SFTP

Chỉ để chạy script bảo trì trên NAS (dọn rác, thống kê). Không dùng để truyền profile.

Sáu lý do chọn SMB, theo thứ tự quan trọng

  1. Đúng hình dạng của workload. Sau quyết định đóng gói .tar, mỗi lần chạy chỉ là 1 file lớn, đọc/ghi tuần tự — đúng thứ SMB3 làm tốt nhất (large MTU, multichannel, không bị phạt round-trip như random I/O). Con số 1.45s ở trên chính là đo trên SMB.
  2. Không thêm dependency lên worker. Windows nói SMB bẩm sinh. File .bat bootstrap đã phải cài 12 gói pip; thêm webdavclient3/paramiko/smbprotocol là thêm bề mặt lỗi trên mọi máy worker, đổi lấy 0 lợi ích.
  3. WebDAV trên NAS2 hiện đang TẮT. Chọn nó = phải bật package, mở cổng, xử lý chứng chỉ.
  4. Đã có IPSec ⇒ dùng thẳng UNC \\172.16.20.201\<share> bằng IP LAN, không cần port-forward, không phụ thuộc DDNS động của WP2 (118.69.136.48 là IP động).
  5. Ghi nguyên tử làm được: upload ra <id>.tar.tmp rồi rename trong cùng share — rename cùng volume là nguyên tử, người đọc không bao giờ thấy file tar dở dang.
  6. Phân quyền sẵn: user cục bộ trên DSM + quyền theo share, tách read-only / read-write.
Cách dùng SMB cho ĐÚNG — bắt buộc tuân thủ:
  • ✅ Dùng UNC trực tiếp \\172.16.20.201\lotus_profiles\<id>.tar. KHÔNG map chữ ổ, KHÔNG cd/pushd vào UNC — bài học đã ghi ngay trong .bat của APP_5.
  • Chỉ copy file .tar. Không bao giờ --user-data-dir trỏ vào share.
  • ✅ Xả nén và chạy trên SSD local, xoá sạch sau khi upload xong.
  • ✅ Credential lưu bằng Windows Credential Manager hoặc net use một lần lúc khởi động dịch vụ; không nhúng mật khẩu vào code.
  • Tắt SMB encryption cho luồng cross-site — traffic đã được IPSec mã hoá rồi, bật thêm chỉ tốn CPU. (Bật nếu có worker nằm ngoài VPN.)
  • ⚙ Bật SMB3 + large MTU trên DSM; giữ SMB1 tắt.

Bố cục share đề xuất trên NAS2

\\172.16.20.201\lotus_profiles\
├── profiles/<profile_id>.tar          ← kho chính (không nén)
├── incoming/<profile_id>.<host>.tmp   ← vùng ghi tạm, rename sang profiles/ khi xong
├── quarantine/<profile_id>.<ts>.tar   ← bản lỗi/hỏng, giữ để điều tra
└── _index/manifest.json               ← sổ cái (size, sha256, status, audit) cho FileStation API đọc nhanh

✅ Kiểm chứng cơ chế — chạy thật end-to-end 2026-08-05

Công cụ tham chiếu docs/tools/profile_store.py đã chạy full vòng đời trực tiếp trên NAS2 — share \\172.16.20.201\lotus_profiles (SMB3, /volume1) — với profile GPM Chrome thật lấy từ …/03_Trans/03_GPM_Profile/.

Đã tạo share lotus_profiles trên NAS2 + dựng cây thư mục thật. Mount SMB3 từ HP1 (WP1) sang NAS2 (WP2) qua VPN IPSec bằng tài khoản meomay22 (là admin DSM), chạy đủ 8 phép thử, rồi xác minh độc lập qua FileStation API (không qua mount) thấy đúng profiles/ incoming/ quarantine/ _index/manifest.json + README.txt. Số throughput dưới đây là cross-VPN WP1→WP2 thật.
Phép thử (chạy thật)Kết quả đo đượcKết luận
init cây kho trên share Tạo profiles/ incoming/ quarantine/ _index/ + manifest rỗng ĐẠT
push 3 profile GPM thật (~250 KB) qua VPN pack 0.14–0.15s · upload+rename 0.15–0.20s ĐẠT
Upload nguyên tử: sau push, incoming/ có còn .tmp? incoming/ rỗng — người đọc không bao giờ thấy tar dở dang ĐẠT
pull + xả nén ra SSD + diff byte-for-byte với nguồn GIỐNG HỆT (trừ config.json do tool sinh) ĐẠT
Profile 50MB cross-VPN (WP1 HP1 → WP2 NAS2, dữ liệu không nén được) pack 0.44s · upload+rename 4.84s (~10 MB/s) · pull 0.24s · extract 0.09s · round-trip giống hệt ĐẠT
AIMD: đẩy --actual-cache-mb 46.5 (≥ 90%×50) hạn mức cache tự nâng 50 → 70 MB; audit total_runs +1 ĐẠT
quarantine một profile "hỏng" rename sang quarantine/<id>.<ts>.tar; manifest ghi status=quarantined ĐẠT
Phát hiện hỏng: lật 1 byte trong .tar rồi verify SHA256 lệch ⇒ báo lỗi, chặn dùng profile hỏng ĐẠT
Xác minh độc lập qua FileStation API (không qua mount) thấy đúng cây profiles/ incoming/ quarantine/ _index/ + README.txt ĐẠT
Đọc số throughput cho đúng. Con số 1.45s "khởi chạy" trong báo cáo gốc = pull + xả nén — đo lại cross-VPN ở đây còn tốt hơn (~0.33s cho 50MB). 4.84s là đường GHI (đóng gói + upload qua VPN, ~10 MB/s), xảy ra sau khi phiên chạy xong nên không nằm trên đường nóng khởi động — đúng như thiết kế. (Đọc 50MB ở đây trúng cache RAM của NAS vì vừa ghi; đọc nguội thực địa sẽ chậm hơn — nhưng vẫn là pull 1 file tuần tự, không phải random I/O.)

Còn hở gì khi lên production (đánh giá thẳng thắn)

Vấn đềMức độXử lý
Khoá phiên: 2 máy cùng pull rồi cùng push một profile ⇒ bản ghi sau đè bản trước (mất lịch sử duyệt của bản kia) XONG Đã thêm lease acquire/release/status/steal (_locks/<id>.lock tạo bằng O_EXCL, có TTL); push tự chối nếu máy khác đang giữ. Test 10 tiến trình đua → đúng 1 thắng (thẻ dưới).
Manifest ghi đè lẫn nhau: read-modify-write không nguyên tử giữa nhiều máy TRUNG BÌNH Manifest chỉ là chỉ mục tăng tốc, không phải nguồn sự thật — sự thật là các file .tar trong profiles/. Có thể rebuild manifest bằng cách quét thư mục. Nếu cần chính xác thời gian thực ⇒ chuyển sổ cái sang Redis (B27 đã có).
Phân quyền share: tách read-only / read-write cho lotus_profiles XONG Tạo WP2_GPM_Profile_RW (rw) + WP2_GPM_Profile_RO (ro); verify mount thật: RW ghi được, RO ghi bị chặn (thẻ dưới). SMB3 bật; SMB1 tắt.
Nối vào worker: APP_5 gọi pull trước open_profile, push sau khi đóng XONG (gated) Đã tạo profile_nas_sync.py + nối 2 hook vào _run_profile_action. Mặc định TẮT (LOTUS_PROFILE_NAS_SYNC=1 để bật), nuốt mọi lỗi. Còn 1 việc: xác nhận LOTUS_GPM_PROFILE_DIR trên worker thật (thẻ dưới).
Dọn rác SSD khi worker crash giữa chừng (thư mục temp_profiles còn sót) THẤP Quét dọn lúc khởi động dịch vụ; profile chưa push thì coi như mất phiên đó (kho vẫn còn bản cũ nhất quán).
Seed 10.000 profile lần đầu lên NAS2 THẤP Dùng rsync (873 mở) một lần — không đi đường nóng. push chỉ cho cập nhật lẻ về sau.

Lệnh đã chạy để kiểm chứng nằm trong docs/tools/profile_store.py (mục docstring có ví dụ đầy đủ). Chi tiết văn bản: docs/great-lotus-e2e-architecture.md §7.5.

✅ Phân quyền · Khoá phiên · Nối vào worker 2026-08-05

(a) Phân quyền share — verify bằng mount thật

Tài khoảnQuyền đặtĐo khi mount
WP2_GPM_Profile_RWread-write đọc OK, ghi OK ĐẠT
WP2_GPM_Profile_ROread-only đọc OK, ghi bị chặn (Permission denied) ĐẠT

Worker mount bằng RW; n8n/kiểm kê (container Linux) dùng RO. Credential ở tầng OS (net use … /user:WP2_GPM_Profile_RW / credentials= file), không nhúng code.

(b) Khoá phiên (lease) — test trực tiếp trên NAS2 qua VPN

Phép thửKết quả
A acquire → B acquire cùng profile B thua (exit 3), thấy A đang giữĐẠT
B push khi A giữ lease TỪ CHỐI push (chống đè bản của A)ĐẠT
10 tiến trình đua acquire cùng 1 profile đúng 1 thắng / 9 thuaO_EXCL trên CIFS nguyên tử ĐẠT
Lease hết hạn (TTL 1s) → máy khác giành lại được (steal tự động)ĐẠT
release lock của máy khác bị chặn (chỉ chủ mới nhả)ĐẠT
Vòng đời đầy đủ: A pull --lease → B bị chặn → A push (tự nhả) → B vào được đúng thứ tự, total_runs tăng dầnĐẠT

(c) Nối vào worker — profile_nas_sync.py + 2 hook, mặc định TẮT

_run_profile_action gọi nas_sync_before_open (giành lease + pull --lease) trước khi mở, và nas_sync_after_close (push + nhả lease) sau khi đóng. Import có guard + nuốt mọi lỗi ⇒ NAS treo/mount rớt không bao giờ làm hỏng thao tác browser.

Phép thử (sync BẬT)Kết quả
Sync TẮT (mặc định)hook no-op 0.000s, không log ĐẠT
before_open profile mới (chưa có trên NAS) giành lease, báo "mới", không lỗiĐẠT
after_closepush + nhả lease tar lên NAS, _locks/ trốngĐẠT
before_open lần 2 (đã xoá bản cục bộ) pull kéo lại, khôi phục Cookies đúng byteĐẠT
WORKER2 before_open khi WORKER1 giữ lease degrade êm — cảnh báo, KHÔNG crash, mở bản cục bộ ĐẠT
Điều duy nhất phải xác nhận trên worker thật: LOTUS_GPM_PROFILE_DIR (+ LOTUS_GPM_PROFILE_GLOB) — thư mục GPM Login giữ profile cục bộ (mẫu quan sát: <id>-<ngày>/Default/…) là tuỳ chọn của app GPM trên từng máy, không suy ra được từ xa. Đặt đúng rồi mới bật LOTUS_PROFILE_NAS_SYNC=1. Cấu hình đầy đủ nằm trong docstring của profile_nas_sync.py.

(d) Sự cố: workflow lỗi / worker crash ⇒ lease có kẹt vĩnh viễn không? KHÔNG

Lease luôn có TTL (env LOTUS_PROFILE_LEASE_TTL), và trong khi profile MỞ worker tự động gia hạn (heartbeat) mỗi TTL/3 giây. Đã test trên NAS2 (TTL 15s, renew 10s):

Kịch bảnĐiều gì xảy ra
Profile đang chạy (phiên dài hơn TTL) luồng nền lease renew mỗi TTL/3 → lease luôn tươi, không hết hạn giữa chừng (đo: không lần nào HẾT HẠN)ĐẠT
Workflow lỗi (không close_profile) không push (NAS giữ bản tốt gần nhất); heartbeat chết theo
Worker crash → heartbeat dừng lease hết hạn trong ≤ 1 TTL (đặt ngắn, VD 300s) → máy khác giành lại nhanh ĐẠT
Chính máy đó chạy lại đòi lại lease của chính nó NGAY (same-host reclaim), không chờ ĐẠT
Máy khác cần profile khi TTL còn bị chặn (đúng — máy kia có thể vẫn chạy); sau TTL tự giành lại ĐẠT
Đọc dữ liệu khi có lock pull thường (không --lease) luôn đọc được bản mới nhất — chỉ ý-định-GHI mới bị khoáĐẠT
Mở khoá thủ công lease steal <id> (xem ai giữ: lease status) ĐẠT
Heartbeat cho phép đặt TTL NGẮN (VD 300s = phục hồi crash ≤ 5 phút) mà vẫn an toàn cho phiên chạy lâu: máy còn sống tự gia hạn, máy chết thì luồng renew chết theo nên lease hết hạn nhanh. Không trạng thái nào khiến profile mất hẳn hay không pull về được. Nhịp renew ép bằng LOTUS_PROFILE_LEASE_RENEW_SEC (mặc định TTL/3).
Ghi chú git: profile_nas_sync.py và file worker nằm dưới Library/* & Application/*bị .gitignore theo đúng quy ước repo (toàn bộ APP_5/Library không track). Đã có trên HP1; tới worker qua cùng đường đồng bộ như phần còn lại của code.

🚀 Cách BẬT sync trên máy worker (3 bước)

Mặc định TẮT — worker chạy y như trước. Bật khi đã sẵn sàng, chỉ trên máy worker (Windows). Cấu hình đặt ngay trong file .bat khởi động (dùng setlocal nên python và các lệnh profile_store.py con tự kế thừa): App\2_GPM_Multi_Remote_Browser_Process_Manager_GUI\2_GPM_multi_remote_browser_process_manager_gui.bat

Bước 1 — Lưu credential một lần (KHÔNG gõ mật khẩu vào file). Chạy trong CMD trên worker:

cmdkey /add:172.16.20.201 /user:WP2_GPM_Profile_RW /pass
rem  (Windows hỏi mật khẩu → gõ pass của WP2_GPM_Profile_RW. Từ đó truy cập \\172.16.20.201\... không cần đăng nhập lại)

Bước 2 — Điền thư mục GPM (dòng đã có sẵn trong .bat). Tìm đường dẫn trong app GPM Login → Cài đặt → Đường dẫn lưu trữ (nơi có các folder <id> hoặc <id>-<ngày>):

set "LOTUS_GPM_PROFILE_DIR=C:\...\GPM_Profiles"   <-- ĐIỀN đúng thư mục này
set "LOTUS_GPM_PROFILE_GLOB={id}"                <-- đổi thành {id}-*  nếu tên folder có hậu tố ngày

Bước 3 — Bật cờ rồi chạy lại .bat:

set "LOTUS_PROFILE_NAS_SYNC=1"

Bảng biến môi trường

BiếnÝ nghĩaMặc định / gợi ý
LOTUS_PROFILE_NAS_SYNCBật/tắt toàn bộ sync 0 (tắt) → đổi 1 để bật
LOTUS_PROFILE_STOREĐường dẫn kho (UNC hoặc mount) \\172.16.20.201\lotus_profiles
LOTUS_GPM_PROFILE_DIR Thư mục GPM giữ profile cục bộ — thứ DUY NHẤT phải xác nhận trên máy (trống → phải điền)
LOTUS_GPM_PROFILE_GLOBMẫu tên folder con của profile {id} · dùng {id}-* nếu có hậu tố ngày
LOTUS_PROFILE_HOSTTên máy (cho lease/audit) %COMPUTERNAME%
LOTUS_PROFILE_LEASE_TTLThời hạn khoá phiên (giây) 180–300 — heartbeat tự gia hạn nên để ngắn (phục hồi crash nhanh)
LOTUS_PROFILE_LEASE_RENEW_SECNhịp tự gia hạn (giây) (trống) = TTL/3
Chỉ dùng tài khoản RW để mount. WP2_GPM_Profile_RW cho worker (đọc-ghi); WP2_GPM_Profile_RO chỉ dành cho n8n/kiểm kê (chỉ đọc). Mật khẩu lưu bằng cmdkey/Credential Manager, không nhúng vào .bat.
Kiểm tra nhanh sau khi bật: n8n chạy một chu trình open_profileclose_profile cho một profile, rồi nhìn \\172.16.20.201\lotus_profiles\profiles\ sẽ thấy <id>.tar. Hoặc push tay: py -3 <repo>\docs\tools\profile_store.py --store \\172.16.20.201\lotus_profiles push <id> "<dir_profile>". Xem log worker có dòng [profile_nas_sync].

📊 B36 — Trang quản lý profile (Profile Fleet Dashboard) 2026-08-06

Container lotus_profile_dashboard (IP 114.114.114.36) — một dashboard CHỈ ĐỌC giống ScanIpProxy, đọc thẳng kho lotus_profiles trên NAS2 và hiển thị theo WP1 / WP2 / WP3: số lượng, dung lượng, profile đang chạy ở máy nào, profile lỗi + lý do, số lần chạy, thời lượng, chạy gần nhất, hạn mức cache.

MụcChi tiết
Truy cậphttp://<host>:20360 · công khai https://profiles.lotus1104.synology.me (qua Caddy) · /help (hướng dẫn + xử lý sự cố) · /docs (Swagger API)
Nguồn dữ liệu_index/manifest.json + _index/runs.jsonl (sổ lịch sử) + _locks/*.lock (đang chạy) + quarantine/*.tar (lỗi) — KHÔNG mở tar (nhanh)
Truy cập NAS2Docker CIFS volume nas2_profiles_ro mount \\172.16.20.201\lotus_profiles bằng WP2_GPM_Profile_RO (chỉ đọc) — daemon Docker tự mount, không cần container privileged
Kỹ thuậtFastAPI + SPA vanilla JS dark-glass (không CDN), tự làm mới mỗi 4s, lọc theo WP/trạng thái, tìm kiếm, sáng/tối
Trạng thái suy raĐang chạy (lock chưa hết hạn) · Sẵn sàng · Cách ly (lỗi) · Khoá treo (lease hết hạn) · Thiếu file
Tương tácTiles bấm để lọc theo trạng thái · bấm 1 dòng → ngăn chi tiết kèm hướng dẫn xử lý + lệnh copy-được (steal lease / push / verify) đúng cho từng trạng thái
Thống kêBiểu đồ cột runs theo ngày / tuần / tháng (xếp chồng theo WP) + top profile dùng nhiều nhất + phân bổ theo WP + máy chạy nhiều nhất — đọc từ runs.jsonl
API (chỉ đọc)/api/health snapshot summary profiles profiles/{id} locks quarantine stats runs — OpenAPI ở /docs, /redoc, /openapi.json
Cache = thực/hạn mứcCache HTTP Chrome đo lần chạy gần nhất / trần AIMDprofile_store.py tự chỉnh. Cache là phần con của profile ⇒ thực tế Cache ≤ cỡ .tar (số demo chỉ minh hoạ)
Chỉ đọc, an toàn: dashboard mount RO nên không thể sửa/xoá gì trên kho — mọi thao tác sửa chạy bằng profile_store.py trên máy worker, dashboard chỉ gợi ý đúng lệnh. work_pool + audit lấy từ manifest do push bơm vào; runs.jsonl cũng do push ghi (chỉ-thêm).
# build + chạy (trên HP1)
cd .../DCP_PRODUCTION
docker compose up -d --build lotus_profile_dashboard
# đổi code Python -> restart để uvicorn nạp lại; static bind-mount nên sửa là thấy ngay
docker compose restart lotus_profile_dashboard

Nguồn IP đến từ đâu

DCOM USB-4G (ZTE/OLAX)  ──USB──►  DELL_Proxy 172.16.20.202  ──admin :6868──►  RapidProxyClient
   (nhà mạng cấp IP mobile)         (trang trại proxy HTTP/Socks5)              (Docker trên Pi1
                                     nội bộ 10.10.10.x / 10.10.11.x)             172.16.20.203)
                                                                                       │ MQTT
                                                                                       ▼
                                                                    RapidProxyHost  ──► Redis B27
                                                                                       │
                                              ScanIpProxy (B30, quét mỗi 1s) ◄─────────┘
                                                 └─ MQTT topic SEND_TO_RAPID_PROXY_MAN
                                                    ({type: changeIpRequest | rebootDcomRequest})

Redis key — namespace LOTUS:

Đã liệt kê trực tiếp trên container lotus_redis (B27, host 20270).

Key Kiểu Ai ghi Nội dung
LOTUS:03_PROXY:01_INFO_JSON_MAN ReJSON RapidProxyHost Toàn bộ DCOM theo Workpool > ProxyName: ISP, localIPv4Address, publicIPv4Address, httpPort, socks5Port, signal, proxyStatus, lastReset
LOTUS:03_PROXY:02_CLIENT_ALIVE_JSON_MAN ReJSON RapidProxyHost Heartbeat từng Raspberry Pi client
LOTUS:03_PROXY:03_NEW_IP LIST (mỗi phần tử 1 chuỗi JSON) ScanIpProxy IP mới, hợp lệ, sẵn sàng dùng
LOTUS:03_PROXY:04_USING_IP LIST n8n / worker Đang dùng; có mốc enteredUsingAt
LOTUS:03_PROXY:05_DONE_IP LIST n8n / worker Xong việc, chờ xoay IP; có enteredDoneAt
LOTUS:03_PROXY:CONFIG:USING_TIMEOUT_SEC String Dashboard Hiện 3600
LOTUS:03_PROXY:CONFIG:AUTO_MOVE String (JSON) Dashboard Hiện {"new_to_using":true,"using_to_done":true,"done_to_new":false,"using_to_done_delay":0}
LOTUS:03_PROXY:HOST_HEALTH String (JSON) RapidProxyHost uptime, mqtt user/dev side, số client
LOTUS:01_MAN:* · LOTUS:02_QUEUE:* ReJSON / LIST hệ NewPyWorker cũ Hệ thống KHÁC (schedule/profile/node/queue của luồng Docker cũ). Đừng nhầm với IP pool.

Hình dạng một phần tử trong 03/04/05

{ "workpool": "WP2",
  "dcomEntry": { "id":"eth1", "name":"eth1", "isp":"VIETNAMMOBILE",
                 "publicIPv4Address":"103.249.23.109", "localIPv4Address":"192.168.164.100",
                 "httpPort":2001, "socks5Port":4001, "proxyStatus":"Running", … },
  "enteredNewAt": 1785868525.12 }   // hoặc enteredUsingAt / enteredDoneAt

Mật khẩu Redis nằm ở REDIS_ARGS --requirepass trong docker-compose.yml — đừng chép ra tài liệu/chat.

Vòng đời IP — state machine

  1. 03_NEW_IP — ScanIpProxy thấy DCOM có publicIPv4Address mới & hợp lệ → RPUSH.
  2. 04_USING_IP(n8n/worker phải tự làm) lấy IP ra, đặt enteredUsingAt.
  3. Timeout — nằm ở USING quá USING_TIMEOUT_SEC (3600s): ScanIpProxy ghi audit ip_timeout, đẩy sang 05_DONE_IP, publish rebootDcomRequest, POST webhook cảnh báo về n8n → Webex.
  4. 05_DONE_IP — xong việc. ScanIpProxy publish changeIpRequest để Client xoay IP rồi xoá khỏi hàng đợi. IP mới xuất hiện lại ở 03_NEW_IP.
  5. Mất IP > 60s (RESET_TIMEOUT_SEC) — publish rebootDcomRequest (reset cổng USB) — self-healing.

Ngoài Redis còn 2 SQLite trong WebDashboard/data/: audit.db (sự kiện 90 ngày, xếp hạng DCOM hay reboot → tìm cổng USB hỏng) và ip_stats.db (tỉ lệ IP trùng, kích thước pool thật của nhà mạng).

API HTTP để n8n dùng thay vì nói chuyện Redis trực tiếp

Base: http://172.16.10.220:20300 hoặc https://scanipproxy.lotus1104.synology.me/

Method Đường dẫn Việc
GET /api/redis/new-ip · /using-ip · /done-ip · /all đọc từng hàng đợi
POST /api/redis/move-ip body {from_key, to_key, public_ip}chuyển trạng thái 1 IP
GET/POST /api/redis/config/using-timeout đọc/đặt timeout
GET/POST /api/redis/config/auto-move đọc/đặt AUTO MOVE
DELETE /api/redis/clear/{alias} xoá sạch một key
WS /ws đẩy full_update mỗi 2s cho dashboard

🔴 HAI VẤN ĐỀ PHẢI XỬ LÝ TRƯỚC KHI DÙNG THẬT

(a) AUTO_MOVE đang BẬT — nó tự tiêu thụ IP, worker không giành nổi.
WebDashboardServer.py chạy một vòng lặp nền mỗi ~1s: nếu new_to_using=true thì tự chuyển phần tử đầu của 03_NEW_IP sang 04_USING_IP; using_to_done=true thì tự đẩy tiếp sang 05_DONE_IP. Hiện cả hai đều true ⇒ hàng đợi bị "chảy" tự động ~1.8s một nhịp, độc lập hoàn toàn với worker thật. Đây là chế độ mô phỏng/demo.
Production phải đặt new_to_using=false, using_to_done=false (POST /api/redis/config/auto-move), để việc claim/release do workflow quyết định.
(b) move_ip_between_states() KHÔNG nguyên tử.
read_list_key() → lọc trong Python → pipe.delete(from_key) + rpush(remaining). Đây là read-modify-write: hai người claim đồng thời có thể nhận cùng một IP, và một lần ghi của ScanIpProxy xen vào giữa sẽ bị xoá mất.
→ Muốn claim an toàn phải dùng LMOVE/LPOP nguyên tử hoặc Lua script, hoặc bọc bằng một khoá phân tán (SET NX EX) trước khi gọi API.

Trạng thái đo được lúc khảo sát 2026-08-05 ~01:40

  • 01_INFO: WP2 / DELL_PROXY_01 / DCOM eth1 (ZTE, VIETNAMMOBILE, LTE, signal 5, proxyStatus: Running) nhưng publicIPv4Address đang là chuỗi rỗng, lastReset cập nhật liên tục ⇒ DCOM đang xoay IP / chưa bắt được mạng.
  • 03_NEW_IP, 04_USING_IP, 05_DONE_IP đều rỗng. Redis xoá key khi list rỗng — key không xuất hiện trong KEYS *bình thường, không phải mất dữ liệu.
  • 02_ALIVE: Pi1 (WP2_Rasp_Pi3_b827eb729e41) aliveStatus: true ⇒ đường Client → Host → Redis đang sống.

Trang quản trị DCOM 172.16.20.202:6868 — truy cập được từ mạng khác không?

Đường thử Kết quả
Từ HP1 (WP1) qua VPN IPSec → 172.16.20.202:6868 ❌ không ping được, cổng đóng/bị lọc
Qua port-forward WAN → lotus2020.synology.me:20251 ❌ không kết nối được
Từ Pi1 (cùng LAN WP2) ✅ đang hoạt động — Redis vẫn nhận dữ liệu DCOM từng giây
Kết luận: máy DELL_Proxy chặn truy cập từ ngoài LAN WP2 (firewall Windows) — VPN vẫn tốt, đây không phải lỗi VPN. Chỉ Pi1 cùng subnet gọi được.
Hai lựa chọn: (1) mở firewall Windows trên DELL_Proxy cho dải 172.16.0.0/16, hoặc (2) giữ nguyên và luôn đi qua API của B30 (/api/mqtt/*) — đã có sẵn đường MQTT SEND_TO_RAPID_PROXY_MAN để ra lệnh changeIpRequest / rebootDcomRequest. Khuyến nghị (2) — an toàn hơn và không phải mở thêm bề mặt tấn công.

Quy trình hoàn chỉnh — truy xuất & chạy một Chrome profile

Bản mô tả gốc có 6 bước và ghi "có thể chưa hoàn thiện". Dưới đây là bản đã bổ sung các bước thiếu.

CÓ = đã có code chạy được CÓ TOOL · chưa nối = có công cụ tham chiếu, chưa gọi trong worker THIẾU = phải viết mới
 n8n workflow                Worker (NUC1)                  Hạ tầng
 ─────────────               ──────────────                 ──────────
 (0) lease profile ─────────────────────────────────────► Redis B27  SET NX EX   [THIẾU]
 (1) claim IP ──────────────────────────────────────────► Redis 03_NEW → 04_USING [THIẾU]
 (2) kiểm tra sức khoẻ proxy ───────────────────────────► CONNECT thử qua proxy   [THIẾU]
 (3) tải + xả .tar ─── MQTT ──► worker ─── SMB ─────────► NAS2 \\…\profiles\<id>.tar [ĐÃ NỐI (C12): node pull_profile → profile_store.py pull]
 (4) update_profile(raw_proxy) ─ MQTT ──► GPM API                                  [CÓ]
 (5) open_profile ────────────── MQTT ──► GPM API ──► Chrome + cổng CDP            [CÓ]
 (6) xác minh IP ra ngoài ───── MQTT ──► browser.url ip-echo → so IP đã claim      [THIẾU]
 (7) chạy nghiệp vụ ─────────── MQTT ──► C10 Playwright / C12 PyAutoGUI            [CÓ]
 (8) quit(remote_port) ──────── MQTT ──► đóng đúng 1 session                       [CÓ]
 (9) close_profile ──────────── MQTT ──► GPM flush profile ra đĩa                  [CÓ]
(10) đóng .tar + upload nguyên tử ── SMB ──► NAS2 incoming/ → rename → profiles/   [ĐÃ NỐI (C12): node push_profile → profile_store.py push]
(11) release IP 04 → 05 ───────────────────────────────► Redis                     [THIẾU]
(12) release lease + xoá thư mục tạm local                                         [THIẾU]

Diễn giải từng bước

  1. Giành quyền dùng profile (lease) THIẾU n8n
    Một profile Chrome không được mở đồng thời trên 2 máy — Chrome khoá --user-data-dir bằng SingletonLock, và tệ hơn là 2 máy cùng upload .tar sẽ ghi đè lẫn nhau ⇒ mất cookie. Dùng Redis B27 (đã có sẵn, đừng dựng hạ tầng mới): SET LOTUS:04_PROFILE:LEASE:<profile_id> "<wp>/<pc>/<run_id>" NX EX 5400. Không lấy được ⇒ chọn profile khác hoặc dừng. TTL bắt buộc để worker chết không khoá vĩnh viễn.
  2. Giành IP THIẾU n8n
    Đọc 03_NEW_IP, chọn 1 phần tử, chuyển sang 04_USING_IP. Từ dcomEntry dựng <proxyDevIp>:<httpPort> (HTTP) hoặc :<socks5Port> (Socks5), kèm user/pass nếu cần → định dạng GPM host:port:user:pass. ⚠ Trước khi làm phải tắt AUTO_MOVE và dùng đường nguyên tử (xem tab IP Pool).
  3. Kiểm tra sức khoẻ proxy TRƯỚC khi mở browser THIẾU worker
    Bài học từ chính lần khảo sát này: DCOM báo proxyStatus: "Running" nhưng publicIPv4Address rỗng. Mở browser với proxy chết = mất 30–60s timeout rồi hỏng cả run. Kiểm tra rẻ tiền: TCP connect tới host:port + 1 request qua proxy tới trang echo IP. Hỏng ⇒ trả IP về 05_DONE_IP (kích hoạt xoay IP) rồi claim IP khác.
  4. Tải profile từ NAS ĐÃ NỐI (C12) worker
    2026-08-09: node manager.pull_profile (nhóm Manager, trên cùng) → worker chạy profile_store.py pull (giành lease + heartbeat), xả nén ra LOTUS_GPM_PROFILE_DIR\<id>\, đọc config.json lấy current_cache_limit_mb. Profile chưa tồn tại trên NAS ⇒ nhánh "tạo mới": manager.create_profile rồi coi như profile trắng. C10 sẽ nhân bản node này sau. Điều kiện worker: có profile_store.py + đã mount kho NAS2 (RW).
  5. Gắn proxy vào profile worker
    manager.update_profile(profile_id, raw_proxy=…). 🔴 Bắt buộc là bước riêng. open_profile (= GET /profiles/start/<id>) chỉ nhận addination_args/remote_port/win_*không có tham số proxy. Profile vẫn chạy được với proxy, nhưng là proxy đã lưu sẵn trên nó (đặt bằng create_profile/update_profile). Muốn đổi proxy MỚI (lấy IP từ pool) ⇒ update_profile TRƯỚC rồi mới open.
  6. Mở profile worker
    manager.open_profile(profile_id, addination_args=…). ✅ AUTO-ARGS (C12, 2026-08-09): bật load_args_from_config (mặc định) ⇒ worker tự đọc config.json (do bước 3 tải về) rồi nhét --disk-cache-size=<current_cache_limit_mb×1MB>, --media-cache-size=1, --disable-pdf-viewer + additional_args vào addination_args (không đè cờ gõ tay). Node C10 rút cổng CDP từ response và gắn vào output remote_debugging_port + remote_port ⇒ mọi node sau tự nối đúng session.
  7. Xác minh IP ra ngoài THIẾU worker
    Proxy có thể bị bỏ qua âm thầm (profile GPM còn giữ proxy cũ, hoặc Chrome bỏ qua cờ khi đã có instance cùng user-data-dir). Điều hướng tới một trang echo IP và so với IP đã claim. Lệch ⇒ hủy run ngay — đừng chạy nghiệp vụ với IP sai, đó chính là thứ làm cháy tài khoản.
  8. Chạy nghiệp vụ n8n worker
    Chuỗi node C10/C12. Nhớ: C12 bị mutex tuần tự hoá (chuột/bàn phím vật lý là tài nguyên độc quyền của OS), C10 chạy song song được.
  9. Đóng session worker
    manager.quit với quit_all = false + đúng remote_port. 🔴 Bật Quit All sẽ giết luôn browser của các nhánh song song.
  10. Đóng profile GPM worker
    manager.close_profile(profile_id) — để GPM/Chrome flush hết cookie/SQLite ra đĩa. Đóng gói khi Chrome còn sống sẽ ra bản .tar thiếu dữ liệu (nằm trong -wal). Mẹo đã kiểm chứng ở A13: nếu buộc phải chụp khi Chrome còn chạy, dùng SQLite backup API cho mọi file có header SQLite format 3 rồi xoá -wal/-shm — đo được 501 cookie so với 500 khi chỉ copy thô.
  11. Đóng gói + upload nguyên tử ĐÃ NỐI (C12) worker
    2026-08-09: node manager.push_profile (nhóm Manager, trên cùng) → worker chạy profile_store.py push: đo cache thật → tính AIMD → ghi config.jsontar không nén → ghi ra incoming/<id>.<host>.tmprename sang profiles/<id>.tar (nguyên tử cùng share) → nhả lease. Chạy sau close_profile. C10 sẽ nhân bản node này sau.
  12. Trả IP THIẾU n8n
    04_USING_IP05_DONE_IP. ScanIpProxy sẽ tự phát changeIpRequest và xoá khỏi hàng đợi.
  13. Dọn dẹp THIẾU n8n worker
    DEL lease key; rmtree C:\temp_profiles\<id>. Không dọn = ổ SSD đầy sau vài trăm run.

Đường thất bại (compensation) — bắt buộc thiết kế

Hỏng ở đâu Cơ chế cứu TT
Workflow chết giữa chừng, IP kẹt ở USING ScanIpProxy: USING_TIMEOUT_SEC=3600 → tự sang DONE + rebootDcomRequest + webhook
Worker chết, profile bị khoá Redis TTL lease EX 5400 tự hết hạn THIẾU
Thư mục tạm rác trên SSD janitor quét C:\temp_profiles, xoá thư mục cũ không có lease sống THIẾU
.tar upload dở quy ước: file *.tmp không bao giờ được đọc; janitor xoá .tmp THIẾU
Profile hỏng (Chrome không mở nổi) chuyển sang quarantine/<id>.<ts>.tar, tạo profile trắng, ghi audit THIẾU
Máy worker mất kết nối B34 Fleet Monitor: >10s không heartbeat = offline; có kill_workers/restart_service/restart_pc

Ai nên làm bước nào — n8n hay worker?

Bước Đặt ở đâu Vì sao
0, 1, 11, 12-lease n8n Là quyết định điều phối. n8n nói HTTP tới B30 và Redis dễ; giữ worker "ngu" và không trạng thái.
2 worker (khuyến nghị) Kiểm tra từ worker mới phản ánh đúng đường mạng thật của worker.
3, 10, 12-tmp worker Chỉ worker mới thấy ổ local. Nên thêm action Manager mới cho C10/C12 (VD manager.fetch_profile_from_nas / manager.publish_profile_to_nas) thay vì để n8n điều khiển từng lệnh copy qua MQTT.
4, 5, 7, 8, 9 worker Đã có sẵn.
6 worker Dùng browser.url + developer.get_full_code là đủ.

Khoảng trống giữa "mô tả" và "code thật"

Mô tả ban đầu Thực tế đã xác minh Việc cần làm
"Profile load từ NAS về, giải nén, đọc config" Không có bất kỳ code NAS/SMB nào trong APP_5/A12/A13/B05. Chỉ có bản thiết kế HTML. Viết 2 action Manager mới + module SMB
"Profile chạy với proxy từ IP Pool" Không có code đọc 03_NEW_IP. open_profile không nhận raw_proxy Thêm bước update_profile + logic claim IP
"Chuyển IP 03 → 04 khi mở browser" AUTO_MOVE đang tự làm việc này ~1.8s/nhịp, độc lập worker; API move không nguyên tử Tắt AUTO_MOVE + claim nguyên tử
"Xong việc chuyển 04 → 05, nén & upload" Không có code Viết bước 10–11
"NUC1 gửi gói thông tin đến B34" Đúng và đang chạy — HTTP heartbeat 2s, đang thấy WP3/PC_1 online
"n8n điều khiển qua port control bằng Playwright hoặc PyAutoGUI" Đúng — cơ chế remote_port chảy xuôi node, C10/C12 nối chéo được
Broker MQTT Đang dùng VPS ngoài 128.199.200.17, không phải B06 nội bộ Cân nhắc chuyển về B06
172.16.20.202:6868 từ mạng khác ❌ Không (firewall trên DELL_Proxy). Chỉ Pi1 cùng LAN gọi được Đi qua API B30

Cạm bẫy đã trả giá — đừng vấp lại

1. Ô bị ẩn bằng displayOptions VẪN gửi giá trị lên. Mọi quan hệ "switch điều khiển ô khác" phải được cưỡng chế lại phía Python, không tin vào việc ô đã bị ẩn. Đã dính 2 lần: use_local_test_profilequit_all.
2. Quit All giết cả nhánh khác. Trong workflow đa session, một node dọn dẹp cuối nhánh bật Quit All sẽ đóng luôn browser của nhánh song song. Dùng quit + remote_port chính xác.
3. Thư mục Test Profile tách theo engine. Sync bằng C10 rồi mở bằng C12 sẽ trỏ vào thư mục rỗng mà không báo lỗi:
  • C10 (A12): %TEMP%\great_lotus_local_test_profiles_in_C10_GPM\chromium\default
  • C12 (A13): %TEMP%\great_lotus_local_test_profiles_in_C12_pyAutoGUI\chromium\default
Cố ý tách, không gộp — hai engine dùng chung một user-data-dir mà chạy đồng thời thì Chrome xung đột.
4. Cạm bẫy "Default" khi sync profile. Chrome hiểu --user-data-dirthư mục User Data gốc. Nếu sync từ …/User Data/Default thì cookie nằm sai tầng và Local State (chứa khoá giải mã cookie) không được chép theo ⇒ bản clone bị đăng xuất. Luôn sync từ …/Chrome/User Data; bỏ trống ô Master Profile Path là an toàn nhất.
5. Library/*Application/* đều bị gitignore. Sửa A12_PlayWright/**, A13_PyAutoGUI_Web/** hay _core/*.py xong thì phải chép tay sang máy worker rồi restart Process Manager GUI. Không có git nào mang giúp. Đối chiếu bằng certutil -hashfile <file> MD5.
6. Nhiều thứ chỉ chạy trên Windows (pygetwindow, ctypes.windll). Trên Linux chúng trả None và code phải suy giảm êm — khi test trên host Linux phải mô phỏng, đừng kết luận "không chạy được".

Lệnh kiểm chứng nhanh chạy trên HP1

Container & mạng

docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
ping -c1 172.16.30.101      # NUC1 (WP3)
ping -c1 172.16.20.201      # NAS2 (WP2)

Fleet Monitor — máy nào đang online?

curl -s http://127.0.0.1:20340/api/state | python3 -m json.tool | head -40

IP Pool qua HTTP (không cần mật khẩu Redis)

curl -s http://127.0.0.1:20300/api/redis/all
curl -s http://127.0.0.1:20300/api/redis/new-ip
curl -s http://127.0.0.1:20300/api/redis/config/auto-move

Redis trực tiếp — mật khẩu lấy từ compose, KHÔNG gõ inline

COMPOSE=Application/APP_1_GREAT_LOTUS_PROJECT/App/A0_DCP_GREATE_LOTUS_NETWORK/DCP_PRODUCTION/docker-compose.yml
export REDISCLI_AUTH=$(grep -m1 -oP '(?<=--requirepass )\S+' "$COMPOSE")
docker exec -e REDISCLI_AUTH lotus_redis redis-cli --no-auth-warning KEYS 'LOTUS:03_PROXY:*'

n8n API — key lấy từ .env

KEY=$(grep -m1 '^N8N_API_KEY=' …/DCP_PRODUCTION/.env | cut -d= -f2-)
curl -s -H "X-N8N-API-KEY: $KEY" http://127.0.0.1:20150/api/v1/workflows?limit=100

NAS2 — cổng nào mở?

for p in 22 139 445 873 5000 5001 5005 6690; do
  timeout 3 bash -c "echo > /dev/tcp/172.16.20.201/$p" 2>/dev/null \
    && echo "$p OPEN" || echo "$p closed"; done
Cần trình duyệt thật (đăng nhập n8n, web UI router ER605 cổng 1024 mã hoá RSA…) ⇒ dùng container B35 (lotus_novnc_chrome): điều khiển bằng Selenium ở http://127.0.0.1:20351, người dùng xem trực tiếp ở http://172.16.10.220:20350/?autoconnect=1&resize=scale.

Danh mục file quan trọng

Việc Đường dẫn
Docker Compose production Application/APP_1_GREAT_LOTUS_PROJECT/App/A0_DCP_GREATE_LOTUS_NETWORK/DCP_PRODUCTION/docker-compose.yml
Caddy (map domain → container) …/DCP_PRODUCTION/B16_Caddy/Caddyfile
Fleet Monitor server (bản chạy) …/DCP_PRODUCTION/B34_GPM_Fleet_Monitor_System/App/gpm_fleet_monitor_server.py
ScanIpProxy (IP pool daemon) …/DCP_PRODUCTION/B30_Python_Proxy_Script/APP_B02_RapidProxyScript/App/ScanIpProxy.py
Web Dashboard IP pool …/APP_B02_RapidProxyScript/WebDashboard/
PRD + Architecture IP pool …/APP_B02_RapidProxyScript/_bamd-ouptut/PRD.md, Doc/architecture.md
Báo cáo thiết kế profile NAS …/DCP_PRODUCTION/B06_MQTT_Broker/www/share/02_Link/UID_00001_profile_optimization_report.html
Worker Process Manager Application/APP_5_GPM_GREAT_LOTUS/App/2_GPM_Multi_Remote_Browser_Process_Manager_GUI/
Node C10 (Playwright) Library/C10_N8N_MQTT_PlayWright/ — 3 file Agent_*.md
Node C12 (PyAutoGUI) Library/C12_N8N_MQTT_PyAutoGUI_Web/ — 4 file Agent_*.md
Node C11 (chỉ GPM) Library/C11_N8N_GPM_Login_Man/
Client Playwright / PyAutoGUI Library/A12_PlayWright/, Library/A13_PyAutoGUI_Web/
Wrapper GPM API Library/B05_GPM_Login_Manager/libWrap.py
Tài liệu mạng docs/network/network-overview.md (+ -WP1/-WP2/-WP3.md, index.html)
Bản Markdown của trang này docs/great-lotus-e2e-architecture.md
Chính file HTML này docs/html_doc/HDID_00001_Great_Lotus_E2E_Architecture.html — bind-mount thẳng vào web server, không có bản thứ hai

Quy ước khi cập nhật tài liệu

Thứ tự ưu tiên nguồn sự thật: thiết bị/API thật > mã nguồn > file cấu hình trong git > sơ đồ/báo cáo HTML.

File config trong repo có thể lệch với máy worker thật (đã gặp: pc_name). Trang HTML này và bản Markdown docs/great-lotus-e2e-architecture.md phải được cập nhật cùng nhau.

Trang này KHÔNG có bản sao. Thư mục docs/html_doc/ trong repo được bind-mount thẳng vào web server (lotus_mqtt, http_dir /mosquitto/www) tại /mosquitto/www/share/02_Link/FID_00005_HTML_DOC ⇒ sửa file trong Git là web đổi ngay, không cần publish. Thêm tài liệu mới: đặt file UID_<5 số>_<Ten>.html vào thư mục đó rồi chạy python3 docs/tools/gen_html_doc_index.py.