Files

1 line
62 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"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/<handle>\" }`(seed.ts 已示范)。\n4. 验证:`curl http://localhost:51200/twitter/user/<handle>` 能返回 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 <server-region-id>\nzeabur template deploy -c B20CX0 --project-id <project-id> # 官方 PostgreSQL\n\n# 2. 部署 app(本地上传,自动用根 Dockerfile)\nzeabur deploy --create --name x2t-app --project-id <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 <app-service-id> --env-id <env-id>\n\n# 4. 远程初始化库(用 Postgres 的【公网】连接串,从本机跑)\nzeabur service instruction --id <postgres-id> --env-id <env-id> # 取 Connection String\nDATABASE_URL=<public-conn-str> pnpm db:push\nDATABASE_URL=<public-conn-str> pnpm db:seed\n\n# 5. 重新部署 app 让变量生效(本地上传服务用 deploy 重传,redeploy 仅限 Git 绑定)\nzeabur deploy --service-id <app-service-id>\n\n# 6. 开域名\nzeabur domain create --domain <name> --id <app-service-id> --env-id <env-id> -g -y\n```\n\n> 更新部署:改完代码后 `zeabur deploy --service-id <app-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/<handle>\" }`(seed.ts 已示范)。\n4. 验证:`curl http://localhost:51200/twitter/user/<handle>` 能返回 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 <server-region-id>\nzeabur template deploy -c B20CX0 --project-id <project-id> # 官方 PostgreSQL\n\n# 2. 部署 app(本地上传,自动用根 Dockerfile)\nzeabur deploy --create --name x2t-app --project-id <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 <app-service-id> --env-id <env-id>\n\n# 4. 远程初始化库(用 Postgres 的【公网】连接串,从本机跑)\nzeabur service instruction --id <postgres-id> --env-id <env-id> # 取 Connection String\nDATABASE_URL=<public-conn-str> pnpm db:push\nDATABASE_URL=<public-conn-str> pnpm db:seed\n\n# 5. 重新部署 app 让变量生效(本地上传服务用 deploy 重传,redeploy 仅限 Git 绑定)\nzeabur deploy --service-id <app-service-id>\n\n# 6. 开域名\nzeabur domain create --domain <name> --id <app-service-id> --env-id <env-id> -g -y\n```\n\n> 更新部署:改完代码后 `zeabur deploy --service-id <app-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/<handle>\" }`(seed.ts 已示范)。\n4. 验证:`curl http://localhost:51200/twitter/user/<handle>` 能返回 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 <server-region-id>\nzeabur template deploy -c B20CX0 --project-id <project-id> # 官方 PostgreSQL\n\n# 2. 部署 app(本地上传,自动用根 Dockerfile)\nzeabur deploy --create --name x2t-app --project-id <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 <app-service-id> --env-id <env-id>\n\n# 4. 远程初始化库(用 Postgres 的【公网】连接串,从本机跑)\nzeabur service instruction --id <postgres-id> --env-id <env-id> # 取 Connection String\nDATABASE_URL=<public-conn-str> pnpm db:push\nDATABASE_URL=<public-conn-str> pnpm db:seed\n\n# 5. 重新部署 app 让变量生效(本地上传服务用 deploy 重传,redeploy 仅限 Git 绑定)\nzeabur deploy --service-id <app-service-id>\n\n# 6. 开域名\nzeabur domain create --domain <name> --id <app-service-id> --env-id <env-id> -g -y\n```\n\n> 更新部署:改完代码后 `zeabur deploy --service-id <app-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/<handle>\" }`(seed.ts 已示范)。\n4. 验证:`curl http://localhost:51200/twitter/user/<handle>` 能返回 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 <server-region-id>\nzeabur template deploy -c B20CX0 --project-id <project-id> # 官方 PostgreSQL\n\n# 2. 部署 app(本地上传,自动用根 Dockerfile)\nzeabur deploy --create --name x2t-app --project-id <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 <app-service-id> --env-id <env-id>\n\n# 4. 远程初始化库(用 Postgres 的【公网】连接串,从本机跑)\nzeabur service instruction --id <postgres-id> --env-id <env-id> # 取 Connection String\nDATABASE_URL=<public-conn-str> pnpm db:push\nDATABASE_URL=<public-conn-str> pnpm db:seed\n\n# 5. 重新部署 app 让变量生效(本地上传服务用 deploy 重传,redeploy 仅限 Git 绑定)\nzeabur deploy --service-id <app-service-id>\n\n# 6. 开域名\nzeabur domain create --domain <name> --id <app-service-id> --env-id <env-id> -g -y\n```\n\n> 更新部署:改完代码后 `zeabur deploy --service-id <app-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/<handle>\" }`(seed.ts 已示范)。\n4. 验证:`curl http://localhost:51200/twitter/user/<handle>` 能返回 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 <server-region-id>\nzeabur template deploy -c B20CX0 --project-id <project-id> # 官方 PostgreSQL\n\n# 2. 部署 app(本地上传,自动用根 Dockerfile)\nzeabur deploy --create --name x2t-app --project-id <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 <app-service-id> --env-id <env-id>\n\n# 4. 远程初始化库(用 Postgres 的【公网】连接串,从本机跑)\nzeabur service instruction --id <postgres-id> --env-id <env-id> # 取 Connection String\nDATABASE_URL=<public-conn-str> pnpm db:push\nDATABASE_URL=<public-conn-str> pnpm db:seed\n\n# 5. 重新部署 app 让变量生效(本地上传服务用 deploy 重传,redeploy 仅限 Git 绑定)\nzeabur deploy --service-id <app-service-id>\n\n# 6. 开域名\nzeabur domain create --domain <name> --id <app-service-id> --env-id <env-id> -g -y\n```\n\n> 更新部署:改完代码后 `zeabur deploy --service-id <app-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/<handle>\" }`(seed.ts 已示范)。\n4. 验证:`curl http://localhost:51200/twitter/user/<handle>` 能返回 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 <server-region-id>\nzeabur template deploy -c B20CX0 --project-id <project-id> # 官方 PostgreSQL\n\n# 2. 部署 app(本地上传,自动用根 Dockerfile)\nzeabur deploy --create --name x2t-app --project-id <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 <app-service-id> --env-id <env-id>\n\n# 4. 远程初始化库(用 Postgres 的【公网】连接串,从本机跑)\nzeabur service instruction --id <postgres-id> --env-id <env-id> # 取 Connection String\nDATABASE_URL=<public-conn-str> pnpm db:push\nDATABASE_URL=<public-conn-str> pnpm db:seed\n\n# 5. 重新部署 app 让变量生效(本地上传服务用 deploy 重传,redeploy 仅限 Git 绑定)\nzeabur deploy --service-id <app-service-id>\n\n# 6. 开域名\nzeabur domain create --domain <name> --id <app-service-id> --env-id <env-id> -g -y\n```\n\n> 更新部署:改完代码后 `zeabur deploy --service-id <app-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/<handle>\" }`(seed.ts 已示范)。\n4. 验证:`curl http://localhost:51200/twitter/user/<handle>` 能返回 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 <server-region-id>\nzeabur template deploy -c B20CX0 --project-id <project-id> # 官方 PostgreSQL\n\n# 2. 部署 app(本地上传,自动用根 Dockerfile)\nzeabur deploy --create --name x2t-app --project-id <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 <app-service-id> --env-id <env-id>\n\n# 4. 远程初始化库(用 Postgres 的【公网】连接串,从本机跑)\nzeabur service instruction --id <postgres-id> --env-id <env-id> # 取 Connection String\nDATABASE_URL=<public-conn-str> pnpm db:push\nDATABASE_URL=<public-conn-str> pnpm db:seed\n\n# 5. 重新部署 app 让变量生效(本地上传服务用 deploy 重传,redeploy 仅限 Git 绑定)\nzeabur deploy --service-id <app-service-id>\n\n# 6. 开域名\nzeabur domain create --domain <name> --id <app-service-id> --env-id <env-id> -g -y\n```\n\n> 更新部署:改完代码后 `zeabur deploy --service-id <app-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/<handle>\" }`(seed.ts 已示范)。\n4. 验证:`curl http://localhost:51200/twitter/user/<handle>` 能返回 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"}}}}