Files
dsh_ai1net_server/交付物/节点与端-互联互通与调用关系-20260926.md
admin c1b5e4d966 chore(工作区): 全量入库 + 补齐 .gitignore(以工作区为准)
- 变更规模:新增 514 / 修改 62 / 重命名 155 / 删除 4(归档重组与文档轮次)
- .gitignore 修:`归档/**/db-cwd归一-备份-*/` —— 原规则写绝对层级(归档/db-cwd归一-…),
  目录搬进 归档/配置与备份/ 后**静默失效**,43 MB 的 DB 备份又变成未跟踪
- .gitignore 补:嵌套 git 内部数据(归档/内嵌git-20261008/、归档/skills-git-旧线-20261007/dotgit-原样移出/)
- .gitignore 补:运行态与部署副本(.workbuddy/collab/、.workbuddy/tools/、.workbuddy/.load-pending、.workbuddy/tmp-*)
- .gitignore 补:备份件(*.bak-*)
- 未跟踪文件从 2190 降到 890(其余为 归档/ 归档件与 .workbuddy/memory/ 知识文件,按口径入库)
2026-10-10 23:13:22 +08:00

153 lines
12 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.
# 节点与端 · 互联互通 · 数据 / 实例 / 三方插件的关系与调用(2026-09-26)
> **本文 = 关系与调用地图(成品·描述性)**。能做/不能做 ⇒ `基础服务插件-能做不能做总清单与拍板路径-20260926.md`;逐模块明细 ⇒ `平台新增功能总盘点与插件化归属-20260926.md`。
> ⚠️ 本文每条关系都带**代码依据**(表名 / 模块 / 端点),⛔ 不含推演结论;推演部分集中放 §7。
---
## 0 三句话判定
1. **四种"端"、两类"网"、一条总入口** —— 所有互联都收敛到同一条链:**先入网(拿身份)→ 再准入(网维度)→ 才有通路(隧道)**。
2. **「节点」在代码里有三个不同所指**,混着说必出分歧 ⇒ 覆盖网络的**注册表节点** / 承载体的 **Worker** / 判决者的 **Manager**。
3. **数据 / 实例 / 三方插件是三层不同的东西**,各有专属通路、⛔ 不共用:**实例** = "跑在哪台机",**数据** = "谁说了算",**插件** = "装进哪个实例"。
---
## 1 全部节点与端(一张表打全)
| 种类 | 在册在哪 | 属于哪张网 | 身份凭据 | 备注 |
|---|---|---|---|---|
| **Manager**(控制面 · 判决者) | relay 的**拨号方**(dialer)白名单 | `ops` | relay 密钥文件 + 拨号方白名单 | 唯一权威;`network_id` 默认 `ops` |
| **Worker**(承载体 · 执行体) | `dsh_hosts` 行(含 `via` 经谁可达 + `network_id`) | `ops` | worker agent 共享密钥头 | **无自己的库**;只被拨入 |
| **中继 relay** | 自己的端点表 + 密钥文件 | —(服务所有网) | 对端密钥 | 两台(只绑回环)+ **443 兜底入口** |
| **服务器实例** | `overlay_devices`(`kind='instance'`) | `u:<userId>` | **设备凭据**(写进实例 home) | 🔴 **实例在网里"是一台设备"**,不是会话 |
| **用户桌面设备** | `overlay_devices`(`kind='desktop'`) | `u:<userId>` | 设备凭据(**密钥对在设备上生成**,私钥永不出机) | 24 h 租约,可续签、可停发 |
| **浏览器会话** | `sessions`(`kind='browser'`) | —(不入网) | `sid` cookie | ⛔ 不入覆盖网络 |
| **桌面会话** | `sessions`(`kind='desktop'`) | —(不入网) | `sid` cookie | 同上 |
**网的两类取值(已定项)**:`ops` = **运维网**(平台自己的机器:Manager / Worker / 中继 / 骨干);`u:<userId>` = **用户网**(该用户名下**全部设备,含桌面客户端**)。
**逻辑名** = `<network_id>/<hostId>`;`network_id` 不含 `/` ⇒ 首个 `/` 即分隔点(唯一入口,⛔ 调用方不许自己拼)。
🔴 **「节点」的三个所指(本文全篇按此区分)**:① 覆盖网络**注册表节点**(网 × hostId,有 `pending` / `approved`)② **Worker**(承载体,`dsh_hosts`)③ **Manager**(判决者)。
⚠️ 「当节点」这类说法**必须点明是哪一个** —— 否则歧义直接从"能不能做"变成"做了两个不同的东西"。
**依据**:`db/schema.ts` v9(`dsh_hosts.network_id` 默认 `ops`)· v12(`overlay_devices` 复合主键 `(network, host_id)`)· v13(`kind` 两处值域)|`net/relay/network.ts`(`OPS_NETWORK` / `logicalName` / `NAME_SEP`)|`net/relay/registry.ts`(注册表 + 准入)。
---
## 2 互联互通:三个必须过的门(⛔ 顺序不可换)
| 门 | 做什么 | 机制 | 不过会怎样 |
|---|---|---|---|
| **① 入网(拿身份)** | 冷启动先找到中继,再换一份**通行的凭据** | 三级链:env > 缓存目录 > 内置种子;目录**签名验签**;凭据只含"哪张网+有效期+一次性 nonce",**⛔ 不含密钥本体** | **失败关闭** —— 签名不对既不写缓存也不用它的地址 |
| **② 准入(网维度)** | 判断"这台机器**在这张网里**能不能拨" | relay 白名单 `Map<network, Set<hostId>>`,**默认拒绝** | **跨网在"能不能拨"这一步就走不到** ⇒ **结构性隔离**,⛔ 不是靠 ACL 兜 |
| **③ 通路(隧道)** | 真正建连接 | 中继 **443**(功能全可用、⛔ 不用开任何端口);UDP 段只服务 P2P(**可选优化**) | 连不上就没通路,⛔ 不静默降级到别的地址 |
**签发一份设备凭据要过五道门**(`net/relay/device-grant.ts`,⛔ 唯一实现):
**停发闸门 → 用户配额 → 签 grant → 注册表落盘 → 台账 → relay 密钥表**。
⚠️ **密钥表写在最后是刻意的**:密钥表一写,这台设备就**真的能拨进来**了;台账写在它之前,则"台账写失败"时还没有任何**功能性**后果。反过来排序会造出"**查不到归属、却能拨进来**"的设备。
**依据**:`net/relay/directory.ts`(三级链 + 三条判据)|`net/relay/registry.ts`(三条硬约束 + `deriveDialers`)|`net/relay/network.ts`(网维度是**结构性**的)|`net/relay/device-grant.ts`(五道门)。
---
## 3 谁能连谁(联通矩阵)
| 从 \ 到 | 控制面 | Worker | 实例 | 用户设备 |
|---|---|---|---|---|
| **控制面** | — | ✅ **拨入** Worker agent(**单向**) | ✅ 文件/配置经 **UserFs seam** | ⛔ 不主动连设备 |
| **Worker** | ⛔ **不反连**(协议纪律) | 同机 | ✅ **本机**(回环 + OS 级运行单元) | — |
| **实例** | ✅ **HTTP** | ⛔ | ✅ **同网内**(P2P 可选,443 中继兜底) | ✅ 同网 |
| **用户设备** | ✅ **HTTPS**(门户 / API) | ⛔ | ✅ 同网(双方都须在拨号方白名单) | 同网 |
**三条由此推出的硬事实**:
1. 🔴 **Worker 单向拨入** —— Worker **不反向连 Manager、不写控制面数据**(协议纪律第一条)⇒ 所以"承载机器"天然可替换、可水平增加。
2. 🔴 **实例内没有平台代码** ⇒ **实例与平台的唯二通路就是 HTTP**(实例凭据或会话 + 插件自称头)。
3. 🔴 **用户之间(跨网)连不上** —— 这是**设计**,不是缺陷:网按用户切,`u:A` 与 `u:B` 之间**在"能不能拨"那一步就走不到**。⇒ **跨用户协作只能走平台(443),⛔ 不是设备直连。**
**依据**:`worker/agent.ts`(四条协议纪律)|`fs/user-fs.ts`(UserFs seam 的归属钉法)|`web/routes/plugin-data.ts`("唯二通路就是 HTTP" 原文)|`net/relay/network.ts`(按用户切网)。
---
## 4 数据 / 实例 / 三方插件 —— 三层,各走各的通路
### 4.1 一句话说清三者关系
| 东西 | 它回答的问题 | 权威在哪 | 谁动它 |
|---|---|---|---|
| **实例** | "**跑在哪台机**" | 控制面(归属 + 租约 + 纪元) | 由 **Worker** 在本机执行起停 |
| **数据** | "**谁说了算**" | **分三层**(见 4.2) | 各有唯一写者 |
| **三方插件** | "**装进哪个实例**" | 控制面(候选池 + 版本) | 装配在**引导层**,⛔ 不由插件自己装 |
⇒ **三者不是包含关系,是三个正交的归属**:一个实例可以装 N 个插件;一个插件可以装进 M 个实例(各一份副本);而每个插件的**数据只有一份权威**(在平台侧,按归属列切分)。
### 4.2 数据的三层(⛔ 混了就会"看得到读不到")
| 层 | 在哪 | 唯一写者 | 踩过的坑 |
|---|---|---|---|
| **平台权威库** | Manager | **只有 Manager** | 双写 = 脑裂 |
| **插件库**(**一插件一库**) | Manager 的 PG | **插件经 HTTP** 写 | 归属列由平台**强制注入**,插件覆盖不了 |
| **实例 home 里的文件** | **Worker 那台机上** | 平台必须走 **UserFs seam** | ⛔ 直写 = **静默空操作**(返回空串、不报错) |
### 4.3 三方插件的四个面(每个面的"谁来做"都不同)
| 面 | 谁来做 | 机制 |
|---|---|---|
| **① 装配面**(把插件放进实例) | **引导层** | 共享**只读包库**(每机一份);装配协议 **`link:`**;⛔ 装配动作不在插件里 |
| **② 运行面**(插件跑在哪) | **用户实例内** | 插件与实例**同进程域** ⇒ 这**正是**它能用"用户自配的模型凭据"的原因(那套凭据只写进实例 home) |
| **③ 取数面**(插件怎么读数据) | 插件 → **HTTP** → 平台 | `GET/POST/PATCH/DELETE /api/im/data/<table>`;**三层授权**(谁在调 → 以哪个插件身份 → 数据归属);契约失败一律 **200 + `{ok:false}`** |
| **④ 回调面**(平台/端侧怎么叫插件) | 插件 SDK + 扩展点 + 反向回调 | 插件**界面半**落在官方已有槽位 |
**取数面的三层授权(顺序不可换)**:
① **谁在调** —— 浏览器会话(用户本人)**或**实例凭据(实例所属用户);
② **以哪个插件的身份** —— 请求头 `x-dsh-im-plugin-id`;平台**只信自己的记录**(在候选池 + 声明合法 + 台账 `state='ready'`)⇒ **未建库的插件取不到任何数**;
③ **数据归属** —— 平台**强制**注入归属列(`user_id` / `room_id`),插件**覆盖不了**。
### 4.4 一次调用的完整链路(谁经谁)
```
用户在插件界面点一下
└─ 插件(跑在用户实例内)
└─ HTTP 到平台:实例凭据 + 插件自称头
└─ 平台三层授权(谁 / 哪个插件 / 归属)
└─ 强制注入归属列 ⇒ 插件自己的库
└─ 返回(失败也 200 + {ok:false})
若这次调用要"碰到别的用户"
└─ ⛔ 不经设备直连 └─ 只有一条路:走平台(443 中继)
```
**依据**:`db/plugin-data/*`(一插件一库 + 声明期校验)|`web/routes/plugin-data.ts`(三层授权 + 端点表 + 残余缺口)|`supervisor/plugin-assembly.ts`(装配与共享层)|`worker/agent.ts`(实例侧入口)|`fs/user-fs.ts`(home 归属)。
---
## 5 三条绕不过的(判据,⛔ 换形态也不变)
1. **身份先于通路** —— 拿不到凭据就拨不进来;而"拿凭据"这件事**只能发生在插件存在之前**(⇒ 身份是信任链的起点)。
2. **网维度是结构性的** —— 隔离靠"这张网里有没有你",⛔ 不是靠一张可写错的 ACL 清单。
3. **实例内没有平台代码** ⇒ 实例与平台**唯二通路是 HTTP**;这既是约束,也是"为什么插件必须在实例内"(否则拿不到用户自配的凭据)。
---
## 6 已知缺口与未定(⛔ 不假装完备)
| # | 内容 | 状态 |
|---|---|---|
| 1 | **插件"自称身份"**:同一用户的实例里,插件 A 可自称插件 B,读到 **B 的、同一用户维度**的行 | ⚠️ **真实缺口**(越不出用户边界;补法 = 每插件令牌,**本期以逐次审计兜底**) |
| 2 | **插件数据面已落地、⛔ 未部署**(47 / 106 未投放) | 待部署 |
| 3 | **节点"提供哪些具体服务"** 未定 | 业务方向,待拍板 |
| 4 | **承载归属迁移**(实例换机)机制 **未设计** | ⛔ 不立项(只有缺点) |
| 5 | 桌面侧**拉取器 + 共享包接 profile 装配** | 已交对接单(桌面线) |
| 6 | "轻节点当内容副本 / 设备当消费者" | 卡在**待拍板项**(设备读包权限) |
| 7 | **跨用户互联**(`u:A` ↔ `u:B` 直连) | ⛔ 现状结构性不通 ⇒ 跨用户协作**只能走平台**;是否要打通属**权限面**决策 |
---
## 7 待拍板(承接 `基础服务插件-能做不能做总清单与拍板路径-20260926.md` §7,⛔ 不复制正文)
- **P1 插件规划走不走**(产品主路径)—— 决定后面全部
- **P2 设备要不要有「读平台插件包」的权**(权限面)
- **P3 节点提供哪些具体服务**(业务方向)
- **P4(本文新登记)跨用户要不要打通** —— 现状是**结构性不通**(各用户一张网),跨用户协作只能走平台;打通 = **扩大可见面**,属权限面决策。⚠️ 本条**排在 P1 之后**:若 P1 走"只装官方宿主 + 插件",跨用户协作**本来就该走平台**,P4 可能自动消失。