Fund Helper Puts Portfolio Tracking into Your Browser, Editor, and Desktop
Project address: https://github.com/ChinaCarlos/fund-helper
Documentation center: https://chinacarlos.github.io/fund-helper/
Releases download: https://github.com/ChinaCarlos/fund-helper/releases
Disclaimer
- This project is for personal learning, research, and technical exchange only. The understanding of Yangjibao's
browser-plug-apiin the project comes from learning and reverse engineering analysis of the public browser plugin's network interactions; this project is not an official Yangjibao product and has no affiliation, authorization, cooperation, or endorsement relationship with Yangjibao or any third-party institution. Please do not use it for commercialization, bulk scraping, bypassing restrictions, or any purpose that violates platform agreements or laws and regulations; the project author is not responsible for any liability arising from the use of this project or secondary development.- The financial data such as funds, stocks, indices, and returns displayed by the project are only for software functionality demonstration and technical research, and do not constitute investment advice, financial recommendations, or return promises. Funds/stocks are risky, investment requires caution, please make your own judgment and bear the risk.
If you also have the habit of watching funds long-term, making fixed investments, and paying attention to position returns, you've probably encountered a very real problem: data is scattered across different platforms. Wanting to see how much you earned overall today, which account is dragging you down, and which funds contributed the most often requires repeatedly opening apps, switching accounts, refreshing, and calculating returns.
The project I made is called Fund Helper. It initially started as a real-time fund return monitoring panel based on Yangjibao's browser-plug-api, and gradually evolved into a multi-platform product: a Web application, a Chrome browser extension, a VS Code/Cursor extension, a JetBrains plugin, and a Tauri desktop client. Its goal is not to replace trading software, but to make the task of "checking positions, viewing returns, watching the market, and receiving notifications" more centralized, lighter, and more suitable for daily use.
This article will review how this project was built from several perspectives: product vision, functional modules, system architecture, key implementations, and engineering.
1. Why Build Fund Helper
Fund investment has a characteristic: the frequency of decision-making is not necessarily high, but the frequency of attention is often very high.
Many people don't frequently buy and sell funds every day, but they pay attention to several questions:
- What is my total return today?
- How are different accounts like Alipay, Tiantian Fund, and Xueqiu performing?
- Which funds are rising, and which are falling?
- Which industries or themes is the market style currently favoring?
- Can I receive reminders via DingTalk, Feishu, or WeCom when returns change?
- Can I check without opening my phone, directly on my computer, browser, or IDE?
This is the starting point of Fund Helper: building a cross-platform, lightweight, deployable, and extensible information panel around "fund position status".
It has three clear goals.
First, reduce the cost of viewing. Users don't need to frequently open mobile apps or switch between multiple pages. By opening the Web, browser extension, desktop client, or editor sidebar, they can see the core position status.
Second, unify data representation. The data returned by the Yangjibao interface is relatively raw, and different fields vary across different fund types. The project normalizes accounts, funds, indices, return curves, and rise/fall directions uniformly before handing them over to each platform for display.
Third, build it as a project that can evolve long-term, not a one-off script. The project includes not only a Web application but also standalone plugins, a desktop client, IDE extensions, Docker deployment, a documentation site, and release scripts, making it closer to a complete product.
2. What It Can Do Now
Fund Helper currently offers five ways to use it.
| Usage Method | Suitable Scenario | Features |
|---|---|---|
| Web Application | Full-featured panel | Positions, market rankings, sector heatmap, notifications, multi-user management |
| Browser Extension | Quick glance | Chrome/Edge toolbar Popup, directly connects to Yangjibao, no backend deployment needed |
| VS Code/Cursor Extension | A quick look while coding | Sidebar, bottom panel, status bar multiple entry points, adapts to editor theme |
| JetBrains Plugin | IntelliJ/WebStorm/PyCharm users | Tool Window + Status Bar + JCEF Webview |
| Desktop Client | Local single-user use | Tauri + Rust + SQLite, supports system tray and local notification configuration |
Core functional modules include:
- WeChat QR Code Login to Yangjibao: Obtain login state via QR code; position-related interfaces are accessed based on token.
- Position Dashboard: Displays total assets, daily return, rate of return, number of rising/falling funds.
- Multi-Account Grouping: Supports grouped viewing by accounts like Alipay, Tiantian Fund, Xueqiu, etc.
- Fund Detail Table: Displays fund code, name, position amount, cost, daily return, change percentage, etc.
- Return Curve: Fetches intraday minute-by-minute return curves, supports aggregate curves and independent account curves.
- Fund Search & Position Management: Supports searching for funds, adding positions, and deleting positions.
- Market Rankings: Displays market-wide fund rankings based on AKShare/East Money data.
- Sector Heatmap: Views industry/concept sector gains and capital flows, and drills down to related funds.
- Notification Push: Supports DingTalk, Feishu, WeCom, supports Webhook and some enterprise application delivery modes.
- Multi-User and Deployment: The Web end supports admin accounts, multi-user isolation, MongoDB persistence, and Docker integrated deployment.
If you just want a glance at your positions, the plugin and IDE extension are light enough; if you want to view the market, configure notifications, and support multi-user use, the Web application is more complete; if you prefer local standalone operation, the desktop client is more suitable.
3. Overall Architecture: Not a Page, but a Multi-Platform Runtime Model
The core of Fund Helper is not a single page, but a runtime model centered around "fund position snapshots." Different clients have different containers, but they all go through a similar chain: startup, check login state, scan QR code to get token, pull upstream data, normalize into a snapshot, render UI, handle refresh and invalidation.
The overall architecture can be viewed like this:
flowchart TD
U[User] --> W[Web Application]
U --> C[Browser Extension]
U --> E[Editor Plugin]
U --> D[Desktop Client]
W --> BFF[FastAPI BFF]
BFF --> Mongo[(MongoDB)]
BFF --> Notify[Notification Service]
BFF --> Market[AKShare / East Money Market Data]
BFF --> YJB[Yangjibao browser-plug-api]
C --> ChromeStore[(chrome.storage.local)]
C --> YJB
E --> Host[Extension Host / Kotlin Host]
Host --> IDEStore[(globalState / PersistentState)]
Host --> YJB
D --> Rust[Tauri Rust Commands]
Rust --> SQLite[(SQLite)]
Rust --> Tray[System Tray / Scheduled Tasks]
Rust --> YJB
A crucial trade-off was made here: the Web end retains full backend capabilities, while the plugin, desktop client, and editor plugin are as independent as possible.
The Web end requires multi-user support, notification configuration, market rankings, heatmaps, and Docker deployment, so it uses FastAPI BFF + MongoDB. The browser extension and editor plugin emphasize being ready to use out of the box; requiring users to deploy a backend first would raise the barrier to entry, so they connect directly to Yangjibao. The desktop client similarly does not depend on the Web backend, but stores tokens, notification configurations, and push throttling information in a local SQLite database.
This is the basic principle of this project: the core business model is unified, and the runtime container is chosen per platform.
4. Web Application: BFF as the "Complexity Convergence Point"
The Web application is the most fully featured end. It uses React 19, TypeScript, Rsbuild, and Ant Design for the frontend, and FastAPI, httpx, Motor, MongoDB, and AKShare for the backend.
Its implementation flow is not "the frontend directly calls a bunch of third-party interfaces," but rather uses a BFF to converge the instability, signatures, tokens, multi-user isolation, and notification configuration of third-party interfaces.
The complete form of the Web end is as follows: navigation and refresh entry at the top, index and summary cards in the middle, and return curves and fund details displayed by account tabs below. The page itself only consumes the snapshot assembled by the backend and does not directly understand the raw Yangjibao fields.
sequenceDiagram
participant User as User
participant Web as React Web
participant API as FastAPI BFF
participant DB as MongoDB
participant YJB as Yangjibao
participant EM as East Money/AKShare
participant Bot as DingTalk/Feishu/WeCom
User->>Web: Open Dashboard
Web->>API: Check application login state
API->>DB: Read Session and user info
API-->>Web: Logged in / Login required / Need to bind Yangjibao
Web->>API: Request position snapshot
API->>DB: Read current user's yjb_token
API->>YJB: Pull summary, indices, account funds after signing
API->>API: Normalize PortfolioSnapshot
API-->>Web: Return unified snapshot
Web->>Web: Render indices, return cards, account tabs, fund table
Web->>API: Access market rankings/heatmap
API->>EM: Pull public market data
API-->>Web: Return rankings and sector data
Web->>API: Save notification config or trigger push
API->>DB: Save user configuration
API->>YJB: Pull latest snapshot before pushing
API->>Bot: Render and send notification
The value of the BFF layer is evident here.
First, it hides Yangjibao's signing rules. Upstream requests require Authorization, Request-Time, Request-Sign, where the signature is an MD5 hash of the path, token, timestamp, and secret key. The frontend doesn't need to know these details; it only cares about "I want a position snapshot."
Second, it combines results from multiple upstream sources into a model the page can directly consume. The position page needs total assets, today's return, details for each account, fund list, index quotes, and trading session flags. The backend pulls the summary, account funds, and indices, then assembles them into a PortfolioSnapshot.
Third, it carries the Web end's extended capabilities. Market rankings and sector heatmaps come from AKShare/East Money, notification configurations are stored in MongoDB, and the same set of position snapshot capabilities is reused before pushing. None of these are suitable to be crammed into the frontend.
5. Browser Extension: A Lightweight Client in a 400×600 Popup
The browser extension is positioned as "a glance is enough." It uses CRXJS, Vite, React 19, TypeScript, Manifest V3, chrome.storage.local, and does not depend on FastAPI or MongoDB.
When not logged in, the Popup directly enters the WeChat QR code scanning page. The technical point here is: the extension does not create a login session through the backend, but generates a QR code within the Popup, polls the scan status, and writes the token to chrome.storage.local upon success.
Its runtime flow is as follows:
graph TD
A["Click browser toolbar icon"] --> B["Load React Popup"]
B --> C{"Token exists locally?"}
C --> D["Not logged in: LoginView creates QR code"]
D --> E["Poll scan status every 2 seconds"]
E --> F{"Scan successful?"}
F --> E
F --> G["Success: Save token to chrome.storage.local"]
C --> H["Logged in: fetchPortfolioSnapshot"]
G --> H
H --> I["Directly connect to Yangjibao and sign request"]
I --> J["Pull account summary, indices, fund list"]
J --> K["Assemble PortfolioSnapshot on extension side"]
K --> L["PortfolioView renders indices, returns, account tabs, fund list"]
L --> M["Scheduled silent refresh"]
M --> H
I --> N["401 Invalid: Clear local login state and return to scan page"]
There are a few implementation points worth mentioning separately.
First, the extension's state machine is simple: boot -> login -> portfolio. On startup, it reads chrome.storage.local; if there's no token, it enters QR code login; if there is a token, it directly fetches positions. When a request returns 401, it clears the local session and returns to the login state.
Second, the extension does not introduce a backend, so signing, field normalization, trading session judgment, and position snapshot aggregation are all done in the frontend TypeScript. This allows users to use the extension upon loading, but the cost is that key algorithms need to be kept in sync with the Web backend.
Finally, the Popup space is very limited. Chrome Popup height is usually controlled around 600px, so the extension does not include market rankings, heatmaps, or complex notification configurations, focusing instead on the most commonly used information: indices, return cards, multi-account tabs, and fund sorting.
After logging in, the extension compresses indices, total assets, daily return, rise/fall counts, account tabs, a sorter, and the fund list into a lightweight panel. There is no server-side state here; refresh, sorting, and 401 invalidation fallback are all handled on the extension side.
6. Desktop Client: Tauri Connects React UI with Rust's Native Capabilities
The desktop client is positioned for "local persistence + native experience." It uses Tauri v2, Rust, React 19, Ant Design, Tailwind v4, and SQLite. Compared to the browser extension, it can leverage more native capabilities: system tray, single instance, background scheduled tasks, local notification configuration, and Feishu/DingTalk/WeCom pushes.
Besides the main window, the desktop client also features a macOS menu bar/status bar entry: the menu bar directly displays the daily return and rate of return, and clicking it pops up a lightweight position panel. This entry is suitable for "a glance while working" without needing to switch to the full application window.
The full main window carries a more complete Dashboard: indices, asset cards, account grouping, return curves, and fund list are all displayed within a native window. The frontend is still React, but all login state, position fetching, and notification configuration are handed over to the Rust layer via Tauri commands.
The desktop client does not have React making direct HTTP calls; instead, it calls Rust commands via Tauri invoke.
sequenceDiagram
participant UI as React UI
participant Tauri as Tauri invoke
participant Rust as Rust Commands
participant DB as SQLite
participant YJB as Yangjibao
participant Tray as System Tray/Menu Bar
participant Bot as Notification Channels
UI->>Tauri: get_auth_status
Tauri->>Rust: Call command
Rust->>DB: Read app_profile
Rust-->>UI: Logged in / Not logged in
UI->>Tauri: create_qr / poll_qr_state
Rust->>YJB: Get QR code and poll status
YJB-->>Rust: token, nickname, avatar
Rust->>DB: Save login state
UI->>Tauri: fetch_portfolio
Rust->>DB: Read token
Rust->>YJB: Pull summary, indices, funds, curves
Rust->>Rust: Normalize snapshot
Rust->>Tray: Sync menu bar return
Rust-->>UI: PortfolioSnapshot
UI->>UI: Render position page
Rust->>Rust: Background scheduler periodic check
Rust->>DB: Read notification config and last push time
Rust->>YJB: Pull latest snapshot before pushing
Rust->>Bot: Send return notification
Its key implementation layers are as follows:
- React UI: Responsible for pages, settings, themes, and user interaction.
- Tauri Commands: Provides commands like
create_qr,poll_qr_state,fetch_portfolio,save_notification_config. - Rust Business Layer: Responsible for Yangjibao signed requests, position aggregation, return curve normalization, and notification orchestration.
- SQLite: Stores tokens, notification configurations, and push throttling state.
- Tray/Menu Bar: Places high-frequency information like "today's return" in a lighter location.
The advantage of the desktop client is its strong native capabilities without needing to deploy a service; the difficulty lies in re-implementing the Web logic in Rust. To avoid data inconsistency across platforms, the project strives to keep logic like NAV field priority, daily return calculation, and trading session judgment isomorphic with the Web backend.
7. Editor Plugins: Webview is Just UI, Real Requests Happen in the Host Process
The editor plugins are divided into the VS Code/Cursor extension and the JetBrains plugin. Both use React Webview for UI, but the hosts are completely different.
The VS Code/Cursor extension is a TypeScript Extension Host + Webview. The JetBrains plugin is a Kotlin Host + JCEF Webview. Their common principle is: Webview is responsible for display, the host process is responsible for networking, persistence, and state synchronization.
In the plugin marketplace, it is installed as a standalone extension. Users search for fund-helper and install it, then use it in editors compatible with the VS Code extension ecosystem, such as VS Code, Cursor, Trae, CodeBuddy, Qoder, etc.
VS Code/Cursor extension flow:
flowchart TD
A[Open sidebar/bottom panel/status bar entry] --> B[Extension activate]
B --> C[Create FundHelperController]
C --> D[Register WebviewViewProvider and commands]
D --> E[Webview loads React static assets]
E --> F[Webview postMessage: boot]
F --> G[Extension Host reads globalState]
G --> H{Token exists?}
H -->|No| I[Webview requests startLogin]
I --> J[Host gets QR code and returns qr message]
J --> K[Webview displays QR code and polls]
K --> L[Host polls Yangjibao scan status]
L --> M[Save session after successful login]
H -->|Yes| N[Host fetches position snapshot]
M --> N
N --> O[Host updates lastSnapshot and status bar]
O --> P[postMessage to all Webviews]
P --> Q[React Webview renders positions]
There is an important reason for this: VS Code Webview has a strict CSP and cannot be treated as a regular webpage that freely connects to external networks. The project places Yangjibao requests in the Extension Host, and the Webview and Host only communicate via postMessage with messages like boot, startLogin, pollQr, refresh, logout.
After logging in, the editor side is not just a sidebar. The project synchronizes the same lastSnapshot to the sidebar, bottom panel, editor title bar entry, and status bar, allowing users to view the same position data from different workflow locations.
The JetBrains plugin flow is similar, just with the bridge layer replaced by JCEF and Kotlin:
flowchart TD
A[Open JetBrains Tool Window] --> B[JCEF loads React Webview]
B --> C[Inject JS Bridge]
C --> D[Webview sends boot/startLogin/refresh]
D --> E[Kotlin FundHelperController receives messages]
E --> F[SessionStorageService reads login state]
F --> G{Token exists?}
G -->|No| H[YjbClient gets QR code]
H --> I[Notify Webview to display QR code]
I --> J[Poll scan status]
J --> K[Save token to PersistentStateComponent]
G -->|Yes| L[PortfolioFetcher fetches and aggregates positions]
K --> L
L --> M[Update lastSnapshot]
M --> N[Notify Tool Window / Bottom Panel / Status Bar]
The biggest challenge for editor plugins is not the API calls, but the host environment constraints:
- VS Code needs to handle Webview CSP, resource URI rewriting, nonce, and
globalStatestorage. - JetBrains needs to handle JCEF resource loading, JS bridge injection, Tool Window lifecycle, and status bar widgets.
- Both ends need to handle multiple entries sharing the same snapshot: the sidebar, bottom panel, and status bar cannot refresh independently in a chaotic manner.
Therefore, the project has a controller role: in VS Code, it's FundHelperController, and JetBrains has a similar Controller. They are both responsible for saving lastSnapshot, managing auto-refresh, and broadcasting results to all UI containers.
8. Business Model: Platforms Can Differ, Snapshots Must Be Unified
One of the most valuable parts of this project is the normalization of raw fund data.
The fund NAV fields returned by Yangjibao are not always stable. For example, for some QDII and Hong Kong stock-related funds, the conventional fields gszzl and zsgzzl might be empty, with the actual usable estimated change percentage being in vgszzl. If the client only reads one field, it can easily lead to empty change percentages during trading hours or incorrect return estimates.
The project uniformly applies the following priorities:
| Normalization Target | Field Priority |
|---|---|
| Estimated Change % | gszzl -> zsgzzl -> vgszzl |
| Published Change % | jzzzl -> rzzl |
| Estimated NAV | gzjz -> zsgz -> gsz -> vgsz |
| Daily Return | money * rate / 100 |
This kind of logic might seem inconspicuous, but it determines whether the data is trustworthy. Especially in a multi-platform project, if the Web, browser extension, desktop client, and IDE extension each calculate differently, it's very easy to end up with a situation where "the same fund shows different returns on different platforms."
Therefore, the project aligns key calculation logic across all platforms:
- Web Backend:
backend/app/yjb/calculator.py - Browser Extension:
chrome-extension/src/lib/portfolio.ts - VS Code Extension:
vscode-extension/src/portfolio.ts - Desktop Client:
desktop/src-tauri/src/portfolio.rs
This is also a lesson from multi-platform projects: UI can differ by platform, but business calculations must be as consistent as possible.
9. Return Curve: The Interface is Just the Entry, the Focus is Account Dimensions and Chart Trade-offs
The return curve part initially seemed like just "calling an interface and drawing a line." During actual implementation, it was discovered that the key wasn't the interface itself, but two issues: how to get curves for different accounts, and how to display them lightly enough across multiple platforms.
The implementation flow is as follows:
flowchart LR
A[Position snapshot loaded] --> B[Extract account ID list]
B --> C[Request aggregate curve]
B --> D[Request independent curves for each account]
C --> E[normalizeIncomeLine]
D --> F[normalizeIncomeLines]
E --> G[Page/Desktop state]
F --> G
G --> H[SVG Return Curve]
H --> I[Hover shows time and rate of return]
H --> J[Red/Green lines based on profit/loss]
There is a tested conclusion here: if you only fetch based on a single account_id, you might still get the aggregate curve; to stably get an account's independent curve, you need to request using an array of account IDs. This conclusion was eventually documented in the Web backend, desktop Rust code, and platform documentation.
The chart layer was also kept restrained. The project did not introduce ECharts but instead built a custom SVG curve. The reason is that the data volume for intraday return curves is limited, and the interactions are clear: time axis, rate of return, red/green trends, hover tooltips. A custom SVG reduces bundle size, which is especially suitable for constrained containers like extension popups and VS Code Webviews that are sensitive to startup speed.
10. Notification Push: Let Return Status Come to You
The problem with many return panels is: you must actively open them.
Fund Helper's notification module aims to turn some information into proactive pushes. For example, pushing position returns every 15 minutes during trading hours, or automatically sending the latest returns to a Feishu/DingTalk group after a manual refresh.
Notification configuration mainly includes:
- Master switch.
- Push frequency: Manual, 1 min, 5 min, 15 min, 30 min, 60 min.
- Whether to push only during trading hours.
- Channel configuration: DingTalk, Feishu, WeCom.
- Delivery method: Webhook or enterprise application.
The push content is not just a simple "it went up today," but includes total return, account returns, top gaining/losing funds, and other information. Feishu also supports interactive cards, making the notification more like a lightweight daily return report.
The design focus here is: the notification is not an independent system, but reuses the same set of position snapshot capabilities. Before pushing, it pulls the latest snapshot, and the template layer is only responsible for rendering the snapshot into formats acceptable to different channels.
11. Deployment and Engineering: Making the Project Usable Software
The project focuses not only on business code but also completes the usage and release chain.
The overall tech stack is as follows:
| Module | Technology |
|---|---|
| Backend | Python 3.12, FastAPI, httpx, Motor, AKShare, bcrypt |
| Web Frontend | React 19, TypeScript, Rsbuild, Ant Design, Sass |
| Browser Extension | CRXJS, Vite, React, Manifest V3 |
| VS Code Extension | Extension Host, WebviewView, React, Vite, esbuild |
| JetBrains Plugin | Kotlin, JCEF, Gradle, React Webview |
| Desktop Client | Tauri v2, Rust, React, SQLite, Tailwind |
| Deployment | Docker, docker compose, MongoDB |
| Documentation | Rspress, GitHub Pages |
| Package Management | pnpm workspace |
For deployment, the Web application supports two modes:
# Local development: MongoDB runs separately, frontend and backend separated
./dev-infra.sh
./start.sh
# Docker integrated deployment: app + MongoDB
docker compose --profile full up -d --build
In Docker mode, FastAPI can host the Web static assets, and users can access it at http://localhost:8080. MongoDB data is persisted via volumes, so login states, users, and notification configurations are not lost when containers are rebuilt.
For releases, the project provides several scripts:
publish-chrome.shpublish-vscode.shpublish-jetbrains.shpublish-desktop.shpublish-image.sh
And it is accompanied by GitHub Actions to build the Chrome extension, VS Code extension, JetBrains plugin, desktop client, and documentation site.
12. A Few Technical Points Truly Encountered
Places where this kind of project is easily underestimated are not in the pages, but in handling the boundaries of different runtime environments. The following are issues that genuinely took time to handle during implementation.
1. QR Login Can't Just Check "Whether a QR Code Was Obtained"
QR code login has three states to handle: QR code creation, scan confirmation, and token storage. The browser extension, VS Code Webview, JetBrains JCEF, and Tauri desktop client all need to go through this process, but the timer and destruction logic are completely different.
The extension uses runIdRef to prevent old polls from continuing to write state after a React effect re-triggers; the VS Code end has the Webview send pollQr, with the actual polling happening in the Extension Host; the JetBrains end needs to execute it in a background thread and push results back to JCEF via a listener; the desktop client splits it into three Tauri commands: create_qr, poll_qr_state, complete_qr_login, writing to SQLite upon success.
If timers are not cleaned up in this process, two problems easily arise: old polls still requesting after the QR code refreshes, and multiple ends repeatedly writing sessions after a successful login.
2. Webview Cannot Be Treated as a Regular Browser Page
VS Code/Cursor's Webview has CSP restrictions; external requests cannot be initiated directly from the Webview, so Yangjibao requests must be placed in the Extension Host. Build artifacts also cannot directly reference ordinary relative paths; resource URLs need to be rewritten via webview.asWebviewUri, and nonces need to be injected into scripts.
JetBrains' JCEF has another set of restrictions: static resources need to be registered with a local handler, message communication relies on a JS bridge, and messages sent before the page finishes loading might be lost. Therefore, the JetBrains plugin implements resyncPanel(), which re-pushes session, loading, and lastSnapshot each time the Webview is ready.
This is also why editor plugins must have a Controller, rather than letting each Webview fetch data on its own. The Controller manages networking, caching, the status bar, and broadcasting; the Webview is only responsible for display.
3. Multi-Entry Shared Snapshot, Otherwise the Status Bar and Panel Will Conflict
The editor plugin has a sidebar, bottom panel, editor area button, and status bar entry. The desktop client also has a main window, system tray, and macOS menu bar popup. If multiple entries refresh independently, it causes request amplification and state inconsistency.
The project uses lastSnapshot as the most recent snapshot cache: during a refresh, only the Host/Rust layer fetches once, then broadcasts to all UIs. In VS Code, postAll() pushes to all Webviews; in JetBrains, it's a listener set; on the desktop, after fetch_portfolio succeeds, it synchronizes the main window and menu bar title.
This design solves not a performance problem, but a user perception problem: the return displayed in the status bar, the return in the bottom panel, and the return in the main window must be the same data.
4. Upstream Fields Are Not a Stable Schema, Normalization Must Be Written as Rules
Fund NAV fields have many exceptions. For example, common funds can read gszzl, but QDII or Hong Kong stock-related funds might only have vgszzl; the published change percentage might be in jzzzl or rzzl. If fields are read directly in the UI, sooner or later some funds will show empty change percentages, sorting will be chaotic, and daily returns will be zero.
So the project does not scatter field judgments in components, but normalizes first:
estimateRate = gszzl -> zsgzzl -> vgszzl
publishedRate = jzzzl -> rzzl
displayRate = estimateRate != 0 ? estimateRate : publishedRate
dayEarn = money * displayRate / 100
This set of logic has an implementation in Python, TypeScript, Rust, and Kotlin. Although this is not the most ideal way to reuse code, it ensures that the return logic displayed by the Web, plugin, desktop client, and editor plugin is consistent.
5. The Account Dimension of Return Curves Comes from Actual Testing, Not Document Deduction
Initially, the return curve was conventionally understood to be fetched using a single account_id, but testing found that this might still return the aggregate curve. The final stable solution was to request using an array of account IDs, then fetch the corresponding curve based on the returned account key.
The chart layer also did not adopt ECharts. The return curve only has minute-level data points, red/green trends, hover tooltips, and start/end time axes; a custom SVG makes it easier to control size and style, and is more suitable for constrained containers like extension popups and VS Code Webviews.
6. The Desktop Client's Tauri Boundary Must Be Cleanly Cut
The desktop client's frontend only handles interaction and display; it does not directly hold tokens or handle notification sending. React calls Rust commands via invoke, and the Rust layer uniformly reads SQLite, signs requests to Yangjibao, assembles snapshots, triggers notifications, and synchronizes the menu bar.
This boundary allows the desktop client's main window, menu bar popup, and background scheduler to all reuse the same set of Rust logic. Otherwise, it would be easy to end up writing one set in the main window and another in the tray, leading to inconsistent behavior between the two.
Conclusion
Fund Helper initially just wanted to solve the small problem of "I want to view fund returns more conveniently," but as it progressed, it became a complete practice involving multi-platform products, third-party interface encapsulation, data normalization, notification pushing, and engineering releases.
Its core value is not how much technology was piled on, but how a daily scenario was broken down into several implementable engineering chains:
- Where to place upstream signing, tokens, and error handling.
- How to keep
PortfolioSnapshotconsistent across Web, plugin, desktop, and editor plugin. - How to cut the boundaries of different containers like Webview, Tauri, and Chrome Popup.
- How to share snapshots in multi-entry scenarios, avoiding the status bar, panel, and main window acting independently.
- How Docker, GitHub Actions, installation packages, and documentation sites support the project being truly used.
If you are interested in directions like fund position panels, multi-platform clients, FastAPI BFF, Tauri, browser extensions, or IDE plugins, you can check out the project source code and documentation. Stars, Issues, and discussions are also welcome.
Project address: https://github.com/ChinaCarlos/fund-helper
Documentation center: https://chinacarlos.github.io/fund-helper/
Download Releases: https://github.com/ChinaCarlos/fund-helper/releases