Skip to content

生产部署 ​

⚠️ 2026-07-05 更新:Web GUI 已退役——后端不再 serve 静态文件,没有网页面板; sync_gui_dist / Caddy file_server 前端段已删(重做时走独立前端连 MCP)。 Web API 本身也已 deprecated:存量端点只服务 remote hub 模式(INVEST_API_BASE 转发)与内部触发,不再新增端点。

多数用户不需要这一章——单机用户直接 pip install openinvest / uvx openinvest 即可(见 QUICK_START)。这一章只服务"我要跑一个 remote hub 给多设备共享账本"的场景。

← 07-extending · Wiki 索引 · 09-troubleshooting →


拓扑总览 ​

客户端(笔记本 CLI / agent skill,INVEST_API_BASE 转发)
  ↓ HTTPS
Cloudflare(DNS 橙色云 + Access JWT 鉴权)
  ↓ HTTP(Flexible 模式,源站不需要 cert)
你的服务器:80 / 443 端口
  ↓
Caddy 反代 (caddy-gateway 容器)
  └─ /api/*  → reverse_proxy 127.0.0.1:8765   (FastAPI, openinvest-web)

为什么这套:

  • 没有 SSR daemon → 一台 1G VPS 就能跑(参考 mc-website 教训:next start 整机三次挂死)
  • CF Access 在边缘鉴权 → 后端不写 auth 代码
  • 127.0.0.1 绑定 → 公网扫不到 8765

0. 容器一键自托管(Docker Compose / GHCR) ​

不想手装 uv / 配 systemd?用容器。镜像 ghcr.io/longsizhuo/openinvest 每个后端版本 tag(v*)由 publish-image.yml 自动发布。GUI 已退役——容器只跑 API + scheduler, 没有网页面板。要 CF Access 保护,仍可在前面挂下面第 1–2 节的 Caddy(反代容器的 127.0.0.1:8765)。

前置:Docker + Docker Compose v2。compose 文件在仓库里,所以这条路径要 clone (这是 hub 部署,不是普通用户安装——普通用户走 uvx openinvest):

bash
git clone https://github.com/longsizhuo/openInvest.git && cd openInvest
cp .env.example .env && $EDITOR .env       # 至少填 DEEPSEEK_API_KEY(没 .env 也能起,但委员会跑不动)

onboarding(建 memory/)——invest-agent(scheduler)缺 memory/user.md 会拒启。 一次性命令走 invest-web(invest-agent 的 entrypoint: ["/bin/sh","-c"] 会吞掉追加参数):

bash
docker compose run --rm invest-web openinvest init
# 或在 Claude Code 里说"帮我初始化 invest"走 5 个问题

起服务:

bash
docker compose up -d --build                 # 本地构建(首次几分钟:uv sync)
# —— 或拉预构建镜像(更快,需该 package 已 Public 或先 docker login ghcr.io)——
docker compose pull && docker compose up -d

验证:curl http://localhost:8765/api/health(探活不需要 token)。

服务作用端口
invest-webFastAPI(deprecated,仅 hub 转发 + 内部触发)宿主 127.0.0.1:8765(默认只绑 loopback)
invest-agentscheduler:跑 jobs/*.yml(daily_report / pnl_snapshot…)无
  • INVEST_API_TOKEN 必填:容器内 invest-web 必须绑 0.0.0.0(宿主端口映射才进得来),而 openinvest-web 入口的守卫规定"非 loopback 绑定 + 无 token = 拒绝启动"。compose 在起进程前先查这个变量,缺了直接给提示退出(不再靠 127.0.0.1:8765 端口映射兜底一个无鉴权的 API)。在 .env 加 INVEST_API_TOKEN=$(openssl rand -hex 32) 的结果;设了之后 /api/* 都要带 Authorization: Bearer <token>(/api/health 探活豁免)。docker compose run --rm invest-web openinvest init 这类一次性命令会替换启动命令,不受影响。
  • 暴露到 LAN/公网:把 invest-web 的 ports 改成 "8765:8765"(token 已是必填);或保持 loopback、前面挂 Caddy + CF Access(见下文第 1–2 节)。
  • 数据持久化:memory/(账本,必挂)/ db/ / cache_data/ / logs/ 都 bind-mount 到宿主,容器重建不丢。
  • 镜像可见性:GHCR 包首发是 private——要 docker compose pull 匿名拉,须在 GitHub Packages 把它设为 Public(或 docker login ghcr.io)。

1. 服务器一次性配置 ​

1.1 装后端(PyPI)+ 数据目录 ​

bash
curl -LsSf https://astral.sh/uv/install.sh | sh
uv tool install openinvest      # 或 pip install openinvest
mkdir -p ~/openInvest           # INVEST_HOME 数据目录(memory/ db/ .env)
cp .env.example ~/openInvest/.env   # 或手写;填 DEEPSEEK_API_KEY / EMAIL_* / 等
openinvest init                 # onboarding 建 memory/

git clone 只用于开发后端本身。hub 服务器上装 wheel 即可;要 Docker compose / 自定义 systemd unit 模板才需要 clone 仓库拿那几个文件。

1.2 拉前端 dist(已退役) ​

2026-07-05 起 GUI 退役:scripts.sync_gui_dist 已删,FastAPI 不再 mount static/,Caddy 也不再需要 file_server 段。invest-gui 仓库封存待重做 (重做走独立前端连 MCP)。

1.3 systemd unit ​

仓库自带 unit 模板(systemd/invest-web.service),核心就是起 openinvest-web:

ini
Environment=INVEST_HOME=%h/openInvest
EnvironmentFile=%h/openInvest/.env
Environment=INVEST_WEB_HOST=127.0.0.1
Environment=INVEST_WEB_PORT=8765
ExecStart=%h/.local/bin/uv run --no-sync openinvest-web
# host/port 走 env:INVEST_WEB_HOST(默认 127.0.0.1)/ INVEST_WEB_PORT(默认 8765)
# 必须走 openinvest-web 入口,不要裸 uvicorn——入口带"非 loopback 绑定必须设 INVEST_API_TOKEN"守卫
Restart=on-failure
ProtectSystem=strict
ReadWritePaths=%h/openInvest
bash
sudo cp systemd/invest-web.service /etc/systemd/system/   # 按需改路径
sudo systemctl daemon-reload
sudo systemctl enable --now invest-web
sudo systemctl status invest-web

→ 仅写 INVEST_HOME 目录(systemd 加固)。 → Restart=on-failure:进程挂了自动起,但升级包后必须手动 restart 拉新。 → 端口已被占(比如旧进程没停干净):openinvest-web 以退出码 1退出并打印提示,Restart=on-failure(compose 的 restart: always 同理)每 15 秒重试直到端口空出,不会静默停在 inactive。

1.4 Caddy 站点配置 ​

caddy-gateway/Caddyfile 加(只剩 API 反代,静态文件段已随 GUI 退役删除):

caddyfile
http://invest.your-domain.com {
    encode gzip

    handle /api/* {
        reverse_proxy 127.0.0.1:8765
    }
}

改完 Caddyfile 必须 restart(不是 reload)—— bind mount 模式 reload 不读新 inode:

bash
docker restart caddy

详见 memory feedback_caddy_bind_mount_reload。


2. Cloudflare Access 配置 ​

2.1 DNS ​

CF Dashboard → DNS → 加 invest A 记录指向服务器 IP,橙色云开启(走 CF proxy)。

2.2 Access Application ​

CF Zero Trust → Access → Applications → Add a self-hosted application:

字段值
Application nameinvest
Application domaininvest.your-domain.com
Session duration30 days(或 24h,按你偏好)
Identity providersOne-time PIN(邮箱)

2.3 Access Policy ​

Action: Allow
Include: Emails → your-email@gmail.com

→ 仅你的邮箱能进。

2.4 验证 ​

  • 退出所有 CF 邮箱会话
  • curl -H "CF-Access-Client-Id: ..." -H "CF-Access-Client-Secret: ..." https://invest.your-domain.com/api/health
  • 无 Service Token 的裸请求应被 CF 挡在边缘

3. 升级流程 ​

bash
uv tool upgrade openinvest      # 或 pip install -U openinvest;uvx 用户 uvx --refresh openinvest doctor
sudo systemctl restart invest-web

重要:systemd unit 不会自动 reload 新代码。每次升级完必须 restart。

stdio MCP 子进程也要重启:agent 宿主(Hermes / OpenClaw / Claude Code 等)按 config 自己 spawn 的 openinvest-mcp stdio 子进程不归 systemd 管,升级包、重启 invest-web/scheduler 都碰不到它——它会一直跑旧代码,直到宿主重启该 MCP client (重启宿主 gateway/会话,或宿主自带的 MCP reload)。凡是改了 MCP 工具背后代码的 版本(例:2026-10 ingest_event 接上委员会触发闸),升级后要同时重启 agent 宿主的 MCP client,否则投喂门还是旧行为、爬虫门已是新行为。

(前端升级流程已随 GUI 退役删除——invest-gui 仓库封存待重做。)


4. 环境变量 ​

必填 ​

bash
DEEPSEEK_API_KEY=sk-xxx
DEEPSEEK_BASE_URL=https://api.deepseek.com
INVEST_WEB_HOST=127.0.0.1
INVEST_WEB_PORT=8765

可选 ​

bash
# CommSec 邮件导入(澳股用户)
EMAIL_SENDER=you@gmail.com
EMAIL_PASSWORD=app-password-16-chars

# 委员会跑完发邮件
SMTP_HOST=smtp.gmail.com
SMTP_USER=you@gmail.com
SMTP_PASS=app-password
SMTP_TO=you@gmail.com

# Discord DM 实时报警(可选):event_watch 事件 / 委员会 verdict 先推 Discord
# 再发邮件。invest 不直接持有 Discord token——POST 给同宿主机 Discord bot 的
# alert server(X-Internal-Key 内网鉴权),由它代发 DM。两个都不填 = 完全禁用,
# 行为与从前一致(邮件仍是保底归档通道)。
CHATBOT_ALERT_URL=http://127.0.0.1:6200/alert/invest
CHATBOT_INTERNAL_KEY=shared-secret-with-your-bot

# 事件触发的委员会 verdict 邮件/DM 里"详情"链接的前缀。不设默认退化成
# http://localhost:8765——在邮件/DM 里点开等于打自己电脑,读信设备打不开。
# 设成你自己 invest-web 的公网可达地址(第 1-2 节配好 Caddy + CF Access 后
# 就有)。就算不设,每个资产也会带一条 explain_decision(decision_id) 的
# agent 调用提示当零配置兜底,但能点开的链接体验更好。
INVEST_API_BASE_URL=https://invest.your-domain.com

# 委员会行为开关(也可运行时经 API/CLI 改,ADR-017;env 仅部署期默认)
# 集中度 lens:false=单资产/刻意集中/全可投资金池不因持仓集中度被建议减仓(ADR-019)
INVEST_VERDICT_CONCENTRATION_LENS_ENABLED=true

NapCat QQ bot connector 已于 2026-07-05 删除,相关 NAPCAT_* / INVEST_WHITELIST_QQ env 不再生效。

.env.example 是 source of truth ​

新增 env 必须更新 .env.example,否则 fork 用户不知道。


5. 加固清单(Server hardening 2026-05) ​

参考 memory project_server_2026_05_02_hardening:

  • ✅ rpcbind / portmapper 关
  • ✅ unattended-upgrades 自动安全补丁
  • ✅ 5 个服务全部绑 127.0.0.1(外网扫不到端口)
  • ✅ wcpp 停服(不用了)
  • ✅ Caddy 仅放行 invest.* longsizhuo.com 和 mc.involutionhell.com
  • ✅ ufw 仅开 22 / 80 / 443
  • ⏳ TODO:Caddy CF IP 白名单(防绕 CF 直连源站)

6. systemd 服务清单 ​

仓库 systemd/ 带三个 unit 模板(%h = 服务 User= 的 home,fork 用户只需改 User=/Group=,路径默认 ~/openInvest):

unit起什么端口备注
invest-web.serviceopeninvest-web(FastAPI)127.0.0.1:8765REST,供 remote hub 转发 / 内部触发;走 openinvest-web 入口(带 token 守卫),别裸 uvicorn
invest-mcp.serviceopeninvest-mcp --http(remote MCP)127.0.0.1:8766spoke agent 经 Caddy /mcp 直连;鉴权复用 INVEST_API_TOKEN
invest-scheduler.servicepython -m openinvest.scheduler.runner无跑 jobs/*.yml 全部定时任务(event_watch / price_sentinel / pnl_snapshot / backup / fund_confirm / job_watchdog…);没有它 job 一个都不跑
bash
# 三个一起装(User=ubuntu 先改成你的用户名;路径不是 ~/openInvest 的话改 INVEST_HOME 等 4 行)
sudo cp systemd/invest-{web,mcp,scheduler}.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now invest-web invest-mcp invest-scheduler
sudo systemctl list-units --type=service | grep invest

升级包后三个都要手动 restart 才拉新代码:sudo systemctl restart invest-web invest-mcp invest-scheduler。 调度器的失败面(job 卡死 / 进程死亡)见下面 §6.1;详见 systemd/README.md 和 scheduler/README.md。

6.1 调度器防静默停摆:job 超时 + 外部心跳 ​

2026-07-30 起 event_watch 因新闻抓取线程池 join 卡死 21 天:APScheduler max_instances=1 把之后每次触发都跳过,job_runs 一行没有。两层防线:

① 每 job 墙钟超时(jobs/*.yml 的 timeout_minutes,默认 30)

  • runner 在 daemon 线程里跑 job,超时 → job_runs.status='timeout'(附线程调用栈)→ 放弃该线程、释放 max_instances=1 槽位 → 下一次触发正常进入;job_watchdog 把 timeout 当失败告警。
  • 失败告警:jobs/*.yml 可选 alert_on_failure: true(目前 backup / verdict_review / price_sentinel / fund_confirm)——最近一次已结束的运行是 failed 时看门狗发一次告警,按 (job, 当日) 去重;没标的 job 失败只落 job_runs。
  • 线程杀不掉,所以有上限:同一 job 累计 3 个超时线程未退出时 runner 拒绝再启动它,看门狗转报"停摆",sudo systemctl restart invest-scheduler 清空。
  • 字段说明与各 job 默认值见 src/openinvest/jobs/README.md。改了要重启调度器。

② 外部心跳(dead-man switch,可选)

看门狗自己跑在调度器里——调度进程死了它也哑。配一个外部"死人开关"服务,调度器每 5 分钟(启动时先一次)GET 它的 ping URL;ping 断了由外部服务告警:

  1. 在 healthchecks.io(免费档够用)建一个 check:Period = 5 minutes,Grace = 10 minutes,通知渠道自选;复制它的 ping URL(https://hc-ping.com/<uuid>)。
  2. 写进 .env(URL 是凭据,别提交进仓库):
    bash
    INVEST_SCHEDULER_HEARTBEAT_URL=https://hc-ping.com/<uuid>
    也可写 core/config/defaults.yaml 的 scheduler.heartbeat_url(git clone 形态)。留空 = 不发。
  3. sudo systemctl restart invest-scheduler,日志里没有 [heartbeat] ping 失败 即通;healthchecks.io 面板应在 1 分钟内变绿。

ping 失败只记 WARNING [heartbeat] ping 失败…,永远不影响调度。心跳本身是调度器线程池里的一个内部任务(不进 job_runs),所以进程死亡和线程池被全部占满都会让 ping 断掉。

6.2 memory/ 清理:*.bak.<时间戳> 与遗留 *.lock ​

  • memory/portfolio.md.bak.<ts>:openinvest init(含被"已有真实持仓"守卫拒绝的那次)/ migrate_profile / migrate_portfolio_to_holdings 覆盖前各留一份,从无清理(生产实测 247 份,其中 107 份内容逐字相同)。自动备份 job 排除 *.bak.*,它们不在任何轮转里。
  • *.lock:memory_store 的 sidecar 锁文件。当前版本用后即删(取锁后核对 inode 再 unlink,并发安全);升级前遗留的(生产实测 158 个)用下面的命令清。
bash
openinvest maintenance cleanup                 # 默认 dry-run:只打印将删除的数量,不动任何文件
openinvest maintenance cleanup --apply         # 确认数量后真删
openinvest maintenance cleanup --keep 30 --days 14   # 调保留策略

保留策略(宁多留):每个 family(如 portfolio.md)最新 20 份,或 mtime 在 7 天内的——并集保留,其余才删。*.lock 只删当前无人持有的(非阻塞 flock 探测,与正在跑的进程不抢锁)。该命令永远只操作本机数据目录,不会被 INVEST_API_BASE 转发走。

顺序:先升级并重启 web / mcp / scheduler 到带 inode 核对的新版本,再 --apply。旧版本进程取锁后不核对 inode,与清理动作的毫秒级时序重叠时理论上可能和新进程同时进临界区;先 dry-run 看数量,没问题再 apply。


7. 备份策略 ​

权威状态散在两个 store——别只看 memory/ ​

"钱"不是只在 memory/。无法重建的权威状态有两块,必须一起备份:

权威状态是什么丢了后果
memory/portfolio.md当前持仓 + 现金丢掉"我现在持有什么"
db/trades.db交易账本(planned→executed)丢掉 9 个月交易史
memory/{.committee,insights,daily,.dreams}历史决议 / 洞察理论可重生,但要烧大量 LLM token
.env凭据 + 配置重新申请 / 填写

⚠️ 常见误区:以为 db/ 整个都是可丢的行情缓存。db/trades.db 是账本不是缓存—— 照"db/ 不需备份"去迁移会丢掉整个交易历史。可丢的只是下面"不需要备份"列的那几个 db。

一键快照 / 迁移(推荐) ​

scripts/snapshot.py(开发仓脚本,不进 wheel——hub 上 clone 一份仓库或单拷这个 文件)把上面权威状态打成单个 tar.gz(WAL 安全的 sqlite online backup + sha256 校验),新机一条命令拉起。迁移 hub 到新机器就用它:

bash
# 旧机:打包(不含 .env 密钥值,只在 manifest 列出要填哪些 key)
INVEST_HOME=~/openInvest uv run python -m scripts.snapshot snapshot --out ~/invest-snapshot.tar.gz

# 新机:装好 openinvest 后还原(默认拒绝覆盖已有账本,--force 才覆盖)
INVEST_HOME=~/openInvest uv run python -m scripts.snapshot restore --in ~/invest-snapshot.tar.gz

异地备份(每周自动) ​

~/openinvest-research-archive/refresh.sh(cron 0 3 * * 0)每周把决议/洞察 **和账本(portfolio.md + trades.db,落 invest_ledger/)**推到私有 repo openinvest-research-archive。这是当前唯一的机外副本——别误删那条 cron。

每天自动本地备份(内置 job backup,默认开) ​

调度器每天 03:30(Asia/Shanghai)跑 jobs/backup.py:先对 db/ 下的 SQLite 库做 checkpoint(TRUNCATE)(把 WAL 里还没落盘的成交刷进主库——调度器长驻连接期间不会自己做; 进包的是 trades.db / insights.db / jobs.sqlite),再把 memory/(排除 *.lock / *.bak.*)+ 这三个库打成 $INVEST_HOME/.backups/auto_<UTC时间戳>.zip,只留最近 14 份 auto_*(手动备份不动)。 每日不含 memory/.backtest*,每周日(Asia/Shanghai)带上(舰队不可重跑):回测产物占整份备份约 89% (全量 271 MB vs 22 MB),但 .backtest/ 里混着 2026-07-24 起的前瞻纸面舰队,那部分重跑不出来, 所以周日那份整份带上,文件名加 _full 后缀(auto_<stamp>_full.zip),共用同一个 14 份轮转,14 天里自然留 2 份。 不备 market_data.db / events.db(可重拉的缓存)与 .env。失败抛异常 → job_runs 记 failed;backup.yml 标了 alert_on_failure: true,job_watchdog 据此告警(同一天只报一次)。 zip 内路径相对数据根目录,恢复用 plugin/skills/invest-backup 的 run.sh restore <zip>。 这是机内冷备,不替代上面的每周异地备份。

每天本地冷备(tar.gz,可选) ​

bash
# 每天 cron——直接复用 snapshot.py,一份 tar 含全部权威状态
0 4 * * *  cd $HOME/repos/openInvest && INVEST_HOME=$HOME/openInvest $HOME/.local/bin/uv run python -m scripts.snapshot snapshot --out /backup/invest-$(date +\%F).tar.gz

Cloudflare Access 配置 ​

CF Dashboard 端的 Access policy 没有 git 备份,建议手动截图保存策略 JSON。

不需要备份(新机首跑自动重建) ​

  • db/market_data.db / db/events.db (行情 + 新闻缓存,可重新拉)
  • db/chroma.sqlite3 / db/jobs.sqlite (向量库 / 调度器 job store,可重建)
  • cache_data/ (HTTP cache)
  • memory/.backtest* (回测研究产物,非账本;例外:.backtest/ 里 2026-07-24 起的前瞻纸面舰队重跑不出来,内置 backup job 每周日的 _full 备份会带上)

8. 监控(可选) ​

后端日志:

bash
sudo journalctl -u invest-web -f --since "1 hour ago"

CF Access 日志:CF Zero Trust → Logs → Access。

数据源健康:uvx openinvest doctor(或 hub 上 curl 127.0.0.1:8765/api/health)。


9. 多设备:hub-and-spoke 远端模式(2026-06) ​

一台机器(hub)持有唯一的 memory/ 并跑 web_api;其他设备(笔记本/另一台 开发机)的 CLI / Claude Code skill 设 INVEST_API_BASE 后所有子命令转发到 hub,读写都走 HTTP——锁仍是 hub 单机 fcntl,零分布式复杂度。中央调参 自动成立:strategy / 角色 prompt 都在 hub,改一处全设备生效。

推荐新路径:remote MCP(BETA,2026-07,REST 退役路线 A) ​

⚠️ BETA:作者本人未在真实多设备环境测试过(仅自动化测试 + hub 本机 curl 端到端)。已知问题与求助清单见 issue(HELP WANTED)。

hub 常驻 openinvest-mcp --http(streamable-HTTP,绑 127.0.0.1:8766, systemd/invest-mcp.service),spoke 机器的 agent 直连 MCP,不再经 CLI→REST 转发。全部工具(读/写/委员会 Direct)均可用,鉴权复用同一 INVEST_API_TOKEN(bearer,/health 豁免):

bash
# spoke 侧注册(Claude Code;其他 MCP client 同理配 url + header)
claude mcp add --transport http openinvest https://invest.your-domain.com/mcp \
    --header "Authorization: Bearer $INVEST_API_TOKEN"
# CF Access Service Token 场景再加:
#   --header "CF-Access-Client-Id: xxx.access" --header "CF-Access-Client-Secret: yyy"

hub 的 Caddy 加一条路由(后端 Host 校验:token 模式默认关闭——反代下游 Host 是公网域名,token 才是信任边界;要收紧可设 INVEST_MCP_ALLOWED_HOSTS=invest.your-domain.com 白名单):

caddy
handle /mcp {
    reverse_proxy 127.0.0.1:8766 {
        flush_interval -1
    }
}

nginx 等反代需要关闭响应缓冲,让 SSE 进度和心跳及时到达客户端:

nginx
location = /mcp {
    proxy_pass http://127.0.0.1:8766;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_buffering off;
    proxy_read_timeout 300s;
}

只靠 CF Access、不设 INVEST_API_TOKEN(Access-only)时必须设 INVEST_MCP_ALLOWED_HOSTS:无 token 时后端保留 MCP SDK 默认的 loopback Host 白名单(127.0.0.1 / localhost / [::1]),反代转发来的公网域名 Host 会直接 被回 421 Invalid Host header。在 hub 的 .env 写上对外域名(逗号分隔, 条目支持 host:* 通配端口):

bash
INVEST_MCP_ALLOWED_HOSTS=invest.your-domain.com

Access Service Token 和应用 Bearer token 是两层独立鉴权,组合使用时客户端必须 同时发送上文的三项 header。

与 REST 转发的关系:CLI→REST 转发(下文 INVEST_API_BASE)进入维护模式, 仍支持但不再演进——它还覆盖 remote MCP 没有的 event_check (Coordinator 协议 2026-10-18 起已有 MCP 工具 prepare_committee / submit_committee)(doctor 已有 MCP 工具);日常读写/Direct 委员会请优先 remote MCP。HTTP 使用 stateless SSE:run_committee 在同一请求中返回最终结果, 期间发送 SDK 心跳,并在客户端提供 progress token 时推送阶段进度。同步工具的 阻塞 IO 在线程池执行,慢查询不会占住事件循环、阻塞其他客户端或心跳。 自写脚本直连 /mcp 时,POST 的 Accept 头必须同时列出 application/json 和 text/event-stream,只写 application/json 会得到 406(官方 SDK、Claude Code、Codex 都已这样发)。

已知限制:心跳不能延长客户端设置的总调用期限,也不能保证所有代理配置都允许 长请求;仍需按真实部署验证 Cloudflare/Access 链路。Cloudflare 的实际限额以 官方 524 文档 为准。stateless 模式不提供断线续传;调用中断后应先查状态/当日委员会缓存, 不要自动重放买卖、入金等非幂等写操作。长任务也可继续由 hub 侧 cron/REST 轮询路径跑。 自动部署(invest-deploy.sh)的 restart 行记得加 invest-mcp.service。


10. 顾问模式(INVEST_ADVISORY_MODE) ​

适用场景:你想在群聊里部署一个"投资顾问版艾露猫",让其他人可以问 "XXX 能不能买"、"怎么看黄金",但不暴露你的真实持仓,也不能买卖操作。

设置 INVEST_ADVISORY_MODE=1 环境变量即可开启:

bash
# 启动 MCP 时注入
INVEST_ADVISORY_MODE=1 uvx openinvest mcp

顾问模式行为变化 ​

顾问模式白名单只放行委员会分析必需的工具 + 新闻源管理(只动本实例自己的 INVEST_HOME/rss_feeds.yml),其余一律拒绝(包括事件入库 ingest_event)(mcp_server.py 的 ADVISORY_ALLOWED_TOOLS;改动需求见 test_mcp_server.py::test_advisory_mode_gate_is_closed_set 的机器强制契约,白名单外新增工具漏加闸会直接 CI 红):

工具正常模式顾问模式
run_committee✅ 任意标的✅ 任意标的
explain_decision✅✅
live_prices✅✅
ingest_event✅ 入库 + 命中持仓时按频控触发委员会❌ 不可用(入库内容会被后续顾问分析召回)
news_sources / add_news_source / remove_news_source✅✅ 管理本实例自己的额外源清单(仅公网 http(s) 地址、直连不走 HTTPS_PROXY + probe 校验 + 上限 30,群聊自助喂源用)
what_if✅❌ 不可用(本质是读真实持仓做假设推演,会泄露仓位/浮盈)
rebalance_plan✅❌ 不可用(对照真实持仓算该买卖多少,会泄露仓位)
record_execution✅❌ 不可用(写真实决策账本,顾问模式下无合法用途)
status / strategy / history / discipline / decisions / doctor✅❌ 不可用
buy / sell / record_existing_position✅ 记账❌ 不可用
deposit / withdraw✅ 记账❌ 不可用
record_dividend / record_fee / record_split / record_return_of_capital✅ 记账(分红·利息 / 费用·税 / 拆股 / 退回本金,ADR-027)❌ 不可用
place_fund_order / confirm_fund_order / cancel_fund_order✅ 记账(场外基金两段式:下单扣现金记在途 / 按净值确认 / 撤销,ADR-027);fund_confirm job 每个工作日晚自动确认❌ 不可用
set_allocations✅❌ 不可用
track_asset / untrack_asset✅❌ 不可用

委员会分析仍然完整运行(Macro / Quant / Risk / CIO 四角色),但:

  • portfolio_summary 显示为顾问模式占位文案,不含任何真实持仓数据;
  • Dreaming 长期洞察(prior_insights)不注入 prompt;
  • 委员会跑完不落盘到你的真实 memory / 决策账本(persist_to_memory=False), 群聊查询不会污染你自己的 history / decisions / path_review 命中率统计。

已知残留风险:explain_decision 允许查看历史委员会 verdict——如果 decision_id 对应的分析是在开启顾问模式之前(也就是正常模式、真实持仓上下文 下)跑出来的,那份历史 transcript 本身就含真实 portfolio_summary。顾问模式没有 (也无法)事后过滤已落盘的历史文件。真正的隔离需要下面这条部署建议里的独立 INVEST_HOME。

另外,如果你给顾问实例的 INVEST_HOME 配了定时抓取,群聊里加的额外新闻源内容会 进入之后的顾问分析;所以顾问实例永远不要和你自己的真实实例共用 INVEST_HOME。

部署建议 ​

强烈建议顾问实例用独立 INVEST_HOME(独立 memory/ 目录,跑 invest-setup skill 或手动建一份空持仓即可), 而不是复用你自己账户的 INVEST_HOME——这样即使 explain_decision 被问到某个 decision_id,读到的也是顾问实例自己积累的历史,不会是你的真实持仓分析。

日常聊天场景(Hermes 等按 config 自己 spawn 子进程的 client)直接声明 stdio server,不需要手动起终端:

yaml
# ~/.hermes/config.yaml —— 与主实例并行注册一个顾问命名空间
mcp_servers:
  openinvest-advisor:
    command: uvx
    args: ["openinvest", "mcp"]
    env:
      INVEST_HOME: ~/openinvest-advisor   # 独立顾问实例的 memory 目录
      INVEST_ADVISORY_MODE: "1"

如果是给群聊 bot 后端复用(多个用户共享同一个常驻顾问实例,或跨机器连),按第 9 节的 remote MCP 方式起 HTTP 常驻服务再注册:

bash
INVEST_HOME=~/openinvest-advisor INVEST_ADVISORY_MODE=1 \
    uvx openinvest mcp --http --port 8767

# client 侧按 remote MCP 一节的方式注册(以 Claude Code 为例):
claude mcp add --transport http openinvest-advisor http://127.0.0.1:8767/mcp

对应的 agent prompt 模板见 20-agent-usage-tutorial.md。

笔记本 (client)                         hub(本机/VPS)
  run.sh status ──HTTP──┐                invest-web.service :8765
  run.sh buy ...        ├──────────────▶   ├─ /api/skill/*      (CLI 等价端点)
  prepare_committee ────┘                  ├─ /api/committee/*  (prepare/save/run)
  (本机零 memory/)                        └─ memory/  ← 唯一账本,fcntl 锁

hub 侧(已有部署零必改) ​

走既有 Caddy + CF Access 的部署什么都不用动——客户端用 CF Access Service Token 过边缘即可(推荐,见下)。没有 CF 的局域网场景才需要:

bash
# .env
INVEST_API_TOKEN=<openssl rand -hex 24>   # 开应用层鉴权
INVEST_WEB_HOST=0.0.0.0                   # 绑出 loopback(局域网直连时)

token 语义:**所有来源(含 loopback)**访问 /api/*(/api/health 豁免) 及 /docs /openapi.json /redoc 都要求 Authorization: Bearer;不设 token 行为完全不变。(原 loopback 豁免已删——反代下连接源恒为 127.0.0.1,豁免即裸奔; token 模式下浏览器裸开 /docs 会 401,用 curl -H 或反代注入 header。)

客户端侧(2 分钟) ​

bash
# 装 skill(run.sh 首跑自动从 PyPI 拉 openinvest),然后 $INVEST_HOME/.env 只要:
INVEST_API_BASE=https://invest.your-domain.com   # 或 http://10.0.0.x:8765
INVEST_API_TOKEN=...                             # hub 开了才需要
~/.claude/skills/invest/scripts/run.sh doctor    # 验证:status ready + remote 段

客户端没有 memory/、不需要 DeepSeek key / Gmail 凭据。init 在远端 模式下被禁用;run_committee 在 hub 上跑(CLI 自动轮询);live_prices / correlate 仍本地跑。写操作落 hub 账本,history 记 source: skill_remote。 buy --existing-position(补录已持有仓位、不扣现金)不走 REST 转发——/api/skill/buy 没有这个字段,客户端直接报错而不是让 hub 扣现金;去 hub 上跑,或用 remote MCP 的 record_existing_position 工具(hub 版本没有这个工具时回 Unknown tool,不会扣现金)。

推荐:Cloudflare Tunnel + Access Service Token(hub 不开公网端口) ​

  1. CF Zero Trust → Access → Service Auth → 创建 Service Token,拿到 Client ID / Secret
  2. Access Application(invest 域名)的 Policy 加一条 Service Auth include
  3. 客户端 .env:
    bash
    INVEST_API_BASE=https://invest.your-domain.com
    CF_ACCESS_CLIENT_ID=xxx.access
    CF_ACCESS_CLIENT_SECRET=yyy
    remote dispatch 会自动带上 CF-Access-Client-Id/Secret 头;浏览器用户 照旧走 SSO。hub 继续只绑 127.0.0.1,后端零改动。

已知限制 ​

  • 委员会跑在独立 daemon 线程(#105),其余阻塞端点(event_watch 扫描、 committee prepare、yfinance / IMAP 读、sqlite)由 FastAPI 线程池执行 (#233-3),不再冻结事件循环,单 worker 即可;委员会长任务走"触发 + 轮询", 不会撞 CF ~100s 代理超时。
  • 客户端与 hub 的"同日 cache"以 hub 的日期为准(取自 /api/health 时间戳),跨时区设备不会错位。

下一步 ​

→ 09-troubleshooting.md — 部署后跑挂了去哪查

→ adr/ — 为什么这套部署模式(不上 SSR / 不上 K8s)