- 变更规模:新增 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/ 知识文件,按口径入库)
12 KiB
节点与端 · 互联互通 · 数据 / 实例 / 三方插件的关系与调用(2026-09-26)
本文 = 关系与调用地图(成品·描述性)。能做/不能做 ⇒
基础服务插件-能做不能做总清单与拍板路径-20260926.md;逐模块明细 ⇒平台新增功能总盘点与插件化归属-20260926.md。 ⚠️ 本文每条关系都带代码依据(表名 / 模块 / 端点),⛔ 不含推演结论;推演部分集中放 §7。
0 三句话判定
- 四种"端"、两类"网"、一条总入口 —— 所有互联都收敛到同一条链:先入网(拿身份)→ 再准入(网维度)→ 才有通路(隧道)。
- 「节点」在代码里有三个不同所指,混着说必出分歧 ⇒ 覆盖网络的注册表节点 / 承载体的 Worker / 判决者的 Manager。
- 数据 / 实例 / 三方插件是三层不同的东西,各有专属通路、⛔ 不共用:实例 = "跑在哪台机",数据 = "谁说了算",插件 = "装进哪个实例"。
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) | ⛔ | ✅ 同网(双方都须在拨号方白名单) | 同网 |
三条由此推出的硬事实:
- 🔴 Worker 单向拨入 —— Worker 不反向连 Manager、不写控制面数据(协议纪律第一条)⇒ 所以"承载机器"天然可替换、可水平增加。
- 🔴 实例内没有平台代码 ⇒ 实例与平台的唯二通路就是 HTTP(实例凭据或会话 + 插件自称头)。
- 🔴 用户之间(跨网)连不上 —— 这是设计,不是缺陷:网按用户切,
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 三条绕不过的(判据,⛔ 换形态也不变)
- 身份先于通路 —— 拿不到凭据就拨不进来;而"拿凭据"这件事只能发生在插件存在之前(⇒ 身份是信任链的起点)。
- 网维度是结构性的 —— 隔离靠"这张网里有没有你",⛔ 不是靠一张可写错的 ACL 清单。
- 实例内没有平台代码 ⇒ 实例与平台唯二通路是 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 可能自动消失。