---
title: 故障排查
description: self-host Multica 常见问题——症状、原因、怎么查、怎么修。
---

import { Callout } from "fumadocs-ui/components/callout";

按症状查问题。每条问题都给**症状 / 可能原因 / 怎么查 / 怎么修**四段。如果你的情况不在下面，到 [GitHub](https://github.com/multica-ai/multica/issues) 提 issue。

## 守护进程连不上服务器

**症状**：[`multica daemon`](/cli) 的 `status` 命令显示 `offline` 或 `connection refused`；服务器日志里没有 `/api/daemon/register` 或 `/api/daemon/heartbeat` 的请求。守护进程机制详见 [守护进程与运行时](/daemon-runtimes)。

**可能原因**：

1. **`MULTICA_SERVER_URL` 指错地址** —— 默认是 `ws://localhost:8080/ws`，self-host 要改成你自己的 server 地址
2. **网络 / 防火墙阻挡** —— daemon 和 server 不在同一网络，或出站被 block
3. **Token 过期或无效** —— 你从来没跑过 `multica login`，或 PAT 被撤销
4. **服务器拒绝注册** —— 你登录的账号不在目标工作区（register 返 403）
5. **DNS 解析失败** —— hostname 在 daemon 机器上解不出来

**怎么查**：

```bash
multica daemon logs --lines 100    # 看 daemon 侧错误
echo $MULTICA_SERVER_URL          # 确认地址配对
curl -i http://<server-host>:8080/health   # 直接戳 server
curl -i http://<server-host>:8080/readyz  # 连同 DB + migration readiness 一起检查
cat ~/.multica/config.json        # 看 api_token 是否存在
multica workspace list            # 确认你是目标工作区成员
```

**怎么修**：按上面原因对症处理。最常见的两个是**改 `MULTICA_SERVER_URL` 重启 daemon**（`multica daemon restart`）和**重新登录**（`multica logout && multica login`）。

## 任务一直卡在 queued

**症状**：把 issue 分给 agent 后，issue 状态立刻变 `in_progress`，但过了很久页面没有 agent 执行的迹象；`multica daemon status` 显示 daemon `online`。

**可能原因**（按触发概率排）：

1. **智能体并发上限已满** —— 该 agent 的 `max_concurrent_tasks`（默认 6）已经被其他正在跑的任务占满
2. **同一 issue 上有另一个同 agent 的任务还没结束** —— 同 agent × 同 issue 强制串行（防止重复执行）
3. **智能体已经被 archive** —— 被归档后新任务仍能入队，但无法被 claim，会卡到 5 分钟超时（code-issue G-01）
4. **Daemon 没在当前工作区注册该 runtime** —— 重启 daemon 或在 UI 重新选一次 runtime
5. **守护进程失联** —— 最近 45 秒没心跳。`daemon status` 看起来 `online` 也可能是刚失联

**怎么查**：

```bash
multica daemon status --output json       # runtime 列表 + last_seen_at
multica agent list                         # 查 agent 的 archived 状态
multica issue show <issue-id>             # 看 task 历史
```

服务器侧（self-host）可以 grep `"no_tasks"` / `"no_capacity"` 看 claim 的结果。

**怎么修**：

- 并发打满 → 等现有任务跑完，或 `multica agent update <id> --max-concurrent-tasks 10` 提升上限
- 同 issue 串行 → 等前一个任务结束，或改分给不同 agent
- Agent 被 archive → `multica agent restore <id>`
- Runtime 未注册 → `multica daemon restart`，daemon 会重新注册

## WebSocket 连不上

**症状**：浏览器控制台报 `WebSocket is closed`；页面不显示实时更新（任务进度、评论、inbox），刷新才能看到；但后台任务仍在执行。

**可能原因**：

1. **Origin 校验失败** —— 你的前端域名不在 server 的 CORS 白名单里。默认白名单只包含 `localhost:3000/5173/5174`，self-host 到公网必须配 `FRONTEND_ORIGIN`
2. **协议不匹配** —— 前端用 `https://` 需要 `wss://`，HTTP 用 `ws://`
3. **反向代理没开 WebSocket upgrade** —— Nginx / Envoy / HAProxy 默认不转发 `Upgrade` header
4. **JWT cookie 过期或丢失** —— 30 天过期后没重登

**怎么查**：

- 浏览器 DevTools → Network → 筛选 "WS"，看连接状态和状态码
- Server 日志里 grep `"rejected origin"` / `"websocket"` —— 如果是 origin 问题会明确写出来
- `curl -i http://<server-host>:8080/ws` 应该返回 `101 Switching Protocols`（需要带 `Upgrade` header）

**怎么修**：

- Origin 错 → 在 server 的 `.env` 设 `FRONTEND_ORIGIN=https://multica.yourdomain.com`（或逗号分隔的 `CORS_ALLOWED_ORIGINS`），重启 server
- 协议不匹配 → 确保 `FRONTEND_ORIGIN` 的协议和前端一致
- 反向代理 → 在 Nginx 加 `proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";`
- Cookie 过期 → 刷新页面重新登录

## 邮件没收到

**症状**：登录或邀请时提交邮箱后，收件箱（和垃圾邮件）里都没有验证码邮件。

**先确认 server 自己认为在用哪个 provider。** 启动时 backend 会打印这三种之一：

- `EmailService: SMTP relay <host>:<port> from=<addr>` —— 走 SMTP（`SMTP_HOST` 非空时优先级高于 Resend）
- `EmailService: Resend API from=<addr>` —— 走 Resend
- `EmailService: DEV mode — codes printed to stdout …` —— 没配任何 provider

```bash
docker compose -f docker-compose.selfhost.yml logs backend | grep "EmailService:"
```

如果应该出现的那行没出现，说明环境变量没进到进程 —— 检查 `.env` 和 `docker compose -f docker-compose.selfhost.yml exec backend env | grep -E 'RESEND_|SMTP_'`。这行启动日志里**不会**打印任何密码。

### Resend 是当前 provider

**可能原因**：

1. **`RESEND_API_KEY` 没配** —— server 会静默回落，**把验证码打到自己的 stdout 里**，不报错。生产部署很容易踩
2. **Resend API key 无效 / 余额不足** —— server 日志会有 `"failed to send verification code"`
3. **`RESEND_FROM_EMAIL` 的域名没在 Resend 验证** —— Resend 会拒发
4. **邮件发出去了但被收件人 ISP 判垃圾** —— 查 Resend dashboard 和 spam 目录

**怎么查**：

- Server 日志里搜 `"[DEV] Verification code for"` —— 如果有，说明 Resend 没配，验证码被打到 stdout
- [Resend dashboard](https://resend.com/) → Emails 看发送记录
- 确认 `RESEND_FROM_EMAIL` 的域名在 Resend console 的 "Verified Domains" 列表里

**怎么修**：

- 没配 API key → 照 [登录与注册配置 → 怎么配 Email](/auth-setup#email--验证码登录怎么工作) 的步骤配完重启 server
- 域名没验证 → Resend console 里走 DNS 验证流程（加 SPF / DKIM 记录）
- 紧急情况下（如内部测试）→ 从 server 日志里抄 `[DEV]` 打印出的验证码

### SMTP 是当前 provider

SMTP 路径把每个失败都按阶段包装好，所以 server 日志已经告诉你 relay 在哪一步拒绝了会话。搜 `"failed to send verification email"` / `"failed to send invitation email"`，看里面包的具体错误：

| 错误日志 | 含义 | 怎么修 |
|---|---|---|
| `smtp dial <host>:<port>: dial tcp …: connect: connection refused` / `i/o timeout` | backend 容器连不上 relay —— host / port 错、防火墙挡了、或者 relay 没开 | 在容器里确认能解析和连通：`docker compose -f docker-compose.selfhost.yml exec backend nslookup <host>` 以及 `nc -vz <host> <port>`；放行从 Multica 主机到 relay 的网络 |
| `smtp starttls: x509: certificate signed by unknown authority`（或 `certificate is not valid for any names`） | relay 用了私有 CA / 自签证书，容器的信任库不接受 | 要么把 CA 装进容器，要么在确认 relay 走的是可信网段后设 `SMTP_TLS_INSECURE=true` |
| `smtp auth: 535 5.7.8 Authentication credentials invalid`（或 `534`/`530`） | `SMTP_USERNAME` / `SMTP_PASSWORD` 不对，或 relay 不接受 `PLAIN` 认证 | 找邮件管理员复核 service account 凭据；Exchange 匿名内部 relay 应当把两者都留空 |
| `smtp MAIL FROM: 550 5.7.1 Client does not have permissions to send as this sender` | relay 不接受 `RESEND_FROM_EMAIL` 作为信封发件人 —— Exchange 常见 "anonymous users not allowed" 或 DMARC 对齐问题 | 把 `RESEND_FROM_EMAIL` 改成 relay 接受的域名；Exchange 上给来源 IP 授 `ms-Exch-SMTP-Accept-Any-Sender` |
| `smtp RCPT TO <addr>: 550 5.7.1 Unable to relay` | relay 的 receive connector 不允许这个子网把邮件中继到外部收件人（匿名内部 relay 发给外部域时常见） | 邀请仅限内部域，或者把 Multica 主机的子网加进 Exchange "Anonymous Users → Relay" 权限列表 |
| `smtp DATA` / `smtp write body` / `smtp end data` | 会话被接受但 body 被丢 —— 通常是消息大小限制、内容过滤、或中途断连 | 在 relay 端按同一个 `Message-ID`（日志里是 `<unixnano>@<host>`）找上下文；必要时调大消息大小限制 |

`MAIL FROM` / `RCPT TO` / `DATA` 的错误日志里都带着 relay 返回的状态码，可以和 Exchange / Postfix 那边的日志对齐。验证码和邀请 token **不会**出现在这些包装的错误里。

**怎么查**：

- 启动时搜 `"EmailService: SMTP relay"` 一次，运行时搜 `"failed to send"` 看具体阶段
- 在 backend 容器里测连通：`docker compose -f docker-compose.selfhost.yml exec backend sh -c 'nc -vz $SMTP_HOST $SMTP_PORT'`
- 确认环境变量真进到了进程：`docker compose -f docker-compose.selfhost.yml exec backend env | grep SMTP_`（这条会带出密码，仅在可信终端运行）

**怎么修**：

- host / port 不对 → 改 `SMTP_HOST` / `SMTP_PORT` 后重启 backend；支持的 relay 模式见 [登录与注册配置 → Option B：SMTP relay](/auth-setup)
- 证书校验失败 → 把 relay 的 CA 装进容器，或在可信网段上临时 `SMTP_TLS_INSECURE=true`
- 认证失败 → 复核凭据；匿名内部 relay 应把 `SMTP_USERNAME` 和 `SMTP_PASSWORD` 都留空
- `Unable to relay` → 邀请仅限内部域，或在 Exchange receive connector 上给 Multica 主机授中继权限

## 固定本地测试验证码登不进去

**症状**：自部署实例，想用 `888888` 这类固定本地测试验证码登录，但被拒 `invalid or expired code`。

**可能原因**（互斥）：

1. **`MULTICA_DEV_VERIFICATION_CODE` 为空** —— 固定验证码默认关闭
2. **`APP_ENV=production`** —— 这是正确的生产配置；固定本地测试验证码在 production 中会被忽略
3. **配置的验证码不是 6 位数字** —— 这个快捷码只接受 6 位数字

**怎么查**：

```bash
cat .env | grep -E 'APP_ENV|MULTICA_DEV_VERIFICATION_CODE'
docker exec <container> env | grep -E 'APP_ENV|MULTICA_DEV_VERIFICATION_CODE'
```

检查邮箱（含 spam）看有没有收到真实验证码。

**怎么修**：

- 生产环境保持 `MULTICA_DEV_VERIFICATION_CODE` 为空，配好 Resend 后使用真实验证码
- 本地开发或内网测试可以从 server 日志抄生成的验证码；如果需要 `888888`，设置 `APP_ENV=development` 和 `MULTICA_DEV_VERIFICATION_CODE=888888`。不要在公网实例启用固定验证码（详见 [登录与注册配置 → 固定本地测试验证码](/auth-setup#固定本地测试验证码)）

## Usage 看板一直是 0

**症状**：agent 执行完任务、原始的 token 用量已经写入数据库，但 **Settings → Usage** 和 **Settings → Runtime** 上输入 / 输出 / 成本全部显示 0。**没有任何报错**——这是静默故障。

**可能原因**：

1. **`rollup_task_usage_hourly()` 没被认领** —— Usage / Runtime 看板读的是派生表 `task_usage_hourly`，这张表必须靠 `rollup_task_usage_hourly()` 周期性填充。从 MUL-2957 起后端通过 DB 后端调度器（`sys_cron_executions`）在进程内跑 rollup；旧版本 binary、未应用 migration `113`、或者所有副本长时间下线，都可能让这张表里没有最近的 SUCCESS 行。
2. **`pg_cron` 作为兼容路径配着、但指向了错的库** —— `pg_cron.database_name` 默认是 `postgres`；如果你的 Multica 数据库名不是 `postgres`，调度任务根本看不到 `rollup_task_usage_hourly()`。进程内调度器不依赖这一项，但如果你刻意拿掉了进程内调度而靠 `pg_cron`，DB 名就必须对得上。
3. **handler 被认领了、但静默报错** —— 比如 migration 没全部应用导致 SQL 函数缺失、或 DB role / search_path 配错了。看 `sys_cron_executions` 里的 FAILED 审计行。

**怎么查**：

```sql
-- 确认原始数据有、hourly 表是空的
SELECT count(*) AS raw_rows FROM task_usage;
SELECT count(*) AS hourly_rows FROM task_usage_hourly;

-- 看进程内调度器的审计日志
SELECT plan_time, status, attempt, runner_id,
       error_code, error_msg, started_at, finished_at
  FROM sys_cron_executions
 WHERE job_name = 'rollup_task_usage_hourly'
 ORDER BY plan_time DESC
 LIMIT 20;

-- watermark —— 如果还是 1970-01-01，说明 rollup 从来没跑过
SELECT watermark_at FROM task_usage_hourly_rollup_state;

-- 兼容路径：以前注册过 pg_cron，确认装没装、指对了库没
SELECT * FROM pg_available_extensions WHERE name = 'pg_cron';
SHOW shared_preload_libraries;
SELECT jobname, schedule, database, active FROM cron.job;
```

**怎么修**：

- 确认至少一个后端副本里调度器真的在跑 —— 每 30 秒应该往 `sys_cron_executions` 的 `rollup_task_usage_hourly` 加一条 SUCCESS 行。
- 手动跑一次 SQL 验证函数本身没问题：`SELECT rollup_task_usage_hourly();` —— 刷新看板；如果数字出来了，SQL 这层 OK，问题在调度器认领路径上。
- 如果 migration `113_sys_cron_executions` 还没应用，重启后端让 migration 跑一遍，或手动 `migrate up`。
- 历史里有遗留的 `pg_cron` 入口也没事 —— SQL 函数里还持有 advisory lock 4246，应用调度器和 `pg_cron` 不会双写；要清掉冗余项见 [Self-host 快速上手 → 用量汇总](/self-host-quickstart#7-usage-rollup-no-operator-action-required) 里的 `cron.unschedule`。

## migration `103` 报 `refusing to drop legacy daily rollups`

**症状**：从 `v0.3.4` 升级到 `v0.3.5+` 时，后端容器起不来（或 `migrate up` 中止），错误信息：

```text
ERROR: refusing to drop legacy daily rollups:
  task_usage_hourly_rollup_state.watermark_at (1970-01-01 ...) trails
  task_usage latest event (...) by more than 01:00:00 — backfill is
  incomplete or pg_cron is not running. Run cmd/backfill_task_usage_hourly
  (and let pg_cron catch up) before re-running migrate
```

**可能原因**：这是 migration `103` 的 fail-closed guard。它要求 `task_usage_hourly` 已经追平了原始的 `task_usage` 之后，才允许丢掉旧的 daily rollup。只要数据库里有历史数据、且 rollup watermark 还停在 epoch（说明还没把历史回填进 hourly 表），这条 guard 就会拦住。

从 MUL-2957 起，migrate 命令在应用 migration `103` 之前会自动跑一次幂等的按月切片 backfill（advisory lock 4246 保护），所以 v0.3.4 → v0.3.5+ 直升一次 `migrate up` 就能搞定。如果你还看到这个错，要么用的是 MUL-2957 之前的二进制，要么 hook 自己也失败了 —— 看 migrate 日志里更早一行的 `task_usage hourly rollup hook` 看具体原因。

**怎么修**：

1. 如果你跑的是 MUL-2957 之前的 binary，又没办法先升级 binary，就对同一个数据库手动跑一次独立 backfill（幂等，可以打断，可以重试）：

   ```bash
   # Docker Compose
   docker compose -f docker-compose.selfhost.yml exec backend \
     ./backfill_task_usage_hourly --sleep-between-slices=2s

   # Kubernetes
   kubectl -n multica exec deploy/multica-backend -- \
     ./backfill_task_usage_hourly --sleep-between-slices=2s
   ```

2. 重新跑升级 —— 重启 backend 容器即可，启动时会自动跑 migration。Guard 看到新的 watermark，`103` 就会通过。
3. 之后由进程内调度器持续推 watermark —— 见 [Self-host 快速上手 → 用量汇总](/self-host-quickstart#7-usage-rollup-no-operator-action-required)。

`--sleep-between-slices=2s` 在有多年历史的生产库上是个比较克制的默认值。如果你只想保留最近 N 个月、可以接受永久丢掉更老的桶，用 `--months-back N --force-partial`。

## 端口冲突

**症状**：`multica server` 或 `multica daemon start` 启动失败，报 `address already in use`。

**可能原因**：

1. **Server 端口被占用**（默认 `8080`）
2. **Daemon health 端口被占用**（默认 `19514`，每个 profile 偏移一个 hash 值）
3. **Web dev server 端口冲突**（`3000` / `5173`）
4. **端口权限不足**（绑 `< 1024` 的 privileged port 需要 sudo）

**怎么查**：

```bash
lsof -i :8080        # macOS / Linux
netstat -ano | findstr :8080    # Windows
```

**怎么修**：

- 杀占用进程（`kill -9 <PID>`），或改环境变量 `PORT=9000` 换端口
- 要用 80 / 443 → 别直接绑，用反向代理（Nginx / Caddy）转发到高位端口

## 在哪看日志

| 组件 | 位置 | 命令 |
|---|---|---|
| **守护进程** | `~/.multica/daemon.log`（后台模式）或前台 stdout | `multica daemon logs -f --lines 100` |
| **服务器（Docker）** | container stdout | `docker logs -f <container>` |
| **服务器（systemd）** | journal | `journalctl -u multica-server -f` |
| **前端（dev）** | `pnpm dev` 所在终端 | 直接看 |
| **前端（browser）** | DevTools → Console | 按 `F12` |

需要更详细的 daemon 日志，把它从后台挪到前台跑：`multica daemon stop && multica daemon start --foreground`。
