{"repo": "actionow-ai/x2t", "files": {"DEPLOY.md": {"f90487430f8d93fa245f81ac7b37a2014301e3b5": {"t": 1785746651.258443, "n": 5422, "h": "9bccb9a4afd7b72c", "c": "# 部署 · 让 X2T 真正可用\n\nX2T 的所有 mock 都遵循同一规则:**对应环境变量留空 → 自动降级为 mock;填上 → 立即变真**。\n所以「从演示到可用」基本是「填对几个 key + 起好抓取源」,无需改代码。\n\n---\n\n## 1. 哪些 key 让什么变真\n\n| 你想要 | 在 `.env` 配置 | 不配时 |\n| --- | --- | --- |\n| 真实 AI 分析 | `LLM_API_KEY`(+ `LLM_BASE_URL` / `LLM_MODEL`)| mock:关键词占位分析 |\n| 真实行情/外部数据 | `FINNHUB_API_KEY` | mock:随机占位行情 |\n| 真实邮件(登录链接 + 摘要)| `CF_EMAIL_ACCOUNT_ID`+`CF_EMAIL_API_TOKEN`+`EMAIL_FROM`(Cloudflare Email Service),或传统 `SMTP_*` | 邮件打到控制台 |\n| 安全的会话 | `AUTH_SECRET`(`openssl rand -hex 32`)| 不安全的默认值 |\n| 限定管理员 | `ADMIN_EMAILS`(逗号分隔)| 任何登录用户都是管理员 |\n| 真实抓取 X | 起 RSSHub + `TWITTER_AUTH_TOKEN` + `RSSHUB_BASE_URL` | 该源抓取失败(其余源不受影响)|\n\n> LLM 推荐 DeepSeek(便宜):`LLM_BASE_URL=https://api.deepseek.com`、`LLM_MODEL=deepseek-chat`。\n> Finnhub 免费档即可起步。\n\n---\n\n## 2. 抓取数据源\n\n抓取连接器是 RSS(`src/lib/connectors/rss.ts`),`sourceConfig` 两种写法:\n\n- **`feedUrl`** —— 任意可直连的真实 RSS(财经新闻 / Substack / 博客)。开箱即用,无需 RSSHub。\n 例:`{ \"connector\": \"rss\", \"feedUrl\": \"https://feeds.marketwatch.com/marketwatch/topstories/\" }`\n- **`feedPath`** —— 经自建 RSSHub 解析(`RSSHUB_BASE_URL` + path)。用于 X / Reddit / StockTwits 等需登录或反爬的源。\n 例:`{ \"connector\": \"rss\", \"feedPath\": \"/twitter/user/serenity\" }`\n\n`feedPath` 把「源路由」与「RSSHub 实例地址」解耦:换实例只改 `RSSHUB_BASE_URL`,无需重新 seed。\n\n### 自建 RSSHub 抓 X(本项目选定方案)\n\n1. 起服务:`docker compose up -d rsshub`(宿主 `51200` → 容器 `1200`)。\n2. X 抓取需 RSSHub 配置访问凭据 `TWITTER_AUTH_TOKEN`(compose 注入 RSSHub 容器):请参照 [RSSHub 官方文档](https://docs.rsshub.app) 自行配置。**注意**:用账号凭据做自动化抓取可能违反第三方平台服务条款,请自行评估合规与账号风险,或优先选用 Substack / 新闻等授权语义更清晰的 RSS 源。\n3. 应用侧把博主 `sourceConfig` 写成 `{ connector: \"rss\", feedPath: \"/twitter/user/\" }`(seed.ts 已示范)。\n4. 验证:`curl http://localhost:51200/twitter/user/` 能返回 XML 即通。\n\nRSSHub 同样支持 `/reddit/...`、`/stocktwits/...` 等大量路由(见 https://docs.rsshub.app)。注意:X 反爬严格,cookie 会过期、路由可能间歇失败 —— 这是该方案的固有风险,必要时考虑 X 官方 API(见 §5)。\n\n---\n\n## 3. 生产部署(docker compose)\n\n> 一键完成本节全部步骤:仓库根目录执行 **`./deploy.sh`**(生成 .env + 随机 AUTH_SECRET → 构建启动全栈 → 建表 + 种子,幂等可重复执行)。下面是手动等价步骤。\n\n镜像见 `Dockerfile`(**单一合并镜像**:一个容器内由进程监督脚本同时跑 web `next start` 与后台 worker,任一退出则整体退出让平台重启)。早期的「多目标 app/worker」方案因 zbpack 限制已废弃,详见下方踩坑。\n\n```bash\n# 0. 准备 .env(至少 AUTH_SECRET;要变真则填 LLM/Finnhub/SMTP/TWITTER_AUTH_TOKEN)\ncp .env.example .env && openssl rand -hex 32 # 把输出填进 AUTH_SECRET\n\n# 1. 构建并启动全栈(db + rsshub + app);app 容器内同时跑 web 与 worker\ndocker compose --profile full up -d --build\n\n# 2. 首次建表 + 灌种子(app 镜像含 prisma CLI 与脚本)\ndocker compose run --rm app pnpm db:push\ndocker compose run --rm app pnpm db:seed\n\n# 3. 访问:app 在宿主 53000(建议挂到反向代理 + HTTPS)\n```\n\n> **单一合并镜像**:`Dockerfile` 构建的镜像在一个容器内同时跑 Next 网页与后台 worker(`CMD` 后台起 `pnpm worker`、前台起 `next start`)。这样无论 `docker compose` 还是 Zeabur,都只需部署「一个应用服务 + 一个数据库」。\n\n- **HTTPS 必需**:Web-Push 与 Service Worker 只在 HTTPS(或 localhost)下工作。生产请在 app 前加 Nginx/Caddy/云负载均衡做 TLS。\n- **数据库**:compose 内置 Postgres 适合自托管;也可用托管库(Neon/Supabase/RDS),把 `DATABASE_URL` 指过去即可(此时可不起 `db` 服务)。\n- **每日摘要**:默认关。两种开法 —— 设 `WORKER_RUN_DIGEST=true`(app 容器内的 worker 按 `DIGEST_INTERVAL_MS` 跑),或用外部 cron:`docker compose run --rm app pnpm digest`。\n\n### 本机开发(只起依赖)\n\n`docker compose up -d db rsshub` 只起依赖;网页用 `pnpm dev`、后台用 `pnpm worker` 在宿主分开跑,便于热更新。合并的 `app` 服务挂在 `full` profile,默认不随 `docker compose up` 启动。\n\n---\n\n## 3b. Zeabur 部署(CLI · 本项目已上线)\n\n本项目已用 Zeabur CLI 部署到自有服务器(AI-Gateway / 2C4G)。要点:\n\n- **私有库 Zeabur 看不到** → app 用 `zeabur deploy`(本地上传,非 Git)。\n- **zbpack 对存在的根 `Dockerfile` 一律使用、且忽略 `ZBPACK_DOCKERFILE_NAME` 等按服务选择** → 无法用两个 Dockerfile 区分两个服务,故采用「单一合并镜像」:一个服务、容器内同时跑 web+worker。\n- 线上服务:**Postgres**(模板)+ **x2t-app**(合并镜像,含 web+worker)+ **RSSHub**(模板 X46PTP,内网专用,注入 `TWITTER_AUTH_TOKEN` 抓 X)+ Redis(RSSHub 缓存)。\n\n```bash\n# 1. 建项目(绑定到目标服务器 region)+ Postgres 模板\nzeabur project create -n x2t -r \nzeabur template deploy -c B20CX0 --project-id # 官方 PostgreSQL\n\n# 2. 部署 app(本地上传,自动用根 Dockerfile)\nzeabur deploy --create --name x2t-app --project-id \n\n# 3. 注入变量(DATABASE_URL 用 Zeabur 引用变量;含 AUTH_SECRET/VAPID 等)\n# .env.zeabur.local 内含一行 DATABASE_URL=${POSTGRES_CONNECTION_STRING}\nzeabur variable env -f .env.zeabur.local --id --env-id \n\n# 4. 远程初始化库(用 Postgres 的【公网】连接串,从本机跑)\nzeabur service instruction --id --env-id # 取 Connection String\nDATABASE_URL= pnpm db:push\nDATABASE_URL= pnpm db:seed\n\n# 5. 重新部署 app 让变量生效(本地上传服务用 deploy 重传,redeploy 仅限 Git 绑定)\nzeabur deploy --service-id \n\n# 6. 开域名\nzeabur domain create --domain --id --env-id -g -y\n```\n\n> 更新部署:改完代码后 `zeabur deploy --service-id ` 重传重建即可。\n> 改了 schema:先用公网连接串 `DATABASE_URL=... pnpm db:push`,再重部署 app。\n\n---\n\n## 4. 升级数据库结构\n\n改了 `prisma/schema.prisma` 后:\n\n```bash\npnpm db:push # 本机\ndocker compose run --rm app pnpm db:push # 容器\n```\n\n> 注意:运行中的应用进程会缓存旧的 Prisma Client,改 enum/字段后需**重启 app/worker** 才生效。\n\n---\n\n## 5. 抓取的替代/兜底方案\n\n- **X 官方 API v2**:最稳定(约 $100/月起,有限流)。在 `src/lib/connectors/` 加一个连接器(实现 `Connector.fetch`),在 `connectors/index.ts` 注册,博主 `sourceConfig.connector` 指向它。\n- **人工提交**:`/submit` 表单已可用(`manual` 连接器),无需任何抓取设施即可录入。\n- **第三方抓取**(如 Apify):同样以新连接器形式接入,按量计费。\n\n---\n\n## 6. 上线安全清单\n\n- [ ] `AUTH_SECRET` 为随机值(`openssl rand -hex 32`),且未提交(`.env` 已在 `.gitignore`)。\n- [ ] `ADMIN_EMAILS` 设为真实管理员白名单(否则任何登录用户都是管理员)。\n- [ ] app 置于 HTTPS 之后(Web-Push 必需)。\n- [ ] 数据库口令非默认 `x2t:x2t`(自托管 Postgres 时改 compose 与 `DATABASE_URL`)。\n- [ ] `TWITTER_AUTH_TOKEN` 等密钥仅在服务器 `.env`,不进版本库、不进日志。\n"}, "0acc06f0578fcce5ed5eae6f4e63493a3d0d0be0": {"t": 1785766576.11563, "n": 5422, "h": "9bccb9a4afd7b72c", "c": "# 部署 · 让 X2T 真正可用\n\nX2T 的所有 mock 都遵循同一规则:**对应环境变量留空 → 自动降级为 mock;填上 → 立即变真**。\n所以「从演示到可用」基本是「填对几个 key + 起好抓取源」,无需改代码。\n\n---\n\n## 1. 哪些 key 让什么变真\n\n| 你想要 | 在 `.env` 配置 | 不配时 |\n| --- | --- | --- |\n| 真实 AI 分析 | `LLM_API_KEY`(+ `LLM_BASE_URL` / `LLM_MODEL`)| mock:关键词占位分析 |\n| 真实行情/外部数据 | `FINNHUB_API_KEY` | mock:随机占位行情 |\n| 真实邮件(登录链接 + 摘要)| `CF_EMAIL_ACCOUNT_ID`+`CF_EMAIL_API_TOKEN`+`EMAIL_FROM`(Cloudflare Email Service),或传统 `SMTP_*` | 邮件打到控制台 |\n| 安全的会话 | `AUTH_SECRET`(`openssl rand -hex 32`)| 不安全的默认值 |\n| 限定管理员 | `ADMIN_EMAILS`(逗号分隔)| 任何登录用户都是管理员 |\n| 真实抓取 X | 起 RSSHub + `TWITTER_AUTH_TOKEN` + `RSSHUB_BASE_URL` | 该源抓取失败(其余源不受影响)|\n\n> LLM 推荐 DeepSeek(便宜):`LLM_BASE_URL=https://api.deepseek.com`、`LLM_MODEL=deepseek-chat`。\n> Finnhub 免费档即可起步。\n\n---\n\n## 2. 抓取数据源\n\n抓取连接器是 RSS(`src/lib/connectors/rss.ts`),`sourceConfig` 两种写法:\n\n- **`feedUrl`** —— 任意可直连的真实 RSS(财经新闻 / Substack / 博客)。开箱即用,无需 RSSHub。\n 例:`{ \"connector\": \"rss\", \"feedUrl\": \"https://feeds.marketwatch.com/marketwatch/topstories/\" }`\n- **`feedPath`** —— 经自建 RSSHub 解析(`RSSHUB_BASE_URL` + path)。用于 X / Reddit / StockTwits 等需登录或反爬的源。\n 例:`{ \"connector\": \"rss\", \"feedPath\": \"/twitter/user/serenity\" }`\n\n`feedPath` 把「源路由」与「RSSHub 实例地址」解耦:换实例只改 `RSSHUB_BASE_URL`,无需重新 seed。\n\n### 自建 RSSHub 抓 X(本项目选定方案)\n\n1. 起服务:`docker compose up -d rsshub`(宿主 `51200` → 容器 `1200`)。\n2. X 抓取需 RSSHub 配置访问凭据 `TWITTER_AUTH_TOKEN`(compose 注入 RSSHub 容器):请参照 [RSSHub 官方文档](https://docs.rsshub.app) 自行配置。**注意**:用账号凭据做自动化抓取可能违反第三方平台服务条款,请自行评估合规与账号风险,或优先选用 Substack / 新闻等授权语义更清晰的 RSS 源。\n3. 应用侧把博主 `sourceConfig` 写成 `{ connector: \"rss\", feedPath: \"/twitter/user/\" }`(seed.ts 已示范)。\n4. 验证:`curl http://localhost:51200/twitter/user/` 能返回 XML 即通。\n\nRSSHub 同样支持 `/reddit/...`、`/stocktwits/...` 等大量路由(见 https://docs.rsshub.app)。注意:X 反爬严格,cookie 会过期、路由可能间歇失败 —— 这是该方案的固有风险,必要时考虑 X 官方 API(见 §5)。\n\n---\n\n## 3. 生产部署(docker compose)\n\n> 一键完成本节全部步骤:仓库根目录执行 **`./deploy.sh`**(生成 .env + 随机 AUTH_SECRET → 构建启动全栈 → 建表 + 种子,幂等可重复执行)。下面是手动等价步骤。\n\n镜像见 `Dockerfile`(**单一合并镜像**:一个容器内由进程监督脚本同时跑 web `next start` 与后台 worker,任一退出则整体退出让平台重启)。早期的「多目标 app/worker」方案因 zbpack 限制已废弃,详见下方踩坑。\n\n```bash\n# 0. 准备 .env(至少 AUTH_SECRET;要变真则填 LLM/Finnhub/SMTP/TWITTER_AUTH_TOKEN)\ncp .env.example .env && openssl rand -hex 32 # 把输出填进 AUTH_SECRET\n\n# 1. 构建并启动全栈(db + rsshub + app);app 容器内同时跑 web 与 worker\ndocker compose --profile full up -d --build\n\n# 2. 首次建表 + 灌种子(app 镜像含 prisma CLI 与脚本)\ndocker compose run --rm app pnpm db:push\ndocker compose run --rm app pnpm db:seed\n\n# 3. 访问:app 在宿主 53000(建议挂到反向代理 + HTTPS)\n```\n\n> **单一合并镜像**:`Dockerfile` 构建的镜像在一个容器内同时跑 Next 网页与后台 worker(`CMD` 后台起 `pnpm worker`、前台起 `next start`)。这样无论 `docker compose` 还是 Zeabur,都只需部署「一个应用服务 + 一个数据库」。\n\n- **HTTPS 必需**:Web-Push 与 Service Worker 只在 HTTPS(或 localhost)下工作。生产请在 app 前加 Nginx/Caddy/云负载均衡做 TLS。\n- **数据库**:compose 内置 Postgres 适合自托管;也可用托管库(Neon/Supabase/RDS),把 `DATABASE_URL` 指过去即可(此时可不起 `db` 服务)。\n- **每日摘要**:默认关。两种开法 —— 设 `WORKER_RUN_DIGEST=true`(app 容器内的 worker 按 `DIGEST_INTERVAL_MS` 跑),或用外部 cron:`docker compose run --rm app pnpm digest`。\n\n### 本机开发(只起依赖)\n\n`docker compose up -d db rsshub` 只起依赖;网页用 `pnpm dev`、后台用 `pnpm worker` 在宿主分开跑,便于热更新。合并的 `app` 服务挂在 `full` profile,默认不随 `docker compose up` 启动。\n\n---\n\n## 3b. Zeabur 部署(CLI · 本项目已上线)\n\n本项目已用 Zeabur CLI 部署到自有服务器(AI-Gateway / 2C4G)。要点:\n\n- **私有库 Zeabur 看不到** → app 用 `zeabur deploy`(本地上传,非 Git)。\n- **zbpack 对存在的根 `Dockerfile` 一律使用、且忽略 `ZBPACK_DOCKERFILE_NAME` 等按服务选择** → 无法用两个 Dockerfile 区分两个服务,故采用「单一合并镜像」:一个服务、容器内同时跑 web+worker。\n- 线上服务:**Postgres**(模板)+ **x2t-app**(合并镜像,含 web+worker)+ **RSSHub**(模板 X46PTP,内网专用,注入 `TWITTER_AUTH_TOKEN` 抓 X)+ Redis(RSSHub 缓存)。\n\n```bash\n# 1. 建项目(绑定到目标服务器 region)+ Postgres 模板\nzeabur project create -n x2t -r \nzeabur template deploy -c B20CX0 --project-id # 官方 PostgreSQL\n\n# 2. 部署 app(本地上传,自动用根 Dockerfile)\nzeabur deploy --create --name x2t-app --project-id \n\n# 3. 注入变量(DATABASE_URL 用 Zeabur 引用变量;含 AUTH_SECRET/VAPID 等)\n# .env.zeabur.local 内含一行 DATABASE_URL=${POSTGRES_CONNECTION_STRING}\nzeabur variable env -f .env.zeabur.local --id --env-id \n\n# 4. 远程初始化库(用 Postgres 的【公网】连接串,从本机跑)\nzeabur service instruction --id --env-id # 取 Connection String\nDATABASE_URL= pnpm db:push\nDATABASE_URL= pnpm db:seed\n\n# 5. 重新部署 app 让变量生效(本地上传服务用 deploy 重传,redeploy 仅限 Git 绑定)\nzeabur deploy --service-id \n\n# 6. 开域名\nzeabur domain create --domain --id --env-id -g -y\n```\n\n> 更新部署:改完代码后 `zeabur deploy --service-id ` 重传重建即可。\n> 改了 schema:先用公网连接串 `DATABASE_URL=... pnpm db:push`,再重部署 app。\n\n---\n\n## 4. 升级数据库结构\n\n改了 `prisma/schema.prisma` 后:\n\n```bash\npnpm db:push # 本机\ndocker compose run --rm app pnpm db:push # 容器\n```\n\n> 注意:运行中的应用进程会缓存旧的 Prisma Client,改 enum/字段后需**重启 app/worker** 才生效。\n\n---\n\n## 5. 抓取的替代/兜底方案\n\n- **X 官方 API v2**:最稳定(约 $100/月起,有限流)。在 `src/lib/connectors/` 加一个连接器(实现 `Connector.fetch`),在 `connectors/index.ts` 注册,博主 `sourceConfig.connector` 指向它。\n- **人工提交**:`/submit` 表单已可用(`manual` 连接器),无需任何抓取设施即可录入。\n- **第三方抓取**(如 Apify):同样以新连接器形式接入,按量计费。\n\n---\n\n## 6. 上线安全清单\n\n- [ ] `AUTH_SECRET` 为随机值(`openssl rand -hex 32`),且未提交(`.env` 已在 `.gitignore`)。\n- [ ] `ADMIN_EMAILS` 设为真实管理员白名单(否则任何登录用户都是管理员)。\n- [ ] app 置于 HTTPS 之后(Web-Push 必需)。\n- [ ] 数据库口令非默认 `x2t:x2t`(自托管 Postgres 时改 compose 与 `DATABASE_URL`)。\n- [ ] `TWITTER_AUTH_TOKEN` 等密钥仅在服务器 `.env`,不进版本库、不进日志。\n"}, "a4652f65524db85697bbc9690d10064d987da674": {"t": 1785766577.5352404, "n": 5318, "h": "9d45b0c8397bfb50", "c": "# 部署 · 让 X2T 真正可用\n\nX2T 的所有 mock 都遵循同一规则:**对应环境变量留空 → 自动降级为 mock;填上 → 立即变真**。\n所以「从演示到可用」基本是「填对几个 key + 起好抓取源」,无需改代码。\n\n---\n\n## 1. 哪些 key 让什么变真\n\n| 你想要 | 在 `.env` 配置 | 不配时 |\n| --- | --- | --- |\n| 真实 AI 分析 | `LLM_API_KEY`(+ `LLM_BASE_URL` / `LLM_MODEL`)| mock:关键词占位分析 |\n| 真实行情/外部数据 | `FINNHUB_API_KEY` | mock:随机占位行情 |\n| 真实邮件(登录链接 + 摘要)| `CF_EMAIL_ACCOUNT_ID`+`CF_EMAIL_API_TOKEN`+`EMAIL_FROM`(Cloudflare Email Service),或传统 `SMTP_*` | 邮件打到控制台 |\n| 安全的会话 | `AUTH_SECRET`(`openssl rand -hex 32`)| 不安全的默认值 |\n| 限定管理员 | `ADMIN_EMAILS`(逗号分隔)| 任何登录用户都是管理员 |\n| 真实抓取 X | 起 RSSHub + `TWITTER_AUTH_TOKEN` + `RSSHUB_BASE_URL` | 该源抓取失败(其余源不受影响)|\n\n> LLM 推荐 DeepSeek(便宜):`LLM_BASE_URL=https://api.deepseek.com`、`LLM_MODEL=deepseek-chat`。\n> Finnhub 免费档即可起步。\n\n---\n\n## 2. 抓取数据源\n\n抓取连接器是 RSS(`src/lib/connectors/rss.ts`),`sourceConfig` 两种写法:\n\n- **`feedUrl`** —— 任意可直连的真实 RSS(财经新闻 / Substack / 博客)。开箱即用,无需 RSSHub。\n 例:`{ \"connector\": \"rss\", \"feedUrl\": \"https://feeds.marketwatch.com/marketwatch/topstories/\" }`\n- **`feedPath`** —— 经自建 RSSHub 解析(`RSSHUB_BASE_URL` + path)。用于 X / Reddit / StockTwits 等需登录或反爬的源。\n 例:`{ \"connector\": \"rss\", \"feedPath\": \"/twitter/user/serenity\" }`\n\n`feedPath` 把「源路由」与「RSSHub 实例地址」解耦:换实例只改 `RSSHUB_BASE_URL`,无需重新 seed。\n\n### 自建 RSSHub 抓 X(本项目选定方案)\n\n1. 起服务:`docker compose up -d rsshub`(宿主 `51200` → 容器 `1200`)。\n2. X 抓取需 RSSHub 配置访问凭据 `TWITTER_AUTH_TOKEN`(compose 注入 RSSHub 容器):请参照 [RSSHub 官方文档](https://docs.rsshub.app) 自行配置。**注意**:用账号凭据做自动化抓取可能违反第三方平台服务条款,请自行评估合规与账号风险,或优先选用 Substack / 新闻等授权语义更清晰的 RSS 源。\n3. 应用侧把博主 `sourceConfig` 写成 `{ connector: \"rss\", feedPath: \"/twitter/user/\" }`(seed.ts 已示范)。\n4. 验证:`curl http://localhost:51200/twitter/user/` 能返回 XML 即通。\n\nRSSHub 同样支持 `/reddit/...`、`/stocktwits/...` 等大量路由(见 https://docs.rsshub.app)。注意:X 反爬严格,cookie 会过期、路由可能间歇失败 —— 这是该方案的固有风险,必要时考虑 X 官方 API(见 §5)。\n\n---\n\n## 3. 生产部署(docker compose)\n\n镜像见 `Dockerfile`(**单一合并镜像**:一个容器内由进程监督脚本同时跑 web `next start` 与后台 worker,任一退出则整体退出让平台重启)。早期的「多目标 app/worker」方案因 zbpack 限制已废弃,详见下方踩坑。\n\n```bash\n# 0. 准备 .env(至少 AUTH_SECRET;要变真则填 LLM/Finnhub/SMTP/TWITTER_AUTH_TOKEN)\ncp .env.example .env && openssl rand -hex 32 # 把输出填进 AUTH_SECRET\n\n# 1. 构建并启动全栈(db + rsshub + app);app 容器内同时跑 web 与 worker\ndocker compose --profile full up -d --build\n\n# 2. 首次建表 + 灌种子(app 镜像含 prisma CLI 与脚本)\ndocker compose run --rm app pnpm db:push\ndocker compose run --rm app pnpm db:seed\n\n# 3. 访问:app 在宿主 53000(建议挂到反向代理 + HTTPS)\n```\n\n> **单一合并镜像**:`Dockerfile` 构建的镜像在一个容器内同时跑 Next 网页与后台 worker(`CMD` 后台起 `pnpm worker`、前台起 `next start`)。这样无论 `docker compose` 还是 Zeabur,都只需部署「一个应用服务 + 一个数据库」。\n\n- **HTTPS 必需**:Web-Push 与 Service Worker 只在 HTTPS(或 localhost)下工作。生产请在 app 前加 Nginx/Caddy/云负载均衡做 TLS。\n- **数据库**:compose 内置 Postgres 适合自托管;也可用托管库(Neon/Supabase/RDS),把 `DATABASE_URL` 指过去即可(此时可不起 `db` 服务)。\n- **每日摘要**:默认关。两种开法 —— 设 `WORKER_RUN_DIGEST=true`(app 容器内的 worker 按 `DIGEST_INTERVAL_MS` 跑),或用外部 cron:`docker compose run --rm app pnpm digest`。\n\n### 本机开发(只起依赖)\n\n`docker compose up -d db rsshub` 只起依赖;网页用 `pnpm dev`、后台用 `pnpm worker` 在宿主分开跑,便于热更新。合并的 `app` 服务挂在 `full` profile,默认不随 `docker compose up` 启动。\n\n---\n\n## 3b. Zeabur 部署(CLI · 本项目已上线)\n\n本项目已用 Zeabur CLI 部署到自有服务器(AI-Gateway / 2C4G)。要点:\n\n- **私有库 Zeabur 看不到** → app 用 `zeabur deploy`(本地上传,非 Git)。\n- **zbpack 对存在的根 `Dockerfile` 一律使用、且忽略 `ZBPACK_DOCKERFILE_NAME` 等按服务选择** → 无法用两个 Dockerfile 区分两个服务,故采用「单一合并镜像」:一个服务、容器内同时跑 web+worker。\n- 线上服务:**Postgres**(模板)+ **x2t-app**(合并镜像,含 web+worker)+ **RSSHub**(模板 X46PTP,内网专用,注入 `TWITTER_AUTH_TOKEN` 抓 X)+ Redis(RSSHub 缓存)。\n\n```bash\n# 1. 建项目(绑定到目标服务器 region)+ Postgres 模板\nzeabur project create -n x2t -r \nzeabur template deploy -c B20CX0 --project-id # 官方 PostgreSQL\n\n# 2. 部署 app(本地上传,自动用根 Dockerfile)\nzeabur deploy --create --name x2t-app --project-id \n\n# 3. 注入变量(DATABASE_URL 用 Zeabur 引用变量;含 AUTH_SECRET/VAPID 等)\n# .env.zeabur.local 内含一行 DATABASE_URL=${POSTGRES_CONNECTION_STRING}\nzeabur variable env -f .env.zeabur.local --id --env-id \n\n# 4. 远程初始化库(用 Postgres 的【公网】连接串,从本机跑)\nzeabur service instruction --id --env-id # 取 Connection String\nDATABASE_URL= pnpm db:push\nDATABASE_URL= pnpm db:seed\n\n# 5. 重新部署 app 让变量生效(本地上传服务用 deploy 重传,redeploy 仅限 Git 绑定)\nzeabur deploy --service-id \n\n# 6. 开域名\nzeabur domain create --domain --id --env-id -g -y\n```\n\n> 更新部署:改完代码后 `zeabur deploy --service-id ` 重传重建即可。\n> 改了 schema:先用公网连接串 `DATABASE_URL=... pnpm db:push`,再重部署 app。\n\n---\n\n## 4. 升级数据库结构\n\n改了 `prisma/schema.prisma` 后:\n\n```bash\npnpm db:push # 本机\ndocker compose run --rm app pnpm db:push # 容器\n```\n\n> 注意:运行中的应用进程会缓存旧的 Prisma Client,改 enum/字段后需**重启 app/worker** 才生效。\n\n---\n\n## 5. 抓取的替代/兜底方案\n\n- **X 官方 API v2**:最稳定(约 $100/月起,有限流)。在 `src/lib/connectors/` 加一个连接器(实现 `Connector.fetch`),在 `connectors/index.ts` 注册,博主 `sourceConfig.connector` 指向它。\n- **人工提交**:`/submit` 表单已可用(`manual` 连接器),无需任何抓取设施即可录入。\n- **第三方抓取**(如 Apify):同样以新连接器形式接入,按量计费。\n\n---\n\n## 6. 上线安全清单\n\n- [ ] `AUTH_SECRET` 为随机值(`openssl rand -hex 32`),且未提交(`.env` 已在 `.gitignore`)。\n- [ ] `ADMIN_EMAILS` 设为真实管理员白名单(否则任何登录用户都是管理员)。\n- [ ] app 置于 HTTPS 之后(Web-Push 必需)。\n- [ ] 数据库口令非默认 `x2t:x2t`(自托管 Postgres 时改 compose 与 `DATABASE_URL`)。\n- [ ] `TWITTER_AUTH_TOKEN` 等密钥仅在服务器 `.env`,不进版本库、不进日志。\n"}, "5f2a6518632faf4f4db722dc531c3133a3bec15e": {"t": 1785766578.8582952, "n": 5283, "h": "bb6e4c37a92a48f4", "c": "# 部署 · 让 X2T 真正可用\n\nX2T 的所有 mock 都遵循同一规则:**对应环境变量留空 → 自动降级为 mock;填上 → 立即变真**。\n所以「从演示到可用」基本是「填对几个 key + 起好抓取源」,无需改代码。\n\n---\n\n## 1. 哪些 key 让什么变真\n\n| 你想要 | 在 `.env` 配置 | 不配时 |\n| --- | --- | --- |\n| 真实 AI 分析 | `LLM_API_KEY`(+ `LLM_BASE_URL` / `LLM_MODEL`)| mock:关键词占位分析 |\n| 真实行情/外部数据 | `FINNHUB_API_KEY` | mock:随机占位行情 |\n| 真实邮件(登录链接 + 摘要)| `CF_EMAIL_ACCOUNT_ID`+`CF_EMAIL_API_TOKEN`+`EMAIL_FROM`(Cloudflare Email Service),或传统 `SMTP_*` | 邮件打到控制台 |\n| 安全的会话 | `AUTH_SECRET`(`openssl rand -hex 32`)| 不安全的默认值 |\n| 限定管理员 | `ADMIN_EMAILS`(逗号分隔)| 任何登录用户都是管理员 |\n| 真实抓取 X | 起 RSSHub + `TWITTER_AUTH_TOKEN` + `RSSHUB_BASE_URL` | 该源抓取失败(其余源不受影响)|\n\n> LLM 推荐 DeepSeek(便宜):`LLM_BASE_URL=https://api.deepseek.com`、`LLM_MODEL=deepseek-chat`。\n> Finnhub 免费档即可起步。\n\n---\n\n## 2. 抓取数据源\n\n抓取连接器是 RSS(`src/lib/connectors/rss.ts`),`sourceConfig` 两种写法:\n\n- **`feedUrl`** —— 任意可直连的真实 RSS(财经新闻 / Substack / 博客)。开箱即用,无需 RSSHub。\n 例:`{ \"connector\": \"rss\", \"feedUrl\": \"https://feeds.marketwatch.com/marketwatch/topstories/\" }`\n- **`feedPath`** —— 经自建 RSSHub 解析(`RSSHUB_BASE_URL` + path)。用于 X / Reddit / StockTwits 等需登录或反爬的源。\n 例:`{ \"connector\": \"rss\", \"feedPath\": \"/twitter/user/serenity\" }`\n\n`feedPath` 把「源路由」与「RSSHub 实例地址」解耦:换实例只改 `RSSHUB_BASE_URL`,无需重新 seed。\n\n### 自建 RSSHub 抓 X(本项目选定方案)\n\n1. 起服务:`docker compose up -d rsshub`(宿主 `51200` → 容器 `1200`)。\n2. X 需要登录 cookie 的 `auth_token`:浏览器登录 x.com → 开发者工具 → Application/Cookies → 复制 `auth_token` 值,填到 `.env` 的 `TWITTER_AUTH_TOKEN`(逗号分隔可多账号),compose 会注入 RSSHub 容器。\n3. 应用侧把博主 `sourceConfig` 写成 `{ connector: \"rss\", feedPath: \"/twitter/user/\" }`(seed.ts 已示范)。\n4. 验证:`curl http://localhost:51200/twitter/user/` 能返回 XML 即通。\n\nRSSHub 同样支持 `/reddit/...`、`/stocktwits/...` 等大量路由(见 https://docs.rsshub.app)。注意:X 反爬严格,cookie 会过期、路由可能间歇失败 —— 这是该方案的固有风险,必要时考虑 X 官方 API(见 §5)。\n\n---\n\n## 3. 生产部署(docker compose)\n\n镜像见 `Dockerfile`(**单一合并镜像**:一个容器内由进程监督脚本同时跑 web `next start` 与后台 worker,任一退出则整体退出让平台重启)。早期的「多目标 app/worker」方案因 zbpack 限制已废弃,详见下方踩坑。\n\n```bash\n# 0. 准备 .env(至少 AUTH_SECRET;要变真则填 LLM/Finnhub/SMTP/TWITTER_AUTH_TOKEN)\ncp .env.example .env && openssl rand -hex 32 # 把输出填进 AUTH_SECRET\n\n# 1. 构建并启动全栈(db + rsshub + app);app 容器内同时跑 web 与 worker\ndocker compose --profile full up -d --build\n\n# 2. 首次建表 + 灌种子(app 镜像含 prisma CLI 与脚本)\ndocker compose run --rm app pnpm db:push\ndocker compose run --rm app pnpm db:seed\n\n# 3. 访问:app 在宿主 53000(建议挂到反向代理 + HTTPS)\n```\n\n> **单一合并镜像**:`Dockerfile` 构建的镜像在一个容器内同时跑 Next 网页与后台 worker(`CMD` 后台起 `pnpm worker`、前台起 `next start`)。这样无论 `docker compose` 还是 Zeabur,都只需部署「一个应用服务 + 一个数据库」。\n\n- **HTTPS 必需**:Web-Push 与 Service Worker 只在 HTTPS(或 localhost)下工作。生产请在 app 前加 Nginx/Caddy/云负载均衡做 TLS。\n- **数据库**:compose 内置 Postgres 适合自托管;也可用托管库(Neon/Supabase/RDS),把 `DATABASE_URL` 指过去即可(此时可不起 `db` 服务)。\n- **每日摘要**:默认关。两种开法 —— 设 `WORKER_RUN_DIGEST=true`(app 容器内的 worker 按 `DIGEST_INTERVAL_MS` 跑),或用外部 cron:`docker compose run --rm app pnpm digest`。\n\n### 本机开发(只起依赖)\n\n`docker compose up -d db rsshub` 只起依赖;网页用 `pnpm dev`、后台用 `pnpm worker` 在宿主分开跑,便于热更新。合并的 `app` 服务挂在 `full` profile,默认不随 `docker compose up` 启动。\n\n---\n\n## 3b. Zeabur 部署(CLI · 本项目已上线)\n\n本项目已用 Zeabur CLI 部署到自有服务器(AI-Gateway / 2C4G)。要点:\n\n- **私有库 Zeabur 看不到** → app 用 `zeabur deploy`(本地上传,非 Git)。\n- **zbpack 对存在的根 `Dockerfile` 一律使用、且忽略 `ZBPACK_DOCKERFILE_NAME` 等按服务选择** → 无法用两个 Dockerfile 区分两个服务,故采用「单一合并镜像」:一个服务、容器内同时跑 web+worker。\n- 线上服务:**Postgres**(模板)+ **x2t-app**(合并镜像,含 web+worker)+ **RSSHub**(模板 X46PTP,内网专用,注入 `TWITTER_AUTH_TOKEN` 抓 X)+ Redis(RSSHub 缓存)。\n\n```bash\n# 1. 建项目(绑定到目标服务器 region)+ Postgres 模板\nzeabur project create -n x2t -r \nzeabur template deploy -c B20CX0 --project-id # 官方 PostgreSQL\n\n# 2. 部署 app(本地上传,自动用根 Dockerfile)\nzeabur deploy --create --name x2t-app --project-id \n\n# 3. 注入变量(DATABASE_URL 用 Zeabur 引用变量;含 AUTH_SECRET/VAPID 等)\n# .env.zeabur.local 内含一行 DATABASE_URL=${POSTGRES_CONNECTION_STRING}\nzeabur variable env -f .env.zeabur.local --id --env-id \n\n# 4. 远程初始化库(用 Postgres 的【公网】连接串,从本机跑)\nzeabur service instruction --id --env-id # 取 Connection String\nDATABASE_URL= pnpm db:push\nDATABASE_URL= pnpm db:seed\n\n# 5. 重新部署 app 让变量生效(本地上传服务用 deploy 重传,redeploy 仅限 Git 绑定)\nzeabur deploy --service-id \n\n# 6. 开域名\nzeabur domain create --domain --id --env-id -g -y\n```\n\n> 更新部署:改完代码后 `zeabur deploy --service-id ` 重传重建即可。\n> 改了 schema:先用公网连接串 `DATABASE_URL=... pnpm db:push`,再重部署 app。\n\n---\n\n## 4. 升级数据库结构\n\n改了 `prisma/schema.prisma` 后:\n\n```bash\npnpm db:push # 本机\ndocker compose run --rm app pnpm db:push # 容器\n```\n\n> 注意:运行中的应用进程会缓存旧的 Prisma Client,改 enum/字段后需**重启 app/worker** 才生效。\n\n---\n\n## 5. 抓取的替代/兜底方案\n\n- **X 官方 API v2**:最稳定(约 $100/月起,有限流)。在 `src/lib/connectors/` 加一个连接器(实现 `Connector.fetch`),在 `connectors/index.ts` 注册,博主 `sourceConfig.connector` 指向它。\n- **人工提交**:`/submit` 表单已可用(`manual` 连接器),无需任何抓取设施即可录入。\n- **第三方抓取**(如 Apify):同样以新连接器形式接入,按量计费。\n\n---\n\n## 6. 上线安全清单\n\n- [ ] `AUTH_SECRET` 为随机值(`openssl rand -hex 32`),且未提交(`.env` 已在 `.gitignore`)。\n- [ ] `ADMIN_EMAILS` 设为真实管理员白名单(否则任何登录用户都是管理员)。\n- [ ] app 置于 HTTPS 之后(Web-Push 必需)。\n- [ ] 数据库口令非默认 `x2t:x2t`(自托管 Postgres 时改 compose 与 `DATABASE_URL`)。\n- [ ] `TWITTER_AUTH_TOKEN` 等密钥仅在服务器 `.env`,不进版本库、不进日志。\n"}, "21442bcd671d7c3d3f6d61ce58a9bdd461fc0642": {"t": 1785766581.008157, "n": 5208, "h": "c6eb402f55a72870", "c": "# 部署 · 让 X2T 真正可用\n\nX2T 的所有 mock 都遵循同一规则:**对应环境变量留空 → 自动降级为 mock;填上 → 立即变真**。\n所以「从演示到可用」基本是「填对几个 key + 起好抓取源」,无需改代码。\n\n---\n\n## 1. 哪些 key 让什么变真\n\n| 你想要 | 在 `.env` 配置 | 不配时 |\n| --- | --- | --- |\n| 真实 AI 分析 | `LLM_API_KEY`(+ `LLM_BASE_URL` / `LLM_MODEL`)| mock:关键词占位分析 |\n| 真实行情/外部数据 | `FINNHUB_API_KEY` | mock:随机占位行情 |\n| 真实邮件(登录链接 + 摘要)| `CF_EMAIL_ACCOUNT_ID`+`CF_EMAIL_API_TOKEN`+`EMAIL_FROM`(Cloudflare Email Service),或传统 `SMTP_*` | 邮件打到控制台 |\n| 安全的会话 | `AUTH_SECRET`(`openssl rand -hex 32`)| 不安全的默认值 |\n| 限定管理员 | `ADMIN_EMAILS`(逗号分隔)| 任何登录用户都是管理员 |\n| 真实抓取 X | 起 RSSHub + `TWITTER_AUTH_TOKEN` + `RSSHUB_BASE_URL` | 该源抓取失败(其余源不受影响)|\n\n> LLM 推荐 DeepSeek(便宜):`LLM_BASE_URL=https://api.deepseek.com`、`LLM_MODEL=deepseek-chat`。\n> Finnhub 免费档即可起步。\n\n---\n\n## 2. 抓取数据源\n\n抓取连接器是 RSS(`src/lib/connectors/rss.ts`),`sourceConfig` 两种写法:\n\n- **`feedUrl`** —— 任意可直连的真实 RSS(财经新闻 / Substack / 博客)。开箱即用,无需 RSSHub。\n 例:`{ \"connector\": \"rss\", \"feedUrl\": \"https://feeds.marketwatch.com/marketwatch/topstories/\" }`\n- **`feedPath`** —— 经自建 RSSHub 解析(`RSSHUB_BASE_URL` + path)。用于 X / Reddit / StockTwits 等需登录或反爬的源。\n 例:`{ \"connector\": \"rss\", \"feedPath\": \"/twitter/user/serenity\" }`\n\n`feedPath` 把「源路由」与「RSSHub 实例地址」解耦:换实例只改 `RSSHUB_BASE_URL`,无需重新 seed。\n\n### 自建 RSSHub 抓 X(本项目选定方案)\n\n1. 起服务:`docker compose up -d rsshub`(宿主 `51200` → 容器 `1200`)。\n2. X 需要登录 cookie 的 `auth_token`:浏览器登录 x.com → 开发者工具 → Application/Cookies → 复制 `auth_token` 值,填到 `.env` 的 `TWITTER_AUTH_TOKEN`(逗号分隔可多账号),compose 会注入 RSSHub 容器。\n3. 应用侧把博主 `sourceConfig` 写成 `{ connector: \"rss\", feedPath: \"/twitter/user/\" }`(seed.ts 已示范)。\n4. 验证:`curl http://localhost:51200/twitter/user/` 能返回 XML 即通。\n\nRSSHub 同样支持 `/reddit/...`、`/stocktwits/...` 等大量路由(见 https://docs.rsshub.app)。注意:X 反爬严格,cookie 会过期、路由可能间歇失败 —— 这是该方案的固有风险,必要时考虑 X 官方 API(见 §5)。\n\n---\n\n## 3. 生产部署(docker compose)\n\n镜像见 `Dockerfile`(多目标:`app` 精简 standalone、`worker` 后台管道)。\n\n```bash\n# 0. 准备 .env(至少 AUTH_SECRET;要变真则填 LLM/Finnhub/SMTP/TWITTER_AUTH_TOKEN)\ncp .env.example .env && openssl rand -hex 32 # 把输出填进 AUTH_SECRET\n\n# 1. 构建并启动全栈(db + rsshub + app);app 容器内同时跑 web 与 worker\ndocker compose --profile full up -d --build\n\n# 2. 首次建表 + 灌种子(app 镜像含 prisma CLI 与脚本)\ndocker compose run --rm app pnpm db:push\ndocker compose run --rm app pnpm db:seed\n\n# 3. 访问:app 在宿主 53000(建议挂到反向代理 + HTTPS)\n```\n\n> **单一合并镜像**:`Dockerfile` 构建的镜像在一个容器内同时跑 Next 网页与后台 worker(`CMD` 后台起 `pnpm worker`、前台起 `next start`)。这样无论 `docker compose` 还是 Zeabur,都只需部署「一个应用服务 + 一个数据库」。\n\n- **HTTPS 必需**:Web-Push 与 Service Worker 只在 HTTPS(或 localhost)下工作。生产请在 app 前加 Nginx/Caddy/云负载均衡做 TLS。\n- **数据库**:compose 内置 Postgres 适合自托管;也可用托管库(Neon/Supabase/RDS),把 `DATABASE_URL` 指过去即可(此时可不起 `db` 服务)。\n- **每日摘要**:默认关。两种开法 —— 设 `WORKER_RUN_DIGEST=true`(app 容器内的 worker 按 `DIGEST_INTERVAL_MS` 跑),或用外部 cron:`docker compose run --rm app pnpm digest`。\n\n### 本机开发(只起依赖)\n\n`docker compose up -d db rsshub` 只起依赖;网页用 `pnpm dev`、后台用 `pnpm worker` 在宿主分开跑,便于热更新。合并的 `app` 服务挂在 `full` profile,默认不随 `docker compose up` 启动。\n\n---\n\n## 3b. Zeabur 部署(CLI · 本项目已上线)\n\n本项目已用 Zeabur CLI 部署到自有服务器(AI-Gateway / 2C4G)。要点:\n\n- **私有库 Zeabur 看不到** → app 用 `zeabur deploy`(本地上传,非 Git)。\n- **zbpack 对存在的根 `Dockerfile` 一律使用、且忽略 `ZBPACK_DOCKERFILE_NAME` 等按服务选择** → 无法用两个 Dockerfile 区分两个服务,故采用「单一合并镜像」:一个服务、容器内同时跑 web+worker。\n- 线上服务:**Postgres**(模板)+ **x2t-app**(合并镜像,含 web+worker)+ **RSSHub**(模板 X46PTP,内网专用,注入 `TWITTER_AUTH_TOKEN` 抓 X)+ Redis(RSSHub 缓存)。\n\n```bash\n# 1. 建项目(绑定到目标服务器 region)+ Postgres 模板\nzeabur project create -n x2t -r \nzeabur template deploy -c B20CX0 --project-id # 官方 PostgreSQL\n\n# 2. 部署 app(本地上传,自动用根 Dockerfile)\nzeabur deploy --create --name x2t-app --project-id \n\n# 3. 注入变量(DATABASE_URL 用 Zeabur 引用变量;含 AUTH_SECRET/VAPID 等)\n# .env.zeabur.local 内含一行 DATABASE_URL=${POSTGRES_CONNECTION_STRING}\nzeabur variable env -f .env.zeabur.local --id --env-id \n\n# 4. 远程初始化库(用 Postgres 的【公网】连接串,从本机跑)\nzeabur service instruction --id --env-id # 取 Connection String\nDATABASE_URL= pnpm db:push\nDATABASE_URL= pnpm db:seed\n\n# 5. 重新部署 app 让变量生效(本地上传服务用 deploy 重传,redeploy 仅限 Git 绑定)\nzeabur deploy --service-id \n\n# 6. 开域名\nzeabur domain create --domain --id --env-id -g -y\n```\n\n> 更新部署:改完代码后 `zeabur deploy --service-id ` 重传重建即可。\n> 改了 schema:先用公网连接串 `DATABASE_URL=... pnpm db:push`,再重部署 app。\n\n---\n\n## 4. 升级数据库结构\n\n改了 `prisma/schema.prisma` 后:\n\n```bash\npnpm db:push # 本机\ndocker compose run --rm app pnpm db:push # 容器\n```\n\n> 注意:运行中的应用进程会缓存旧的 Prisma Client,改 enum/字段后需**重启 app/worker** 才生效。\n\n---\n\n## 5. 抓取的替代/兜底方案\n\n- **X 官方 API v2**:最稳定(约 $100/月起,有限流)。在 `src/lib/connectors/` 加一个连接器(实现 `Connector.fetch`),在 `connectors/index.ts` 注册,博主 `sourceConfig.connector` 指向它。\n- **人工提交**:`/submit` 表单已可用(`manual` 连接器),无需任何抓取设施即可录入。\n- **第三方抓取**(如 Apify):同样以新连接器形式接入,按量计费。\n\n---\n\n## 6. 上线安全清单\n\n- [ ] `AUTH_SECRET` 为随机值(`openssl rand -hex 32`),且未提交(`.env` 已在 `.gitignore`)。\n- [ ] `ADMIN_EMAILS` 设为真实管理员白名单(否则任何登录用户都是管理员)。\n- [ ] app 置于 HTTPS 之后(Web-Push 必需)。\n- [ ] 数据库口令非默认 `x2t:x2t`(自托管 Postgres 时改 compose 与 `DATABASE_URL`)。\n- [ ] `TWITTER_AUTH_TOKEN` 等密钥仅在服务器 `.env`,不进版本库、不进日志。\n"}, "f6c9ddc0027c6d4154341738b346e0cc4e9e6e33": {"t": 1785766582.314832, "n": 5170, "h": "99f3168b150cbd75", "c": "# 部署 · 让 X2T 真正可用\n\nX2T 的所有 mock 都遵循同一规则:**对应环境变量留空 → 自动降级为 mock;填上 → 立即变真**。\n所以「从演示到可用」基本是「填对几个 key + 起好抓取源」,无需改代码。\n\n---\n\n## 1. 哪些 key 让什么变真\n\n| 你想要 | 在 `.env` 配置 | 不配时 |\n| --- | --- | --- |\n| 真实 AI 分析 | `LLM_API_KEY`(+ `LLM_BASE_URL` / `LLM_MODEL`)| mock:关键词占位分析 |\n| 真实行情/外部数据 | `FINNHUB_API_KEY` | mock:随机占位行情 |\n| 真实邮件(登录链接 + 摘要)| `CF_EMAIL_ACCOUNT_ID`+`CF_EMAIL_API_TOKEN`+`EMAIL_FROM`(Cloudflare Email Service),或传统 `SMTP_*` | 邮件打到控制台 |\n| 安全的会话 | `AUTH_SECRET`(`openssl rand -hex 32`)| 不安全的默认值 |\n| 限定管理员 | `ADMIN_EMAILS`(逗号分隔)| 任何登录用户都是管理员 |\n| 真实抓取 X | 起 RSSHub + `TWITTER_AUTH_TOKEN` + `RSSHUB_BASE_URL` | 该源抓取失败(其余源不受影响)|\n\n> LLM 推荐 DeepSeek(便宜):`LLM_BASE_URL=https://api.deepseek.com`、`LLM_MODEL=deepseek-chat`。\n> Finnhub 免费档即可起步。\n\n---\n\n## 2. 抓取数据源\n\n抓取连接器是 RSS(`src/lib/connectors/rss.ts`),`sourceConfig` 两种写法:\n\n- **`feedUrl`** —— 任意可直连的真实 RSS(财经新闻 / Substack / 博客)。开箱即用,无需 RSSHub。\n 例:`{ \"connector\": \"rss\", \"feedUrl\": \"https://feeds.marketwatch.com/marketwatch/topstories/\" }`\n- **`feedPath`** —— 经自建 RSSHub 解析(`RSSHUB_BASE_URL` + path)。用于 X / Reddit / StockTwits 等需登录或反爬的源。\n 例:`{ \"connector\": \"rss\", \"feedPath\": \"/twitter/user/serenity\" }`\n\n`feedPath` 把「源路由」与「RSSHub 实例地址」解耦:换实例只改 `RSSHUB_BASE_URL`,无需重新 seed。\n\n### 自建 RSSHub 抓 X(本项目选定方案)\n\n1. 起服务:`docker compose up -d rsshub`(宿主 `51200` → 容器 `1200`)。\n2. X 需要登录 cookie 的 `auth_token`:浏览器登录 x.com → 开发者工具 → Application/Cookies → 复制 `auth_token` 值,填到 `.env` 的 `TWITTER_AUTH_TOKEN`(逗号分隔可多账号),compose 会注入 RSSHub 容器。\n3. 应用侧把博主 `sourceConfig` 写成 `{ connector: \"rss\", feedPath: \"/twitter/user/\" }`(seed.ts 已示范)。\n4. 验证:`curl http://localhost:51200/twitter/user/` 能返回 XML 即通。\n\nRSSHub 同样支持 `/reddit/...`、`/stocktwits/...` 等大量路由(见 https://docs.rsshub.app)。注意:X 反爬严格,cookie 会过期、路由可能间歇失败 —— 这是该方案的固有风险,必要时考虑 X 官方 API(见 §5)。\n\n---\n\n## 3. 生产部署(docker compose)\n\n镜像见 `Dockerfile`(多目标:`app` 精简 standalone、`worker` 后台管道)。\n\n```bash\n# 0. 准备 .env(至少 AUTH_SECRET;要变真则填 LLM/Finnhub/SMTP/TWITTER_AUTH_TOKEN)\ncp .env.example .env && openssl rand -hex 32 # 把输出填进 AUTH_SECRET\n\n# 1. 构建并启动全栈(db + rsshub + app);app 容器内同时跑 web 与 worker\ndocker compose --profile full up -d --build\n\n# 2. 首次建表 + 灌种子(app 镜像含 prisma CLI 与脚本)\ndocker compose run --rm app pnpm db:push\ndocker compose run --rm app pnpm db:seed\n\n# 3. 访问:app 在宿主 53000(建议挂到反向代理 + HTTPS)\n```\n\n> **单一合并镜像**:`Dockerfile` 构建的镜像在一个容器内同时跑 Next 网页与后台 worker(`CMD` 后台起 `pnpm worker`、前台起 `next start`)。这样无论 `docker compose` 还是 Zeabur,都只需部署「一个应用服务 + 一个数据库」。\n\n- **HTTPS 必需**:Web-Push 与 Service Worker 只在 HTTPS(或 localhost)下工作。生产请在 app 前加 Nginx/Caddy/云负载均衡做 TLS。\n- **数据库**:compose 内置 Postgres 适合自托管;也可用托管库(Neon/Supabase/RDS),把 `DATABASE_URL` 指过去即可(此时可不起 `db` 服务)。\n- **每日摘要**:默认关。两种开法 —— 设 `WORKER_RUN_DIGEST=true`(app 容器内的 worker 按 `DIGEST_INTERVAL_MS` 跑),或用外部 cron:`docker compose run --rm app pnpm digest`。\n\n### 本机开发(只起依赖)\n\n`docker compose up -d db rsshub` 只起依赖;网页用 `pnpm dev`、后台用 `pnpm worker` 在宿主分开跑,便于热更新。合并的 `app` 服务挂在 `full` profile,默认不随 `docker compose up` 启动。\n\n---\n\n## 3b. Zeabur 部署(CLI · 本项目已上线)\n\n本项目已用 Zeabur CLI 部署到自有服务器(AI-Gateway / 2C4G)。要点:\n\n- **私有库 Zeabur 看不到** → app 用 `zeabur deploy`(本地上传,非 Git)。\n- **zbpack 对存在的根 `Dockerfile` 一律使用、且忽略 `ZBPACK_DOCKERFILE_NAME` 等按服务选择** → 无法用两个 Dockerfile 区分两个服务,故采用「单一合并镜像」:一个服务、容器内同时跑 web+worker。\n- 线上服务只有两个:**Postgres**(模板)+ **x2t-app**(合并镜像)。RSSHub 暂未部署(X/Reddit 源需 X cookies 才有用),需要时再加。\n\n```bash\n# 1. 建项目(绑定到目标服务器 region)+ Postgres 模板\nzeabur project create -n x2t -r \nzeabur template deploy -c B20CX0 --project-id # 官方 PostgreSQL\n\n# 2. 部署 app(本地上传,自动用根 Dockerfile)\nzeabur deploy --create --name x2t-app --project-id \n\n# 3. 注入变量(DATABASE_URL 用 Zeabur 引用变量;含 AUTH_SECRET/VAPID 等)\n# .env.zeabur.local 内含一行 DATABASE_URL=${POSTGRES_CONNECTION_STRING}\nzeabur variable env -f .env.zeabur.local --id --env-id \n\n# 4. 远程初始化库(用 Postgres 的【公网】连接串,从本机跑)\nzeabur service instruction --id --env-id # 取 Connection String\nDATABASE_URL= pnpm db:push\nDATABASE_URL= pnpm db:seed\n\n# 5. 重新部署 app 让变量生效(本地上传服务用 deploy 重传,redeploy 仅限 Git 绑定)\nzeabur deploy --service-id \n\n# 6. 开域名\nzeabur domain create --domain --id --env-id -g -y\n```\n\n> 更新部署:改完代码后 `zeabur deploy --service-id ` 重传重建即可。\n> 改了 schema:先用公网连接串 `DATABASE_URL=... pnpm db:push`,再重部署 app。\n\n---\n\n## 4. 升级数据库结构\n\n改了 `prisma/schema.prisma` 后:\n\n```bash\npnpm db:push # 本机\ndocker compose run --rm app pnpm db:push # 容器\n```\n\n> 注意:运行中的应用进程会缓存旧的 Prisma Client,改 enum/字段后需**重启 app/worker** 才生效。\n\n---\n\n## 5. 抓取的替代/兜底方案\n\n- **X 官方 API v2**:最稳定(约 $100/月起,有限流)。在 `src/lib/connectors/` 加一个连接器(实现 `Connector.fetch`),在 `connectors/index.ts` 注册,博主 `sourceConfig.connector` 指向它。\n- **人工提交**:`/submit` 表单已可用(`manual` 连接器),无需任何抓取设施即可录入。\n- **第三方抓取**(如 Apify):同样以新连接器形式接入,按量计费。\n\n---\n\n## 6. 上线安全清单\n\n- [ ] `AUTH_SECRET` 为随机值(`openssl rand -hex 32`),且未提交(`.env` 已在 `.gitignore`)。\n- [ ] `ADMIN_EMAILS` 设为真实管理员白名单(否则任何登录用户都是管理员)。\n- [ ] app 置于 HTTPS 之后(Web-Push 必需)。\n- [ ] 数据库口令非默认 `x2t:x2t`(自托管 Postgres 时改 compose 与 `DATABASE_URL`)。\n- [ ] `TWITTER_AUTH_TOKEN` 等密钥仅在服务器 `.env`,不进版本库、不进日志。\n"}, "e712ffb5746b3bfb20f451dc3f0dddd6ef7287e1": {"t": 1785766583.473446, "n": 5107, "h": "7330cba81d9749e4", "c": "# 部署 · 让 X2T 真正可用\n\nX2T 的所有 mock 都遵循同一规则:**对应环境变量留空 → 自动降级为 mock;填上 → 立即变真**。\n所以「从演示到可用」基本是「填对几个 key + 起好抓取源」,无需改代码。\n\n---\n\n## 1. 哪些 key 让什么变真\n\n| 你想要 | 在 `.env` 配置 | 不配时 |\n| --- | --- | --- |\n| 真实 AI 分析 | `LLM_API_KEY`(+ `LLM_BASE_URL` / `LLM_MODEL`)| mock:关键词占位分析 |\n| 真实行情/外部数据 | `FINNHUB_API_KEY` | mock:随机占位行情 |\n| 真实邮件(登录链接 + 摘要)| `SMTP_HOST/PORT/USER/PASS/FROM` | 邮件打到控制台 |\n| 安全的会话 | `AUTH_SECRET`(`openssl rand -hex 32`)| 不安全的默认值 |\n| 限定管理员 | `ADMIN_EMAILS`(逗号分隔)| 任何登录用户都是管理员 |\n| 真实抓取 X | 起 RSSHub + `TWITTER_AUTH_TOKEN` + `RSSHUB_BASE_URL` | 该源抓取失败(其余源不受影响)|\n\n> LLM 推荐 DeepSeek(便宜):`LLM_BASE_URL=https://api.deepseek.com`、`LLM_MODEL=deepseek-chat`。\n> Finnhub 免费档即可起步。\n\n---\n\n## 2. 抓取数据源\n\n抓取连接器是 RSS(`src/lib/connectors/rss.ts`),`sourceConfig` 两种写法:\n\n- **`feedUrl`** —— 任意可直连的真实 RSS(财经新闻 / Substack / 博客)。开箱即用,无需 RSSHub。\n 例:`{ \"connector\": \"rss\", \"feedUrl\": \"https://feeds.marketwatch.com/marketwatch/topstories/\" }`\n- **`feedPath`** —— 经自建 RSSHub 解析(`RSSHUB_BASE_URL` + path)。用于 X / Reddit / StockTwits 等需登录或反爬的源。\n 例:`{ \"connector\": \"rss\", \"feedPath\": \"/twitter/user/serenity\" }`\n\n`feedPath` 把「源路由」与「RSSHub 实例地址」解耦:换实例只改 `RSSHUB_BASE_URL`,无需重新 seed。\n\n### 自建 RSSHub 抓 X(本项目选定方案)\n\n1. 起服务:`docker compose up -d rsshub`(宿主 `51200` → 容器 `1200`)。\n2. X 需要登录 cookie 的 `auth_token`:浏览器登录 x.com → 开发者工具 → Application/Cookies → 复制 `auth_token` 值,填到 `.env` 的 `TWITTER_AUTH_TOKEN`(逗号分隔可多账号),compose 会注入 RSSHub 容器。\n3. 应用侧把博主 `sourceConfig` 写成 `{ connector: \"rss\", feedPath: \"/twitter/user/\" }`(seed.ts 已示范)。\n4. 验证:`curl http://localhost:51200/twitter/user/` 能返回 XML 即通。\n\nRSSHub 同样支持 `/reddit/...`、`/stocktwits/...` 等大量路由(见 https://docs.rsshub.app)。注意:X 反爬严格,cookie 会过期、路由可能间歇失败 —— 这是该方案的固有风险,必要时考虑 X 官方 API(见 §5)。\n\n---\n\n## 3. 生产部署(docker compose)\n\n镜像见 `Dockerfile`(多目标:`app` 精简 standalone、`worker` 后台管道)。\n\n```bash\n# 0. 准备 .env(至少 AUTH_SECRET;要变真则填 LLM/Finnhub/SMTP/TWITTER_AUTH_TOKEN)\ncp .env.example .env && openssl rand -hex 32 # 把输出填进 AUTH_SECRET\n\n# 1. 构建并启动全栈(db + rsshub + app);app 容器内同时跑 web 与 worker\ndocker compose --profile full up -d --build\n\n# 2. 首次建表 + 灌种子(app 镜像含 prisma CLI 与脚本)\ndocker compose run --rm app pnpm db:push\ndocker compose run --rm app pnpm db:seed\n\n# 3. 访问:app 在宿主 53000(建议挂到反向代理 + HTTPS)\n```\n\n> **单一合并镜像**:`Dockerfile` 构建的镜像在一个容器内同时跑 Next 网页与后台 worker(`CMD` 后台起 `pnpm worker`、前台起 `next start`)。这样无论 `docker compose` 还是 Zeabur,都只需部署「一个应用服务 + 一个数据库」。\n\n- **HTTPS 必需**:Web-Push 与 Service Worker 只在 HTTPS(或 localhost)下工作。生产请在 app 前加 Nginx/Caddy/云负载均衡做 TLS。\n- **数据库**:compose 内置 Postgres 适合自托管;也可用托管库(Neon/Supabase/RDS),把 `DATABASE_URL` 指过去即可(此时可不起 `db` 服务)。\n- **每日摘要**:默认关。两种开法 —— 设 `WORKER_RUN_DIGEST=true`(app 容器内的 worker 按 `DIGEST_INTERVAL_MS` 跑),或用外部 cron:`docker compose run --rm app pnpm digest`。\n\n### 本机开发(只起依赖)\n\n`docker compose up -d db rsshub` 只起依赖;网页用 `pnpm dev`、后台用 `pnpm worker` 在宿主分开跑,便于热更新。合并的 `app` 服务挂在 `full` profile,默认不随 `docker compose up` 启动。\n\n---\n\n## 3b. Zeabur 部署(CLI · 本项目已上线)\n\n本项目已用 Zeabur CLI 部署到自有服务器(AI-Gateway / 2C4G)。要点:\n\n- **私有库 Zeabur 看不到** → app 用 `zeabur deploy`(本地上传,非 Git)。\n- **zbpack 对存在的根 `Dockerfile` 一律使用、且忽略 `ZBPACK_DOCKERFILE_NAME` 等按服务选择** → 无法用两个 Dockerfile 区分两个服务,故采用「单一合并镜像」:一个服务、容器内同时跑 web+worker。\n- 线上服务只有两个:**Postgres**(模板)+ **x2t-app**(合并镜像)。RSSHub 暂未部署(X/Reddit 源需 X cookies 才有用),需要时再加。\n\n```bash\n# 1. 建项目(绑定到目标服务器 region)+ Postgres 模板\nzeabur project create -n x2t -r \nzeabur template deploy -c B20CX0 --project-id # 官方 PostgreSQL\n\n# 2. 部署 app(本地上传,自动用根 Dockerfile)\nzeabur deploy --create --name x2t-app --project-id \n\n# 3. 注入变量(DATABASE_URL 用 Zeabur 引用变量;含 AUTH_SECRET/VAPID 等)\n# .env.zeabur.local 内含一行 DATABASE_URL=${POSTGRES_CONNECTION_STRING}\nzeabur variable env -f .env.zeabur.local --id --env-id \n\n# 4. 远程初始化库(用 Postgres 的【公网】连接串,从本机跑)\nzeabur service instruction --id --env-id # 取 Connection String\nDATABASE_URL= pnpm db:push\nDATABASE_URL= pnpm db:seed\n\n# 5. 重新部署 app 让变量生效(本地上传服务用 deploy 重传,redeploy 仅限 Git 绑定)\nzeabur deploy --service-id \n\n# 6. 开域名\nzeabur domain create --domain --id --env-id -g -y\n```\n\n> 更新部署:改完代码后 `zeabur deploy --service-id ` 重传重建即可。\n> 改了 schema:先用公网连接串 `DATABASE_URL=... pnpm db:push`,再重部署 app。\n\n---\n\n## 4. 升级数据库结构\n\n改了 `prisma/schema.prisma` 后:\n\n```bash\npnpm db:push # 本机\ndocker compose run --rm app pnpm db:push # 容器\n```\n\n> 注意:运行中的应用进程会缓存旧的 Prisma Client,改 enum/字段后需**重启 app/worker** 才生效。\n\n---\n\n## 5. 抓取的替代/兜底方案\n\n- **X 官方 API v2**:最稳定(约 $100/月起,有限流)。在 `src/lib/connectors/` 加一个连接器(实现 `Connector.fetch`),在 `connectors/index.ts` 注册,博主 `sourceConfig.connector` 指向它。\n- **人工提交**:`/submit` 表单已可用(`manual` 连接器),无需任何抓取设施即可录入。\n- **第三方抓取**(如 Apify):同样以新连接器形式接入,按量计费。\n\n---\n\n## 6. 上线安全清单\n\n- [ ] `AUTH_SECRET` 为随机值(`openssl rand -hex 32`),且未提交(`.env` 已在 `.gitignore`)。\n- [ ] `ADMIN_EMAILS` 设为真实管理员白名单(否则任何登录用户都是管理员)。\n- [ ] app 置于 HTTPS 之后(Web-Push 必需)。\n- [ ] 数据库口令非默认 `x2t:x2t`(自托管 Postgres 时改 compose 与 `DATABASE_URL`)。\n- [ ] `TWITTER_AUTH_TOKEN` 等密钥仅在服务器 `.env`,不进版本库、不进日志。\n"}, "3f439e4523f851d20387d3a168fee7fc6b08cbac": {"t": 1785766584.922866, "n": 3542, "h": "faa13388292bf2ed", "c": "# 部署 · 让 X2T 真正可用\n\nX2T 的所有 mock 都遵循同一规则:**对应环境变量留空 → 自动降级为 mock;填上 → 立即变真**。\n所以「从演示到可用」基本是「填对几个 key + 起好抓取源」,无需改代码。\n\n---\n\n## 1. 哪些 key 让什么变真\n\n| 你想要 | 在 `.env` 配置 | 不配时 |\n| --- | --- | --- |\n| 真实 AI 分析 | `LLM_API_KEY`(+ `LLM_BASE_URL` / `LLM_MODEL`)| mock:关键词占位分析 |\n| 真实行情/外部数据 | `FINNHUB_API_KEY` | mock:随机占位行情 |\n| 真实邮件(登录链接 + 摘要)| `SMTP_HOST/PORT/USER/PASS/FROM` | 邮件打到控制台 |\n| 安全的会话 | `AUTH_SECRET`(`openssl rand -hex 32`)| 不安全的默认值 |\n| 限定管理员 | `ADMIN_EMAILS`(逗号分隔)| 任何登录用户都是管理员 |\n| 真实抓取 X | 起 RSSHub + `TWITTER_AUTH_TOKEN` + `RSSHUB_BASE_URL` | 该源抓取失败(其余源不受影响)|\n\n> LLM 推荐 DeepSeek(便宜):`LLM_BASE_URL=https://api.deepseek.com`、`LLM_MODEL=deepseek-chat`。\n> Finnhub 免费档即可起步。\n\n---\n\n## 2. 抓取数据源\n\n抓取连接器是 RSS(`src/lib/connectors/rss.ts`),`sourceConfig` 两种写法:\n\n- **`feedUrl`** —— 任意可直连的真实 RSS(财经新闻 / Substack / 博客)。开箱即用,无需 RSSHub。\n 例:`{ \"connector\": \"rss\", \"feedUrl\": \"https://feeds.marketwatch.com/marketwatch/topstories/\" }`\n- **`feedPath`** —— 经自建 RSSHub 解析(`RSSHUB_BASE_URL` + path)。用于 X / Reddit / StockTwits 等需登录或反爬的源。\n 例:`{ \"connector\": \"rss\", \"feedPath\": \"/twitter/user/serenity\" }`\n\n`feedPath` 把「源路由」与「RSSHub 实例地址」解耦:换实例只改 `RSSHUB_BASE_URL`,无需重新 seed。\n\n### 自建 RSSHub 抓 X(本项目选定方案)\n\n1. 起服务:`docker compose up -d rsshub`(宿主 `51200` → 容器 `1200`)。\n2. X 需要登录 cookie 的 `auth_token`:浏览器登录 x.com → 开发者工具 → Application/Cookies → 复制 `auth_token` 值,填到 `.env` 的 `TWITTER_AUTH_TOKEN`(逗号分隔可多账号),compose 会注入 RSSHub 容器。\n3. 应用侧把博主 `sourceConfig` 写成 `{ connector: \"rss\", feedPath: \"/twitter/user/\" }`(seed.ts 已示范)。\n4. 验证:`curl http://localhost:51200/twitter/user/` 能返回 XML 即通。\n\nRSSHub 同样支持 `/reddit/...`、`/stocktwits/...` 等大量路由(见 https://docs.rsshub.app)。注意:X 反爬严格,cookie 会过期、路由可能间歇失败 —— 这是该方案的固有风险,必要时考虑 X 官方 API(见 §5)。\n\n---\n\n## 3. 生产部署(docker compose)\n\n镜像见 `Dockerfile`(多目标:`app` 精简 standalone、`worker` 后台管道)。\n\n```bash\n# 0. 准备 .env(至少 AUTH_SECRET;要变真则填 LLM/Finnhub/SMTP/TWITTER_AUTH_TOKEN)\ncp .env.example .env && openssl rand -hex 32 # 把输出填进 AUTH_SECRET\n\n# 1. 构建并启动全栈(db + rsshub + app + worker)\ndocker compose --profile full up -d --build\n\n# 2. 首次建表 + 灌种子(worker 镜像含 prisma CLI)\ndocker compose run --rm worker pnpm db:push\ndocker compose run --rm worker pnpm db:seed\n\n# 3. 访问:app 在宿主 53000(建议挂到反向代理 + HTTPS)\n```\n\n- **HTTPS 必需**:Web-Push 与 Service Worker 只在 HTTPS(或 localhost)下工作。生产请在 app 前加 Nginx/Caddy/云负载均衡做 TLS。\n- **数据库**:compose 内置 Postgres 适合自托管;也可用托管库(Neon/Supabase/RDS),把 `DATABASE_URL` 指过去即可(此时可不起 `db` 服务)。\n- **每日摘要**:默认关。两种开法 —— 设 `WORKER_RUN_DIGEST=true`(worker 内置按 `DIGEST_INTERVAL_MS` 跑),或用外部 cron:`docker compose run --rm worker pnpm digest`。\n\n### 本机开发(只起依赖)\n\n`docker compose up -d db rsshub` 只起依赖;app/worker 用 `pnpm dev` / `pnpm worker` 在宿主跑,便于热更新。`app`、`worker` 服务挂在 `full` profile,默认不随 `docker compose up` 启动。\n\n---\n\n## 4. 升级数据库结构\n\n改了 `prisma/schema.prisma` 后:\n\n```bash\npnpm db:push # 本机\ndocker compose run --rm worker pnpm db:push # 容器\n```\n\n> 注意:运行中的应用进程会缓存旧的 Prisma Client,改 enum/字段后需**重启 app/worker** 才生效。\n\n---\n\n## 5. 抓取的替代/兜底方案\n\n- **X 官方 API v2**:最稳定(约 $100/月起,有限流)。在 `src/lib/connectors/` 加一个连接器(实现 `Connector.fetch`),在 `connectors/index.ts` 注册,博主 `sourceConfig.connector` 指向它。\n- **人工提交**:`/submit` 表单已可用(`manual` 连接器),无需任何抓取设施即可录入。\n- **第三方抓取**(如 Apify):同样以新连接器形式接入,按量计费。\n\n---\n\n## 6. 上线安全清单\n\n- [ ] `AUTH_SECRET` 为随机值(`openssl rand -hex 32`),且未提交(`.env` 已在 `.gitignore`)。\n- [ ] `ADMIN_EMAILS` 设为真实管理员白名单(否则任何登录用户都是管理员)。\n- [ ] app 置于 HTTPS 之后(Web-Push 必需)。\n- [ ] 数据库口令非默认 `x2t:x2t`(自托管 Postgres 时改 compose 与 `DATABASE_URL`)。\n- [ ] `TWITTER_AUTH_TOKEN` 等密钥仅在服务器 `.env`,不进版本库、不进日志。\n"}}}}