Files
MoShiro/docs/project-layout.md
T
2026-10-05 09:38:05 -07:00

354 lines
24 KiB
Markdown
Raw 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.
# 项目结构与工程约束
## 1. 目标
1. **三个独立项目加一个前端**:Probe、Agent、Backend 各自拥有解决方案,可以单独构建、测试和发布;Web 是独立的 Vite 项目(`web/`)。
2. **通用能力只写一次**:TCP/UDP/TLS/ICMP 采集积木(类似 iperf3 的能力,但只记录原始数据)放在 `Monitor.Measurement` 中,Probe 和 Agent 共用;采集方法(Kind)由积木组合声明而成。
3. **节点机制通用化**:注册、身份、连接、RPC、可靠上报、状态同步、宿主、更新,由与业务无关的 **Fleet** 框架提供(见 [fleet/overview.md](fleet/overview.md))。Monitor 只写业务,其他产品也可以复用 Fleet。
4. **改一处,整条链路受益**:协议、原始数据格式、测试方案、指标、错误码、版本号、编译设置、代码风格都只有**一个真源**。改动之后,下游要么自动更新,要么在编译期或 CI 中直接报错。
5. **模块化**:Probe/Agent 由可插拔的模块组成;Backend 按层分成多个项目,层之间的依赖在编译期强制。
6. **Probe/Agent 轻量**:运行时只依赖 BCL 和仓库内的库,不引用任何 NuGet 包。
## 2. 仓库布局
```text
monitor/
├─ README.md .gitignore
├─ docs/ docs/fleet/
├─ scripts/ # publish / package / smoke / stability 等脚本(check-deps 等检查也放这里)
├─ packaging/ # install.sh、systemd 单元、OpenWrt 文件
├─ web/ # 前端:Vite + React + TypeScript(自写),只依赖 backend/openapi/v1.json
│
├─ global.json Directory.Build.props Directory.Packages.props Monitor.slnx
├─ .editorconfig BannedSymbols.txt # 风格与禁用 API(构建期强制)
├─ BannedSymbols.Node.txt # 仅用于常驻节点进程:纯采集边界
│
└─ backend/ # 所有 C# 代码(构建产物在 backend/artifacts/)
├─ fleet/ # 通用节点框架(不认识 Monitor,以后可以拆成独立仓库)
│ ├─ src/ Fleet.Core Fleet.Identity Fleet.Rpc Fleet.Link Fleet.Hosting Fleet.Update
│ │ Fleet.Enrollment.Client Fleet.Server.{Enrollment,Link,Releases,Sqlite}
│ ├─ tools/ fleet-enroll fleet-updater fleet-release
│ ├─ samples/ EchoNode EchoServer
│ └─ tests/
├─ shared/ # Monitor 自己的共享库
│ ├─ Monitor.Protocol/ # 契约真源:Kind/Plan 目录、任务集、monitor.* 方法、UDP 包、RawChunk、错误码
│ ├─ Monitor.Measurement/ # 采集引擎:积木、Recorder、任务调和(见 measurement.md)
│ └─ tests/
├─ probe/ src/Monitor.Probe/ tests/ # 家宽端采集器
├─ agent/ src/Monitor.Agent/ tests/ # VPS 端采集器
├─ src/ # Backend(面板)
│ ├─ Monitor.Backend/ # 宿主与组合根(Program.cs)
│ ├─ Monitor.Backend.Api/ # API 层:HTTP 端点、DTO、错误映射
│ ├─ Monitor.Backend.Gateway/ # 在 Fleet.Server.Link 上注册 Monitor 的方法、状态与收件箱
│ ├─ Monitor.Backend.Application/ # 用例层 + 端口接口
│ ├─ Monitor.Backend.Analysis/ # 分析(纯计算,见 analysis.md)
│ └─ Monitor.Backend.Storage.Sqlite/ # 端口接口的实现,同时实现 IFleetStore
├─ tests/
├─ openapi/v1.json # 构建生成并提交
└─ Monitor.Backend.slnx
```
节点机器上的发布物是三个独立进程,使用不同的系统用户:`monitor-agent`(或 `monitor-probe`)、`fleet-enroll`(一次性运行)、`fleet-updater`(常驻或定时运行)。
## 3. 依赖关系
```text
┌────────┐ ┌────────┐ ┌──────────────────────────────┐ ┌─────┐
│ Probe │ │ Agent │ │ Backend (Api/Gateway/App/ │─openapi─>│ Web │
└─┬──┬───┘ └─┬──┬───┘ │ Analysis/Storage) │ └─────┘
│ └───┬───┘ │ └───┬───────────────┬──────────┘
│ ▼ │ │ │
│ ┌───────────┴┐ │ │
│ │Measurement │ │ │
│ └─────┬──────┘ │ │
▼ ▼ ▼ ▼
┌──────────────────────────────────┐ ┌─────────────────────────┐
│ Monitor.Protocol │ │ Fleet.Server.* │
└────────────────┬─────────────────┘ └───────────┬─────────────┘
▼ ▼
┌──────────────────────────────────────────────────────────────────┐
│ Fleet(节点侧):Core · Identity · Rpc · Link · Hosting │
└──────────────────────────────────────────────────────────────────┘
反向依赖一律禁止:fleet/** ✗→ Monitor.*
```
简要来说:
```text
Probe, Agent ──> Measurement ──> Monitor.Protocol ──> Fleet.Core / Fleet.Rpc
Probe, Agent ──> Fleet.Link / Fleet.Hosting / Fleet.Identity
Backend.* ──> Monitor.Protocol + Fleet.Server.* (不引用 Measurement)
fleet-enroll ──> Fleet.Enrollment.Client ──> Fleet.Identity / Core
fleet-updater ──> Fleet.Update ──> Fleet.Core (不引用 Link,也不读取节点身份)
Web ──> openapi/v1.json
```
| 项目 | 允许依赖 | 禁止 |
| --- | --- | --- |
| `Fleet.*`(节点侧) | BCL、其他节点侧 Fleet 库 | 任何 NuGet 运行时包;任何 `Monitor.*` |
| `Fleet.Server.*` | ASP.NET Core、`Microsoft.Data.Sqlite`(仅 Sqlite 项目)、节点侧 Fleet 库 | 任何 `Monitor.*` |
| `Monitor.Protocol` | Fleet.Core、Fleet.Rpc | 任何 I/O、套接字、日志 |
| `Monitor.Measurement` | Monitor.Protocol、Fleet.Core | NuGet;日志(只通过计数接收器上报);读取配置;统计或解释逻辑 |
| Probe / Agent | 上述库,以及 Fleet.Link / Hosting / Identity | NuGet 运行时包;彼此互相引用;Fleet.Update;Fleet.Enrollment.Client |
| Backend.* | Monitor.Protocol、Fleet.Server.*,以及第 5.2 节列出的包 | Measurement、EF Core 等依赖反射的库 |
分析器类包(`PrivateAssets="all"`)不受限制。`scripts/check-deps` 在 CI 中检查上述规则。
## 4. 共享库
### 4.1 Fleet(通用节点框架)
组件、扩展点与接入方式见 [fleet/overview.md](fleet/overview.md),连接细节见 [fleet/link.md](fleet/link.md),更新见 [fleet/update.md](fleet/update.md)。原来的 `Monitor.Common` 和 `Monitor.NodeHost` 已经并入 Fleet:
| 原位置 | 现位置 |
| --- | --- |
| `Monitor.Common`(Result、缓冲区、VarInt、有界队列、重放窗口、退避、ID、令牌哈希) | `Fleet.Core` |
| `Monitor.NodeHost`(模块宿主、日志、配置、ConfigStore、服务外壳) | `Fleet.Hosting` |
| 节点密钥与签名 | `Fleet.Identity` |
| JSON-RPC 信封与路由 | `Fleet.Rpc` |
| Agent 注册 | `Fleet.Enrollment.Client` + `fleet-enroll` |
### 4.2 Monitor.Protocol(Monitor 的契约真源)
```text
Monitor.Protocol/
├─ ProtocolVersion ProtocolLimits MonitorErrorCode(在 Fleet 错误码基础上扩展)
├─ Ids/ AgentId(= Fleet NodeId)、ProbeId、TaskId、TestId、ChunkId、StreamId
├─ Catalog/
│ ├─ Kinds/ KindSpec 声明(积木组合 + ParamSpec + 记录列 + 预算)
│ ├─ Plans/ 多阶段测试方案
│ └─ ParamSpec 参数定义(类型、默认值、上下限),节点与 Backend 共用同一套校验
├─ Tasks/ TaskSet(Fleet 期望状态的文档类型)、调和结果
├─ Rpc/ monitor.* 方法名、params/result、二进制通道号(从 16 开始)
├─ Wire/ UDP 包、数据连接首帧
├─ Raw/ 自描述 RawChunk:头部、列类型、列编解码、Brotli、crc32c
└─ Json/ AgentRpcContext、ProbeRpcContext(按信道拆分,便于裁剪)
```
累加器、直方图等统计类型**不在 Protocol 中**,只属于 `Backend.Analysis`。
### 4.3 Monitor.Measurement(采集引擎)
包含:
- 积木:Transport、Pacer、Payload、Behavior、Sampler;
- 根据列定义自动生成的 Recorder;
- `KindAssembler`;
- `TaskReconciler`(实现 Fleet 的 `IStateReconciler<TaskSet>`);
- `BudgetGuard`。
详见 [measurement.md](measurement.md) 第 6 节。
## 5. 模块化
### 5.1 Agent 与 Probe:模块组装
模块在 `Program.cs` 中**显式组装**,不使用 DI 容器,也不做反射扫描。连接、认证、重连、发件箱、状态同步都由 Fleet 提供,Monitor 只写业务模块:
```text
Agent Program.cs
identity = NodeIdentity.Load(dir) ← 由 fleet-enroll 生成
link = FleetLink.ToServer(cfg, identity, AgentRpcContext.Default)
.WithState<TaskSet>(…, taskReconciler) 期望任务集 → 调和
.WithOutbox<RawChunk>(…, 64 MiB) 原始块可靠上报
.On(monitor.test.run / monitor.test.cancel, …)
peers = FleetLink.PeerListener(cfg.Listen, identity, ProbeRpcContext.Default) Probe 会话
host = new ModuleHost(log)
├─ link、peers (Fleet)
├─ ProbeDirectoryRelay 转发 monitor.probe.agents / agents_changed
├─ UdpMux / TcpAcceptor UDP/TCP/TLS 数据端口(Measurement 共享),按 (session, stream) 分发给监听方积木;AgentAdmission 提供重放、限速
├─ ChunkModule 合并 Probe 与本端的列 → 切块 → link.Outbox
└─ TestModule 按需测试的阶段编排
Probe Program.cs
├─ AgentDirectoryModule 维护 Agent 列表(bootstrap → monitor.probe.agents → agents_changed)
├─ PeerPool(Fleet) 每个 Agent 一个 FleetLink.ToPeer:认证、重连
├─ TaskReconciler 每个会话各自的期望状态(与 Agent 相同的代码)
├─ ColumnShipper 本端的列通过会话的 Outbox 发送(二进制通道 17)
└─ TestModule
```
模块之间通过构造函数传入的接口或 `Channel<T>` 通信,禁止通过静态全局状态共享数据。
**新增 RPC 方法**:在 `Monitor.Protocol/Rpc` 中定义 params/result,然后在组装处加一行 `.On(Methods.X, HandleX)`。
### 5.2 Backend:分层与 API 层
```text
HTTP (public | admin) wss /api/agent/v1/rpc(Fleet.Server.Link) SSE /api/public/live
│ │ ▲
┌──────────────▼───────────────┐ ┌───────────▼──────────┐ │
│ Backend.Api │ │ Backend.Gateway │ │
│ Public/ Admin/ │ │ 注册 monitor.* 方法 │ │
│ 认证(Cookie/CSRF/令牌) │ │ TaskSet 状态提供者 │ │
│ 错误映射 · OpenAPI · SSE 端点 │ │ RawChunk 收件箱 │ │
└──────────────┬───────────────┘ └───────────┬──────────┘ │
└───────────────┬──────────────┘ │
▼ │
┌─────────────────────────────────────────────────────────┐ ┌───────────────────┴──┐
│ Backend.Application │───>│ LiveHub(Application)│
│ Agents · Probe · Tasks(有效任务集的计算与下发) │ │ 内存快照 + 主题订阅 │
│ Tests · Ingest(chunk → 存储 → 分析队列)· SeriesQuery │ └──────────────────────┘
│ AnalysisScheduler · Downsampler · Auth · Releases 视图 │ 调用 ─> Backend.Analysis
└───────────────────────────▲─────────────────────────────┘ 纯计算:算子/指标/模型
│ 实现端口
┌───────────────────────────┴─────────────────────────────┐
│ Backend.Storage.Sqlite:段文件 + SQLite(只做读写) │
│ 同时实现 IFleetStore(Fleet 的节点和令牌表放在 monitor.db 中)│
└─────────────────────────────────────────────────────────┘
端口:IAgentStore、ITaskStore、IRawStore、IMetricStore、ITestStore、IAuthStore
```
规则:
- **Api 不能引用 Storage**(编译期保证);Api 和 Gateway 只调用 Application。
- Application 返回 `Result<T>`。**错误码到 HTTP 状态的映射只在 Api 层做一次**(`ErrorCodeHttpMap`);到 JSON-RPC 错误的映射由 Fleet.Rpc 统一完成。
- Api 层按调用方分目录:`Public/`、`Admin/`,公共部分放在 `Common/`(分页、ETag、认证过滤器、ProblemFactory、`EndpointConventions`)。每类资源一个端点文件,新增资源时在 `ApiModule.Map` 中加一行。
- 注册端点、签名认证、连接注册表、状态推送、收件箱 ACK、发布通道都来自 Fleet(`MapFleetEnrollment`、`MapFleetLink`、`MapFleetReleases`)。Monitor 只提供:
- `IFleetStore` 的实现;
- `TaskSet` 的状态提供者;
- `RawChunk` 的收件箱;
- `monitor.*` 方法。
- public 层的 DTO 单独定义,只暴露可以公开的字段,并统一经过“可见性过滤器”。
- 请求校验:简单字段约束使用 .NET 10 内置的 Minimal API 校验;任务参数统一用 `ParamSpec` 校验(与节点共用);跨字段规则放在 Application 层。
- Backend 使用 ASP.NET Core 自带的 DI 容器,只在组合根显式注册。
Backend.Analysis 只依赖 Monitor.Protocol 和 Fleet.Core,没有 I/O,可以单独做成离线 CLI 使用。
Backend 允许的包:ASP.NET Core 共享框架、`Microsoft.Data.Sqlite`、`Microsoft.AspNetCore.OpenApi`、`Microsoft.Extensions.ApiDescription.Server`(仅构建期使用)。
## 6. “改一处,整条链路受益”的机制
| 改动 | 真源 | 链路上的效果 |
| --- | --- | --- |
| 采集密度(频率、包大小、采样间隔) | 任务参数(API) | 经 `fleet.state.sync` 下发后立即生效,无需发布 |
| 新增采集方法(现有积木可以组合) | `Protocol/Catalog/Kinds` 中加 1 条 | 节点执行、Recorder 自动生成列、Backend 校验和目录、Analysis 按列名读取 |
| 新增采集方法 | 在 `Kinds` 里加声明(含流的 `Driver`/`Key`),在 `Measurement/Blocks` 里实现它的拨号方和监听方并登记到 `BlockCatalog` | Probe 和 Agent 同时获得,合并不需要写代码 |
| 原始数据格式 | `Protocol/Raw`(自描述) | 新增列不需要升级版本;四处使用同一份编解码器 |
| 新增统计指标 | `Analysis/Metrics` 中加 1 行 `MetricDef` | 存储、降采样、API、SSE、目录全部自动处理;回填 30 天历史 |
| 服务评分 | `Analysis/Models` | 查询时计算,立即对全部历史生效 |
| 新增 RPC 能力 | `Monitor.Protocol/Rpc` + 组装处加 1 行 | 通过 Fleet 的 capabilities 协商,旧节点不受影响 |
| 注册、认证、重连、发件箱、状态同步 | `backend/fleet/` | Monitor 和所有其他产品同时受益 |
| 更新机制 | `Fleet.Update` + `fleet-updater` | 所有产品共用;产品只提供发布清单 |
| REST DTO 与端点 | `Backend.Api` | 生成 OpenAPI,CI 检查 diff,前端的 TS 类型随之更新 |
| 错误码 | Fleet 错误码 + `MonitorErrorCode` | REST、JSON-RPC、OpenAPI、TS 全部一致 |
| 版本、编译设置、风格、禁用 API | `backend/` 下的 props、`.editorconfig`、`BannedSymbols*.txt` | 所有项目 |
## 7. 编译设置
`backend/Directory.Build.props`(所有项目):
```xml
<Project>
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<AnalysisLevel>latest-recommended</AnalysisLevel>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<IsAotCompatible>true</IsAotCompatible>
<InvariantGlobalization>true</InvariantGlobalization>
<UseArtifactsOutput>true</UseArtifactsOutput>
<Deterministic>true</Deterministic>
<Version>0.1.1</Version>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" PrivateAssets="all" />
<AdditionalFiles Include="$(MSBuildThisFileDirectory)BannedSymbols.txt" />
</ItemGroup>
</Project>
```
`backend/fleet/src/*` 另外启用 `Microsoft.CodeAnalysis.PublicApiAnalyzers`(`PublicAPI.Shipped.txt` / `PublicAPI.Unshipped.txt`),公开 API 的任何变更都会在 PR 中显示出来。
`backend/probe/`、`backend/agent/` 下的 `Directory.Build.props`(先导入根 props,再追加轻量设置):
```xml
<PropertyGroup>
<PublishAot>true</PublishAot>
<OptimizationPreference>Size</OptimizationPreference>
<UseSystemResourceKeys>true</UseSystemResourceKeys>
<EventSourceSupport>false</EventSourceSupport>
<DebuggerSupport>false</DebuggerSupport>
<HttpActivityPropagationSupport>false</HttpActivityPropagationSupport>
<ServerGarbageCollection>false</ServerGarbageCollection>
<ConcurrentGarbageCollection>false</ConcurrentGarbageCollection>
<!-- 保留 StackTraceSupport:线上崩溃需要可读的调用栈 -->
</PropertyGroup>
<ItemGroup>
<!-- 纯采集边界:编译期禁止 -->
<AdditionalFiles Include="$(MSBuildThisFileDirectory)../BannedSymbols.Node.txt" />
</ItemGroup>
```
`BannedSymbols.Node.txt` 适用于常驻节点进程(Probe、Agent,以及 Fleet 的节点侧运行库),主要条目:
- `System.Diagnostics.Process`;
- `System.Reflection.Assembly.Load*`;
- `System.Runtime.InteropServices.NativeLibrary.Load*`;
- `System.IO.File.Write*`、`File.Create`、`FileStream` 构造函数(只有 `Fleet.Hosting.ConfigStore` 可以通过 `#pragma` 豁免);
- `System.Net.Http.HttpClient`(**不再有豁免**:注册已经移到 `fleet-enroll`)。
`fleet-enroll` 和 `fleet-updater` 是独立的工具,使用各自的禁用清单:
- `fleet-updater` 允许写安装目录、调用服务管理接口,但禁止读取节点身份目录;
- `fleet-enroll` 允许 HTTP,允许写入身份目录。
## 8. 轻量预算
M0 阶段先测出基线,之后作为 CI 的回归门槛(超出 10% 即失败):
| 程序 | 发布体积 | 空闲常驻内存 | 负载 |
| --- | --- | --- | --- |
| Probe | ≤ 8 MB(Linux/OpenWrt,静态链接了 .NET 的加密、压缩、网络库;Windows 实测 5.6 MB) | ≤ 32 MB(Linux 实测约 21 MB) | 同时连接 15 个 Agent 并运行默认模板时 CPU < 2%;`hy2_ladder` 100 Mbps 时 CPU < 50%(单核) |
| Agent | ≤ 10 MB(Linux 实测 8.4 MB) | ≤ 48 MB(Linux 实测约 23 MB) | 运行默认模板时 ≤ 64 MB;同时跑一个 200 Mbps 测试时 CPU < 60%(1 vCPU) |
| fleet-enroll | ≤ 4 MB | — | 一次性运行 |
| fleet-updater | ≤ 4 MB | ≤ 16 MB | 空闲时每小时轮询一次 |
| Backend | 不设硬性限制 | ≤ 150 MB | — |
为满足预算,实现上需要遵守:
- 热路径使用 `Span<T>` 和 `ArrayPool<byte>`,不逐包分配;
- UDP 使用 `SocketAddress` 系列非分配 API;
- stream 和 bulk 发送使用预先填充的缓冲区,只统计计数,不复制数据;
- 不使用 LINQ。
## 9. 发布
- 目标平台:
| 程序 | RID | 说明 |
| --- | --- | --- |
| Agent | `linux-x64`、`linux-arm64` | VPS |
| Probe | `linux-x64`、`linux-arm64`、`win-x64` | 普通 PC 或服务器 |
| Probe(OpenWrt) | `linux-musl-x64`、`linux-musl-arm64` | 正式支持;`linux-musl-arm`(armv7)视 M0 的工具链验证结果决定是否支持 |
| fleet-enroll / fleet-updater | 与所在节点相同 | |
| Backend | `linux-x64`、`linux-arm64`、`win-x64` | |
- **MIPS 不支持**(NativeAOT 没有 MIPS 目标)。使用 MIPS 路由器(例如 MT7621)时,把 Probe 运行在局域网内的其他设备上,比如 NAS、树莓派或 PC,测量结果同样代表这条家宽。
- NativeAOT 需要目标平台的原生工具链:musl 目标在 Alpine 容器中构建,arm64 使用交叉编译镜像。CI 矩阵为 Windows、Linux glibc、Linux musl。
- `scripts/publish.(sh|ps1) <probe|agent|backend|fleet-enroll|fleet-updater> <rid>` 输出到 `artifacts/publish/<project>/<rid>/`。
- 发布时由 `fleet-release` 在 CI 中生成并签名清单(见 [fleet/update.md](fleet/update.md)),节点通过 `fleet-updater` 更新。
- Linux 上 TLS 依赖系统 OpenSSL(`libssl.so.3`)。
### 9.1 OpenWrt 上的 Probe
| 事项 | 做法 |
| --- | --- |
| C 库 | musl:使用 `linux-musl-*` 目标发布 |
| TLS | .NET 在 Linux 上依赖 OpenSSL,而 OpenWrt 默认使用 mbedTLS/wolfSSL。需要安装 `libopenssl3`(安装脚本自动检查并提示)。启动时自检,缺少时明确报错,不会静默降级 |
| 全球化 | 已启用 `InvariantGlobalization`,不依赖 ICU |
| 服务管理 | procd:提供 `/etc/init.d/monitor-probe`(`procd_set_param respawn`,前台运行,日志输出到 logd);`Fleet.Hosting.ServiceShell` 在 procd 下不需要 `sd_notify` |
| 存储 | 闪存很小,而且需要避免频繁写入:二进制放在 `/usr/lib/monitor/`,配置和身份放在 `/etc/monitor/`(sysupgrade 时通过 `/lib/upgrade/keep.d/` 保留);测量数据本来就不落盘;`ConfigStore` 只在 Agent 列表变化时写入 |
| 内存 | 预算 ≤ 32 MB,适合 ≥ 256 MB 内存的路由器;在本地配置中限制重负载测试的码率(`limits.max_test_mbps`),防止路由器 CPU 成为瓶颈,反过来影响测量结果 |
| 测量偏差 | Probe 运行在网关上时,自己没有 NAT;一旦路由器 CPU 饱和,结果就会失真。因此 Probe 在 `monitor.event` 中上报自身的 CPU 繁忙标记(仅进程级别),分析层据此标注置信说明 |
| 打包 | 提供 `tar.gz` 和 procd 脚本;包格式(ipk 或 apk)按目标 OpenWrt 版本提供 |
| 更新 | `fleet-updater` 使用 procd 适配器;`keep_versions = 1`,下载前检查剩余空间,避免写满闪存 |
## 10. Web(待开发)
唯一输入是 `backend/openapi/v1.json`:用 `openapi-typescript` 生成类型,`openapi-fetch` 作为请求客户端;技术栈等前端立项时再决定。