项目地址:github.com/ChinaCarlos...
文档中心:chinacarlos.github.io/fund-helper...
Releases 下载:github.com/ChinaCarlos...

免责声明
- 本项目仅用于个人学习、研究和技术交流。项目中对养基宝
browser-plug-api的理解来自对公开浏览器插件网络交互的学习与反向工程分析;本项目非养基宝官方产品,与养基宝或任何第三方机构不存在从属、授权、合作或背书关系。请勿用于商业化、批量抓取、绕过限制或任何违反平台协议、法律法规的用途;因使用本项目或二次开发产生的任何责任与项目作者无关。- 项目展示的基金、股票、指数、收益等金融数据仅用于软件功能展示和技术研究,不构成投资建议、理财推荐或收益承诺。基金/股票有风险,投资需谨慎,请自行判断并承担风险。
如果你也有长期看基金、做定投、关注持仓收益的习惯,大概率遇到过一个很真实的问题:数据分散在不同平台里,想看今天整体赚了多少、哪个账户拖了后腿、哪些基金贡献最大,经常要来回打开 App、切账户、刷新、算收益。
我做的这个项目叫 Fund Helper 。它最初只是一个基于养基宝 browser-plug-api 的基金收益实时监控面板,后来逐步演进成了一个多端产品:Web 应用、Chrome 浏览器插件、VS Code/Cursor 扩展、JetBrains 插件,以及 Tauri 桌面端。它的目标不是替代交易软件,而是把"看持仓、看收益、看市场、收通知"这件事做得更集中、更轻、更适合日常使用。
这篇文章会从产品愿景、功能模块、系统架构、关键实现和工程化几个角度,复盘这个项目是怎么做出来的。
一、为什么要做 Fund Helper
基金投资有一个特点:决策频率不一定高,但关注频率往往很高。
很多人每天并不会频繁买卖基金,但会关注几个问题:
- 今天我的总收益是多少?
- 支付宝、天天基金、蛋卷等不同账户分别表现如何?
- 哪些基金在涨,哪些基金在跌?
- 市场风格现在偏向哪些行业或主题?
- 能不能在收益变化时通过钉钉、飞书、企业微信收到提醒?
- 能不能不打开手机,直接在电脑、浏览器或 IDE 里看一眼?
这就是 Fund Helper 的出发点:围绕"基金持仓状态"构建一个跨端、轻量、可部署、可扩展的信息面板。
它有三个明确目标。
第一,降低查看成本。用户不需要频繁打开手机 App,也不需要在多个页面之间切换。打开 Web、浏览器插件、桌面端或者编辑器侧边栏,就能看到核心持仓状态。
第二,统一数据表达。养基宝接口返回的是比较原始的数据,不同字段在不同基金类型上还有差异。项目会把账户、基金、指数、收益曲线、涨跌方向等统一归一化,再交给各端展示。
第三,做成可长期演进的工程,而不是一次性脚本。项目中不仅有 Web 应用,还有独立插件、桌面端、IDE 扩展、Docker 部署、文档站和发版脚本,让它更接近一个完整产品。
二、它现在能做什么
Fund Helper 当前提供五种使用方式。
| 使用方式 | 适合场景 | 特点 |
|---|---|---|
| Web 应用 | 完整功能面板 | 持仓、市场排行、板块热力图、通知、多用户管理 |
| 浏览器插件 | 快速查看 | Chrome/Edge 工具栏 Popup,直连养基宝,无需部署后端 |
| VS Code/Cursor 扩展 | 写代码时顺手看一眼 | 侧边栏、底部面板、状态栏多入口,适配编辑器主题 |
| JetBrains 插件 | IntelliJ/WebStorm/PyCharm 用户 | Tool Window + 状态栏 + JCEF Webview |
| 桌面端 | 本地单用户使用 | Tauri + Rust + SQLite,支持托盘和本地通知配置 |
核心功能模块包括:
- 微信扫码登录养基宝:通过二维码获取登录态,持仓相关接口基于 token 访问。
- 持仓 Dashboard:展示总资产、当日收益、收益率、上涨/下跌基金数量。
- 多账户分组:支持按支付宝、天天基金、蛋卷等账户分组查看。
- 基金明细表格:展示基金代码、名称、持仓金额、成本、当日收益、涨跌幅等。
- 收益曲线:拉取当日分时收益曲线,并支持汇总曲线与账户独立曲线。
- 基金搜索与持仓管理:支持搜索基金、添加持仓、删除持仓。
- 市场排行:基于 AKShare/东方财富数据展示全市场基金排行。
- 板块热力图:查看行业/概念板块涨幅、资金流向,并下钻关联基金。
- 通知推送:支持钉钉、飞书、企业微信,支持 Webhook 和部分企业应用投递模式。
- 多用户和部署:Web 端支持管理员账号、多用户隔离、MongoDB 持久化、Docker 一体部署。
如果只是看一眼持仓,插件和 IDE 扩展足够轻;如果要看市场、配置通知、多用户使用,Web 应用更完整;如果希望本地独立运行,桌面端更合适。
三、整体架构:不是一个页面,而是一套多端运行模型
Fund Helper 的核心不是某一个页面,而是一套围绕"基金持仓快照"的运行模型。不同客户端的容器不同,但都会经过类似的链路:启动、检查登录态、扫码获取 token、拉取上游数据、归一化为快照、渲染 UI、处理刷新和失效。
整体架构可以这样看:
这里做了一个很关键的取舍:Web 端保留完整后端能力,插件、桌面端、编辑器插件则尽量独立。
Web 端需要多用户、通知配置、市场排行、热力图和 Docker 部署,所以它走 FastAPI BFF + MongoDB。浏览器插件和编辑器插件强调打开即用,如果还要求用户先部署后端,使用门槛会变高,所以它们直连养基宝。桌面端同样不依赖 Web 后端,但把 token、通知配置和推送节流信息放进本地 SQLite。
这就是这个项目的基本原则:核心业务模型统一,运行容器按端选择。
四、Web 应用:BFF 承担"复杂性收口"
Web 应用是功能最完整的一端。它使用 React 19、TypeScript、Rsbuild、Ant Design 做前端,后端使用 FastAPI、httpx、Motor、MongoDB、AKShare。
它的实现流程不是"前端直接调一堆第三方接口",而是用 BFF 把第三方接口的不稳定性、签名、token、多用户隔离、通知配置全部收口。
Web 端完整形态如下:顶部是导航和刷新入口,中间是指数与汇总卡片,下面按账户 Tab 展示收益曲线和基金明细。页面本身只消费后端组装好的快照,不直接理解养基宝原始字段。

BFF 层的价值在这里很明显。
第一,它隐藏了养基宝的签名规则。上游请求需要 Authorization、Request-Time、Request-Sign,签名由路径、token、时间戳和密钥组合后 MD5 得到。前端不需要知道这些细节,只关心"我要一个持仓快照"。
第二,它把多个上游结果组合成一个页面可直接消费的模型。持仓页需要总资产、今日收益、各账户明细、基金列表、指数行情、交易时段标识。后端会拉取汇总、账户基金和指数,再统一组装成 PortfolioSnapshot。
第三,它承载 Web 端的扩展能力。市场排行和板块热力图来自 AKShare/东方财富,通知配置存在 MongoDB,推送前复用同一套持仓快照能力。这些都不适合塞进前端。
五、浏览器插件:400×600 Popup 里的轻量客户端
浏览器插件的定位是"看一眼就够"。它使用 CRXJS、Vite、React 19、TypeScript、Manifest V3、chrome.storage.local,不依赖 FastAPI 和 MongoDB。
未登录时,Popup 会直接进入微信扫码页。这里的技术点是:插件端不经过后端创建登录会话,而是在 Popup 内生成二维码、轮询扫码状态,并在成功后把 token 写入 chrome.storage.local。

它的运行流程如下:
这端有几个实现点值得单独说。
首先,插件的状态机很简单:boot -> login -> portfolio。启动时读取 chrome.storage.local,没有 token 就进入扫码登录,有 token 就直接拉持仓。请求返回 401 时清空本地 session,回到登录态。
其次,插件没有引入后端,所以签名、字段归一化、交易时段判断和持仓快照聚合都放在前端 TypeScript 里完成。这样用户加载插件就能用,但代价是关键算法需要和 Web 后端保持同步。
最后,Popup 空间非常有限。Chrome Popup 高度通常控制在 600px 左右,所以插件端没有做市场排行、热力图、复杂通知配置,而是聚焦在指数、收益卡片、多账户 Tab、基金排序这些最常用信息。
登录后,插件会把指数、总资产、当日收益、涨跌数量、账户 Tab、排序器和基金列表压缩进一个轻量面板里。这里没有服务端状态,刷新、排序、401 失效回退都在插件端完成。

六、桌面端:Tauri 把 React UI 和 Rust 本地能力接起来
桌面端的定位是"本地常驻 + 原生体验"。它使用 Tauri v2、Rust、React 19、Ant Design、Tailwind v4、SQLite。相比浏览器插件,它可以做更多本地能力:系统托盘、单实例、后台定时任务、本地通知配置、飞书/钉钉/企微推送。
桌面端除了主窗口,还做了 macOS 菜单栏/状态栏入口:菜单栏直接显示当日收益和收益率,点击后弹出轻量持仓面板。这个入口适合"工作中扫一眼",不需要切到完整应用窗口。

完整主窗口则承载更完整的 Dashboard:指数、资产卡片、账户分组、收益曲线和基金列表都在一个原生窗口里展示。前端仍然是 React,但所有登录态、持仓拉取和通知配置都通过 Tauri 命令交给 Rust 层处理。

桌面端不是 React 直接发 HTTP,而是通过 Tauri invoke 调用 Rust 命令。
它的关键实现分层如下:
- React UI:负责页面、设置、主题和用户交互。
- Tauri Commands:提供
create_qr、poll_qr_state、fetch_portfolio、save_notification_config等命令。 - Rust 业务层:负责养基宝签名请求、持仓聚合、收益曲线归一化、通知编排。
- SQLite:保存 token、通知配置、推送节流状态。
- 托盘/菜单栏:把"今日收益"这种高频信息放到更轻的位置。
桌面端的优势在于本地能力强,不需要部署服务;难点在于 Web 逻辑要在 Rust 中再实现一套。为了避免多端数据不一致,项目把净值字段优先级、当日收益计算、交易时段判断等逻辑都尽量与 Web 后端保持同构。
七、编辑器插件:Webview 只是 UI,真正请求发生在宿主进程
编辑器插件分为 VS Code/Cursor 扩展和 JetBrains 插件。两者 UI 都使用 React Webview,但宿主完全不同。
VS Code/Cursor 扩展是 TypeScript Extension Host + Webview。JetBrains 插件是 Kotlin Host + JCEF Webview。它们共同的原则是:Webview 负责展示,宿主进程负责网络、持久化和状态同步。
在插件市场里,它作为一个独立扩展安装。用户搜索 fund-helper 后安装,后续在 VS Code、Cursor、Trae、CodeBuddy、Qoder 等兼容 VS Code 扩展生态的编辑器里使用。

VS Code/Cursor 扩展流程:
这里有一个重要原因:VS Code Webview 有严格 CSP,不能把它当普通网页随便直连外网。项目把养基宝请求放在 Extension Host 里,Webview 和 Host 之间只通过 postMessage 传递 boot、startLogin、pollQr、refresh、logout 等消息。
登录后的编辑器端并不是只有一个侧边栏。项目把同一份 lastSnapshot 同步给侧边栏、底部面板、编辑器标题栏入口和状态栏,用户可以在不同工作流位置查看同一份持仓数据。

JetBrains 插件流程类似,只是桥接层换成 JCEF 和 Kotlin:
编辑器插件最大的挑战不是接口调用,而是宿主环境约束:
- VS Code 需要处理 Webview CSP、资源 URI 重写、nonce、
globalState存储。 - JetBrains 需要处理 JCEF 资源加载、JS bridge 注入、Tool Window 生命周期、状态栏 Widget。
- 两端都要处理多个入口共享同一份快照:侧边栏、底部面板、状态栏不能各自乱刷新。
所以项目里有一个控制器角色:VS Code 是 FundHelperController,JetBrains 也是类似的 Controller。它们都负责保存 lastSnapshot、管理自动刷新、把结果广播给所有 UI 容器。
八、业务模型:多端可以不同,快照必须统一
这个项目里最有价值的部分之一,是对原始基金数据的归一化。
养基宝返回的基金净值字段并不总是稳定。例如部分 QDII、港股相关基金,常规字段 gszzl、zsgzzl 可能为空,真实可用的估值涨跌幅在 vgszzl。如果客户端只读一个字段,很容易导致交易时段涨跌幅为空或收益估算错误。
项目里统一做了这样的优先级:
| 归一化目标 | 字段优先级 |
|---|---|
| 估算涨跌幅 | gszzl -> zsgzzl -> vgszzl |
| 公布涨跌幅 | jzzzl -> rzzl |
| 估算净值 | gzjz -> zsgz -> gsz -> vgsz |
| 当日收益 | money * rate / 100 |
这类逻辑看起来不显眼,但它决定了数据是否可信。尤其是多端项目,如果 Web、浏览器插件、桌面端、IDE 扩展各算各的,很容易出现"同一个基金在不同端收益不一致"的问题。
因此项目把关键计算逻辑在各端保持对齐:
- Web 后端:
backend/app/yjb/calculator.py - 浏览器插件:
chrome-extension/src/lib/portfolio.ts - VS Code 扩展:
vscode-extension/src/portfolio.ts - 桌面端:
desktop/src-tauri/src/portfolio.rs
这也是多端项目的一个经验:UI 可以因端而异,但业务计算必须尽量一致。
九、收益曲线:接口只是入口,重点是账户维度和图表取舍
收益曲线这一块最初看起来只是"调一个接口然后画线",真正实现时才发现,关键不在接口本身,而在两个问题:如何拿到不同账户自己的曲线,以及如何在多端里用足够轻的方式展示。
实现流程如下:
这里有一个实测结论:如果只按单个账户参数去取,可能拿到的仍然是汇总曲线;要稳定拿到账户独立曲线,需要按账户 ID 数组去拉。这个结论最后沉淀到了 Web 后端、桌面端 Rust 和各端文档里。
图表层也做了克制。项目没有引入 ECharts,而是自研 SVG 曲线。原因是收益分时曲线的数据量有限,交互也明确:时间轴、收益率、红绿趋势、Hover 提示。自研 SVG 可以减少包体,尤其适合浏览器插件和编辑器 Webview 这种对启动速度敏感的容器。
十、通知推送:让收益状态主动找你
很多收益面板的问题是:你必须主动打开它。
Fund Helper 的通知模块希望把一部分信息变成主动推送。比如交易时段内每 15 分钟推送一次持仓收益,或者手动刷新后自动把最新收益发到飞书/钉钉群。
通知配置主要包括:
- 总开关。
- 推送频率:手动、1 分钟、5 分钟、15 分钟、30 分钟、60 分钟。
- 是否仅交易时段推送。
- 渠道配置:钉钉、飞书、企业微信。
- 投递方式:Webhook 或企业应用。
推送内容不是简单一句"今天涨了",而是包括总收益、账户收益、Top 涨跌基金等信息。飞书还支持交互卡片,让通知更像一张轻量收益日报。
这里的设计重点是:通知不是独立系统,而是复用同一套持仓快照能力。推送前会拉取最新快照,模板层只负责把快照渲染成不同渠道能接受的格式。
十一、部署与工程化:把项目做成可使用的软件
项目不只关注业务代码,也补齐了使用和发布链路。
技术栈整体如下:
| 模块 | 技术 |
|---|---|
| 后端 | Python 3.12、FastAPI、httpx、Motor、AKShare、bcrypt |
| Web 前端 | React 19、TypeScript、Rsbuild、Ant Design、Sass |
| 浏览器插件 | CRXJS、Vite、React、Manifest V3 |
| VS Code 扩展 | Extension Host、WebviewView、React、Vite、esbuild |
| JetBrains 插件 | Kotlin、JCEF、Gradle、React Webview |
| 桌面端 | Tauri v2、Rust、React、SQLite、Tailwind |
| 部署 | Docker、docker compose、MongoDB |
| 文档 | Rspress、GitHub Pages |
| 包管理 | pnpm workspace |
部署上,Web 应用支持两种模式:
bash
# 本地开发:MongoDB 单独跑,前后端分离
./dev-infra.sh
./start.sh
# Docker 一体部署:app + MongoDB
docker compose --profile full up -d --build
Docker 模式下,FastAPI 可以托管 Web 静态资源,用户访问 http://localhost:8080 即可使用。MongoDB 数据通过 volume 持久化,登录态、用户和通知配置不会随容器重建丢失。
发版方面,项目提供了多个脚本:
publish-chrome.shpublish-vscode.shpublish-jetbrains.shpublish-desktop.shpublish-image.sh
并配套 GitHub Actions 构建 Chrome 插件、VS Code 扩展、JetBrains 插件、桌面端和文档站。
十二、几个真正踩过的技术点
这类项目容易被低估的地方不在页面,而在不同运行环境的边界处理。下面这些是实现时真正花时间处理过的问题。
1. QR 登录不能只看"是否拿到二维码"
扫码登录有三个状态要处理:二维码创建、扫码确认、token 落库。浏览器插件、VS Code Webview、JetBrains JCEF、Tauri 桌面端都要走这个流程,但定时器和销毁逻辑完全不同。
插件端用 runIdRef 防止 React effect 重复触发后旧轮询继续写状态;VS Code 端由 Webview 发送 pollQr,真正轮询发生在 Extension Host;JetBrains 端要放到后台线程执行,再通过 listener 把结果推回 JCEF;桌面端则拆成 create_qr、poll_qr_state、complete_qr_login 三个 Tauri command,成功后写 SQLite。
这个流程如果没有清理 timer,很容易出现两个问题:二维码刷新后旧轮询仍在请求,以及登录成功后多个端重复写 session。
2. Webview 不能当普通浏览器页面用
VS Code/Cursor 的 Webview 有 CSP 限制,外部请求不能直接从 Webview 发起,所以养基宝请求必须放在 Extension Host。构建产物也不能直接引用普通相对路径,需要通过 webview.asWebviewUri 重写资源 URL,并给脚本注入 nonce。
JetBrains 的 JCEF 又是另一套限制:静态资源要注册本地 handler,消息通信要靠 JS bridge,页面加载完成前发送消息可能丢失。因此 JetBrains 插件里做了 resyncPanel(),每次 Webview ready 后重新推一次 session、loading 和 lastSnapshot。
这也是为什么编辑器插件里一定要有 Controller,而不是让每个 Webview 自己拉数据。Controller 管网络、缓存、状态栏和广播,Webview 只负责展示。
3. 多入口共享快照,否则状态栏和面板会打架
编辑器插件有侧边栏、底部 Panel、编辑区按钮、状态栏入口。桌面端也有主窗口、系统托盘、macOS 菜单栏弹窗。多个入口如果各自刷新,会造成请求放大和状态不一致。
项目里用 lastSnapshot 做最近一次快照缓存:刷新时只由 Host/Rust 层拉取一次,然后广播给所有 UI。VS Code 里是 postAll() 推给所有 Webview;JetBrains 里是 listener set;桌面端则在 fetch_portfolio 成功后同步主窗口和菜单栏标题。
这个设计解决的不是性能问题,而是用户感知问题:状态栏显示的收益、底部面板里的收益、主窗口里的收益必须是同一份数据。
4. 上游字段不是稳定 schema,归一化要写成规则
基金净值字段有不少例外。比如常见基金可以读 gszzl,但 QDII 或港股相关基金可能只有 vgszzl;已公布涨跌幅又可能在 jzzzl 或 rzzl。如果直接在 UI 里读字段,迟早会出现某些基金涨跌幅为空、排序错乱、当日收益为 0。
所以项目没有把字段判断散在组件里,而是先归一化:
text
estimateRate = gszzl -> zsgzzl -> vgszzl
publishedRate = jzzzl -> rzzl
displayRate = estimateRate != 0 ? estimateRate : publishedRate
dayEarn = money * displayRate / 100
这套逻辑在 Python、TypeScript、Rust、Kotlin 中各有一份实现。虽然这不是最理想的复用方式,但它保证了 Web、插件、桌面端、编辑器插件展示出来的收益逻辑一致。
5. 收益曲线的账户维度来自实测,不是文档推导
收益曲线一开始按常规理解会尝试用单个 account_id 拉取,但实测发现这样可能拿到的仍是汇总曲线。最后稳定方案是按账户 ID 数组请求,再按返回的账户 key 取对应曲线。
图表层也没有上 ECharts。收益曲线只有分钟级点位、红绿趋势、Hover 提示和首尾时间轴,自研 SVG 更容易控制体积和样式,也更适合插件 Popup、VS Code Webview 这类受限容器。
6. 桌面端的 Tauri 边界要切干净
桌面端前端只做交互和展示,不直接持有 token,也不直接处理通知发送。React 通过 invoke 调 Rust command,Rust 层再统一读 SQLite、签名请求养基宝、组装快照、触发通知、同步菜单栏。
这样的边界让桌面端的主窗口、菜单栏弹窗、后台 scheduler 都能复用同一套 Rust 逻辑。否则很容易变成主窗口里写一套、托盘里再写一套,最后两边表现不一致。
结语
Fund Helper 一开始只是想解决"我想更方便地看基金收益"这个小问题,但做着做着,它变成了一个关于多端产品、第三方接口封装、数据归一化、通知推送和工程化发布的完整实践。
它的核心价值不是堆了多少技术,而是把一个日常场景拆成了几条可以落地的工程链路:
- 上游签名、token、错误处理放在哪一层。
PortfolioSnapshot如何在 Web、插件、桌面端、编辑器插件里保持一致。- Webview、Tauri、Chrome Popup 这些不同容器的边界怎么切。
- 多入口场景下如何共享快照,避免状态栏、面板、主窗口各自为政。
- Docker、GitHub Actions、安装包和文档站如何支撑项目真正被使用。
如果你对基金持仓面板、多端客户端、FastAPI BFF、Tauri、浏览器插件、IDE 插件这些方向感兴趣,可以看看项目源码和文档,也欢迎 Star、Issue 或一起交流。
项目地址:github.com/ChinaCarlos...
文档中心:chinacarlos.github.io/fund-helper...
下载 Releases:github.com/ChinaCarlos...