Files
justme c9beeee8e3 docs: describe the unified Agent topology and link controls
Align architecture, protocol, API, storage, security and deployment documentation with default mutual measurements and the Agent link board.
2026-10-21 11:55:55 +08:00

112 lines
9.3 KiB
Markdown
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.
# C# 代码风格
基线是 [dotnet/runtime 编码风格](https://github.com/dotnet/runtime/blob/main/docs/coding-guidelines/coding-style.md),再加上本项目的补充规则。所有能自动检查的规则都写在 `.editorconfig` 或 `BannedSymbols.txt` 里,违反即编译失败(`EnforceCodeStyleInBuild` + `TreatWarningsAsErrors`);无法自动检查的规则由代码评审把关。
## 1. 格式与命名(`.editorconfig` 强制)
- 使用 Allman 大括号(左大括号独占一行),四空格缩进,不使用 Tab;`if`/`for` 等语句必须加大括号。
- 使用文件作用域 namespace;namespace 与目录一致(`MoShiro.Agent.Sessions` 对应 `Sessions/`)。
- `using` 放在文件顶部、namespace 之外,`System.*` 排在最前面,其余按字母排序。
- 始终显式写出可见性,并且放在修饰符的第一位;`static readonly` 的顺序固定。
| 元素 | 规则 | 示例 |
| --- | --- | --- |
| 类型、方法、属性、常量、局部函数 | PascalCase | `SessionTable`、`MaxFrameBytes` |
| 私有和内部实例字段 | `_camelCase` | `_sessions` |
| 静态字段 | `s_camelCase` | `s_idleTimeout` |
| 线程静态字段 | `t_camelCase` | `t_buffer` |
| 局部变量、参数、主构造函数参数 | camelCase | `windowSeq` |
| 接口 | `I` 前缀 | `IMeasurement` |
| 异步方法 | `Async` 后缀 | `SendReportAsync` |
- `var` 只在右侧类型显而易见时使用(`new`、强制转换);类型名使用语言关键字(`int`,而不是 `Int32`)。
- 用 `nameof(...)` 代替字符串字面量;不写多余的 `this.`。
- 字段放在类型的最前面;一个文件只放一个主要类型,文件名与类型名相同。
- 内部类和私有类默认使用 `sealed`(或 `static`),除非确实需要被继承。
## 2. 命名语义
- 名称里带单位:`timeoutMs`、`rttUs`、`payloadBytes`。API 内部统一用 `TimeSpan`,只在边界(线协议、配置、JSON)使用带单位后缀的整数。
- 不使用 `Manager`、`Helper`、`Util`、`Service`(ASP.NET 约定位置除外)之类的模糊名称,名称要说明职责:`UploadQueue`、`SessionTable`、`ResultWriter`。
- 布尔值使用 `is`/`has`/`can` 前缀;返回“成功与否加输出值”的方法使用 `TryXxx(out ...)`。
## 3. 类型与数据
- 契约和领域值使用 `sealed record` 或 `readonly record struct`,并且不可变。
- 热路径上的计数器和缓冲区使用可变 `struct` 或 `sealed class`,所有权要写清楚(谁创建、谁归还池)。
- ID 使用强类型(`readonly record struct NodeId(string Value)`),不直接传裸 `string`。
- 枚举序列化成 snake_case 字符串(`JsonStringEnumConverter<T>` + `JsonStringEnumMemberName`);新增枚举值不属于破坏性变更,但所有消费方都必须能处理未知值。
- 可空性:公共成员必须标注;使用 `!` 抑制警告时,同一行要写注释说明理由。
## 4. 错误处理
- 预期内的失败不抛异常:超时、拒绝连接、认证失败、校验失败都返回结构化结果,其中包含 `ErrorCode`。
- 异常只用于程序缺陷或无法继续运行的状态;只在边界(会话循环、请求管线、进程入口)捕获,并记录日志。
- 禁止空 `catch`;捕获 `OperationCanceledException` 时必须确认它确实来自自己的取消令牌。
## 5. 异步与并发
- 所有 I/O 都是异步的,并且接受、传递 `CancellationToken`(作为最后一个参数)。
- 禁止使用 `.Result`、`.Wait()`、`Thread.Sleep`、`async void`(事件处理函数除外),`BannedSymbols.txt` 强制执行。
- 库代码中的 `await` 不需要 `ConfigureAwait(false)`,因为三个程序都没有同步上下文,统一省略即可。
- 生产者和消费者之间使用有界 `Channel<T>`,明确指定 `BoundedChannelFullMode`。
- 共享可变状态要么只由单个循环拥有(推荐),要么使用 `lock` 保护;不要混用多种方式。
## 6. 时间
- 时间来源一律通过 `TimeProvider` 获得:墙钟使用 `GetUtcNow()`,耗时使用 `GetTimestamp()`/`GetElapsedTime()`。
- `DateTime.Now`、`DateTime.UtcNow`、`DateTimeOffset.Now`、`Stopwatch.StartNew` 列入禁用名单。测试使用 `FakeTimeProvider` 驱动窗口和超时(这个包只出现在测试项目中)。
## 7. NativeAOT 与轻量化
- 序列化只使用 `System.Text.Json` 源生成上下文(或手写的二进制编解码);禁用无上下文的 `JsonSerializer.Serialize<T>(T)` 重载。
- 不做程序集扫描、`Activator.CreateInstance`、`Type.GetType(string)`、动态代理或 `Reflection.Emit`。
- Agent:不使用 DI 容器,在 `Program.cs` 中手工组装对象,依赖关系一眼可见;不使用 LINQ;不使用 `HttpClient`(Agent 的 WebSocket 使用 `ClientWebSocket`)。
- Backend:使用 `CreateSlimBuilder` + Minimal API;只能通过 `Microsoft.Data.Sqlite` 执行手写的参数化 SQL,禁止字符串拼接 SQL。
- 热路径:使用 `Span<T>`、`stackalloc`(≤ 256 B)、`ArrayPool<byte>.Shared`;每个包的处理过程中不分配内存、不写日志。
- 新增任何第三方包之前,需要先更新 [project-layout.md](project-layout.md) 第 3 节的依赖表,并在 PR 中说明理由。
## 8. 日志
- 统一使用 logfmt 字段名:`ts`、`level`、`msg`、`agent`、`dialer`、`session`、`task`、`result`、`err`。
- Backend 使用 `Microsoft.Extensions.Logging` 源生成的 `[LoggerMessage]`;Agent 使用 `NodeLog`,两者输出的字段名相同。
- 令牌、`session_key` 和 MAC 不能出现在日志里;IP 地址可以记录。
- 每个会话或连接的生命周期事件记录为 Info;逐包事件不记录,只累加计数。
## 9. 节点能力边界(Agent 与 Fleet 节点侧)
- 由 `BannedSymbols.Node.txt` 在编译期强制:禁止启动进程、动态加载程序集或本机库、使用 `HttpClient`、在配置目录以外写文件。唯一的豁免点是 `Fleet.Hosting.ConfigStore`,豁免必须写 `#pragma` 并附上理由注释。注册在 `fleet-enroll` 中完成,更新由节点宿主机上的更新器完成,常驻进程当前不需要这些能力。
- 节点不能新增任何“由远端触发的非采集动作”对应的 RPC 方法。凡是新增 RPC 方法的 PR,都要在描述中说明它不会扩大权限边界。
- Backend 下发的任何参数都要先经过 `ParamSpec` 校验,再经过本地硬上限(`BudgetGuard`)检查,两道校验都不能省略。
- 日志中不能出现私钥、签名、`session_key`、令牌或 MAC。
## 9.1 Fleet 的通用性
- `backend/fleet/**` 不能引用任何 `MoShiro.*`,也不能出现产品相关的概念、命名或默认值(例如端口 7443、`moshiro` 字样),由 `ArchitectureTests` 和代码评审把关。
- 新增的公开 API 如果只有 MoShiro 使用,应先放在 MoShiro 中,等出现第二个使用者后再下沉到 Fleet。
- Fleet 不提供 exec、shell、文件读写或远程下发代码的方法;确需此类能力时走 extension 方案,见 [security.md](../security/security.md) 第 9 节。
- Fleet 公开 API 的变更,PR 描述需说明是否属于破坏性变更(不使用 PublicApiAnalyzers)。
## 10. 声明式扩展的写法
- Kind:只放在 `Protocol/Catalog/Kinds`,一个 Kind 一个静态字段;参数一律使用 `P.Int`、`P.Enum` 等 `ParamSpec` 写出默认值和上下限,禁止写没有上下限的参数。
- 积木:实现拨号方 / 监听方接口,在 `BlockCatalog` 中登记 Kind 到积木的映射;积木内部不能读取全局配置,所有输入都来自参数。
- 指标:只放在 `Analysis/Metrics`,一行一个 `MetricDef`,并加入 `MetricRegistry.All`;指标 id 使用 `领域.名称`(如 `tcp.stall`),导出值为 `id.统计量`(如 `rtt.p95`)。已发布的 id 不能改名,只能新增。
- 算子:纯函数,输入列视图,输出累加器更新;每个算子都要有边界用例测试(空输入、单个样本、乱序、时钟回绕)。
- 模型:只读取指标,不读取原始列;评分函数需要有单调性测试(指标变差时,分数不能升高)。
## 11. 测试
- 使用 xUnit v3。测试项目与被测项目一一对应,放在同名目录下的 `tests/` 中。
- 测试名采用 `方法_场景_期望` 格式,例如 `Merge_TwoHistograms_PreservesP95`。
- 协议编解码必须有“黄金字节”测试:固定的输入对应固定的字节序列。布局发生变化时测试会失败,提醒升级协议版本。
- 采集端必须覆盖:逐包记录的完整性(发送数 = 记录数)、丢失标记、重复、乱序、非法长度、重放、会话过期、版本不兼容。
- 分析模型必须有“黄金原始块 → 期望输出”的测试;模型升版时,同时更新期望输出并说明原因。
- 端到端测试应针对 AOT 发布产物运行,而不是 JIT 构建。
## 12. 提交与评审
- 提交信息使用 `<范围>: <摘要>` 格式,范围取 `dialer`、`agent`、`backend`、`shared`、`web`、`docs`、`scripts`;是否触发 CI 的标记见 [AGENTS.md](../../AGENTS.md)。
- 修改 `backend/shared/MoShiro.Protocol` 的 PR 必须说明兼容性影响(新增字段、行为变化、需要升级版本)。
- 修改 `backend/openapi/v1.json` 的 PR 必须按照 [api.md](../api/api.md) 第 7 节的「版本与兼容性」判断是否属于破坏性变更。