152 lines
12 KiB
Markdown
152 lines
12 KiB
Markdown
# 节点与端 · 互联互通 · 数据 / 实例 / 三方插件的关系与调用(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 可能自动消失。
|