Mở đầu nói kết luận
Tôi là một nhà phát triển độc lập (mặc dù không phải loại rất cao cấp). Tôi đã mất một tháng rưỡi để bắt đầu từ con số không và làm ra một phần mềm giám sát tài sản Web3, có tên là “powerpei Web3 哨兵”.
Phần mềm này có thể giám sát chuỗi EVM và chuỗi Solana
Nó tích hợp các chức năng phân tích giao dịch AI, thông báo đa kênh, tổng hợp dữ liệu trên chuỗi.
Tính năng tôi đặc biệt thích: nó có thể giám sát các dự án chó đất trên chuỗi ETH và SOL, còn có thể theo dõi chuyển động của ví thông minh.
Tính năng này cho phép tôi nắm bắt được những cơ hội sớm trên chuỗi theo thời gian thực.
Phần mềm đã hoạt động ổn định được khoảng 1 tháng.
Tôi đã viết một tóm tắt kỹ thuật trước đó.
Tuy nhiên, tôi luôn cảm thấy rằng bài viết đó chưa đủ sâu sắc về việc giải thích cốt lõi.
Vì vậy, tôi quyết định viết bài hồi tưởng sâu sắc này. Tôi sẽ chia sẻ mà không giữ lại những quyết định thiết kế và chi tiết thực hiện được giấu trong mã từ góc độ kiến trúc tiến trình, mô hình I/O, tính nhất quán dữ liệu, hiện thực hóa AI.
Tôi hy vọng bài viết này có thể giúp ích cho những nhà phát triển đang khám phá lĩnh vực Web3.
---
Một, kiến trúc tiến trình: Tại sao tôi chọn 'tiến trình chính + nhiều subprocess'?
Nhiều tập lệnh giám sát Python trên thị trường đều là một tiến trình asyncio.
Tuy nhiên, tôi đã chọn kiến trúc 'tiến trình chính (GUI) + subprocess độc lập (EVM/SOL)' từ đầu.
➤ Tính cách ly và độ ổn định là quan trọng nhất.
Kết nối WebSocket dài hạn rất dễ kích hoạt kết nối lại bất thường khi có sự biến động mạng. Ngay cả các mở rộng C của thư viện bên dưới có thể gặp sự cố vì lý do không xác định.
Nếu bạn kết hợp GUI và logic giám sát trong một tiến trình, bất kỳ ngoại lệ không được bắt hoặc vi phạm truy cập bộ nhớ nào cũng có thể khiến toàn bộ ứng dụng máy tính để bàn bị tắt đột ngột.
Tôi đã tách biệt logic giám sát EVM và Solana thành subprocess bằng cách sử dụng `subprocess.Popen`.
Làm như vậy có hai lợi ích:
Cách ly vật lý: Nếu `http://Evm.py` gặp sự cố hoặc bị giết, cửa sổ chính vẫn hoạt động bình thường. Biểu tượng khay sẽ không biến mất, người dùng có thể nhấp 'Khởi động' để khởi động lại.
Đường truyền nhật ký: Tiến trình chính bắt stdout của subprocess thông qua ống dẫn.
Tôi đã sử dụng luồng `_forward_output` để đọc từng dòng, làm sạch mã màu ANSI.
Sau đó, thông qua `window.evaluate_js`, tôi tiêm vào DOM phía trước. Bằng cách này, nhật ký sẽ được làm mới theo thời gian thực, và luồng UI vẫn giữ nhẹ.
➤ Chi tiết quản lý vòng đời.
Trong `core_process.py`, dừng subprocess không chỉ đơn giản là `terminate()`. Tôi đã triển khai một cơ chế giết mạnh mẽ:
```python
proc.terminate()
proc.wait(timeout=3)
if proc.poll() is None:
proc.kill()
proc.wait(timeout=2)
if proc.poll() is None:
os.system(f'taskkill /F /PID {http://proc.pid}')
```
“''python proc.terminate() proc.wait(timeout=3) Nếu proc.poll() là None: proc.kill() proc.wait(timeout=2) Nếu proc.poll() là None: os.system(f'taskkill /F /PID { http://proc.pid }')
```
Bộ bài này đảm bảo rằng ngay cả khi trình thông dịch Python bị treo, hệ thống Windows vẫn có thể dọn dẹp cây tiến trình hoàn toàn.
Điều này tránh được việc tiến trình còn lại chiếm giữ cổng hoặc khóa cơ sở dữ liệu, dẫn đến việc khởi động lần tiếp theo thất bại.
---
Hai, mô hình I/O và kết nối khả dụng cao: không chỉ là asyncio.
➢ Chế độ giám sát hỗn hợp: WSS thời gian thực + RPC bù.
Đồng tiền gốc của chuỗi EVM không có nhật ký sự kiện Transfer tiêu chuẩn.
Bạn không thể đăng ký qua WSS.
Tôi đã thiết kế một bộ bù định kỳ.
Nó mỗi 60 giây lấy `eth_getBalance` qua RPC, sau đó so sánh với ảnh chụp bộ nhớ. Sự chênh lệch vượt quá 1e-18 sẽ kích hoạt việc gửi.
Cơ chế này có vẻ rất đơn giản.
Nhưng thực ra nó là hàng rào cuối cùng khi đăng ký WSS không hoạt động.
Đối với token và NFT, hệ thống đăng ký `logs`. Tôi đã xử lý cẩn thận sự kiện `TransferSingle` và `TransferBatch` của ERC1155.
Đặc biệt là `TransferBatch`, trường `data` của nó chứa mảng động. Tôi đã thực hiện phân tích độ lệch thủ công dựa trên tiêu chuẩn ABI, thay vì dựa vào thư viện nặng.
Điều này đã giảm đáng kể chi phí phân tích.
➢ Sự giảm chậm của WSS và chuyển đổi nóng giữa các nút.
Trong môi trường sản xuất, các nút RPC/WSS công cộng có thể bị giới hạn hoặc ngừng hoạt động bất cứ lúc nào.
Tôi đã triển khai chiến lược chuyển đổi và kết nối lại cho nút trong `chain_wss_monitor_direction`:
→ Hồ bơi nút: Nhiều `WSS_NODES` được cấu hình cho mỗi chuỗi trong tệp cấu hình.
Chọn một trong số đó một cách ngẫu nhiên hoặc theo thứ tự khi khởi động.
→ Thuật toán giảm chậm: Sau khi ngắt kết nối, khoảng thời gian thử lại bắt đầu từ 5 giây, mỗi lần gấp đôi cho đến 60 giây.
Điều này tránh được cơn bão kết nối lại kiểu DDoS trước khi nút phục hồi.
→ Thích ứng với môi trường mạng trong nước: Hệ thống hỗ trợ cấu hình địa chỉ HTTP của phần mềm proxy địa phương.
Thông qua thư viện `websockets_proxy`, chuyển tiếp lưu lượng WSS tới proxy, khôi phục và duy trì giao tiếp ổn định với các nút nước ngoài.
➢ Dòng phân tích bất đồng bộ của Solana.
Tốc độ tạo khối của Solana rất nhanh và cấu trúc giao dịch phức tạp.
Để tránh việc gọi API Helius làm chậm việc nhận tin nhắn WSS, tôi đã thiết kế một mô hình tách biệt giữa sản xuất và tiêu dùng:
→ Nhà sản xuất: WSS `logsSubscribe` nhận được chữ ký sẽ ngay lập tức đưa nó vào `asyncio.Queue` hoặc trực tiếp kích hoạt một `asyncio.create_task` nền.
→ Người tiêu dùng: Nhiệm vụ bất đồng bộ độc lập chịu trách nhiệm gọi giao diện Helius `/v0/transactions`, phân tích các trường `nativeTransfers`, `tokenTransfers`, `events.nft`, v.v.
Điều này đảm bảo rằng vòng lặp `recv()` của kết nối WSS sẽ không bị chặn bởi các yêu cầu HTTP chậm.
Điều này đảm bảo tính tức thời của tin nhắn trong môi trường TPS cực cao.
---
Ba, tính nhất quán dữ liệu: từ loại bỏ bộ nhớ đến ràng buộc SQLite.
➤ Phía EVM: chỉ mục duy nhất trong cơ sở dữ liệu.
Trong giám sát EVM, một giao dịch có thể được xử lý nhiều lần vì lý do kết nối lại WSS, bù định kỳ, v.v.
Chỉ dựa vào `set` bộ nhớ không thể đối phó với việc khởi động lại tiến trình. Vì vậy, tôi đã thiết kế ràng buộc duy nhất phức hợp cho bảng `tx_history`:
```sql
UNIQUE(tx_hash, log_index, address)
```
Bất kỳ việc chèn trùng lặp nào sẽ bị SQLite `ON CONFLICT IGNORE` lặng lẽ bỏ qua. Điều này đảm bảo tính idempotent từ cấp độ nhân của cơ sở dữ liệu.
Đối với các giao dịch đồng tiền gốc không có `log_index`, tôi đã hạ cấp sử dụng `tx_hash + address` làm khóa kết hợp.
➤ Phía Solana: loại bỏ chữ ký trong khoảng thời gian.
Giao dịch Solana không có khái niệm `log_index`. Hơn nữa, phân tích Helius có thể tạo ra nhiều bản ghi.
Tôi đã sử dụng `set` bộ nhớ để lưu trữ các chữ ký đã xử lý gần đây.
Tôi đã áp dụng ý tưởng biến thể của `LimitedSizeDict`, tự động xóa một nửa khi tập hợp chữ ký vượt quá 1000 (hoặc sử dụng `OrderedDict` để loại bỏ mục cũ nhất).
Cửa sổ trượt này đã đạt được sự cân bằng tốt giữa hiệu suất và độ chính xác.
---
Bốn, hiện thực hóa AI: khả năng chịu lỗi nhiều nhà cung cấp và nghệ thuật phân tích JSON.
➢ Lập lịch động dựa trên đặc điểm.
`MultiAIClient` là cốt lõi của mô-đun AI.
Nó không chỉ là các nhánh if-else đơn giản, mà là một bộ lập lịch dựa trên cờ đặc điểm.
Trong tệp cấu hình, mỗi nhà cung cấp được xác định các chức năng hỗ trợ (như `transaction_insight`, `daily_report`, v.v.).
Khi yêu cầu giải thích, hệ thống sẽ lọc ra danh sách nhà cung cấp hỗ trợ đặc điểm đó và phát động yêu cầu theo thứ tự ưu tiên.
Nếu hết thời gian hoặc thất bại, nó sẽ tự động hạ cấp xuống cái tiếp theo.
➢ Phân tích bảo vệ cho đầu ra LLM.
Đầu ra JSON không ổn định của mô hình lớn là điều bình thường. Quy trình xử lý của tôi phức tạp hơn nhiều so với `json.loads`.
Làm sạch: loại bỏ các dấu hiệu khối mã Markdown ```` ```json ```` và ```` ``` ````.
Trích xuất regex: Nếu phân tích thất bại, tôi sẽ trực tiếp sử dụng biểu thức chính quy `r'"insight"\s*:\s*"([^"]*)"'` để trích xuất trường. Đây là hàng rào cuối cùng.
Ánh xạ trường: tương thích với nhiều tên khóa như `insight` / `Insight` / `解读`.
Cơ chế này đảm bảo rằng ngay cả khi Moonshot hoặc DeepSeek trả về 'nửa JSON', giao diện trước vẫn có thể hiển thị giải thích hợp lệ.
Nó sẽ không ném ra `JSONDecodeError` dẫn đến màn hình trắng.
---
Năm, bộ nhớ và hiệu suất: LimitedSizeDict và hàng đợi bất đồng bộ.
➢ Từ điển có dung lượng hạn chế tùy chỉnh.
Thư viện chuẩn Python không có từ điển LRU tích hợp và bị giới hạn kích thước.
Tôi đã triển khai `LimitedSizeDict` dựa trên `collections.OrderedDict`:
```python
def setitem(self, key, value):
if len(self) >= self.max_size:
self.popitem(last=False) # Loại bỏ mục được chèn sớm nhất.
super().__setitem__(key, value)
```
Cấu trúc dữ liệu đơn giản này được sử dụng để lưu trữ thông tin Token, lưu trữ giá cả, và lưu trữ siêu dữ liệu NFT.
Nó đảm bảo rằng trong thời gian chạy dài, mức sử dụng bộ nhớ sẽ không tăng theo dạng tuyến tính với số lượng địa chỉ giám sát.
Mức sử dụng bộ nhớ ổn định ở mức rất thấp.
➢ Ghi nhật ký bất đồng bộ cho Solana.
Nhật ký giao dịch chi tiết của Solana cần được ghi vào tệp JSON.
Nếu nhiều coroutine cùng lúc `json.dump`, rất dễ dẫn đến hỏng định dạng tệp.
Tôi đã đưa vào `asyncio.Queue`:
- Tất cả yêu cầu ghi nhật ký đưa dữ liệu vào hàng đợi.
- Chỉ có một coroutine nền `_file_writer` chờ đợi hàng đợi, lấy dữ liệu và thực hiện I/O tệp.
Điều này vừa tránh được các khóa luồng phức tạp, vừa đảm bảo an toàn dữ liệu dưới đặc tính bất đồng bộ trong điều kiện cao.
---
Sáu, giao tiếp hai chiều giữa frontend và backend: tích hợp sâu pywebview.
➤ Tiêm API JS.
`pywebview` cho phép bạn trực tiếp phơi bày các phương thức của đối tượng Python cho JavaScript phía trước.
Lớp `Api` của tôi kế thừa từ nhiều Mixin.
Tất cả các phương thức công cộng bắt đầu bằng `def` đều tự động trở thành thành viên của `window.pywebview.api`.
Điều này cho phép tôi thực hiện tách biệt frontend và backend với chi phí rất thấp. Frontend chỉ cần chú ý đến tương tác UI.
➤ Các phiên bản độc lập của cửa sổ nổi và giao tiếp.
Cửa sổ nổi không phải là một DIV con của cửa sổ chính, mà là một cửa sổ độc lập thứ hai do `pywebview` tạo ra.
Tôi quản lý vòng đời của nó thông qua `FloatingWindowManager`. Tôi sử dụng phiên bản `js_api` của tiến trình chính để tiêm mã JS vào cửa sổ nổi (`evaluate_js`).
Điều này thực hiện việc đẩy nhật ký từ cửa sổ chính đến cửa sổ nổi theo thời gian thực.
Thiết kế này đảm bảo rằng cửa sổ nổi sẽ không ảnh hưởng đến việc chạy nhiệm vụ giám sát chính ngay cả khi bị đóng.
---
Bảy, đóng gói ứng dụng máy tính để bàn: những cái bẫy sâu của PyInstaller và thực hành hiện đại.
Để giao dự án Python cho người dùng cuối không có nền tảng kỹ thuật, bạn phải đóng gói thành EXE độc lập.
PyInstaller có vẻ đơn giản chỉ cần một lệnh. Nhưng trong các dự án phức tạp, nó ẩn chứa vô số chi tiết có thể khiến nhà phát triển sụp đổ.
Trong phần này, tôi chia sẻ một số cái bẫy điển hình và giải pháp mà tôi đã gặp trong quá trình đóng gói 'powerpei Web3 sentinel'.
➤ Nhập ẩn và --hidden-import
PyInstaller thông qua phân tích tĩnh câu lệnh `import` trong tệp đầu vào để xây dựng cây phụ thuộc.
Tuy nhiên, nhiều thư viện (như `pystray`, `websockets`) sử dụng `importlib.import_module` hoặc `__import__` để tải động các mô-đun con. Điều này dẫn đến việc tệp EXE sau khi đóng gói bị ném ra lỗi `ModuleNotFoundError` khi chạy.
Giải quyết vấn đề này là cần xác định ngược lại mô-đun bị thiếu dựa trên thông báo lỗi.
Sau đó, trong lệnh đóng gói, hãy khai báo rõ ràng `--hidden-import`.
Ví dụ, trong dự án này, chức năng biểu tượng khay phải được thêm vào:
```bash
--hidden-import pystray._win32
--hidden-import pystray._util
--hidden-import win32event
--hidden-import win32api
```
Điều này yêu cầu nhà phát triển phải có một hiểu biết nhất định về cấu trúc nội bộ của thư viện phụ thuộc.
Thông thường, cần kết hợp đọc mã nguồn và thử nghiệm lặp đi lặp lại để liệt kê đầy đủ tất cả các phụ thuộc ẩn.
➤ --collect-all và bẫy tệp tài nguyên
`pywebview` không chỉ chứa mã Python
mà còn phụ thuộc vào tệp tài nguyên chạy HTML/JS và Edge WebView2.
Phân tích mặc định của PyInstaller không thể nhận biết những tài nguyên không phải mã này.
Nếu bạn không xử lý, chương trình sau khi đóng gói sẽ gặp phải màn hình trắng vì không tìm thấy `index.html` hoặc `webview.js`.
Cách xử lý đúng là sử dụng `--collect-all pywebview`.
Tham số này buộc PyInstaller sao chép tất cả các tệp trong thư mục gói `pywebview` (bao gồm tệp nhị phân và tài nguyên tĩnh) vào thư mục gói.
Đây là thao tác tiêu chuẩn để xử lý các thư viện GUI 'nặng' như vậy.
➤ Phụ thuộc nhị phân và nén UPX
Các thư viện phụ thuộc của dự án như `pywin32`, `Pillow` chứa các tệp nhị phân `.pyd` và `.dll`.
Những tệp này có kích thước tương đối lớn và sẽ không được tự động nén sau khi PyInstaller đóng gói.
Bằng cách tích hợp công cụ UPX và chỉ định `--upx-dir` trong lệnh đóng gói, bạn có thể nén các tệp nhị phân bên trong EXE cuối cùng với tỷ lệ cao (thường có thể giảm 30%-50% kích thước).
Cần lưu ý rằng một số phần mềm diệt virus cũ có thể đưa ra thông báo sai đối với các chương trình được bọc UPX.
Tuy nhiên, đối với nhóm người dùng có kỹ thuật, xác suất này rất thấp và có thể được khắc phục bằng cách gửi mẫu.
➤ Đường dẫn 'đóng băng' và sys._MEIPASS
Đây là khái niệm cốt lõi nhất trong đóng gói PyInstaller.
Trong giai đoạn phát triển, chương trình truy cập tệp cấu hình, tài nguyên hình ảnh thông qua `__file__` hoặc đường dẫn tương đối.
Sau khi được đóng gói thành một tệp EXE đơn, tất cả tài nguyên sẽ được giải nén vào một thư mục tạm thời.
Đường dẫn của thư mục này được lưu trữ trong biến `sys._MEIPASS`.
Nhà phát triển phải thay thế toàn cầu tất cả logic truy cập tệp trong mã. Mô hình điển hình như sau:
```python
def get_resource_path(relative_path):
if getattr(sys, 'frozen', False):
base = sys._MEIPASS
else:
base = os.path.abspath(".")
return os.path.join(base, relative_path)
```
Bỏ qua sự điều chỉnh này sẽ dẫn đến việc chương trình không thể tìm thấy bất kỳ tệp bên ngoài nào khi chạy.
Đây là điều mà những người mới thường gặp phải nhiều nhất trong xã hội đường dẫn.
Cách của tôi là gói hàm này trong `http://utils.py`, gọi chung trong toàn bộ dự án. Điều này đảm bảo hành vi đường dẫn nhất quán trước và sau khi đóng gói.
➤ Xử lý đặc biệt cho subprocess
Hệ thống này áp dụng kiến trúc khởi động subprocess từ tiến trình chính.
Sau khi đóng gói, các tập lệnh subprocess `http://Evm.py` và `http://Sol.py` cũng được đóng gói bên trong EXE.
Nếu tiến trình chính vẫn cố gắng khởi động bằng `python.exe http://Evm.py`, sẽ thất bại vì không tìm thấy tệp.
Giải pháp của tôi là khi tiến trình chính khởi động subprocess, phát hiện động xem có đang ở chế độ đóng gói hay không.
Sau đó, truyền tham số `--main-exe-dir` chính xác, để subprocess có thể xác định vị trí thư mục bên ngoài nơi tệp cấu hình nằm.
Chi tiết logic phần này tôi đã đề cập trong chương kiến trúc tiến trình, không nhắc lại ở đây.
---
Tám, cuối cùng nói vài câu
Tôi đã nhìn lại toàn bộ quá trình phát triển
Từ một tập lệnh đơn đến kiến trúc đa tiến trình, từ WebSocket trần trụi đến nhóm nút có khả năng cao, từ nhật ký print đơn giản đến lưu trữ SQLite có cấu trúc, mỗi bước đều là sự sâu sắc hơn trong hiểu biết về kỹ thuật.
Khám phá trong giai đoạn đóng gói càng khiến tôi cảm nhận sâu sắc rằng: chỉ cần phần mềm chạy ổn định là bước đầu tiên, làm cho người dùng dễ dàng sử dụng mới là điều thực sự giao hàng.
Đây không chỉ là một công cụ giám sát, mà còn là tập hợp kinh nghiệm thực hành của tôi trong lập trình bất đồng bộ, quản lý tiến trình, tích hợp AI, phát triển phần mềm máy tính để bàn.
Nếu bạn quan tâm đến bất kỳ chi tiết kỹ thuật nào trong bài viết, rất hoan nghênh bạn ghé thăm GitHub của tôi (bút danh của tôi trên Juejin, Diyan, Zhihu là 潇楠).
---
Tác giả: Powerpei
Người phát triển độc lập toàn diện & Web3, chuyên về ứng dụng máy tính để bàn Python, phân tích dữ liệu blockchain và hiện thực hóa AI.
