第 2 周 · Tether 源码课
把大型仓库读成一张地图
学完你能做到
- 用 solution、project、reference、test 四个词描述 .NET 仓库
- 从目录名判断 Cordis 框架层与 Tether 产品层
- 沿一个能力找到 seam、Provider、Consumer 和镜像测试
- 只运行与当前问题有关的一组测试
课程进度
- 第 1 周
- 第 2 周
- 第 3 周
- 第 4 周
- 第 5 周
- 第 6 周
- 第 7 周
- 第 8 周
- 第 9 周
- 第 10 周
- 第 11 周
- 第 12 周
- 第 13 周
- 第 14 周
- 第 15 周
- 第 16 周
一句话先懂
大型仓库不是一本必须从第一页读到最后一页的小说,而是一座有路牌的城市。
Tether.slnx 是总地图,目录是街区,.csproj 的 ProjectReference 是道路,测试项目是写明道路规则的可执行说明书。
这一周不追业务流程。你要学的是一种更耐用的能力:面对几十个项目时,先找边界和关系,再决定读哪几行代码。
先看大图
一个类比:城市地图与道路
- Solution 像城市总图:列出有哪些 project,但不等于它们被合成一个巨大程序。
- Project 像一个街区:有自己的边界、编译产物与公开入口。
- ProjectReference 像单向道路:A 引用 B,表示 A 编译时可以使用 B;不代表 B 也能看见 A。
- Test project 像道路法规与验收站:它通过真实输入和断言说明“这条路应当怎样工作”。
类比边界:.NET 程序集不是物理街区。运行时加载、NuGet package、internal/public 可见性和依赖注入都比道路类比精细;今天只用它理解静态项目边界与引用方向。
四个先学会的词
| 词 | 在仓库里怎么看 | 你要问的问题 |
|---|---|---|
| Solution | 根目录 Tether.slnx | 这座仓库包含哪些可构建项目? |
| Project | 一个 .csproj 及其源码 | 它对外承诺什么职责? |
| Reference | .csproj 中的 ProjectReference | 依赖从谁指向谁?有没有越层? |
| Test | tests/X.Tests/ | 哪条调用方可观察语义被固定下来? |
打开 Tether.slnx,你会看到项目按 apps、src、tests 分组。它是一份清单,不是学习顺序。学习顺序由问题决定。
三层,而不是六十个孤岛
Cordis:不知道 Agent 是什么
src/Cordis/ 是通用插件框架。它可以认识 Context、service 和 event,却不能引用任何 Tether.* 包。这样框架层不会被产品概念反向污染。
Tether:把 Agent 能力放在 Cordis 之上
src/Tether.*/ 包含 LLM、session、tool、filesystem、shell 等产品能力。它们借 Cordis 挂载和连接,但 Cordis 不需要知道这些能力的含义。
Apps:只负责把程序启动起来
apps/Tether.Cli 与 apps/Tether.Headless 是进程入口。它们处理参数、日志和宿主生命周期;插件树由 Boot 与 YAML composition 装配。
完整分层规则可查架构总览。今天只记一句:越稳定、越通用的规则越靠下;依赖只向下。
源码放大镜:一个能力的三种角色
用文件系统能力做例子:
src/Tether.Fs/ seam:接口与数据类型
src/Tether.Fs.Local/ Provider:本地磁盘实现
src/Tether.Fs.Tools/ Consumer:把文件能力暴露成模型工具
tests/Tether.Fs.Local.Tests/ 镜像测试
先打开 src/Tether.Fs/Tether.Fs.csproj。它没有任何 ProjectReference:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
</Project>
再看 src/Tether.Fs.Local/Tether.Fs.Local.csproj 的关键引用:
<ProjectReference Include="..\Cordis\Cordis.csproj" />
<ProjectReference Include="..\Tether.Fs\Tether.Fs.csproj" />
<ProjectReference Include="..\Tether.Invariants\Tether.Invariants.csproj" />
<ProjectReference Include="..\Tether.Sandbox\Tether.Sandbox.csproj" />
关系是单向的:Provider 知道 seam;seam 不知道本地 Provider。以后替换成远程或沙箱实现时,Consumer 仍只认同一个接口边界。
动手实验:只问仓库一个小问题
实验目标:不运行全仓库测试,只验证“无依赖插件会立即 apply”这一小块 Cordis 语义。
1. 预测
先打开 tests/Cordis.Tests/PluginTests.cs,只读前两个测试名:
Plugin_without_inject_applies_immediately
Plugin_waits_for_injected_services_then_applies
预测:两者唯一关键差别是什么?第二个插件要等到哪件事发生?
2. 运行
dotnet test tests/Cordis.Tests/Cordis.Tests.csproj \
--filter FullyQualifiedName~PluginTests
PowerShell 同样可以使用反引号换行,或把命令写在一行。--filter 让测试 runner 只选择全名包含 PluginTests 的测试。
3. 观察
- 编译只会拉入
Cordis.Tests所需的依赖,不会启动 CLI。 - 测试 runner 会列出通过/失败数量;正常基线应全部通过。
- 测试名已经表达行为,比“test1”“works”更像一条可执行规格。
4. 解释
你刚才沿这条路线阅读仓库:
问题:插件何时 apply?
→ tests/Cordis.Tests/PluginTests.cs
→ src/Cordis/Plugin.cs + Registry.cs
→ 运行过滤后的测试验证理解
这比从 src/Cordis/Context.cs 第一行开始漫无目的地读,更接近真实软件工程工作。
地图练习
在 Tether.slnx 中任选一个带 .Local 的项目,完成下表:
| 要找的东西 | 你的答案 |
|---|---|
| seam 项目 | 例如 Tether.Fs |
| Provider 项目 | 例如 Tether.Fs.Local |
| 它引用的 seam | 查看 .csproj |
| 镜像测试项目 | 查看 tests/ |
| 一个说明行为的测试名 | 不要只抄 class 名 |
查看一个参考答案
Tether.Shell.Local 是本地 Shell Provider,引用 Tether.Shell seam;镜像项目是 tests/Tether.Shell.Local.Tests。具体测试名应以你当前检出的源码为准。
检查理解
1. Tether.Fs.Local 引用 Tether.Fs,能否因此推出 Tether.Fs 也引用 Local?
查看答案
不能。ProjectReference 是单向边。恰恰因为 seam 不引用 Provider,调用方才能在不改接口边界的情况下替换实现。
2. 为什么测试项目要镜像源码项目,而不是把所有测试放进一个巨大项目?
查看答案
镜像结构让依赖和职责清晰:看到一个源码项目就能找到对应测试,也避免一个“万能测试项目”不受控制地引用所有实现、掩盖错误分层。
3. apps/Tether.Cli 能引用很多实现包,是否违反依赖只向下?
查看答案
不违反。进程入口处于最上层,负责选择和启动实现。规则禁止的是底层 Cordis 或 seam 反向依赖上层产品与具体 Provider。
本周带走
- 先用 solution 找项目,用
.csproj找依赖,用测试找语义。 - Cordis 是通用框架层,Tether 是产品层,apps 是 composition root 的进程外壳。
- seam、Provider、Consumer 的引用方向让实现可替换;目录命名本身就是路牌。
下一周开始进入最底层的日常入口:Plugin 与 Context。如果想先查看完整依赖图,可读架构总览与能力服务。