electron 开发中运行失败

解决方案 pnpm exec node node_modules/electron/install.js

查 Solana 钱包交易:Solscan API vs 原生 RPC

Solana 生态里,Solscan 是最常用的区块浏览器(类似以太坊的 Etherscan)。除了网页查询,它也有 API——加上 Solana 原生 RPC,就够做钱包监控 / 交易分析 / DeFi 数据抓取了。 Solscan 网页能查什么交易:交易 hash → 转账明细、手续费、状态 钱包地址:余额、SPL Token、NFT、历史交易 区块:高度、出块时间、确认数 代币:合约详情、持币地址、24h 交易量 NFT:铸造、转移 DeFi:部分 DEX / 质押 / 借贷协议数据日常查东西直接搜地址或 hash 就行。要批量、定时、集成到自己系统里就得走 API。 Solscan Pro API 老公共 API public-api.solscan.io 现在已经不推荐了,改用 Pro API pro-api.solscan.io(免费额度也有)。 账户交易列表: GET https://pro-api.solscan.io/v2.0/account/transactions?address=<WALLET>&limit=10 Header: token: <API_KEY>账户详情(余额、类型): GET https://pro-api.solscan.io/v2.0/account/detail?address=<WALLET>代币持仓: GET https://pro-api.solscan.io/v2.0/account/token-accounts?address=<WALLET>去 pro-api.solscan.io 申请免费 API Key。速率限制看套餐,免费大概 60 次/分钟。 Solana 原生 RPC(更灵活) 不想被限流、想拿完整交易数据,走 JSON-RPC。任何 Solana 节点都支持。 列钱包最近交易签名: POST https://api.mainnet-beta.solana.com Content-Type: application/json{ "jsonrpc": "2.0", "id": 1, "method": "getSignaturesForAddress", "params": [ "<WALLET>", { "limit": 20, "commitment": "finalized" } ] }查单笔交易详情: { "jsonrpc": "2.0", "id": 1, "method": "getTransaction", "params": [ "<SIGNATURE>", { "encoding": "jsonParsed", "maxSupportedTransactionVersion": 0 } ] }jsonParsed 编码会把系统程序、Token 程序的指令解析成人可读的 JSON,比 base58 好读多了。 Python 版本:定时监控新交易 import time import requestsRPC = "https://api.mainnet-beta.solana.com" WALLET = "<你的钱包地址>"seen = set()def rpc(method, params): r = requests.post(RPC, json={ "jsonrpc": "2.0", "id": 1, "method": method, "params": params, }, timeout=10) return r.json().get("result")def poll(): sigs = rpc("getSignaturesForAddress", [WALLET, {"limit": 20}]) or [] for s in reversed(sigs): sig = s["signature"] if sig in seen: continue seen.add(sig) tx = rpc("getTransaction", [ sig, {"encoding": "jsonParsed", "maxSupportedTransactionVersion": 0}, ]) if not tx: continue block_time = tx.get("blockTime") err = tx["meta"]["err"] print(f"新交易 {sig[:16]}... block_time={block_time} err={err}") # 这里根据业务解析 innerInstructions 判断是转账 / swap / mintwhile True: try: poll() except Exception as e: print("poll error:", e) time.sleep(15)要点:seen 记忆已处理签名,避免重复 reversed() 让最老的先出,保持时间顺序 生产上把 seen 落地到 Redis / SQLite,重启不丢免费 RPC 的坑 api.mainnet-beta.solana.com 是 Solana Labs 的公共节点,速率限制很紧,高频请求会 429。生产环境走托管 RPC:Helius — 免费 10 万请求/月 QuickNode Alchemy Solana Chainstack免费额度足够个人项目。 拿 SPL Token 余额 { "jsonrpc": "2.0", "id": 1, "method": "getTokenAccountsByOwner", "params": [ "<WALLET>", { "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" }, { "encoding": "jsonParsed" } ] }programId 是 SPL Token 程序的地址,返回该钱包持有的所有 SPL Token 账户,包含 mint 地址、余额、decimals。 一句话总结 Solscan API 好用但有限流,Solana 原生 RPC 更自由。核心组合 getSignaturesForAddress + getTransaction,生产环境别用免费公共节点,走 Helius / QuickNode。

Plasmo 浏览器扩展项目 tsconfig 与 `~` 别名

用 Plasmo 写 Chrome 扩展项目,脚手架给的 tsconfig.json 是这样: { "extends": "plasmo/templates/tsconfig.base", "exclude": ["node_modules"], "include": [ ".plasmo/index.d.ts", "./**/*.ts", "./**/*.tsx" ], "compilerOptions": { "baseUrl": ".", "paths": { "~*": ["./*"] } } }一份不到 20 行,但每一行都干活。 每行在做什么extends: "plasmo/templates/tsconfig.base" — 继承 Plasmo 的严格模式基线(strict、esModuleInterop、jsx: react 等),只覆盖必要项 exclude: ["node_modules"] — 不给 TS 检查依赖 include: [".plasmo/index.d.ts", ...] — 包含 Plasmo 生成的类型声明和项目源码 baseUrl: "." — 相对根目录解析 paths: { "~*": ["./*"] } — ~utils 会被解析成 ./utils想把代码搬进 src/ Plasmo 默认允许代码散在根目录(popup.tsx、background.ts 直接扔外面)。你想集中放 src/ 下,tsconfig 需要相应改: { "extends": "plasmo/templates/tsconfig.base", "exclude": ["node_modules"], "include": [ "src/**/*.ts", "src/**/*.tsx", ".plasmo/index.d.ts" ], "compilerOptions": { "baseUrl": "src", "paths": { "~*": ["./*"], "@*": ["./*"] } } }三处变化:include 从 ./** 改成 src/** baseUrl 从 . 改成 src paths 里 ~ 和 @ 都指到 src 根之后代码里: import Button from "~/components/Button"; import { fetchUser } from "@/api/user";会被解析为 src/components/Button.tsx 和 src/api/user.ts。 Plasmo 的 url: 前缀 Plasmo 基于 Parcel 打包,导入 HTML 或图片资源有个特殊语法: import fontPickerHTML from "url:./panels/font-picker/index.html";url: 前缀告诉 Parcel"不要把这个当代码解析、当静态资源处理,导出打包后的 URL"。编译产物大致是: const fontPickerHTML = "/assets/font-picker.abc123.html";用途:chrome.windows.create({ url: fontPickerHTML }) 打开一个扩展页面。 代码搬进 src 后要配合修改: import fontPickerHTML from "url:~/panels/font-picker/index.html";因为 ~ 现在指向 src/。 除了 url: 还有 Parcel 支持多个前缀,Plasmo 都能用:url: — 当资源,返回 URL 字符串 data-url: — 编码成 base64 data URL raw: — 读文件原始内容为字符串 bundle: — 把整个 JS 打成单独 bundle data-text: — 直接嵌入文本例如把某个 css 内联进背景 script: import css from "raw:~/styles/inject.css"; chrome.scripting.insertCSS({ target: { tabId }, css });Chrome 扩展 tsconfig 常见字段 Plasmo base 已经配好,但了解一下: { "compilerOptions": { "target": "ES2020", "module": "ESNext", "moduleResolution": "Bundler", "jsx": "react", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "types": ["chrome"] // 让 chrome.* API 有类型 } }types: ["chrome"] 是关键——扩展项目里 chrome.storage、chrome.runtime 得靠它。Plasmo base 里默认带了。 一句话总结 Plasmo 的 ~ 别名跟着 baseUrl 走:搬到 src/ 就把 baseUrl 改成 src、include 换成 src/**。url: 前缀是 Parcel 特有的静态资源导入,改路径时别忘了同步。

Python logging 同时输出到文件和控制台

Python 的 logging.basicConfig 有一个常见坑:一旦指定了 filename,默认只写文件,不往控制台打印。 问题复现 logging.basicConfig( filename="app.log", level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s" )print("print 能看到") logging.info("logger 看不到") # 只写文件,控制台不显示同时输出到文件和控制台 需要显式添加 StreamHandler: import logging import sys from datetime import datetimedef init_logger(log_file="app.log"): logger = logging.getLogger() logger.setLevel(logging.INFO) fmt = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s") # 文件输出 fh = logging.FileHandler(log_file, encoding="utf-8") fh.setFormatter(fmt) logger.addHandler(fh) # 控制台输出 ch = logging.StreamHandler(sys.stdout) ch.setFormatter(fmt) logger.addHandler(ch) return logger或者用 basicConfig + 手动追加: logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", filename="app.log", encoding="utf-8" )# 追加控制台 Handler console = logging.StreamHandler() console.setLevel(logging.INFO) console.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")) logging.getLogger().addHandler(console)带结构化参数的 log 方法 class MyService: def __init__(self): self.logger = logging.getLogger(self.__class__.__name__) def log(self, *args, **kwargs): msg = " ".join(str(a) for a in args) if kwargs: extra = " ".join(f"{k}={v!r}" for k, v in kwargs.items()) msg = f"{msg} | {extra}" self.logger.info(msg)# 使用 svc = MyService() svc.log("任务完成", task_id=42, duration=1.5) # 输出: 任务完成 | task_id=42 duration=1.5PyInstaller --noconsole 下看不到日志 用 --noconsole 打包的 exe 绑定到 Windows subsystem,stdout / stderr 不存在,StreamHandler 无输出目标。 解决方案: 1. 只写文件,用 tail 等工具实时查看 # PowerShell(Windows 自带,等价于 tail -f) Get-Content app.log -Tail 50 -Wait2. 自定义 GUI 日志窗口(Tkinter) import tkinter as tk from tkinter.scrolledtext import ScrolledTextclass TextHandler(logging.Handler): def __init__(self, widget): super().__init__() self.widget = widget def emit(self, record): msg = self.format(record) self.widget.insert(tk.END, msg + "\n") self.widget.see(tk.END)root = tk.Tk() text = ScrolledText(root, width=80, height=20) text.pack()handler = TextHandler(text) handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")) logging.getLogger().addHandler(handler)logging.info("程序启动") root.mainloop()3. 调试 / 发布双版本 :: build.bat pyinstaller --onefile --console --name=控制器_debug scripts.py pyinstaller --onefile --noconsole --name=控制器 scripts.py开发用 debug 版,用户用 release 版。

TUN/TAP 虚拟网卡与 VPN 代理:TUN0 路由拦截与 TAP0901 驱动

TUN vs TAP特性 TUN TAP模拟对象 IP 网络接口 以太网卡工作层 第三层(IP 层) 第二层(数据链路层)处理单元 IP 数据包 以太网帧支持协议 仅 IP IP、ARP、广播等典型用途 全局路由代理 局域网桥接CPU 开销 较小 较大TUN 适合"全部流量走 VPN",TAP 适合"需要访问远程 LAN 内设备"的桥接场景。 TUN 模式的流量拦截 VPN 客户端启用 TUN 模式的流程:创建虚拟网卡 tun0 修改系统路由表,将默认路由(0.0.0.0/0)指向 tun0 所有出站 IP 包经过 tun0 → VPN 客户端加密封装 → 通过真实网卡发送到 VPN 服务器[本地应用] | (IP 包) v [tun0 虚拟网卡] | (加密封装) v [VPN 客户端程序] | v [VPN 服务器 -> 目标网络]TUN 全局模式会代理 localhost 开启全局代理时,若未排除环回地址,127.0.0.1 的流量也会被 tun0 拦截,导致本地服务访问失败。 排除 localhost(OpenVPN 配置) route 127.0.0.1 255.0.0.0 net_gateway或在客户端设置中勾选"分流模式",只代理外网 IP。 TAP0901 驱动(Windows) TAP-Windows Adapter V9(tap0901)是 OpenVPN 在 Windows 上使用的虚拟以太网卡驱动,安装后在网络适配器列表中以普通网卡形式出现。 驱动通过设备文件暴露接口: \\.\Global\{TAP-Adapter-GUID}.tapJava 通过 JNA 读写数据包 // 打开设备文件 HANDLE h = Kernel32.INSTANCE.CreateFile( "\\\\.\\Global\\{GUID}.tap", WinNT.GENERIC_READ | WinNT.GENERIC_WRITE, 0, null, WinNT.OPEN_EXISTING, WinNT.FILE_ATTRIBUTE_NORMAL, null );// 读取以太网帧 byte[] buf = new byte[2048]; IntByReference bytesRead = new IntByReference(); Kernel32.INSTANCE.ReadFile(h, buf, buf.length, bytesRead, null);// 写入以太网帧 Kernel32.INSTANCE.WriteFile(h, buf, bytesRead.getValue(), bytesRead, null);操作虚拟网卡需要管理员权限。TAP0901 只在 Windows 上可用;Linux 和 macOS 使用系统内置的 TUN/TAP 接口。 实现 VPN 客户端的选型目标 方案 难度仅代理指定应用流量 纯 Socket / SOCKS5 代理 低全局系统流量拦截 TUN/TAP 驱动 + JNI/JNA 高Java 只能做用户空间的协议与加密逻辑;全局流量拦截必须借助内核级虚拟网卡驱动(TUN/TAP)。