跳到主要内容

第 20 章:安全、部署、观测、测试与发布

20.1 先画出信任边界

Multica 不是“一个 Web 服务加一个数据库”。部署时至少有六个安全域:

每条边都使用不同证明:

  • Browser:JWT Cookie + CSRF,或 Bearer Token;
  • CLI:mul_ PAT;
  • Cloud Node:mcn_ PAT;
  • Daemon:mdt_ Token,兼容旧 PAT/JWT;
  • Agent Task:mat_ Token;
  • Webhook:URL Secret、HMAC 或 Provider Token;
  • Workspace:认证之后再做 Membership/Role 校验。

安全分析必须把“谁在调用”“代表谁”“作用于哪个 Workspace”“是否属于某个 Task”拆开。

20.2 Token 只在创建时出现明文

auth/jwt.go 使用 crypto/rand 生成 20 字节随机值,再编码成 40 位 Hex,并加类型前缀:

前缀身份主要绑定
mul_人的 Personal Access TokenUser
mcn_Cloud Node PATCloud Owner,由 Fleet 验证
mdt_Daemon TokenWorkspace + Daemon
mat_Agent Task TokenUser + Agent + Task + Workspace

数据库保存 SHA-256 Hash,而不是 Token 明文。请求到达后 Hash 再查询;缓存也用 Hash 作 Key。明文只应存在于创建响应、用户配置或任务进程环境。

Prefix 不是权限证明,只是让 Middleware 尽早选择正确验证器;真正身份来自数据库绑定或 Cloud Fleet 响应。

20.3 Auth Middleware 重写身份,而不是信任 Header

middleware/auth.go 的关键规则是:

  1. 先删除客户端传入的 X-Actor-Source
  2. Bearer 优先于 Cookie;
  3. mat_ 查 Task Token Row;
  4. 用 Row 中的 User/Agent/Task/Workspace 覆盖客户端 Header;
  5. 将 Actor Source 标成 task_token
  6. 下游只信任这个 Server-set 标记。

因此,Agent 不能带一个合法 Task Token,再伪造另一个 X-Agent-IDX-Workspace-ID 扩大作用域。

mcn_ 也单独分支:若 Fleet Verifier 未配置,直接 401;若 Cloud 暂时不可达,返回 503 让客户端重试,而不是把有效 Token 误判成失效。

20.4 Cookie、CSRF 与 PAT 的差异

浏览器 JWT 可放在 multica_auth HttpOnly Cookie。对会改变状态的方法,Cookie 路径必须通过 CSRF 校验。

Bearer Token 不依赖浏览器自动附带 Cookie,所以不走相同 CSRF 条件;它的风险转为 Token 泄漏与本机存储安全。

JWT 只接受 HMAC Signing Method,再读取 sub 作为 User ID。Server 若没有 JWT_SECRET 会使用开发默认值并记录警告;这让本地启动简单,但生产必须把它当硬性配置错误处理。

20.5 Workspace 是认证后的第二道门

middleware/workspace.go 将请求解析为 Workspace UUID,再查询 Member:

  • 无 Workspace 标识:400;
  • Slug 不存在:404;
  • User 不是 Member:对外仍表现为 Not Found;
  • Role 不足:403。

对 Task Token,Workspace 绑定优先于所有 Slug/Header/Query;即使路由从 URL 参数解析 Workspace,Middleware 最后还会再对比 Token 绑定,形成 Catch-all。

这就是多租户系统的核心不变量:

对象 ID、Workspace Header 和用户身份三者不能任选其一作为授权证明。

20.6 Owner-only 操作为什么还要看 Actor Source

Task Token Row 中包含可追责 User,但这不表示 User 本人刚刚批准了敏感操作。

Agent 环境变量、账号级计费等 Owner-only Endpoint 会拒绝 task_tokencloud_pat 这种机器身份。否则“可代表用户发表评论的 Agent”会意外继承“可修改用户秘密或账户设置”的能力。

身份归因和人类授权不是同一概念。

20.7 Daemon 的认证迁移

middleware/daemon_auth.go 优先处理 mdt_

  • Token 绑定 Workspace 与 Daemon ID;
  • Expiry 限制缓存 TTL;
  • Context 记录认证路径供日志与指标使用。

为了兼容旧 Daemon,Middleware 仍允许 mul_ PAT、mcn_ 和 JWT。新路径把机器长期身份从人的 PAT 中拆出,缩小撤销和审计边界。

Daemon 登录使用存储 Profile Token完成注册/续约,但 Agent 子进程拿到的是 mat_,不是 Daemon Token。

20.8 CORS、WebSocket Origin 与 CSP

cmd/server/router.go 统一配置:

  • CORS Allowed Origins;
  • Browser 可发送的自定义 Header;
  • Credentials;
  • WebSocket Allowed Origins;
  • Trusted Proxy CIDR。

若 Header 未加入 CORS Allowlist,请求会死在浏览器 Preflight,Handler 根本看不到它。新增客户端协议 Header 时必须同步更新这里。

middleware/csp.go 默认:

  • Script 只允许 Self;
  • Object 禁止;
  • Base/Form 限制 Self;
  • Frame Ancestors 禁止。

Attachment Preview 路由单独允许同源 Frame,既支持 PDF/HTML Preview,又不把全站 Frame Policy 放宽。

20.9 Reverse Proxy 信任必须显式声明

Webhook 限流和 WebSocket Host 判断不能无条件信任 X-Forwarded-For / X-Forwarded-Host

源码的默认是:

  • MULTICA_TRUSTED_PROXIES 为空:不信任代理 Header;
  • 只在直接连接 IP 落入配置 CIDR 时读取代理链;
  • 从右向左找最后一个非受信 Hop 作为客户端 IP;
  • 非法 CIDR 记录警告并跳过。

如果把公网任意地址列成 Trusted Proxy,攻击者就能伪造来源;如果忘记配置实际反代,所有请求可能按代理 IP 限流。

MULTICA_PUBLIC_URL 也不从 Host Header 动态推导,避免错误反代配置下的 Host Spoofing 进入 Webhook URL。

20.10 Rate Limit 是可选 Redis 能力

middleware/ratelimit.go 用 Redis Lua 原子执行 INCR + EXPIRE,覆盖:

  • 发送验证码;
  • 验证验证码;
  • Google Login;
  • Contact Sales。

Redis 未配置或运行时出错时,限流 Fail-open

  • 单节点开发可工作;
  • Redis 故障不拖垮登录;
  • 但公网生产会失去应用层暴力破解/滥用保护。

所以生产反代/WAF 的限流不能完全依赖这一层,且应对 Redis Error Log 建告警。

20.11 Webhook 的认证矩阵

公开路由不等于无认证:

入口证明Workspace 来源
AutopilotURL 中高熵 Trigger TokenTrigger Row
GitHubRaw Body + HMAC-SHA256Installation Binding
Forgejo/GiteaConnection Secret HMACConnection Row
GitLabConnection Secret TokenConnection Row
Slack/LarkProvider Signature/Challenge 与 InstallationInstallation Row
OAuth Callback密封或签名 StateState Payload/Server Row

所有路径都应先验证 Raw Body/State,再解析业务字段;不能从未经验证的 Payload 或 Workspace Header选择租户。

Autopilot 与 VCS Handler 对 Body 设置大小上限;未知但合法事件通常 ACK,避免 Provider 无休止重试。

20.12 集成 Secret 的静态保护

util/secretbox 提供对称加密盒。Slack/Lark/VCS 等使用部署级 Key 加密每 Workspace 的:

  • Bot/App Token;
  • App Secret;
  • VCS Access Token;
  • VCS Webhook Secret。

能力开关与加密 Key 分开:

  • Product Surface 可用但未配置 Key:返回 Operator 配置错误;
  • Managed Deployment 可完全关闭某种集成;
  • 列表接口不回传明文;
  • Webhook Secret 只在创建或 Rotate 时展示一次。

轮换部署级 Key 不是简单改环境变量:旧 Ciphertext 必须先解密再重加密,否则新进程无法读取历史连接。

20.13 Agent 是受约束身份,不是无害沙箱

Task Token 限制的是 Server API 权限;Agent Process 仍拥有 Daemon 为它提供的本机能力:

  • Workdir 文件读写;
  • Git Credential;
  • Provider CLI 的模型账号;
  • 任务 Skills 与 MCP;
  • Agent Custom Env;
  • 用户允许的 Local Directory。

不同 Provider 的原生 Sandbox 能力不完全一致。Daemon 做进程树终止、路径检查、任务 HOME/Config 隔离和 Marker 注入,但不能把“运行第三方 CLI/仓库代码”变成绝对安全。

安全部署应把 Daemon 当高权限开发工具:

  1. 用专用 OS 账号运行;
  2. 不让其 Home 暴露无关生产凭据;
  3. 谨慎配置 Custom Env;
  4. Local Directory 只选明确项目目录;
  5. 云 Runtime 用一次性或隔离机器;
  6. 对 Provider CLI 与 Skill 来源做供应链审查。

20.14 文件与附件边界

Server 抽象 Local Storage 与 S3:

  • 无 S3 时写 /app/data/uploads
  • S3 可配 CDN、CloudFront Signed URL、S3 Presign 或 Server Proxy;
  • 下载 URL 有 TTL;
  • Private Object 不把 Raw URL 当永久公开地址;
  • Upload、Archive Import、Webhook 等对 Body 限长;
  • 本地 Static Route 仍经过 Attachment 安全 Header。

Compose 将 Local Upload Directory 挂到 Volume。Kubernetes 若使用多 Backend Replica:

  • S3 是最直接共享方案;
  • 或选择 ReadWriteMany Storage;
  • 默认 ReadWriteOnce PVC 会阻止第二个 Pod正常挂载。

删除数据库 Attachment 与删除对象存储不是一个原子事务;实现包含补偿清理,但运维仍应监测孤儿对象。

20.15 Docker 镜像构造

Dockerfile 使用 Go Multi-stage Build:

  • Builder:Go 1.26 Alpine;
  • 构建 Server、CLI、Migrate 与两个 Backfill 工具;
  • CGO_ENABLED=0
  • Runtime:Alpine 3.21,只加入 CA 与时区;
  • 带全部 SQL Migration;
  • Entrypoint 先 Migrate,再 Exec Server。

Dockerfile.web

  • Node 22 Alpine;
  • Corepack 激活仓库声明的 pnpm;
  • Frozen Lockfile 安装;
  • Next Standalone Output;
  • Runtime 使用非 Root nextjs 用户。

Go Runtime Image 当前未显式切换非 Root 用户;容器边界、只读 Root FS 与 Pod Security Context 需要部署侧补齐。

20.16 Compose Self-host 的安全默认

docker-compose.selfhost.yml 启动:

  • PostgreSQL 17 + pgvector;
  • Backend;
  • Frontend;
  • Postgres 与 Upload Volume。

Backend/Frontend 端口只绑定 127.0.0.1。文件注释明确警告不要直接改成 0.0.0.0,因为 Docker Port Publishing 可能绕过主机 Firewall 预期。

公网访问应由 Caddy/nginx/Tunnel:

  1. 终止 TLS;
  2. 转发到 Loopback;
  3. 设置正确 Origin;
  4. 仅把反代 CIDR列入 Trusted Proxies;
  5. 对 WebSocket Upgrade 与超时做配置。

make selfhost 首次会从模板生成随机 JWT、Postgres Password 和 VCS Key。直接手写 Compose 又不提供环境变量时,仍会落到示例默认值;不能把模板中的 change-me-in-production 当生产 Secret。

20.17 Compose 不是多节点模板

Self-host Compose 没有默认 Redis,也没有对象存储服务。它代表简单单节点模式:

  • Realtime Hub 在内存;
  • Request Store/Cache 回退本进程或数据库;
  • Rate Limit 关闭;
  • Upload 在本地 Volume;
  • 一个 Backend。

需要 HA 时不能只把 docker compose up --scale backend=3 当完成。必须补 Redis Relay/Store、共享对象存储、负载均衡、TLS、Session/Cookie Domain 和监控。

20.18 Helm Chart 的资源模型

deploy/helm/multica 提供:

  • Backend Deployment、Service 与 Upload PVC;
  • Frontend Deployment 与 Service;
  • 内置 PostgreSQL Deployment、Service 与 PVC,或 External PostgreSQL;
  • Frontend/Backend Ingress;
  • ConfigMap;
  • 可选 PrometheusRule。

Secret 不由 Chart 模板生成。Operator 先创建 existingSecret,让真实值不进入 Values/Git。

Backend 和 Frontend Image Tag 默认取 Chart.appVersion,Release Pipeline 会把它同步成 Git Tag,降低 Chart 与 Image 版本漂移。

20.19 Kubernetes Probe 与迁移

容器 Entrypoint 每次启动都运行 migrate upcmd/migrate 使用 PostgreSQL Session-level Advisory Lock:

  1. 固定一条 Pool Connection;
  2. 阻塞获取稳定 Lock Key;
  3. 逐 Migration 检查是否已应用;
  4. 后到进程排队,获取锁后把已完成项视为 No-op;
  5. Session 结束自动释放 Lock。

迁移不包成一个大事务,因为仓库存在 CREATE INDEX CONCURRENTLY。特殊版本还有幂等 Pre-migration Backfill Hook。

Chart 的 Startup Probe 给迁移约五分钟预算;Readiness/Liveness 都访问 /healthz

cmd/server/health.go 区分:

  • /health:只证明进程活着;
  • /healthz / /readyz:Ping DB,并检查所有当前二进制要求的 Migration Version,不是只看最高版本。

Chart 用 Readiness 端点做 Liveness,意味着持续 DB/Migration 故障也会触发 Pod 重启;这是当前运维语义,不应误读为纯进程存活检查。

20.20 多实例条件清单

子系统单节点默认多实例需要
MigrationEntrypoint 串行执行Advisory Lock 已处理并发
Browser Realtime内存 HubRedis Streams Relay
Daemon Wakeup本进程 HubRedis Relay
Auth/Member Cache可无缓存查 DBRedis 可降低热点 DB
Local Skill Request Store内存可用Redis 共享 Pending Queue
Rate LimitRedis 缺失即关闭共享 Redis 或外部 Gateway
UploadLocal VolumeS3 或 RWX Volume
Scheduler/Worker单实例直观依赖 DB Lease/幂等;监测重复与滞留
Web单 Next 实例无状态副本 + 同一 API/WS Origin

源码注释认为 API/WS 多副本可行,但 Chart 仍默认一个 Backend,并使用 Recreate Strategy。水平扩容要逐项满足表中共享状态条件。

20.21 Server 启动与优雅关闭

cmd/server/main.go 的启动顺序大致是:

  1. 初始化 Logger 与配置警告;
  2. 建 PostgreSQL Pool;
  3. 选择 Redis/Realtime Hub;
  4. 注册同步 Event Listener;
  5. 构建 Metrics;
  6. 组合 Router/Handler/Service;
  7. 启动 Runtime Sweeper、Heartbeat Batch、Autopilot Monitor、Webhook Worker 等;
  8. 启动 HTTP 与独立 Metrics Server。

关闭时先停止接收信号、取消 Worker Context、Flush Heartbeat,再关闭 HTTP/Metrics 与外部 Client。顺序的目标是避免新请求进入后后台消费者已消失,也避免未 Flush 心跳造成 Runtime 误下线。

20.22 Prometheus 暴露方式

internal/metrics 只有在 METRICS_ADDR 非空时启用,并启动独立 Listener/metrics

  • Go Runtime/Process;
  • Build Info;
  • HTTP 请求数、耗时与 In-flight;
  • PostgreSQL Pool;
  • Browser Realtime;
  • Daemon WebSocket;
  • Task Queue/Run/Failure;
  • LLM Token、请求与估算成本;
  • Scheduler Expiry;
  • 产品/运行事件;
  • Scrape-time Business Sampler。

非 Loopback Metrics 地址会记录警告,但 Handler 本身没有认证。生产应放在私网、NetworkPolicy、Allowlist 或带认证的监控代理之后。

Helm Chart 提供可选 PrometheusRule,但当前没有 ServiceMonitor 模板,也没有把独立 Metrics Port加入 Backend Service;Operator 需要自行暴露并配置 Scrape。

20.23 为什么 Business Sampler 使用独立连接池

Scrape-time 查询会读 Active User、Workspace、Queued/Running/Stuck Task、Runtime Heartbeat 等聚合。

为了不让 Prometheus Scrape 抢占业务连接:

  • Sampler 使用独立小 Pool;
  • 每条查询有紧凑 Statement Timeout;
  • 查询失败只让指标变旧,不拖垮 /metrics
  • 自身暴露 Query Error 与 Latency;
  • PrometheusRule 对持续错误和 p95 超过 300 ms 告警。

这是一种“观测系统不得反向击穿业务”的隔离。

20.24 指标标签为什么受约束

Task/LLM 指标只使用规范化的 Source、Runtime Mode、Provider、Model、Status 和 Canonical Failure Reason。

不能把 Task ID、Issue ID、Workspace ID 或任意错误文本放进 Label,否则时间序列基数会随业务对象增长。

源码将 Failure Reason 预热和标签表集中维护,并用测试防止 Event-time Metric 与 Sampler 派生逻辑漂移。

20.25 Realtime 的双重观测

除了 Prometheus,还有 /health/realtime JSON:

  • Connection;
  • Slow-client Eviction;
  • Event Send/Drop;
  • Redis Connect/XADD/XREAD/ACK;
  • Mirror Divergence;
  • Daemon Wakeup Hit/Miss。

配置 REALTIME_METRICS_TOKEN 时要求 Bearer;未配置时只允许 Loopback。它适合快速排障,Prometheus 适合趋势和告警。

20.26 日志与 Analytics

请求链使用 Chi Request ID 和结构化 slog。Client Platform/Version/OS、Daemon Auth Path、Workspace/Task 等受控字段可辅助关联,但 Secret、Raw Token 和大型 Prompt 不应进入日志。

Analytics Client 在 POSTHOG_API_KEY 缺失或 ANALYTICS_DISABLED 时 No-op,队列满时丢事件而不阻塞请求。当前快照的 Server Event Contract 已标成 Prometheus-only;PostHog 基础设施仍在,但不能据此假设每个 Server Event 都会外发。

20.27 测试金字塔

当前快照约有:

  • 489 个 Go *_test.go 文件;
  • 423 个 TS/TSX Test/Spec 文件;
  • 10 个命名为 Spec/E2E 的端到端文件。

数量不是质量证明,但覆盖面显示测试并非只集中在 Handler:

  • SQL/Service 事务与竞态;
  • Auth、Workspace、Rate Limit、CSP;
  • Daemon Poll/WS/Lease/Cancel;
  • Exec Env、Worktree、Path 与 Process Tree;
  • 17 类 Agent Protocol Parser;
  • Frontend Schema、Store、Hook 与 View;
  • Webhook 乱序、去重与签名;
  • Installer、Compose 与 Helm Config。

20.28 本地验证入口

Makefile 的关键目标:

  • make setup:安装依赖、确保 DB、迁移;
  • make start:迁移后启动 Server/Web;
  • make check:Typecheck、TS Test、Go Test 与 Playwright E2E;
  • make test:迁移后执行 Go Race Test;
  • make sqlc:重新生成 Query Code;
  • make db-reset:只允许 Localhost DB,拒绝 Remote Target。

Worktree 有独立 Env/Port/Database 初始化,避免多个分支测试相互覆盖。

20.29 CI 的分层

ci.yml

  • Frontend 按 Path Filter 跳过无关 PR,但主分支总跑;
  • Node 22 + pnpm;
  • Reserved Slug 生成物做 Drift Check;
  • Turbo Build/Typecheck/Lint/Test;
  • Backend 使用 pgvector PostgreSQL 17 和 Redis 7;
  • Go 1.26.1 Build、Migration、Race Test;
  • Helm Config Test;
  • Windows 专项验证 Process Tree、PowerShell Argv/Stdin、Codex Cleanup;
  • Linux/macOS 验证 Installer。

mobile-verify.yml 独立跑 Mobile Typecheck/Lint/Test。

desktop-smoke.yml 手动构建 Linux/Windows x64/arm64 安装包。

把 Windows Exec Env 单列非常重要:Linux 模拟无法证明 Job Object 与 PowerShell 重序列化行为。

20.30 Release Pipeline

release.ymlvX.Y.Z 或带 Suffix 的 Tag 触发,并再次严格验证:

  1. Go Race Test;
  2. GoReleaser 构建 Darwin/Linux/Windows、amd64/arm64 CLI;
  3. 生成 Legacy 与 Versioned Archive;
  4. 生成 checksums.txt
  5. 更新 Homebrew Tap;
  6. Backend/Web 分别在原生 amd64/arm64 Runner 构建;
  7. 按 Digest 合并 Multi-arch Manifest;
  8. Stable Release 才打 latest
  9. 同步 Chart Version/AppVersion;
  10. Lint、打包并推送 OCI Helm Chart;
  11. 发布 Linux/Windows Desktop。

macOS Desktop 仍走手工签名/Notarization 流程。工作流注释也说明 Linux/Windows Desktop 当前未代码签名。

20.31 发布供应链还覆盖了什么、没覆盖什么

已有保护:

  • Tag 格式校验;
  • Release 前 Race Test;
  • Frozen Dependency Lockfile 的镜像构建;
  • Image 按 Digest 组装;
  • CLI Checksum Manifest;
  • Homebrew Formula;
  • Upstream Owner Guard,Fork Tag 不向官方 Registry 发布。

当前工作流中未看到:

  • OCI Image Signature;
  • Provenance Attestation;
  • SBOM 生成/发布;
  • Container Vulnerability Scan;
  • Linux/Windows Desktop Code Signing。

这是对当前源码快照的静态观察,不表示外部 Registry 或组织策略一定没有额外保护;若部署受合规约束,应在发布链补齐并独立验证。

20.32 生产上线检查表

Secret

  • 随机且唯一的 JWT Secret;
  • 强 PostgreSQL Credential,外部 DB 使用 TLS;
  • Slack/Lark/VCS/Composio Key;
  • GitHub/Webhook/SMTP/S3 Key;
  • Secret 不进 Values、Git、日志或 Client Bundle;
  • 明确 Key Rotation 与历史 Ciphertext 迁移方案。

Network

  • Backend/Frontend 只经 TLS Reverse Proxy 暴露;
  • 精确 CORS Origin;
  • 正确 WebSocket Upgrade;
  • 最小 Trusted Proxy CIDR;
  • Metrics 走私网;
  • Redis/PostgreSQL 不暴露公网;
  • Webhook Body/Timeout/Rate Limit 配置合理。

State

  • PostgreSQL Backup 与恢复演练;
  • S3 Version/Lifecycle 或 Upload Volume Backup;
  • Redis 是否只承载可重建状态逐项确认;
  • Migration 前备份;
  • 多实例满足 Redis、共享 Upload 和 DB Lease 条件。

Runtime

  • Daemon 使用专用 OS 账号;
  • Agent Provider 与 Skill 来源可信;
  • Local Directory 最小化;
  • Custom Env 定期审计;
  • Workdir/Repo Cache 容量与 GC 告警;
  • Task Token、Daemon Token 可撤销。

Observability

  • Health 与 Readiness 分开监测;
  • Queue Wait、Stuck、Lease Expiry、Runtime Offline;
  • Task Failure Reason;
  • LLM Cost/Unpriced Token;
  • Realtime Drop/Slow Eviction/Redis Error;
  • Migration、Webhook Worker 与 Scheduler 日志;
  • Rate Limiter Fail-open 告警。

20.33 当前设计中值得显式接受的取舍

取舍得到代价
Redis 可选单机安装简单无限流、无跨节点实时共享
Entrypoint 自动迁移升级一步完成Pod 启动依赖 DB 与迁移时长
Local Upload 默认不依赖对象存储多副本与备份更复杂
Task TokenAgent 细粒度归因任务环境注入/撤销链更复杂
Provider CLI 本地执行复用用户订阅与原生能力本机权限/供应链风险
Metrics 独立 Listener与业务路由隔离需额外暴露、认证与 Scrape 配置
同步事件总线事务后顺序直观慢 Listener 可能影响请求
Recreate + 单副本默认运维简单默认不是无停机 HA

20.34 本章结论

Multica 已在源码中建立了相当完整的安全与运维骨架:

  • Credential 类型化并 Hash 存储;
  • Task Token 绑定四元身份;
  • Workspace/Role 二次授权;
  • Webhook 独立验签;
  • Secret 静态加密;
  • Redis-backed 多节点中继;
  • Migration Advisory Lock;
  • Readiness 校验完整 Migration Set;
  • Prometheus、Race Test 与 Multi-arch 发布。

但“支持 Self-host”不等于“默认配置就是生产 HA”。最关键的 Operator 责任仍是:替换所有开发 Secret、正确终止 TLS、配置 Redis/共享存储、隔离 Daemon、保护 Metrics,并为发布 Artifact 增加组织要求的签名与供应链证明。

下一章把前 20 章压成一次任务的完整调用链。