From e6207aa6914ff26bee7e999b72db153d00e2d096 Mon Sep 17 00:00:00 2001 From: maogeigei Date: Thu, 24 Sep 2026 07:25:16 +0800 Subject: [PATCH] =?UTF-8?q?chore(=E4=BB=93=E5=BA=93=E5=AF=B9=E9=BD=90):=20?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E5=BA=93=E7=BB=93=E6=9E=84=E6=B2=BB=E7=90=86?= =?UTF-8?q?=20+=20IM/=E6=8F=92=E4=BB=B6=E7=BA=BF=E8=90=BD=E5=9C=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 文档库:目录改为编号制(01-规范/02-架构设计/03-数据库/04-调整方案/ 05-交接单/06-ops/07-scripts/08-skills/09-archive),顶层散文件归入 01-规范/; INDEX.md 与 docs-manifest.json 重刷(档案 146 篇);旧目录名引用全量对齐。 IM 线:src/im/**(SDK / hub / store / presence / ws / gateway-token)、 src/web/routes/im.ts、src/db/plugin-data/**、src/supervisor/plugin-assembly.ts 及对应 test/**。 插件线:poc/{im-agent-bridge,im-connection-gateway,im-conversation-tabs, business-plugins-im,carbon-mcp-probe}、src/web/routes/{sessions,overlay-device}.ts、 src/net/relay/{device-grant,instance-credential}.ts。 仓库卫生:清出 40 个历史误入库 / 已改名文件(34 个交接单归档 + 6 个旧结构, 本地均有副本);dsh-server-docs/.gitignore 补 tmp/;交接单不入库(政策)。 --- .gitignore | 2 +- config/README.md | 6 + config/platform.env.example | 29 + dsh-server-docs/.gitignore | 6 +- .../{ => 01-规范}/01-规划与架构.md | 16 +- dsh-server-docs/{ => 01-规范}/02-运维手册.md | 8 +- .../{ => 01-规范}/03-路线图与待办.md | 254 +-- .../{ => 01-规范}/06-工作台UI规范.md | 6 +- .../{ => 01-规范}/07-实例UI分区登记表.md | 6 +- .../01-规范/08-插件开发与对接规范.md | 216 ++ .../01-规范/09-IM插件SDK与扩展点契约.md | 134 ++ dsh-server-docs/02-架构设计/README.md | 45 + .../02-架构设计/数据-分库与权威存储架构.md | 136 ++ .../02-架构设计/覆盖网络-顶层架构全貌.md | 251 +++ dsh-server-docs/03-数据库/DB-00-专区入口.md | 57 + dsh-server-docs/03-数据库/DB-01-接入指南.md | 94 + .../03-数据库/DB-02-表结构台账与迭代.md | 100 + .../03-数据库/DB-03-插件数据面规范.md | 278 +++ .../08-实例常驻上限与单活跃会话.md | 7 + .../117-覆盖网络-参数表与观测口径.md | 3 +- ...网络-低熵块治理方案-C域分离与D非确定性.md | 2 +- .../142-IM群组对话-需求基线与方案.md | 211 ++ ...143-MCN工作台入口失效与语言切换显示修复.md | 101 + .../144-插件规模化投放与版本一致性.md | 303 +++ ...45-覆盖网络-序47-会话并存与设备来源维度.md | 210 ++ ...子域503-中继per-port额度被连接泄漏占满.md | 249 +++ .../147-多节点形态下的插件投放与数据面.md | 262 +++ .../148-多Manager多区域联邦形态.md | 329 +++ .../04-调整方案/149-覆盖网络顶层架构全貌.md | 278 +++ dsh-server-docs/04-调整方案/README.md | 14 +- .../{ops => 06-ops}/nginx/alotbuy.com.conf | 0 .../nginx/dsh.alotbuy.com.conf(旧域301) | 0 .../scripts/switch-domain-alotbuy.sh | 0 .../analyze-session.mjs | 0 .../{scripts => 07-scripts}/analyze-turn.mjs | 0 .../bash-output-guard.py | 2 +- .../{scripts => 07-scripts}/cf-dns.py | 0 .../{scripts => 07-scripts}/cf-probe.py | 0 .../docs-archive-index.py | 10 +- .../{scripts => 07-scripts}/docs-audit.py | 8 +- .../docs-consistency.py | 8 +- .../{scripts => 07-scripts}/docs-dedupe.py | 4 +- .../docs-index-stats.py | 6 +- .../{scripts => 07-scripts}/docs-manifest.py | 20 +- .../{scripts => 07-scripts}/docs-search.py | 20 +- .../docs-shrink-guard.py | 8 +- .../docs-sync-check.sh | 13 +- .../extract-user-voice.py | 8 +- dsh-server-docs/07-scripts/handoff-guard.sh | 562 ++++++ dsh-server-docs/07-scripts/handoff-status.py | 68 + dsh-server-docs/07-scripts/lock-guard-hook.py | 380 ++++ .../{scripts => 07-scripts}/nginx-slow.py | 0 .../{scripts => 07-scripts}/op-lock.sh | 12 +- .../plugin-compat-check.mjs | 0 dsh-server-docs/07-scripts/preflight-lock.sh | 137 ++ .../{scripts => 07-scripts}/sess-analyze.mjs | 0 .../sess-classify-tools.mjs | 0 .../sess-list-presets.mjs | 0 .../sess-probe-schema.mjs | 0 .../skill-load-guard.py | 110 +- .../stop-dialog-guard.py | 72 +- .../08-skills/agent-operating-rules/SKILL.md | 386 ++++ .../references/01-协作与上抛判据.md | 287 +++ .../references/02-工作区纪律.md | 266 +++ .../references/03-多棒接力编排.md | 196 ++ .../references/04-去AI味与说话方式.md | 253 +++ .../dsh-architecture-lifecycle/SKILL.md | 244 +++ .../dsh-auto-handoff-chain/SKILL.md | 128 +- .../scripts/chain_report.py | 0 .../08-skills/dsh-change-workflow/SKILL.md | 319 +++ .../references/00-平台速查.md | 89 + .../references/01-档案模板.md | 23 + .../references/02-红线详解-R5-R7-R8.md | 77 + .../references/03-沙箱与技能机制.md | 80 + .../references/04-运维锚点与取证.md | 95 + .../references/05-插件与数据源口径.md | 54 + .../references/06-实例可见面与共享边界.md | 316 +++ .../references/07-并行调度详解.md | 60 + .../references/08-浏览器验证栈详解.md | 73 + .../dsh-decision-method/SKILL.md | 21 +- .../references/素材库-A-AI推理.md | 4 +- .../references/素材库-U-用户决策.md | 4 +- .../references/素材库-反例-X.md | 0 .../08-skills/dsh-desktop-dev-shell/SKILL.md | 321 +++ .../dsh-env-bootstrap/SKILL.md | 9 +- .../references/常驻规则-快照.md | 29 +- .../scripts/resident-rules.py | 6 +- .../dsh-feature-first/SKILL.md | 123 +- .../dsh-instance-diagnose/SKILL.md | 33 +- .../08-skills/dsh-knowledge-upkeep/SKILL.md | 385 ++++ .../references/10-技能重组-千行技能拆分.md | 109 + .../scripts/split_skill.py | 159 ++ .../08-skills/dsh-opensource-release/SKILL.md | 534 +++++ .../references/01-保留移除与脱敏口径.md | 109 + .../references/02-多远端推送.md | 49 + .../references/03-实测坑.md | 92 + .../references/04-交互式图示-archify.md | 83 + .../references/05-已知待办与漂移.md | 295 +++ .../dsh-plugin-diagnose/SKILL.md | 73 +- .../RUNBOOK-cutover.md | 0 dsh-server-docs/BRIEF.md | 22 +- dsh-server-docs/CODEBUDDY.md | 80 +- dsh-server-docs/DEPLOY-本部署.md | 31 +- dsh-server-docs/INDEX.md | 196 +- dsh-server-docs/README.md | 54 +- dsh-server-docs/archive-summaries.json | 48 +- dsh-server-docs/archive/_tmp_r6_s8.md | 167 -- .../archive/_自动接续简报_20260916.md | 126 -- .../dsh-improvement-plan-20260909-full.md | 555 ------ .../T01-档案16阶段3-4-实例内我的技能.md | 152 -- .../T02-文档库收尾-编号消歧与INDEX瘦身.md | 165 -- .../T03-plugin_package整合为单插件并投放.md | 345 ---- ...4-并发治理落地-commit常态化与服务器侧锁.md | 174 -- .../交接单-已完成/T05-插件兼容性预检.md | 137 -- .../T06-模型设置页-多厂家条目可开关.md | 79 - .../交接单-已完成/T07-内置dsh安装路径探测.md | 71 - ...VoxEMW全云API化接入dsh修订方案_20260909.md | 222 --- .../VoxEMW接入dsh调研与落地方案_20260909.md | 353 ---- .../插件管理面_调整方案草案_20260910.md | 205 -- dsh-server-docs/docs-manifest.json | 1758 ++++++++++++----- .../ops/接续入口_覆盖网络线_20260916.md | 278 --- .../ops/接续包_覆盖网络线_20260916.md | 66 - dsh-server-docs/scripts/handoff-guard.sh | 292 --- dsh-server-docs/scripts/lock-guard-hook.py | 207 -- .../skills/dsh-change-workflow/SKILL.md | 1038 ---------- .../skills/dsh-knowledge-upkeep/SKILL.md | 229 --- .../skills/dsh-opensource-release/SKILL.md | 594 ------ dsh-server-docs/交接单/README.md | 156 -- .../交接单-已完成/T08-执行标记-已释放.md | 20 - .../T08-集群化落地-兼容单例模式.md | 740 ------- .../交接单-已完成/T09-覆盖网络落地执行.md | 255 --- .../交接单-已完成/T10-网抽象与地址规划R6.md | 203 -- .../交接单-已完成/T11-relay落地R2-R4.md | 488 ----- .../archive/交接单-已完成/T12-443兜底.md | 413 ---- .../archive/交接单-已完成/T13-中继失败切流.md | 426 ---- .../archive/交接单-已完成/T14-切流冷却语义.md | 488 ----- .../交接单-已完成/T15-presence在线态.md | 412 ---- .../交接单-已完成/T16-一机一钥与信任根.md | 426 ---- .../archive/交接单-已完成/T17-参数表与观测.md | 408 ---- .../交接单-已完成/T18-检测时延与deadline.md | 557 ------ .../交接单-已完成/T19-最小形态真机批次.md | 434 ---- .../交接单-已完成/T20-观测口径与在册缺陷.md | 780 -------- .../archive/交接单-已完成/T21-在册收尾.md | 454 ----- .../交接单/覆盖网络-序24-内容分发块级寻址.md | 167 -- .../交接单/覆盖网络-序25-实例逐步拉起.md | 162 -- .../覆盖网络-序26-骨干稳定选路与加密.md | 177 -- .../覆盖网络-序45-低熵块治理-测熵与实现.md | 619 ------ package.json | 4 +- .../mud-rate/cordis.patch.yml | 8 + poc/business-plugins-im/mud-rate/package.json | 1 + .../office-tasks/cordis.patch.yml | 8 + .../office-tasks/package.json | 17 + .../trpg-turn/cordis.patch.yml | 8 + .../trpg-turn/package.json | 1 + poc/carbon-mcp-probe/cordis.patch.yml | 23 + poc/carbon-mcp-probe/lib/client.js | 7 + poc/carbon-mcp-probe/lib/index.js | 132 ++ poc/carbon-mcp-probe/package.json | 29 + poc/carbon-mcp-probe/stub-mcp-server.mjs | 155 ++ poc/im-agent-bridge/cordis.patch.yml | 14 + poc/im-agent-bridge/lib/index.js | 498 +++++ poc/im-agent-bridge/package.json | 17 + poc/im-connection-gateway/README.md | 166 ++ poc/im-connection-gateway/config.json | 58 + poc/im-connection-gateway/nginx-location.conf | 40 + poc/im-conversation-tabs/cordis.patch.yml | 13 + poc/im-conversation-tabs/lib/client.js | 1626 +++++++++++++++ poc/im-conversation-tabs/lib/index.js | 25 + poc/im-conversation-tabs/package.json | 25 + poc/portal-entry/lib/client.js | 17 +- poc/portal-entry/package.json | 4 +- scripts/check-layering.mjs | 12 +- scripts/install-plugin-for-user.cjs | 205 ++ scripts/verify-portal-entry.mjs | 5 +- src/config.ts | 45 + src/db/adapter.ts | 47 + src/db/pg.ts | 185 +- src/db/plugin-data/datastore.ts | 621 ++++++ src/db/plugin-data/diff.ts | 428 ++++ src/db/plugin-data/schema.ts | 925 +++++++++ src/db/repo.ts | 199 +- src/db/schema.ts | 294 +++ src/db/sqlite.ts | 81 + src/db/types.ts | 183 ++ src/fs/user-fs.ts | 8 +- src/im/agent-bridge.ts | 370 ++++ src/im/backends/gateway.ts | 378 ++++ src/im/backends/native.ts | 90 + src/im/clock.ts | 118 ++ src/im/connection-backend.ts | 230 +++ src/im/db.ts | 134 ++ src/im/gateway-token.ts | 189 ++ src/im/hub.ts | 444 +++++ src/im/instance-token.ts | 190 ++ src/im/keepalive.ts | 214 ++ src/im/presence-ingest.ts | 234 +++ src/im/sdk/host.ts | 695 +++++++ src/im/sdk/index.ts | 38 + src/im/sdk/types.ts | 458 +++++ src/im/store.ts | 574 ++++++ src/im/types.ts | 240 +++ src/im/ws.ts | 562 ++++++ src/net/relay/device-grant.ts | 380 ++++ src/net/relay/index.ts | 4 +- src/net/relay/instance-credential.ts | 515 +++++ src/net/relay/main.ts | 25 +- src/net/relay/network.ts | 227 ++- src/net/relay/server.ts | 239 ++- src/supervisor/leased-spawner.ts | 12 +- src/supervisor/orchestrator.ts | 208 +- src/supervisor/plugin-assembly.ts | 488 +++++ src/supervisor/proxy.ts | 30 + src/supervisor/remote-spawner.ts | 106 +- src/supervisor/spawner.ts | 33 + src/web/auth.ts | 14 + src/web/routes/auth.ts | 65 +- src/web/routes/business-plugins.ts | 1633 ++++++++++++--- src/web/routes/im.ts | 894 +++++++++ src/web/routes/overlay-device.ts | 99 + src/web/routes/overlay-nodes.ts | 77 + src/web/routes/sessions.ts | 155 ++ src/web/server.ts | 104 +- src/worker/agent.ts | 70 + test/db.test.mjs | 100 + test/im-agent.test.mjs | 541 +++++ test/im-connection-backend.test.mjs | 547 +++++ test/im-gateway-access.test.mjs | 361 ++++ test/im-presence-ingest.test.mjs | 277 +++ test/im-sdk.test.mjs | 1019 ++++++++++ test/im-store.test.mjs | 571 ++++++ test/im-ui.test.mjs | 587 ++++++ test/instance-credential.test.mjs | 327 +++ test/overlay-device.test.mjs | 641 ++++++ test/overlay-direct.test.mjs | 73 +- test/overlay-network.test.mjs | 18 + test/plugin-assembly.test.mjs | 403 ++++ test/plugin-data.test.mjs | 616 ++++++ test/plugin-shared-catalog.test.mjs | 174 ++ web/portal.html | 311 ++- 239 files changed, 34477 insertions(+), 14633 deletions(-) rename dsh-server-docs/{ => 01-规范}/01-规划与架构.md (95%) rename dsh-server-docs/{ => 01-规范}/02-运维手册.md (97%) rename dsh-server-docs/{ => 01-规范}/03-路线图与待办.md (90%) rename dsh-server-docs/{ => 01-规范}/06-工作台UI规范.md (97%) rename dsh-server-docs/{ => 01-规范}/07-实例UI分区登记表.md (92%) create mode 100644 dsh-server-docs/01-规范/08-插件开发与对接规范.md create mode 100644 dsh-server-docs/01-规范/09-IM插件SDK与扩展点契约.md create mode 100644 dsh-server-docs/02-架构设计/README.md create mode 100644 dsh-server-docs/02-架构设计/数据-分库与权威存储架构.md create mode 100644 dsh-server-docs/02-架构设计/覆盖网络-顶层架构全貌.md create mode 100644 dsh-server-docs/03-数据库/DB-00-专区入口.md create mode 100644 dsh-server-docs/03-数据库/DB-01-接入指南.md create mode 100644 dsh-server-docs/03-数据库/DB-02-表结构台账与迭代.md create mode 100644 dsh-server-docs/03-数据库/DB-03-插件数据面规范.md create mode 100644 dsh-server-docs/04-调整方案/142-IM群组对话-需求基线与方案.md create mode 100644 dsh-server-docs/04-调整方案/143-MCN工作台入口失效与语言切换显示修复.md create mode 100644 dsh-server-docs/04-调整方案/144-插件规模化投放与版本一致性.md create mode 100644 dsh-server-docs/04-调整方案/145-覆盖网络-序47-会话并存与设备来源维度.md create mode 100644 dsh-server-docs/04-调整方案/146-实例子域503-中继per-port额度被连接泄漏占满.md create mode 100644 dsh-server-docs/04-调整方案/147-多节点形态下的插件投放与数据面.md create mode 100644 dsh-server-docs/04-调整方案/148-多Manager多区域联邦形态.md create mode 100644 dsh-server-docs/04-调整方案/149-覆盖网络顶层架构全貌.md rename dsh-server-docs/{ops => 06-ops}/nginx/alotbuy.com.conf (100%) rename dsh-server-docs/{ops => 06-ops}/nginx/dsh.alotbuy.com.conf(旧域301) (100%) rename dsh-server-docs/{ops => 06-ops}/scripts/switch-domain-alotbuy.sh (100%) rename dsh-server-docs/{scripts => 07-scripts}/analyze-session.mjs (100%) rename dsh-server-docs/{scripts => 07-scripts}/analyze-turn.mjs (100%) rename dsh-server-docs/{scripts => 07-scripts}/bash-output-guard.py (99%) rename dsh-server-docs/{scripts => 07-scripts}/cf-dns.py (100%) rename dsh-server-docs/{scripts => 07-scripts}/cf-probe.py (100%) rename dsh-server-docs/{scripts => 07-scripts}/docs-archive-index.py (93%) rename dsh-server-docs/{scripts => 07-scripts}/docs-audit.py (94%) rename dsh-server-docs/{scripts => 07-scripts}/docs-consistency.py (94%) rename dsh-server-docs/{scripts => 07-scripts}/docs-dedupe.py (96%) rename dsh-server-docs/{scripts => 07-scripts}/docs-index-stats.py (94%) rename dsh-server-docs/{scripts => 07-scripts}/docs-manifest.py (92%) rename dsh-server-docs/{scripts => 07-scripts}/docs-search.py (87%) rename dsh-server-docs/{scripts => 07-scripts}/docs-shrink-guard.py (92%) rename dsh-server-docs/{scripts => 07-scripts}/docs-sync-check.sh (86%) rename dsh-server-docs/{scripts => 07-scripts}/extract-user-voice.py (93%) create mode 100644 dsh-server-docs/07-scripts/handoff-guard.sh create mode 100644 dsh-server-docs/07-scripts/handoff-status.py create mode 100644 dsh-server-docs/07-scripts/lock-guard-hook.py rename dsh-server-docs/{scripts => 07-scripts}/nginx-slow.py (100%) rename dsh-server-docs/{scripts => 07-scripts}/op-lock.sh (90%) rename dsh-server-docs/{scripts => 07-scripts}/plugin-compat-check.mjs (100%) create mode 100644 dsh-server-docs/07-scripts/preflight-lock.sh rename dsh-server-docs/{scripts => 07-scripts}/sess-analyze.mjs (100%) rename dsh-server-docs/{scripts => 07-scripts}/sess-classify-tools.mjs (100%) rename dsh-server-docs/{scripts => 07-scripts}/sess-list-presets.mjs (100%) rename dsh-server-docs/{scripts => 07-scripts}/sess-probe-schema.mjs (100%) rename dsh-server-docs/{scripts => 07-scripts}/skill-load-guard.py (52%) rename dsh-server-docs/{scripts => 07-scripts}/stop-dialog-guard.py (91%) create mode 100644 dsh-server-docs/08-skills/agent-operating-rules/SKILL.md create mode 100644 dsh-server-docs/08-skills/agent-operating-rules/references/01-协作与上抛判据.md create mode 100644 dsh-server-docs/08-skills/agent-operating-rules/references/02-工作区纪律.md create mode 100644 dsh-server-docs/08-skills/agent-operating-rules/references/03-多棒接力编排.md create mode 100644 dsh-server-docs/08-skills/agent-operating-rules/references/04-去AI味与说话方式.md create mode 100644 dsh-server-docs/08-skills/dsh-architecture-lifecycle/SKILL.md rename dsh-server-docs/{skills => 08-skills}/dsh-auto-handoff-chain/SKILL.md (57%) rename dsh-server-docs/{skills => 08-skills}/dsh-auto-handoff-chain/scripts/chain_report.py (100%) create mode 100644 dsh-server-docs/08-skills/dsh-change-workflow/SKILL.md create mode 100644 dsh-server-docs/08-skills/dsh-change-workflow/references/00-平台速查.md create mode 100644 dsh-server-docs/08-skills/dsh-change-workflow/references/01-档案模板.md create mode 100644 dsh-server-docs/08-skills/dsh-change-workflow/references/02-红线详解-R5-R7-R8.md create mode 100644 dsh-server-docs/08-skills/dsh-change-workflow/references/03-沙箱与技能机制.md create mode 100644 dsh-server-docs/08-skills/dsh-change-workflow/references/04-运维锚点与取证.md create mode 100644 dsh-server-docs/08-skills/dsh-change-workflow/references/05-插件与数据源口径.md create mode 100644 dsh-server-docs/08-skills/dsh-change-workflow/references/06-实例可见面与共享边界.md create mode 100644 dsh-server-docs/08-skills/dsh-change-workflow/references/07-并行调度详解.md create mode 100644 dsh-server-docs/08-skills/dsh-change-workflow/references/08-浏览器验证栈详解.md rename dsh-server-docs/{skills => 08-skills}/dsh-decision-method/SKILL.md (94%) rename dsh-server-docs/{skills => 08-skills}/dsh-decision-method/references/素材库-A-AI推理.md (98%) rename dsh-server-docs/{skills => 08-skills}/dsh-decision-method/references/素材库-U-用户决策.md (99%) rename dsh-server-docs/{skills => 08-skills}/dsh-decision-method/references/素材库-反例-X.md (100%) create mode 100644 dsh-server-docs/08-skills/dsh-desktop-dev-shell/SKILL.md rename dsh-server-docs/{skills => 08-skills}/dsh-env-bootstrap/SKILL.md (90%) rename dsh-server-docs/{skills => 08-skills}/dsh-env-bootstrap/references/常驻规则-快照.md (80%) rename dsh-server-docs/{skills => 08-skills}/dsh-env-bootstrap/scripts/resident-rules.py (96%) rename dsh-server-docs/{skills => 08-skills}/dsh-feature-first/SKILL.md (72%) rename dsh-server-docs/{skills => 08-skills}/dsh-instance-diagnose/SKILL.md (83%) create mode 100644 dsh-server-docs/08-skills/dsh-knowledge-upkeep/SKILL.md create mode 100644 dsh-server-docs/08-skills/dsh-knowledge-upkeep/references/10-技能重组-千行技能拆分.md create mode 100644 dsh-server-docs/08-skills/dsh-knowledge-upkeep/scripts/split_skill.py create mode 100644 dsh-server-docs/08-skills/dsh-opensource-release/SKILL.md create mode 100644 dsh-server-docs/08-skills/dsh-opensource-release/references/01-保留移除与脱敏口径.md create mode 100644 dsh-server-docs/08-skills/dsh-opensource-release/references/02-多远端推送.md create mode 100644 dsh-server-docs/08-skills/dsh-opensource-release/references/03-实测坑.md create mode 100644 dsh-server-docs/08-skills/dsh-opensource-release/references/04-交互式图示-archify.md create mode 100644 dsh-server-docs/08-skills/dsh-opensource-release/references/05-已知待办与漂移.md rename dsh-server-docs/{skills => 08-skills}/dsh-plugin-diagnose/SKILL.md (52%) rename dsh-server-docs/{ops => 09-archive}/域名迁移_ai1net_20260919/RUNBOOK-cutover.md (100%) delete mode 100644 dsh-server-docs/archive/_tmp_r6_s8.md delete mode 100644 dsh-server-docs/archive/_自动接续简报_20260916.md delete mode 100644 dsh-server-docs/archive/dsh-improvement-plan-20260909-full.md delete mode 100644 dsh-server-docs/archive/交接单-已完成/T01-档案16阶段3-4-实例内我的技能.md delete mode 100644 dsh-server-docs/archive/交接单-已完成/T02-文档库收尾-编号消歧与INDEX瘦身.md delete mode 100644 dsh-server-docs/archive/交接单-已完成/T03-plugin_package整合为单插件并投放.md delete mode 100644 dsh-server-docs/archive/交接单-已完成/T04-并发治理落地-commit常态化与服务器侧锁.md delete mode 100644 dsh-server-docs/archive/交接单-已完成/T05-插件兼容性预检.md delete mode 100644 dsh-server-docs/archive/交接单-已完成/T06-模型设置页-多厂家条目可开关.md delete mode 100644 dsh-server-docs/archive/交接单-已完成/T07-内置dsh安装路径探测.md delete mode 100644 dsh-server-docs/archive/工作区草案/VoxEMW全云API化接入dsh修订方案_20260909.md delete mode 100644 dsh-server-docs/archive/工作区草案/VoxEMW接入dsh调研与落地方案_20260909.md delete mode 100644 dsh-server-docs/archive/工作区草案/插件管理面_调整方案草案_20260910.md delete mode 100644 dsh-server-docs/ops/接续入口_覆盖网络线_20260916.md delete mode 100644 dsh-server-docs/ops/接续包_覆盖网络线_20260916.md delete mode 100644 dsh-server-docs/scripts/handoff-guard.sh delete mode 100644 dsh-server-docs/scripts/lock-guard-hook.py delete mode 100644 dsh-server-docs/skills/dsh-change-workflow/SKILL.md delete mode 100644 dsh-server-docs/skills/dsh-knowledge-upkeep/SKILL.md delete mode 100644 dsh-server-docs/skills/dsh-opensource-release/SKILL.md delete mode 100644 dsh-server-docs/交接单/README.md delete mode 100644 dsh-server-docs/交接单/archive/交接单-已完成/T08-执行标记-已释放.md delete mode 100644 dsh-server-docs/交接单/archive/交接单-已完成/T08-集群化落地-兼容单例模式.md delete mode 100644 dsh-server-docs/交接单/archive/交接单-已完成/T09-覆盖网络落地执行.md delete mode 100644 dsh-server-docs/交接单/archive/交接单-已完成/T10-网抽象与地址规划R6.md delete mode 100644 dsh-server-docs/交接单/archive/交接单-已完成/T11-relay落地R2-R4.md delete mode 100644 dsh-server-docs/交接单/archive/交接单-已完成/T12-443兜底.md delete mode 100644 dsh-server-docs/交接单/archive/交接单-已完成/T13-中继失败切流.md delete mode 100644 dsh-server-docs/交接单/archive/交接单-已完成/T14-切流冷却语义.md delete mode 100644 dsh-server-docs/交接单/archive/交接单-已完成/T15-presence在线态.md delete mode 100644 dsh-server-docs/交接单/archive/交接单-已完成/T16-一机一钥与信任根.md delete mode 100644 dsh-server-docs/交接单/archive/交接单-已完成/T17-参数表与观测.md delete mode 100644 dsh-server-docs/交接单/archive/交接单-已完成/T18-检测时延与deadline.md delete mode 100644 dsh-server-docs/交接单/archive/交接单-已完成/T19-最小形态真机批次.md delete mode 100644 dsh-server-docs/交接单/archive/交接单-已完成/T20-观测口径与在册缺陷.md delete mode 100644 dsh-server-docs/交接单/archive/交接单-已完成/T21-在册收尾.md delete mode 100644 dsh-server-docs/交接单/覆盖网络-序24-内容分发块级寻址.md delete mode 100644 dsh-server-docs/交接单/覆盖网络-序25-实例逐步拉起.md delete mode 100644 dsh-server-docs/交接单/覆盖网络-序26-骨干稳定选路与加密.md delete mode 100644 dsh-server-docs/交接单/覆盖网络-序45-低熵块治理-测熵与实现.md create mode 100644 poc/business-plugins-im/mud-rate/cordis.patch.yml create mode 100644 poc/business-plugins-im/mud-rate/package.json create mode 100644 poc/business-plugins-im/office-tasks/cordis.patch.yml create mode 100644 poc/business-plugins-im/office-tasks/package.json create mode 100644 poc/business-plugins-im/trpg-turn/cordis.patch.yml create mode 100644 poc/business-plugins-im/trpg-turn/package.json create mode 100644 poc/carbon-mcp-probe/cordis.patch.yml create mode 100644 poc/carbon-mcp-probe/lib/client.js create mode 100644 poc/carbon-mcp-probe/lib/index.js create mode 100644 poc/carbon-mcp-probe/package.json create mode 100644 poc/carbon-mcp-probe/stub-mcp-server.mjs create mode 100644 poc/im-agent-bridge/cordis.patch.yml create mode 100644 poc/im-agent-bridge/lib/index.js create mode 100644 poc/im-agent-bridge/package.json create mode 100644 poc/im-connection-gateway/README.md create mode 100644 poc/im-connection-gateway/config.json create mode 100644 poc/im-connection-gateway/nginx-location.conf create mode 100644 poc/im-conversation-tabs/cordis.patch.yml create mode 100644 poc/im-conversation-tabs/lib/client.js create mode 100644 poc/im-conversation-tabs/lib/index.js create mode 100644 poc/im-conversation-tabs/package.json create mode 100644 scripts/install-plugin-for-user.cjs create mode 100644 src/db/plugin-data/datastore.ts create mode 100644 src/db/plugin-data/diff.ts create mode 100644 src/db/plugin-data/schema.ts create mode 100644 src/im/agent-bridge.ts create mode 100644 src/im/backends/gateway.ts create mode 100644 src/im/backends/native.ts create mode 100644 src/im/clock.ts create mode 100644 src/im/connection-backend.ts create mode 100644 src/im/db.ts create mode 100644 src/im/gateway-token.ts create mode 100644 src/im/hub.ts create mode 100644 src/im/instance-token.ts create mode 100644 src/im/keepalive.ts create mode 100644 src/im/presence-ingest.ts create mode 100644 src/im/sdk/host.ts create mode 100644 src/im/sdk/index.ts create mode 100644 src/im/sdk/types.ts create mode 100644 src/im/store.ts create mode 100644 src/im/types.ts create mode 100644 src/im/ws.ts create mode 100644 src/net/relay/device-grant.ts create mode 100644 src/net/relay/instance-credential.ts create mode 100644 src/supervisor/plugin-assembly.ts create mode 100644 src/web/routes/im.ts create mode 100644 src/web/routes/overlay-device.ts create mode 100644 src/web/routes/sessions.ts create mode 100644 test/im-agent.test.mjs create mode 100644 test/im-connection-backend.test.mjs create mode 100644 test/im-gateway-access.test.mjs create mode 100644 test/im-presence-ingest.test.mjs create mode 100644 test/im-sdk.test.mjs create mode 100644 test/im-store.test.mjs create mode 100644 test/im-ui.test.mjs create mode 100644 test/instance-credential.test.mjs create mode 100644 test/overlay-device.test.mjs create mode 100644 test/plugin-assembly.test.mjs create mode 100644 test/plugin-data.test.mjs create mode 100644 test/plugin-shared-catalog.test.mjs diff --git a/.gitignore b/.gitignore index 305c193..5373ea6 100644 --- a/.gitignore +++ b/.gitignore @@ -42,5 +42,5 @@ config/*.env.local /tmp/ /_tmp*/ /_中间产物*/ -dsh-server-docs/交接单/ +dsh-server-docs/05-交接单/ diff --git a/config/README.md b/config/README.md index 52b0ab8..9cbdadf 100644 --- a/config/README.md +++ b/config/README.md @@ -57,9 +57,15 @@ cfg.installDir // 代码安装根 | `DSHS_DATA_ROOT` | 数据根(每用户 home/ws、平台库) | `~/.dshs` | | `DSH_PLATFORM_DIR` | 平台私有目录的父目录 | `/platform` | | `DSH_INSTALL_DIR` | 代码安装根(`lib/`、`scripts/`) | 模块相对路径推导 | +| `DSHS_BUNDLED_PLUGIN_DIR` | 共享只读插件包库(节点一份;每节点需自建 0755 root:root) | `/bundled-plugins` | | `DSHS_BASE_DOMAIN` | 对外域名 | 空(子域功能关闭) | | `DSHS_COOKIE_DOMAIN` | 会话 cookie 域 | 空(host-only) | | `DSHS_OVERLAY_BOOTSTRAP_SEEDS` | 覆盖网络引导种子,逗号分隔 | 空(功能关闭) | +| `DSHS_OVERLAY_SIGNER_KEY_FILE` | 在线签名者私钥(签发 per-device grant) | `/etc/dshs/overlay-signer-key.pem` | +| `DSHS_RELAY_KEYS_FILE` | relay HMAC 密钥表(控制面写、relay 热加载) | `/etc/dshs/relay-keys.json` | +| `DESKTOP_GRANT_TTL_HOURS` | 设备 grant 租约(小时) | 24 | +| `DESKTOP_GRANT_MAX_DEVICES` | 每用户设备数上限 | 10 | +| `DSHS_INSTANCE_GRANT_RENEW_MS` | 服务器实例设备凭据的定时续签间隔(毫秒;`0` = 关) | 300000 | | `DSHS_CLUSTER_HOST_ID` | 本机在 `dsh_hosts.id` 里的标识 | 空 | | `DSHS_CLUSTER_AGENT_TOKEN` | Worker 注册凭据 | 空 | | `DSHS_TUNNEL_TARGET` | 反向隧道目标 | 空(隧道关闭) | diff --git a/config/platform.env.example b/config/platform.env.example index 9404f29..12a5201 100644 --- a/config/platform.env.example +++ b/config/platform.env.example @@ -17,6 +17,11 @@ DSH_PLATFORM_DIR=/opt/dsh # --- 代码安装根(lib/ 与 scripts/ 所在)---------------------------------- DSH_INSTALL_DIR=/opt/dshs +# --- 共享只读插件包库(节点一份;默认 /bundled-plugins)---- +# 每个节点各自建目录:`install -d -m 0755 -o root -g root <该路径>`。 +# 实例内以 `--ro-bind-try` 只读挂入;留空 = 关闭该功能。 +DSHS_BUNDLED_PLUGIN_DIR=/var/lib/dshs/bundled-plugins + # --- 对外域名 ------------------------------------------------------------- DSHS_BASE_DOMAIN=example.com DSHS_COOKIE_DOMAIN=.example.com @@ -35,6 +40,30 @@ DSHS_CLUSTER_WORKER_DATA_ROOT=/var/lib/dshs # --- 覆盖网络引导种子(逗号分隔;留空 = 功能关闭)------------------------ DSHS_OVERLAY_BOOTSTRAP_SEEDS= +# --- 覆盖网络 · 设备登录接入(序㊻ S0)------------------------------------ +# 在线签名者私钥(签发 per-device grant 用)。⚠️ 只读、0600;留空 = 取代码内缺省路径。 +DSHS_OVERLAY_SIGNER_KEY_FILE= +# relay HMAC 密钥表(控制面签发设备时写入;relay 侧**热加载**本文件)。 +# ⚠️ 与 relay 单元的 DSHS_RELAY_KEYS_FILE **必须是同一个文件**,否则签了也不生效。 +DSHS_RELAY_KEYS_FILE= +# 设备 grant 租约(小时)与每用户设备上限。 +DESKTOP_GRANT_TTL_HOURS=24 +DESKTOP_GRANT_MAX_DEVICES=10 + +# --- 会话并存的每用户上限(序㊼ S1 / E3)---------------------------------- +# 同一账号**同时**可持有的平台会话数上限;超限淘汰 `created_at` 最小者并落 +# `audit_log: session_evicted`。留空 = 取代码内中性默认 20。 +# ⚠️ 它只约束**平台会话**;设备 grant 配额是上面的 DESKTOP_GRANT_MAX_DEVICES。 +DSHS_MAX_SESSIONS_PER_USER=20 + +# --- 服务器实例的设备凭据(序㊼ S4/S5)------------------------------------ +# 实例(role=main)设备凭据的**定时续签**扫描间隔(毫秒)。控制面按此间隔扫 +# `overlay_devices`(kind='instance' 且 status='active'),租约剩余不足 TTL 一半即重签。 +# 留空 = 取代码内中性默认 300000(5 分钟);0 = 关闭定时续签(只剩"实例启动时检查")。 +# ⚠️ 实例凭据的**文件**落在实例 home(`.overlay-device.json`),平台不留私钥副本。 +# ⚠️ 与浏览器会话的 TTL(DSHS_SESSION_TTL)无关:实例常驻靠**设备租约**续签。 +DSHS_INSTANCE_GRANT_RENEW_MS=300000 + # --- 本机地址(出网护栏脚本用;填自己的公网/内网地址)-------------------- DSH_HOST_PUBLIC_IP= DSH_HOST_LAN_IP= diff --git a/dsh-server-docs/.gitignore b/dsh-server-docs/.gitignore index 100f177..4057818 100644 --- a/dsh-server-docs/.gitignore +++ b/dsh-server-docs/.gitignore @@ -7,9 +7,11 @@ Thumbs.db *.bak # 并行执行/占用锁(纯本机标记,不入库、不 scp —— 见证 2026-09-12) -交接单/.doing-* -交接单/.exec-lock +05-交接单/.doing-* +05-交接单/.exec-lock # Python 字节码缓存(可再生,勿入库/勿同步) __pycache__/ *.pyc +# 本地运行态中间产物(⛔ 不入库;根 .gitignore 的 /tmp/ 只覆盖仓库根) +tmp/ diff --git a/dsh-server-docs/01-规划与架构.md b/dsh-server-docs/01-规范/01-规划与架构.md similarity index 95% rename from dsh-server-docs/01-规划与架构.md rename to dsh-server-docs/01-规范/01-规划与架构.md index b4380ef..50d5799 100644 --- a/dsh-server-docs/01-规划与架构.md +++ b/dsh-server-docs/01-规范/01-规划与架构.md @@ -68,7 +68,7 @@ dsh Web UI 已单实例部署并跑通(外网 http://47.77.182.89/,模型对 /opt/dsh/ ├── .env # 系统级模型 key(DEEPSEEK_API_KEY),容器 --env-file 注入 ├── std/ # ★ 系统标准层(只读,管理员维护) -│ ├── agents/skills/ # → 容器 /root/.agents/skills(rank400 兜底标准技能库) +│ ├── agents/08-skills/ # → 容器 /root/.agents/skills(rank400 兜底标准技能库) │ │ └── mcn-video-script/ # 公司标准技能(SKILL.md + references,只读) │ └── profile-skel/ # 新用户 profile 种子(含公共插件 bundles 声明) ├── users/ # ★ 每用户 = 一个主目录(实例隔离边界) @@ -81,13 +81,13 @@ dsh Web UI 已单实例部署并跑通(外网 http://47.77.182.89/,模型对 │ │ │ ├── cordis.patch.yml # ★ home 级用户覆盖层(优先级高于 profile 层) │ │ │ ├── .credentials.yaml # ★ 个人模型 key(用户在 Models 页填写) │ │ │ ├── settings.yaml # 用户全局设置 -│ │ │ ├── skills/ # ★ 个人技能 rank300(可覆盖系统标准同名技能) +│ │ │ ├── 08-skills/ # ★ 个人技能 rank300(可覆盖系统标准同名技能) │ │ │ ├── sessions/ # 会话数据 │ │ │ └── storages/ # 存储数据 │ │ └── workspaces/ # → 容器 /root/workspaces │ │ ├── maiyamcn/ # 工作区项目(=容器内 workspace 根) -│ │ │ ├── .dsh/skills/ # 项目级技能 rank100(最高优先,随项目分发) -│ │ │ ├── .agents/skills/ # 项目级技能备选 rank200 +│ │ │ ├── .dsh/08-skills/ # 项目级技能 rank100(最高优先,随项目分发) +│ │ │ ├── .agents/08-skills/ # 项目级技能备选 rank200 │ │ │ ├── AGENTS.md # 项目指令 │ │ │ └── ... # 项目文件/素材/脚本 │ │ └── <其他项目>/ @@ -129,10 +129,10 @@ dsh 配置与插件天然分层,以下机制是方案成立的前提(无需 | rank | 目录 | 性质 | |---|---|---| - | 100 | 工作区根 `/.dsh/skills/` | 项目级(随项目分发) | - | 200 | 工作区根 `/.agents/skills/` | 项目级备选 | - | 300 | `~/.dsh/skills/` | 用户个人 | - | 400 | `~/.agents/skills/` | 用户级全局(**系统标准库落点,只读兜底**) | + | 100 | 工作区根 `/.dsh/08-skills/` | 项目级(随项目分发) | + | 200 | 工作区根 `/.agents/08-skills/` | 项目级备选 | + | 300 | `~/.dsh/08-skills/` | 用户个人 | + | 400 | `~/.agents/08-skills/` | 用户级全局(**系统标准库落点,只读兜底**) | 3. **插件安装**:`dsh plugin --profile add <源>`(npm/GitHub/git/tarball/link),写入该 profile 的 bundles 层与 node_modules,**只影响该用户实例**。 4. **模型 provider**:即插件;key 走 credentials service(UI Models 页 → `.credentials.yaml`)或环境变量(`DEEPSEEK_API_KEY`)。 diff --git a/dsh-server-docs/02-运维手册.md b/dsh-server-docs/01-规范/02-运维手册.md similarity index 97% rename from dsh-server-docs/02-运维手册.md rename to dsh-server-docs/01-规范/02-运维手册.md index 4d03197..70a91ce 100644 --- a/dsh-server-docs/02-运维手册.md +++ b/dsh-server-docs/01-规范/02-运维手册.md @@ -262,9 +262,9 @@ node /opt/dshs/ensure-role-profile-patch.cjs [--restart] |---|---| | `/opt/dsh/switch-domain-alotbuy.sh` | 域名切换(含 drain) | | `/opt/dshs/ensure-role-profile-patch.cjs` | 角色化 profile patch(非 admin;`--force` 升级、`--restart`) | -| `/opt/dshs/scripts/ci.sh` | 类型检查 + 构建 + 单测(**不含**需要实例/凭据的 smoke) | +| `/opt/dshs/07-scripts/ci.sh` | 类型检查 + 构建 + 单测(**不含**需要实例/凭据的 smoke) | | `/opt/dshs/poc/workspace-scoped-picker/test/poc.mjs` | 目录选择器插件自检(26 项,须以实例 uid 从可读路径运行) | -| `docs/scripts/docs-sync-check.sh` | 文档库双端对账(0=全绿) | +| `docs/07-scripts/docs-sync-check.sh` | 文档库双端对账(0=全绿) | ### C.4 排障速查(本手册新增条目) @@ -311,7 +311,7 @@ node /opt/dshs/ensure-role-profile-patch.cjs [--restart] | `purge-trash.sh 30` | 每天 04:40 | 回收站超 30 天 | `/var/log/dsh-trash-purge.log` | - **所有清理都先 `mv` 到 `/trash/<日期>-<任务>/`**(30 天可恢复);只有 `purge-trash.sh` 会真删。 -- 手动排查:`node scripts/ws-cleanup.cjs`(dry-run 出画像)、`node scripts/session-gc.cjs`、`node scripts/clean-ws-pollution.cjs`。 +- 手动排查:`node 07-scripts/ws-cleanup.cjs`(dry-run 出画像)、`node 07-scripts/session-gc.cjs`、`node 07-scripts/clean-ws-pollution.cjs`。 - 三档口径:T1 平台产物/明确垃圾直接清;T2 顶层一次性脚本 _超 90 天_ 才清;**T3 其余(含所有目录)永不自动删**。 (域名 / 证书 / nginx / 平台脚本 / 排障速查) @@ -320,7 +320,7 @@ node /opt/dshs/ensure-role-profile-patch.cjs [--restart] **问题**:`/opt/dsh/backup.sh` 存在但**无执行权限**(`-rw-------`)且**未接任何调度** → 最后一次全量备份停留在 2026-09-08。 **现状(已修)**: -- 脚本:`/opt/dsh/backup.sh`(副本入库 `scripts/backup-platform.sh`),`chmod +x`,并加固为 +- 脚本:`/opt/dsh/backup.sh`(副本入库 `07-scripts/backup-platform.sh`),`chmod +x`,并加固为 **① SQLite 一致性快照**(`sqlite3 .backup`,避免运行中只拷到 `-wal` 造成库不一致) **② 归档内容扩展**:`var/lib/dshs`(用户数据)+ `etc/dshs.env` + systemd 单元 + `opt/dsh/artifacts`(插件包)+ `opt/dshs/scripts` + `etc/cron.d/dsh-*` + `nftables-dsh-egress.nft` + `www/server/panel/vhost/nginx`(可重建) - 调度:`/etc/cron.d/dsh-backup` —— **每周日 05:00 全量** + 05:30 清理超 60 天的旧备份;日志 `/var/log/dsh-backup.log` diff --git a/dsh-server-docs/03-路线图与待办.md b/dsh-server-docs/01-规范/03-路线图与待办.md similarity index 90% rename from dsh-server-docs/03-路线图与待办.md rename to dsh-server-docs/01-规范/03-路线图与待办.md index 7f772cd..9a7ab33 100644 --- a/dsh-server-docs/03-路线图与待办.md +++ b/dsh-server-docs/01-规范/03-路线图与待办.md @@ -1,127 +1,127 @@ -# 03 路线图与待办 - -> 状态(刷新 · 2026-09-12 15:05):本文件长期晚于实际进度(此前停在 09-11 23:05)→ 本次补登记**档案 57–68**、清理已完成行、并把待办口径与 `交接单/README.md §一` 对齐(**已规划待执行的以交接单为准**,本文件只保留指针)。原文:**2026-09-11 21:20 合并版**(补回 14~22 与 B1/B2/B3/B4、档案 28、备份加固;保留「MCN 改造暂缓」「白名单源码安装」等新决策)。 - -## 一、待验证项(Open Questions)结论 - -| # | 原问题 | 结论(实测日期) | -|---|---|---| -| 1 | `.env` 注入 `DEEPSEEK_API_KEY` vs 用户个人 key 优先级 | ✅ 已定:**统一 admin key 模型**(commit df5cc84,2026-09-09)——resolveApiKey 忽略 userId、取 role=admin 启用 key,spawn env 注入;keys 路由 requireAdmin;无"个人 key 覆盖"场景 | -| 2 | web profile 空 `.dsh` 首启自动初始化(add-user 流程) | ✅ 已解答:dshs 模式无此问题——provision 建 OS 账号 + profile 首次 spawn 即自动初始化;testuser 注册→审核→桌面→DSH 全链路已实测通过 | -| 3 | 前端 loopback 检测能否 `--patch` 放行(用户自配模型) | ✅ 已实测(2026-09-09):proxy.ts 将 Host 伪装为 `127.0.0.1:` → dsh 的 /api trust fence 与 loopback 特权校验**全放行**,Settings 面板在门户代理后可用 → **暴露面成立:登录用户可进 Settings 自配 key/模型**,作为已知安全边界记录(第二道防线),暂无封锁需求 | -| 4 | dshs 对 web Settings 限制 | ✅ 同上实测:Settings 面板可用(portal-entry v0.4.1「平台管理」分区即在此验证) | -| 5 | 容器内 `dsh plugin add` 走 npmmirror | ⏹ 过时归档:架构切换后插件走**宿主全局 dsh + profile 层 pnpm 转发**(`dsh plugin --profile web`);装法 = 本地 `npm pack` 出 tgz → `dsh plugin add `(实测 v0.1.0→v0.4.1 全链路) | - -## 二、路线图 - -### 已完成(2026-09-08 ~ 09-09) - -- [x] **P0 迁移**:data → users/main(2026-09-08 17:30) -- [x] **P2 调研**:dshs 选型前置调研(2026-09-08) -- [x] **域名接入 + 通配 HTTPS**:dsh.alotbuy.com 通配证书(2026-09-08 18:00-18:40,certbot dns-cloudflare) -- [x] **架构切换**:dshs 多租户 + account 硬隔离(2026-09-08 18:30) -- [x] **登录直达 v1+v2**:POST /api/dsh/enter 按角色分流 + token 自动携带(commit 6f63108 / e39628a / 29c8907) -- [x] **统一 KEY 管理员管控**(df5cc84) -- [x] **用户管理**:注册/审核/删除用户(4943c8c)+ 权限收紧(chmod 系列) -- [x] **实例生命周期**:last-wins 单活跃会话 + idle-reap 常驻上限(928dde1 / 6336c53) -- [x] **门户功能插件化 PoC**:`@dsh-local/portal-entry` v0.4.1 浏览器验收通过(2026-09-09)——设置面板「平台管理」分区(门户地址 + 打开管理台 + 退出登录),档案 05 -- [x] **技能管理面**:共享 + 个人技能 API 与页面、zip-only 两阶段替换(2026-09-09,档案 11) -- [x] **文档库路径统一(2026-09-10)**:工作区 `D:\AgentSkill\aliyun-work-space` → `D:\AI技能\aliyun-work-space`;文档目录 `dsh-docker\docs\` → **`dsh-server-docs\`(内容提升到根,与服务器 `/opt/dsh/docs` 同构)**;6 处旧路径改写 + 双端同步,25/25 md5 一致;对账脚本 `scripts/docs-sync-check.sh` 落地 -- [x] **工作区迁移 D: → E:(2026-09-13)**:工作区 `D:\AI技能\aliyun-dsh-server` → **`E:\ProgramData\AI技能\aliyun-dsh-server`**(`D:\AI技能` 已无实体目录)。同步改的**活路径**:`~/.workbuddy/settings.json`(两个 hook 命令)、`dsh-server-docs/scripts/lock-guard-hook.py`(`DSH_DOCS_ROOT` 默认值)、`scripts/docs-sync-check.sh`(`DOCS_LOCAL_DIR` 默认值,本库 + 项目根共两份)、`dsh-server-docs/scripts/extract-user-voice.py`(目录名示例)、`dsh-server-docs/README.md` 与 `INDEX.md` 的本机路径、档案 73 的 hook 安装片段。**历史档案(01-规划与架构 / 04-11 / 04-12 / archive/ 下各篇)中的 `D:\AI技能\...` 保留原值**(写于迁移前,属历史,按「档案只增不改」不回改)。**代码仓 `D:\github\dsh_shenxian` 未变动**(不在 `AI技能` 之下)。 - ⚠️ 排查期间曾临时建 `D:/AI技能 → E:/ProgramData/AI技能` 目录联接,用于救活**会话启动时快照**里已失效的旧 hook 路径;**已按用户要求移除**。⇒ **改 hook 路径后,已在跑的会话需「完全重启」(关窗 ≠ 退出)或新开会话才生效**(2026-09-13 实测:本会话 06:47 启动、06:55 改配置,07:05 拆联接后写操作仍报旧路径)。 -- [x] **档案 78 · 崩溃熔断冷却期与告警(2026-09-13)**:修 **档案 77 §八 遗留 1** —— 原实现熔断即 `resetCrashState()` 清预算 ⇒ 只要有重试路径(用户 F5 / 注入脚本自愈 / 脚本直铺)崩溃循环就能**无限重来**,且只留一行 stderr。改为:熔断态**跨轮存活** + **指数冷却**(10 min → 封顶 6 h),冷却期内 `launch()` 拒绝隐式启动(HTTP 503 `instance_circuit_open`),冷却过后只给**一次**干净预算;熔断**双通道告警**(stderr + `/var/log/dsh-crash-breaker.log`)并把 `breaker` 暴露到 `/api/dsh/status`。改 6 个文件(含 3 条新单测);`npm run build` rc=0、`npm test` **45 pass / 0 fail**。**已部署并验证**(2026-09-13 08:02 重启载入:PID 410287→413817、门户 200、孤儿 scope 0、`/api/dsh/status` 返回新字段 `breaker`)→ 档案 78 §九 -- [x] **登录直达冷启动竞态 404 修复(2026-09-10)**:enter 复用/AlreadyRunning 分支等 launch token 到位再返回 URL,杜绝启动窗口 404(commit `fa718d2`,档案 13) -- [x] **敏感信息暴露面审计与加固(2026-09-10,档案 14)**:判定跨租户/提权不成立;封云元数据端点 `100.100.100.200` + 外联观测(`/etc/nftables-dsh-egress.nft` + `dsh-egress.service`) -- [x] **会话失效 401 未跳登录页修复(2026-09-10,档案 15)**:desktop/admin/skills 三页注入 401 守卫;核心插件开关收归 admin -- [x] **页面导航定稿 + 插件三层归属(2026-09-10,档案 16)**:门户 SPA 化、技能/插件独立页面、功能插件候选池投放 + 实例设置启停;阶段 0-2 已实施(`bad6ed7`/`6f86a5f`/`b7fd85d`) -- [x] **实例沙箱隔离(档案 16)**:bwrap + systemd-run scope(512M/CPU 150%/TasksMax 128)+ 私有 /tmp + `--unshare-pid`(commit `449f28b`) -- [x] **工作区选择器暴露面核查(2026-09-10,档案 17)**:判定非越权;记录 P1 写边界=会话 cwd / P2 picker 无根白名单 / P3 nft 文件可读 -- [x] **文档库去重 + 单一来源(2026-09-10,档案 19 §D4)**:删根级 05 副本、`04/README.md` 改指针 -- [x] **崩溃自愈加固(2026-09-11,档案 20)**:指数退避 + 窗口熔断(5 次/10 min)+ 观测(`restarts`/`lastCrashedAt`)+ handoff 停写;**live 自愈实测通过**(kill → 1 s 后 `[crash-restart]` 拉起);commit `7cba3e3` -- [x] **CI 脚本 + 清理失效 smoke(2026-09-11,档案 19 §C3)**:新增 `scripts/ci.sh`;删 `smoke-plugins.mjs`(目标路由已移除)与 `smoke-watchdog.mjs`(watchdog 从不启动) -- [x] **访问域名迁移(2026-09-11,档案 22)**:门户 `alotbuy.com` + 用户 `<用户名>.alotbuy.com`;通配证书 DNS-01(传播 60 s)、CF 真实 IP 还原、旧域 301 含用户名映射 -- [x] **nginx 性能修复(2026-09-11,档案 21)**:`proxy_buffering off`(修「一次性输出」)、`gzip_proxied any`(930 KB bundle 压缩)、`/assets/` 30d 缓存 -- [x] **官方白名单插件来源(2026-09-11,档案 29)**:门户「插件管理」接入 awesome-dsh-plugin 官方目录(`plugins.json`,3408 条 / 23 分类),admin 搜索/筛选/多选 → 导入功能插件候选池;导入口径 = **仅预构建**(npm registry tarball / release 资产),可导入 **1792 条(52.6%)**、热门插件全覆盖,平台侧不执行任何第三方构建脚本 -- [x] **编排器孤儿实例清理(2026-09-11,档案 30)**:spawn 前清 `dsh--*` 孤儿 + 启动时清一次;两场景 live 验证通过 - -- [x] **档案 14~22 全部落地(2026-09-10~11)**:敏感信息暴露面审计(封云元数据端点 + 外联观测)、会话失效 401 跳登录、页面导航定稿 + 功能插件三层归属、实例沙箱隔离(bwrap+scope)、工作区可见面核查、目录选择器收敛 v3、崩溃自愈(退避+熔断+观测)、CI 脚本、**访问域名迁移到 alotbuy.com**(旧域 301)、nginx 性能修复(buffering/gzip/assets) -- [x] **写保护 B1(档案 17 §P1)**:实例内 `--ro-bind-try` 覆盖 `cordis.patch.yml`/`package.json`/`pnpm-lock.yaml`(写测试 ✅ + 真实 dsh 启动验证 ✅) -- [x] **编排器自愈 B2(档案 18 收尾)**:`ensurePickerProfile(userId)` 在 launch 前 + spawn 后各异步补齐(幂等) -- [x] **上传安全扫描分级 B4(档案 19 §C6)**:P0 阻断 / P1 告警 + 扩展名 19→31 + shebang/二进制/8MB(冒烟通过) -- [x] **死代码子集 B3(档案 19 §C5)**:删 `src/runtime.ts`;`folder_plugins`/`workspaces` 加废弃注释(**完整版清理已关闭**:跨 10 文件且含 k8s/PG 未验证路径,删除风险不对称) -- [x] **用户数据清理三件套(档案 28)**:`ws-cleanup`(T1 平台产物 / T2 顶层一次性脚本 >90 天 / T3 永不删)+ `session-gc`(**365 天**保留)+ `purge-trash`(回收站 30 天)+ 每小时用量快照 + 门户「存储用量」面板(`GET /api/admin/storage`) -- [x] **备份可用性修复(档案 02 附录 C.8)**:发现 `backup.sh` **无执行权限且未调度**(最后备份停在 09-08)→ 修好 + SQLite 一致性快照 + 纳入 artifacts/scripts/cron/nginx vhost + `/etc/cron.d/dsh-backup` 每周日 05:00(恢复演练已验证) - -### 已完成(2026-09-11 ~ 09-12,本次补登记 57–68) - -- [x] **档案 57 · 设置面板「用户管理」入口 + 全员安装**(2026-09-12 00:04) -- [x] **档案 58 · 实例内存优化与配额下调**:512M → **384 MiB** + 编译缓存(2026-09-12 00:23) -- [x] **档案 59 · 重连反馈:实例启动中的加载动画**(2026-09-12 01:30)|⚠️ 遗留:`wake.html` 本机 4708B vs 服务器 4591B **待核对同步** -- [x] **档案 60 · 设置面板分区改名「功能插件」→「功能管理」**(2026-09-12 08:30) -- [x] **档案 61 · 插件管理页官方插件列表加高 + 底部留白 200px**(2026-09-12 09:14) -- [x] **档案 62 · 插件目录缓存状态可见化 + 「重新拉取目录」按钮**(2026-09-12 09:21,`be42155`) -- [x] **档案 73 · 让锁真正拦得住人**(2026-09-12 17:0x):复盘发现三把锁**当天被跳过两次**(含本会话)→ 根因①无强制入口(`settings.json` hooks 段为 null)②guard 输出「无锁」被读成「可以开工」。治:① 措辞修正(guard 三处 + 交接单语义)② `scripts/lock-guard-hook.py`(PreToolUse 无锁拒写 + SessionStart 提示,**作用域仅本库/本代码库**;四态单点验证通过);⚠️ 钩子待用户在 `/hooks` 审核启用 -- [x] **档案 72 · 实例回收/关闭后回到页面自动唤醒并重建连接**(2026-09-12 16:55,`b23e386`):注入脚本原来只认 401,实例回收时代理返回的 `404 not_running` 无人处理 → 用户必须手动刷新。补:① `hit()` 识别 not_running ② `visibilitychange`/`focus`/`pageshow` 时主动探活 `/api/dsh/status` ③ `recover()` 跳 `wake.html`(走 `enter` 拿**新 token**,避免 reload 带旧 token 再撞 401) -- [x] **档案 71 · 插件兼容性预检:导入 / 上传即判定**(2026-09-12 16:35,`8d19e89`):semver 依赖范围 + 运行时导出符号比对,不兼容在导入时即拒收(承接档案 70 的教训)→ 方案 `04-71` -- [x] **档案 70 收尾 · AnySearch 彻底放弃 + 候选池存量体检**(2026-09-12 18:56):用户选 B → 下架 anysearch(`audit` 175);**顺手发现预检只覆盖"新导入"、覆盖不到存量** → 用 `lib/web/plugin-compat.js` 体检池内两条:`dsh-univer-office` = `ok`(保留)、`@liustack/modlens` = `unknown` 且有 `plugin_incident`(audit 172/173)→ 一并下架(`audit` 176,tgz 备份 `/opt/dsh/backups/plugin-pool-20260912/`)。全程**未重启服务、未中断用户**。另落 **R9 红线**(禁止人工删锁/接管)→ 档案 70 §九 · 档案 73 §十一 -- [x] **档案 70 · anysearch 插件与 dsh `0.1.2-rc.1` 不兼容 → admin 实例崩溃循环**(2026-09-12 15:32 **已止损**):根因 = 插件在 import 阶段引用 `@deepseek-ai/dsh-llm` 未导出的 `assertNever`,而 dsh 的 plugin tree 加载**全或无** → 启动中止、崩溃自愈反复重启;摘掉该 bundle 即恢复。**✅ 该待办已于 2026-09-12 18:56 关闭**:用户选 B · **彻底放弃**(候选池条目下架 + 存量体检)→ 见本节「档案 70 收尾」行与 §二 待办表「已处置」行 -- [x] **档案 69 · 并发治理落地(T04)**(2026-09-12 15:10,`exec-session-C`):文档库 commit 常态化 + 服务器侧操作锁 `/opt/dsh/state/.op-lock/`(实测往返通过)+ 交接单目录权限统一;三把锁进预检脚本 → 单子已归档 -- [x] **编号 63 为空号**:该号只出现在当日工作日志的「事故 63」里(清理残留 tgz 致依赖断裂),**无对应档案**,勿补占 -- [x] **档案 64 · 接入 AnySearch 搜索 provider(B 方案)**(2026-09-12):平台现用 DeepSeek 官方搜索;AnySearch 前置验证 4 项全绿、**admin 侧已实施**;⚠️ 待办改以**档案 65 §7.3** 为准 —— **2026-09-12 用户拍板:三方插件一律走「admin 导入候选池 → 用户自助启用」,不再向用户 profile 直铺**(§8.3 第 2 条直铺命令已作废) -- [x] **档案 65 · 功能插件启停 ↔ web provider 配置联动**(2026-09-12):根治「候选池一禁用就 `CONFIGURED_MISSING`」→ 平台按当前 bundles **重算托管段**,启/禁两态皆正确;**代码完成 + 两态实测,⏸ 待部署(需 R8 窗口)** -- [x] **档案 66 · 业务插件 P0 误报 → admin 显式信任**(2026-09-12 11:15 已部署):安全检测改 **fail-closed + 逐条回显 + admin 声明信任后放行并留痕** -- [x] **档案 67 · 「功能管理」section 按 UI 规范重做(v0.2.4)**(2026-09-12):用途说明做主视觉 + 字号/组件对齐 `06` -- [x] **档案 68 · 候选池启停的 root 属主污染根治**(2026-09-12):候选池 `install/uninstall` 改 **`setpriv` 降权** + 改插件前自动属主自愈 + 清存量 **561** 项;附带修 `npm pack` 把上一版 tgz 打进产物(加 `.npmignore`) - -### 进行中 / 待办 - -| 优先级 | 事项 | 说明 | -|---|---|---| -| ✅ **已完成** | ~~**档案 65 部署**(功能插件启停 ↔ web provider 托管段联动)~~ | **本就是生效状态**:服务器 `lib/` 00:13 构建 → **00:15:23 重启即已载入**(08:02 再载入);**§7.3 第 3 条已完成**(`ensure-anysearch-admin.cjs` 加硬拦退役,默认 `exit 2`,备份 `.bak-retire-20260913`)| -| ✅ **已完成(留一项 L1)** | ~~**档案 78 部署**(崩溃熔断冷却 + 告警)~~ | **2026-09-13 08:02 已部署并验证**(见档案 78 §九)。**遗留 L1**:故意把实例反复搞崩以验熔断(需 6 次真崩 + 该用户冷却 10 min)→ **留维护窗口**做 | -| ✅ **已完成并归档**(2026-09-13 18:0x) | **T03 · 7 插件整合投放** | **guest 启用已成功**(任务 `c20739a6dbe8975c`:`success`/`restarted=true`;配额自动 672→**800 MiB**); **admin 侧 8 项实证通过**(包结构 v0.3.9 / 上传扫描 P0=0 / 池内 / `[mcn-suite] loaded` 且 duplicate=0 / 旧 7 包已下架 / 技能落 `/skills/mcn-short-video` / 幂等跳过 / BRIEF 384M);**剩 6 项需浏览器或造场景**(D 入口逐个点开 / J agent 按名加载 / L 凭据扫描 / N 死引用扫描 / P·Q 撞名三场景)→ 见 `交接单/T03` §九 | -| ✅ **已消解(无需窗口)** | ~~**实例内存预算上调**(`--max-old-space-size` 160→256 + cgroup `MemoryMax` 384→512 MiB)~~ | **2026-09-13 13:3x 实测:R1-④ 已把它改成「按插件集合动态计算」,比原方案更优** —— `instanceMemMb()` = 160 + Σ插件预估(clamp 384–1024),`heapMbFor()` = 配额 − 96(cap 256)。运行中实证:guest `NODE_OPTIONS=--max-old-space-size=256` / scope `MemoryMax=544 MiB`(= 160 + univer 384);**admin 也是 256**(不再是 160)。平台 env 里那个写死的 `160` 已被代码侧 `withHeap()` 覆盖、**不是生效值**。⇒ **T03 的 guest 启用不再被容量阻塞**(装上 mcn-suite 后配额会自动变 672 MiB)。原「须重启服务(R8)」的前提已不成立 | -| 🟡 **待排期** | **档案 81 重构总纲的 R2/R4/R5**(**R0/R1/R3 已完成**) | **R2** 管理面就地化 → 🚧 **代码已完成(2026-09-13 17:5x,档案 82)**:插件 `@dsh-local/business-plugins` **0.2.8→0.2.9**,新增原生「平台管理」只读分区(仅 admin;数据调平台只读 API;写操作回跳管理台);`node --check` 通过、tgz 已出;**待投放 + 启用 + 浏览器验收(卡在需 admin 登录态)**;`/admin` 路由族与删 3 桩页**仍未做**。⚠️ **形态已改定(2026-09-13 用户):用「原生弹窗」,不用 iframe**(原话「iframe 不如原生弹窗体验好」)⇒ 改为**在自研插件里原生渲染只读子集**(调平台 `/api/*`,非 iframe 嵌 `portal.html`);仍按 R5 只读优先,登录后 30 天内无需重做门户全量 UI ⇒ 见档案 81 §9.2 修正|**R3** 内部标识统一为 **`dshs`** → ✅ **已完成(2026-09-13 15:4x–16:0x)**:代码 89 文件 / 382 处 + 本机其余 135 文件 / 863 处 + **服务器原子切换**(单元真名 `dshs.service`、`/etc/dshs.env`、`/opt/dshs`、`/var/lib/dshs`、`dshs.db`;含 **WAL 归位**与 DB **VACUUM**);上游具名引用全清(含移除 `git remote upstream`);两仓 git 历史已删重建为单提交|**R4** 文档/代码同仓 → ⏸ **待用户选 a(并入代码仓)/ b(单向导出)**(用户 09-13 反馈「没看懂 R4 要做什么」⇒ 需先讲清动机再选)|**R5** **多语言 i18n** → ✅ **方案已定(2026-09-13 用户):「匹配 dsh 官方方案」= 走官方 locale 体系**(`ctx.locale.addLanguage()` + `register(ns)`,平台页/注入层/自研插件全走官方扩展点、**零官方改动**);⚠️ 覆盖率边界见档案 81 §10.7 ⑥ —— 官方**仍有 20+ 个 UI 包未迁 locale**,那些包切语言不变(属官方自己的待办,不为我们可改范围)| -| ✅ **已修复并验收**(2026-09-13 17:2x) | ~~**档案 76 · `/univer-api/state` 生产持续 400**~~ | **本会话独立复核**:近 2 小时 400 = **0**、近 30 分钟 6 次请求**无 400**;实例 env `UNIVER_DSH_GATEWAY_SOCKET=auto` 已生效(档案 76 追加节=真因+修复+验收)。原述 2026-09-13 07:36 实测:guest 用户在 1 秒内被连续 ~10 次 400(平台侧只记状态码)→ **根因待取响应体**;⚠️ 同时发现 **guest `ws` 下 0 个 `.univer` 文件**(目录在、文件不在)→ 需沿「MCN 生成 `.univer` → Univer 打开」链路查 → 档案 76 追加节 | -| ✅ **已处置** | ~~AnySearch 能力是否保留~~(档案 70 §八) | **用户选 B · 彻底放弃**(2026-09-12 18:56):候选池条目已下架(HTTP 200 / `audit` 175)、凭据无残留、无用户启用过;**顺带体检池内存量** → `dsh-univer-office` = `ok`(保留)、`@liustack/modlens` = `unknown` + 有 incident → 一并下架(`audit` 176,tgz 已备份)。**未重启服务、未中断用户** → 档案 70 §九 | -| ✅ 已完成 | **T05 · 插件兼容性预检**(导入/上传即判定兼容性) | 2026-09-12 16:35 落地(`8d19e89`):判据模块 + 上传/导入双入口接入 + build + 重启;验收全绿 → 方案 `04-71`,单子已归档 | -| **档案内挂起** | 档案 57 四项(picker store 隐患/dep spec 指向 `ws` 会被清理/`portal_ping` 失效工具/插件源码两处存放)|~~档案 59 `wake.html` 本机 4708B vs 服务器 4591B 待核对同步~~ ✅ **2026-09-13 09:4x 实测双端一致**(均 4962 字节、md5 `00b81728…`;变大是因档案 78 给 wake.html 加了熔断文案)|档案 66 三项(官方目录**批量导入**未接信任入口/信任状态未持久化→建议并入 migration v6/`ensure-anysearch-admin.cjs` 覆写段待退役,否则与档案 65 托管段互相覆盖)|档案 68 平台 setpriv 路径待用户会话自然验证|档案 42 三项(③ **已闭环并实测通过**:子代理派发 `bash -c 'echo subagent-ok'` 正常返回 ⇒ shell 可用、平台零改动;见档案 42 末「追加结论 + 实测结果」)/档案 38b 三项/档案 15(`admin.html` 缺页面层 role 拦截)/档案 32(`dsh-market` 可绕过第三层管控,**未成档**) | 详见各档案 §待办段 | -| ✅ **已修复并部署** | ~~**插件启停的两处平台缺陷(档案 79)**~~ | **2026-09-13 13:4x 逐条核实产物(档案 79 §七)**:**D1** `await uninstall(…)` ✅(产物 `:587`/`:617`)|**D2** `remove` 已改 `-w`(`:575`,全仓无效 flag 仅剩 `add` 一处=合法)✅|**加固 a** 路由外层 `.catch()` ✅(`:613`+`:718`)。**D3 已消解**:R1-④ 把堆限与 `MemoryMax` 改为按插件集合动态计算(实测 guest `heap 256` / `MemoryMax 544 MiB`)⇒ 不再需要窗口。⚠️ **仍缺端到端验收**(重现「注定失败的启用」→ 期望 任务 `failed` + profile 回滚 + 服务不退出)→ 建议与 T03 的 guest 启用合并验。**加固 b**(半应用态主动自愈)未做,可选 | -| 触发式 | dsh 升级回归(档案 26 六类耦合点)|会话 GC | 升级 / 容量触发 | -| P3 | 门户「浏览文件」补下载入口 | 档案 56 §七:门户 `#/files` 目前只能列表/上传,可复用 `/api/fs/download` 加"下载"按钮 | -| ✅ | ~~老会话权限档位对齐(检测 + 提示)~~(档案 56) | **已完成(2026-09-11)**:`GET /api/dsh/session-permission` + 实例页顶部提示条(说明原因 + 切档位/新建会话两条路);实测 `stale=true` 触发正常。**不自动改档位**(安全语义变更需用户知情) | 档案 55:档位是**会话级播种** —— 09-10 及更早的会话仍 `workspace-write` → 沙箱 fail-closed,**bash 被拒 11 次**(实测:同一会话 22:16 手动切 `danger-full-access` 后立刻恢复)。建议 `/api/dsh/enter` 检测会话 preset 与平台默认不一致 → **页面提示 + 一键切换**(不自动改) | -| ✅ | ~~实例能力清单(自检)~~(档案 56) | **已完成(2026-09-11)**:`scripts/gen-capabilities.cjs`(cron 每日)→ `/opt/dsh/state/capabilities.json` + 共享技能 `platform-capabilities`(agent 可读);实例页「🧭 能力」面板同源展示 | 档案 55:agent 在 16:48 / 18:21 / 22:14 **三轮重复现场探测**,每次撞同样 4 类墙(`/etc` 白名单 / `127.0.0.1` 被 SSRF 拒 / skill 不存在 / 写边界),用户随之三次追问"能力有变化吗" → 平台注入能力说明(可读可写范围、网络边界、工具与技能清单、档位含义) | -| ✅已定 | 平台技能投放现状对齐 | **结论(用户原话):「业务技能要打包进插件里一起安装和使用,不要分开管理」** → 投放方式 = **随功能插件包投放**(见 `交接单/T03` §4.1),**不走**门户技能管理单独投放;平台自描述技能 `platform-capabilities` 照旧共享投放。「是否投放待定」的旧表述作废(档案 55 当时的背景是 `bundled-skills` 为空 → agent 撞 `unknown skill`) | -| ❌复核 | ~~管理类插件化~~(档案 05 PoC-2 ①) | **复核结论:不建议做** —— 门户 `portal.html` 已有完整管理面(服务/密钥/用户/技能/插件/运行环境),admin 在会话内经 `portal-entry` 卡片**整页跳门户**;把 admin 能力搬进实例子域反而扩大权限执行面(与档案 39 收窄方向相反) | -| ✅复核 | ~~插件↔门户鉴权令牌~~(档案 05 PoC-2 ②) | **复核结论:已实现(非令牌方式)** —— 共享会话 Cookie(`Domain=.alotbuy.com`)+ `server.ts` 的 CORS 白名单(`isAllowedOrigin` 允许 baseDomain 及其子域 + `Allow-Credentials`),功能插件 v0.2.1 已生产跑通;再做独立令牌属重复建设 | -| ❌复核 | ~~`portal_ping` 端到端~~(档案 05 PoC-2 ③) | **复核结论:原验收项作废** —— 该工具 `fetch(127.0.0.1:3080)`,而档案 39 已封 `127.0.0.0/8`(实测 BLOCKED)→ 必然失败;若要验证"host 插件给 agent 注册工具",需**另立不依赖 loopback 的探针** | -| 暂缓 | **MCN 工作台插件平台化改造**(档案 27)—— **【暂缓】需先测试插件兼容性**(2026-09-11 决策) | 3 处 P0:① `homedir()/.dsh` → `process.env.DSH_HOME ?? …`(config.js 3 处;`mcp.js` 已是正确写法可对照)② 技能路径硬编码 13 处 → 只报技能名 ③ agent preset(`.agent-presets/mcn/`)随包投放;P1:MCP 预装(现在 `npx myai-mcp`)、DB 重建兜底、技能依赖声明 | -| ✅关闭 | ~~白名单源码安装支持~~(档案 29 §七) | **评估结论:不做(2026-09-11)**:源码安装需让第三方构建脚本以 root 在平台机执行(供应链 + 可复现性 + 性能三重风险),与"平台不执行第三方构建脚本"红线冲突。**替代路径**:admin 本地构建后走「上传 tgz」通道(已有 P0/P1 扫描);如需开启须先满足沙箱构建 + `--ignore-scripts` + 仅 admin + 审计等全部前提 | -| 降级 | B5:给 portal-entry / business-plugins 加加载标记 | **降级为"顺手做"(2026-09-11 决策)**:不解决当前问题、源码不在仓库、且仅提升"升级时排查速度";下次改这两个插件时顺手加(零边际成本) | -| ✅ | ~~熔断 live 实测~~(档案 20 附) | **已完成(2026-09-11)**:由真实故障用户(档案 52)的日志完成实测 —— 退避 1000→2000→4000→8000ms、`restartsInWindow` 1→4、第 **5** 次触发 `crash-loop-circuit-open`(窗口 600000ms / 5 of 5),当日 2 次开断;无需再人为 kill 复现 | -| ✅ **已完成**(2026-09-15) | ~~档案 16 阶段 3/4(实例内「我的技能」)~~ → **= 交接单 `T01`,已归档** | **档案 100**|插件 `business-plugins` **0.3.20 → 0.3.21** 已投放两实例。① **形态修正(09-15)**:由「新增 section」改为**并入既有「功能管理」section 内分组**(依据用户口径「不要分开管理」+ 档案 60 分区命名;**仅入口层合并、机制层分离**)→ 见 `T01 §四 决策 6` 与 **`T01 §九`**;② 实现:技能行(名称/来源/状态/事实/动作)+ zip 拖拽上传 + 同名两阶段替换 + 页内确认弹窗 + 锁定行(共享技能)无动作按钮 + zh/en 46 条词条;③ 验收:`npm run verify` 全绿(含新 `scripts/verify-my-skills.mjs` 34 条断言)、06 §7 三段式全绿、端到端 **19/19**(409 / 400 守卫通过);④ 阶段 4 收口 = 未新增 section + 未改角色补丁 ⇒ 可见性不变。⚠️ **后续(档案 101)**:该分区已改名「**能力管理**」并改为**页内 tab 分页**(功能插件 / 我的技能)。原述:门户 skills.html 仅 admin → 普通用户需实例内入口 | -| ✅ **已消解** | ~~摘除 guest 仍在启用的已下架插件 `@liustack/modlens`~~ | **09-13 07:0x 实测:已无需处理** —— guest `profile/web` 的 `bundles` **与 `node_modules` 均已无 `@liustack/modlens`**(原「已下架却仍启用」的不一致态已不复存在)。历史:该包 09-12 从候选池下架(档案 70 收尾)时曾留在 bundles | -| ❌已复核 | ~~Cookie 域收窄~~ | **不做(2026-09-12 用户决定:把这条待办删掉)** —— 共享 Cookie(`Domain=.alotbuy.com`)是**有意设计**:支撑「功能插件 ↔ 门户」**免令牌鉴权**(配合 `server.ts` 的 CORS 白名单),功能插件 v0.2.1 已生产跑通 → **收窄会破坏该链路,收益为零**;结论见档案 05 PoC-2 ② | - -| 🟡 **待排期** | **T08 集群化的收尾项**(主体与生产切换已完成) | ① `join-worker.sh` 一键装机(现在 join = 装 unit + 起 agent + 注册,三步手工)~~② 隧道服务化~~ ✅ 已收口(本地 20s 定时器自愈,实测 30s 内恢复)③ 集中日志 / metrics ④ `smoke-domain` 定性 ⑤ drop-in 里的 PG 口令建议进一步收权限(现 root 可读)|详见 `交接单/T08 §16.5` | -| 🟡 **待排期** | **覆盖网络 / 客户端化这条线**(**本轮零代码、零服务器改动**) | 已落 **档案 103–112**(10 份规划转正式档案;入口 = 工作区根 `接续入口_覆盖网络线_20260916.md`)。📌 **第一件实事 = 把会合 / 中继从 Manager 里拆成可独立部署的组件**(现为绑在 Manager 上的**单中心 SSH 反向隧道**;所有后续多区域 / 多中心 / 骨干层的前置)。**已定口径**:只做技术实现(跨境合规由使用者自负)· **单机自用 ≠ 不需互联** · 按**异构**设计(中继 45% 设计 / 55% 留余量 + **必须补 443/TCP 兜底**)· 权威状态(归属 / 租约 / 资格)**必须单点控制面** · 游戏重点 = **MMORPG(2D/2.5D) / MUD / 传奇类**。⚠️ **唯一待拍板 = 骨干节点服务范围**(A 只服务自己名下设备 / B 服务全网;**倾向 A→B 渐进**,见档案 105 §七)。之后按 **档案 112「只做三件事」** 开工 | - -### 历史决策记录(保留,均已落地) - -1. **域名决策**(2026-09-08):无域名 → 先解决域名再回来做多用户改造 → 已落地:dsh.alotbuy.com + CF 通配 + 源站 443。 -2. **选型决策**(2026-09-08):dshs 模式 A(account 硬隔离)优先,理由:现成密码注册/审核/网页桌面 + 每用户独立实例;替代(自研 Node 网关 + Docker 每用户、dsh-webui-auth 单实例锁)归档为备选。 -3. **内存边界**:1.8G 内存 / 2 核 → 1-3 人规模(每用户一个常驻 DSH 子进程),扩容前不超 3 用户。 -4. **镜像 v2 / std 技能库归档(2026-09-09)**:旧规划 19 章第十一节的「P1 镜像 v2:内置公共插件 + `std/agents/skills` 标准技能库(rank400 层)」「P1 系统标准技能库内容确认(MCN 脚本创作技能等随镜像/`std/` 分发)」——形态前提是「每用户一个 `dsh-web:` Docker 容器,nginx 按子域/端口分流 + 容器内挂载 `/opt/dsh/std/agents/skills` 兜底层」。**已被 2026-09-08 决策 2(dshs 多租户形态)取代**,无需再做: - - 公共插件分发路径:**profile 层 `dsh plugin --profile add/remove `** 官方机制(pnpm 转发,cordis 补丁自动入 bundles);portal-entry v0.1.0→v0.4.3 走的就是这条。 - - 标准技能库分发路径:**dsh 原生分层**(项目级 `.dsh/skills` rank100 / `.agents/skills` rank200 / 个人 `~/.dsh/skills` rank300) + 后续「管理员预置 profile-skel 与工作区种子」= 新形态下的 std 技能库等价物(原 P3「编导工作区模板」已于 2026-09-11 删除)。 - - 原 dsh-web Docker 容器与镜像(`dsh-web:0.1.2` + `/opt/dsh/dsh-web-0.1.2.tar`)**已全清(2026-09-09)**——确认不回退,回退路径由 `/opt/dsh/backups/` 承担(db.bak 20260909_162739 + profile-web-admin-poc tgz + src-pre-reap tgz)。 - - **结论**:原方案归档废弃,不再构建 dsh-web:0.1.x 增强镜像;后续若需"统一技能/插件基线"通过管理员对 profile 模板操作实现,不走镜像层。 - ---- - -## 附:红线(硬性,2026-09-09 版) - -1. 禁止启动 dsh 时自动获取最新版本;版本升级走独立"升级测试→评估→修复"流程(档案 07)。 -2. 不改官方 dsh 主程序与缓存(/usr/local/lib/node_modules/@deepseek-ai/dsh);扩展只走 profile 层 `dsh plugin` 机制。 -3. client bundle 严禁 `exports.default`(loader ESM interop 取函数 → 无 inject → 注册静默失败;v0.4.0→v0.4.1 实证)。 -4. 服务器 docs 只读归档(root 600);技能/文档同步类红线见各 skill MEMORY 约定。 +# 03 路线图与待办 + +> 状态(刷新 · 2026-09-12 15:05):本文件长期晚于实际进度(此前停在 09-11 23:05)→ 本次补登记**档案 57–68**、清理已完成行、并把待办口径与 `05-交接单/README.md §一` 对齐(**已规划待执行的以交接单为准**,本文件只保留指针)。原文:**2026-09-11 21:20 合并版**(补回 14~22 与 B1/B2/B3/B4、档案 28、备份加固;保留「MCN 改造暂缓」「白名单源码安装」等新决策)。 + +## 一、待验证项(Open Questions)结论 + +| # | 原问题 | 结论(实测日期) | +|---|---|---| +| 1 | `.env` 注入 `DEEPSEEK_API_KEY` vs 用户个人 key 优先级 | ✅ 已定:**统一 admin key 模型**(commit df5cc84,2026-09-09)——resolveApiKey 忽略 userId、取 role=admin 启用 key,spawn env 注入;keys 路由 requireAdmin;无"个人 key 覆盖"场景 | +| 2 | web profile 空 `.dsh` 首启自动初始化(add-user 流程) | ✅ 已解答:dshs 模式无此问题——provision 建 OS 账号 + profile 首次 spawn 即自动初始化;testuser 注册→审核→桌面→DSH 全链路已实测通过 | +| 3 | 前端 loopback 检测能否 `--patch` 放行(用户自配模型) | ✅ 已实测(2026-09-09):proxy.ts 将 Host 伪装为 `127.0.0.1:` → dsh 的 /api trust fence 与 loopback 特权校验**全放行**,Settings 面板在门户代理后可用 → **暴露面成立:登录用户可进 Settings 自配 key/模型**,作为已知安全边界记录(第二道防线),暂无封锁需求 | +| 4 | dshs 对 web Settings 限制 | ✅ 同上实测:Settings 面板可用(portal-entry v0.4.1「平台管理」分区即在此验证) | +| 5 | 容器内 `dsh plugin add` 走 npmmirror | ⏹ 过时归档:架构切换后插件走**宿主全局 dsh + profile 层 pnpm 转发**(`dsh plugin --profile web`);装法 = 本地 `npm pack` 出 tgz → `dsh plugin add `(实测 v0.1.0→v0.4.1 全链路) | + +## 二、路线图 + +### 已完成(2026-09-08 ~ 09-09) + +- [x] **P0 迁移**:data → users/main(2026-09-08 17:30) +- [x] **P2 调研**:dshs 选型前置调研(2026-09-08) +- [x] **域名接入 + 通配 HTTPS**:dsh.alotbuy.com 通配证书(2026-09-08 18:00-18:40,certbot dns-cloudflare) +- [x] **架构切换**:dshs 多租户 + account 硬隔离(2026-09-08 18:30) +- [x] **登录直达 v1+v2**:POST /api/dsh/enter 按角色分流 + token 自动携带(commit 6f63108 / e39628a / 29c8907) +- [x] **统一 KEY 管理员管控**(df5cc84) +- [x] **用户管理**:注册/审核/删除用户(4943c8c)+ 权限收紧(chmod 系列) +- [x] **实例生命周期**:last-wins 单活跃会话 + idle-reap 常驻上限(928dde1 / 6336c53) +- [x] **门户功能插件化 PoC**:`@dsh-local/portal-entry` v0.4.1 浏览器验收通过(2026-09-09)——设置面板「平台管理」分区(门户地址 + 打开管理台 + 退出登录),档案 05 +- [x] **技能管理面**:共享 + 个人技能 API 与页面、zip-only 两阶段替换(2026-09-09,档案 11) +- [x] **文档库路径统一(2026-09-10)**:工作区 `D:\AgentSkill\aliyun-work-space` → `D:\AI技能\aliyun-work-space`;文档目录 `dsh-docker\docs\` → **`dsh-server-docs\`(内容提升到根,与服务器 `/opt/dsh/docs` 同构)**;6 处旧路径改写 + 双端同步,25/25 md5 一致;对账脚本 `07-scripts/docs-sync-check.sh` 落地 +- [x] **工作区迁移 D: → E:(2026-09-13)**:工作区 `D:\AI技能\aliyun-dsh-server` → **`E:\ProgramData\AI技能\aliyun-dsh-server`**(`D:\AI技能` 已无实体目录)。同步改的**活路径**:`~/.workbuddy/settings.json`(两个 hook 命令)、`dsh-server-docs/07-scripts/lock-guard-hook.py`(`DSH_DOCS_ROOT` 默认值)、`07-scripts/docs-sync-check.sh`(`DOCS_LOCAL_DIR` 默认值,本库 + 项目根共两份)、`dsh-server-docs/07-scripts/extract-user-voice.py`(目录名示例)、`dsh-server-docs/README.md` 与 `INDEX.md` 的本机路径、档案 73 的 hook 安装片段。**历史档案(01-规范/01-规划与架构 / 04-11 / 04-12 / 09-archive/ 下各篇)中的 `D:\AI技能\...` 保留原值**(写于迁移前,属历史,按「档案只增不改」不回改)。**代码仓 `D:\github\dsh_shenxian` 未变动**(不在 `AI技能` 之下)。 + ⚠️ 排查期间曾临时建 `D:/AI技能 → E:/ProgramData/AI技能` 目录联接,用于救活**会话启动时快照**里已失效的旧 hook 路径;**已按用户要求移除**。⇒ **改 hook 路径后,已在跑的会话需「完全重启」(关窗 ≠ 退出)或新开会话才生效**(2026-09-13 实测:本会话 06:47 启动、06:55 改配置,07:05 拆联接后写操作仍报旧路径)。 +- [x] **档案 78 · 崩溃熔断冷却期与告警(2026-09-13)**:修 **档案 77 §八 遗留 1** —— 原实现熔断即 `resetCrashState()` 清预算 ⇒ 只要有重试路径(用户 F5 / 注入脚本自愈 / 脚本直铺)崩溃循环就能**无限重来**,且只留一行 stderr。改为:熔断态**跨轮存活** + **指数冷却**(10 min → 封顶 6 h),冷却期内 `launch()` 拒绝隐式启动(HTTP 503 `instance_circuit_open`),冷却过后只给**一次**干净预算;熔断**双通道告警**(stderr + `/var/log/dsh-crash-breaker.log`)并把 `breaker` 暴露到 `/api/dsh/status`。改 6 个文件(含 3 条新单测);`npm run build` rc=0、`npm test` **45 pass / 0 fail**。**已部署并验证**(2026-09-13 08:02 重启载入:PID 410287→413817、门户 200、孤儿 scope 0、`/api/dsh/status` 返回新字段 `breaker`)→ 档案 78 §九 +- [x] **登录直达冷启动竞态 404 修复(2026-09-10)**:enter 复用/AlreadyRunning 分支等 launch token 到位再返回 URL,杜绝启动窗口 404(commit `fa718d2`,档案 13) +- [x] **敏感信息暴露面审计与加固(2026-09-10,档案 14)**:判定跨租户/提权不成立;封云元数据端点 `100.100.100.200` + 外联观测(`/etc/nftables-dsh-egress.nft` + `dsh-egress.service`) +- [x] **会话失效 401 未跳登录页修复(2026-09-10,档案 15)**:desktop/admin/skills 三页注入 401 守卫;核心插件开关收归 admin +- [x] **页面导航定稿 + 插件三层归属(2026-09-10,档案 16)**:门户 SPA 化、技能/插件独立页面、功能插件候选池投放 + 实例设置启停;阶段 0-2 已实施(`bad6ed7`/`6f86a5f`/`b7fd85d`) +- [x] **实例沙箱隔离(档案 16)**:bwrap + systemd-run scope(512M/CPU 150%/TasksMax 128)+ 私有 /tmp + `--unshare-pid`(commit `449f28b`) +- [x] **工作区选择器暴露面核查(2026-09-10,档案 17)**:判定非越权;记录 P1 写边界=会话 cwd / P2 picker 无根白名单 / P3 nft 文件可读 +- [x] **文档库去重 + 单一来源(2026-09-10,档案 19 §D4)**:删根级 05 副本、`04/README.md` 改指针 +- [x] **崩溃自愈加固(2026-09-11,档案 20)**:指数退避 + 窗口熔断(5 次/10 min)+ 观测(`restarts`/`lastCrashedAt`)+ handoff 停写;**live 自愈实测通过**(kill → 1 s 后 `[crash-restart]` 拉起);commit `7cba3e3` +- [x] **CI 脚本 + 清理失效 smoke(2026-09-11,档案 19 §C3)**:新增 `07-scripts/ci.sh`;删 `smoke-plugins.mjs`(目标路由已移除)与 `smoke-watchdog.mjs`(watchdog 从不启动) +- [x] **访问域名迁移(2026-09-11,档案 22)**:门户 `alotbuy.com` + 用户 `<用户名>.alotbuy.com`;通配证书 DNS-01(传播 60 s)、CF 真实 IP 还原、旧域 301 含用户名映射 +- [x] **nginx 性能修复(2026-09-11,档案 21)**:`proxy_buffering off`(修「一次性输出」)、`gzip_proxied any`(930 KB bundle 压缩)、`/assets/` 30d 缓存 +- [x] **官方白名单插件来源(2026-09-11,档案 29)**:门户「插件管理」接入 awesome-dsh-plugin 官方目录(`plugins.json`,3408 条 / 23 分类),admin 搜索/筛选/多选 → 导入功能插件候选池;导入口径 = **仅预构建**(npm registry tarball / release 资产),可导入 **1792 条(52.6%)**、热门插件全覆盖,平台侧不执行任何第三方构建脚本 +- [x] **编排器孤儿实例清理(2026-09-11,档案 30)**:spawn 前清 `dsh--*` 孤儿 + 启动时清一次;两场景 live 验证通过 + +- [x] **档案 14~22 全部落地(2026-09-10~11)**:敏感信息暴露面审计(封云元数据端点 + 外联观测)、会话失效 401 跳登录、页面导航定稿 + 功能插件三层归属、实例沙箱隔离(bwrap+scope)、工作区可见面核查、目录选择器收敛 v3、崩溃自愈(退避+熔断+观测)、CI 脚本、**访问域名迁移到 alotbuy.com**(旧域 301)、nginx 性能修复(buffering/gzip/assets) +- [x] **写保护 B1(档案 17 §P1)**:实例内 `--ro-bind-try` 覆盖 `cordis.patch.yml`/`package.json`/`pnpm-lock.yaml`(写测试 ✅ + 真实 dsh 启动验证 ✅) +- [x] **编排器自愈 B2(档案 18 收尾)**:`ensurePickerProfile(userId)` 在 launch 前 + spawn 后各异步补齐(幂等) +- [x] **上传安全扫描分级 B4(档案 19 §C6)**:P0 阻断 / P1 告警 + 扩展名 19→31 + shebang/二进制/8MB(冒烟通过) +- [x] **死代码子集 B3(档案 19 §C5)**:删 `src/runtime.ts`;`folder_plugins`/`workspaces` 加废弃注释(**完整版清理已关闭**:跨 10 文件且含 k8s/PG 未验证路径,删除风险不对称) +- [x] **用户数据清理三件套(档案 28)**:`ws-cleanup`(T1 平台产物 / T2 顶层一次性脚本 >90 天 / T3 永不删)+ `session-gc`(**365 天**保留)+ `purge-trash`(回收站 30 天)+ 每小时用量快照 + 门户「存储用量」面板(`GET /api/admin/storage`) +- [x] **备份可用性修复(档案 02 附录 C.8)**:发现 `backup.sh` **无执行权限且未调度**(最后备份停在 09-08)→ 修好 + SQLite 一致性快照 + 纳入 artifacts/07-scripts/cron/nginx vhost + `/etc/cron.d/dsh-backup` 每周日 05:00(恢复演练已验证) + +### 已完成(2026-09-11 ~ 09-12,本次补登记 57–68) + +- [x] **档案 57 · 设置面板「用户管理」入口 + 全员安装**(2026-09-12 00:04) +- [x] **档案 58 · 实例内存优化与配额下调**:512M → **384 MiB** + 编译缓存(2026-09-12 00:23) +- [x] **档案 59 · 重连反馈:实例启动中的加载动画**(2026-09-12 01:30)|⚠️ 遗留:`wake.html` 本机 4708B vs 服务器 4591B **待核对同步** +- [x] **档案 60 · 设置面板分区改名「功能插件」→「功能管理」**(2026-09-12 08:30) +- [x] **档案 61 · 插件管理页官方插件列表加高 + 底部留白 200px**(2026-09-12 09:14) +- [x] **档案 62 · 插件目录缓存状态可见化 + 「重新拉取目录」按钮**(2026-09-12 09:21,`be42155`) +- [x] **档案 73 · 让锁真正拦得住人**(2026-09-12 17:0x):复盘发现三把锁**当天被跳过两次**(含本会话)→ 根因①无强制入口(`settings.json` hooks 段为 null)②guard 输出「无锁」被读成「可以开工」。治:① 措辞修正(guard 三处 + 交接单语义)② `07-scripts/lock-guard-hook.py`(PreToolUse 无锁拒写 + SessionStart 提示,**作用域仅本库/本代码库**;四态单点验证通过);⚠️ 钩子待用户在 `/hooks` 审核启用 +- [x] **档案 72 · 实例回收/关闭后回到页面自动唤醒并重建连接**(2026-09-12 16:55,`b23e386`):注入脚本原来只认 401,实例回收时代理返回的 `404 not_running` 无人处理 → 用户必须手动刷新。补:① `hit()` 识别 not_running ② `visibilitychange`/`focus`/`pageshow` 时主动探活 `/api/dsh/status` ③ `recover()` 跳 `wake.html`(走 `enter` 拿**新 token**,避免 reload 带旧 token 再撞 401) +- [x] **档案 71 · 插件兼容性预检:导入 / 上传即判定**(2026-09-12 16:35,`8d19e89`):semver 依赖范围 + 运行时导出符号比对,不兼容在导入时即拒收(承接档案 70 的教训)→ 方案 `04-71` +- [x] **档案 70 收尾 · AnySearch 彻底放弃 + 候选池存量体检**(2026-09-12 18:56):用户选 B → 下架 anysearch(`audit` 175);**顺手发现预检只覆盖"新导入"、覆盖不到存量** → 用 `lib/web/plugin-compat.js` 体检池内两条:`dsh-univer-office` = `ok`(保留)、`@liustack/modlens` = `unknown` 且有 `plugin_incident`(audit 172/173)→ 一并下架(`audit` 176,tgz 备份 `/opt/dsh/backups/plugin-pool-20260912/`)。全程**未重启服务、未中断用户**。另落 **R9 红线**(禁止人工删锁/接管)→ 档案 70 §九 · 档案 73 §十一 +- [x] **档案 70 · anysearch 插件与 dsh `0.1.2-rc.1` 不兼容 → admin 实例崩溃循环**(2026-09-12 15:32 **已止损**):根因 = 插件在 import 阶段引用 `@deepseek-ai/dsh-llm` 未导出的 `assertNever`,而 dsh 的 plugin tree 加载**全或无** → 启动中止、崩溃自愈反复重启;摘掉该 bundle 即恢复。**✅ 该待办已于 2026-09-12 18:56 关闭**:用户选 B · **彻底放弃**(候选池条目下架 + 存量体检)→ 见本节「档案 70 收尾」行与 §二 待办表「已处置」行 +- [x] **档案 69 · 并发治理落地(T04)**(2026-09-12 15:10,`exec-session-C`):文档库 commit 常态化 + 服务器侧操作锁 `/opt/dsh/state/.op-lock/`(实测往返通过)+ 交接单目录权限统一;三把锁进预检脚本 → 单子已归档 +- [x] **编号 63 为空号**:该号只出现在当日工作日志的「事故 63」里(清理残留 tgz 致依赖断裂),**无对应档案**,勿补占 +- [x] **档案 64 · 接入 AnySearch 搜索 provider(B 方案)**(2026-09-12):平台现用 DeepSeek 官方搜索;AnySearch 前置验证 4 项全绿、**admin 侧已实施**;⚠️ 待办改以**档案 65 §7.3** 为准 —— **2026-09-12 用户拍板:三方插件一律走「admin 导入候选池 → 用户自助启用」,不再向用户 profile 直铺**(§8.3 第 2 条直铺命令已作废) +- [x] **档案 65 · 功能插件启停 ↔ web provider 配置联动**(2026-09-12):根治「候选池一禁用就 `CONFIGURED_MISSING`」→ 平台按当前 bundles **重算托管段**,启/禁两态皆正确;**代码完成 + 两态实测,⏸ 待部署(需 R8 窗口)** +- [x] **档案 66 · 业务插件 P0 误报 → admin 显式信任**(2026-09-12 11:15 已部署):安全检测改 **fail-closed + 逐条回显 + admin 声明信任后放行并留痕** +- [x] **档案 67 · 「功能管理」section 按 UI 规范重做(v0.2.4)**(2026-09-12):用途说明做主视觉 + 字号/组件对齐 `06` +- [x] **档案 68 · 候选池启停的 root 属主污染根治**(2026-09-12):候选池 `install/uninstall` 改 **`setpriv` 降权** + 改插件前自动属主自愈 + 清存量 **561** 项;附带修 `npm pack` 把上一版 tgz 打进产物(加 `.npmignore`) + +### 进行中 / 待办 + +| 优先级 | 事项 | 说明 | +|---|---|---| +| ✅ **已完成** | ~~**档案 65 部署**(功能插件启停 ↔ web provider 托管段联动)~~ | **本就是生效状态**:服务器 `lib/` 00:13 构建 → **00:15:23 重启即已载入**(08:02 再载入);**§7.3 第 3 条已完成**(`ensure-anysearch-admin.cjs` 加硬拦退役,默认 `exit 2`,备份 `.bak-retire-20260913`)| +| ✅ **已完成(留一项 L1)** | ~~**档案 78 部署**(崩溃熔断冷却 + 告警)~~ | **2026-09-13 08:02 已部署并验证**(见档案 78 §九)。**遗留 L1**:故意把实例反复搞崩以验熔断(需 6 次真崩 + 该用户冷却 10 min)→ **留维护窗口**做 | +| ✅ **已完成并归档**(2026-09-13 18:0x) | **T03 · 7 插件整合投放** | **guest 启用已成功**(任务 `c20739a6dbe8975c`:`success`/`restarted=true`;配额自动 672→**800 MiB**); **admin 侧 8 项实证通过**(包结构 v0.3.9 / 上传扫描 P0=0 / 池内 / `[mcn-suite] loaded` 且 duplicate=0 / 旧 7 包已下架 / 技能落 `/08-skills/mcn-short-video` / 幂等跳过 / BRIEF 384M);**剩 6 项需浏览器或造场景**(D 入口逐个点开 / J agent 按名加载 / L 凭据扫描 / N 死引用扫描 / P·Q 撞名三场景)→ 见 `05-交接单/T03` §九 | +| ✅ **已消解(无需窗口)** | ~~**实例内存预算上调**(`--max-old-space-size` 160→256 + cgroup `MemoryMax` 384→512 MiB)~~ | **2026-09-13 13:3x 实测:R1-④ 已把它改成「按插件集合动态计算」,比原方案更优** —— `instanceMemMb()` = 160 + Σ插件预估(clamp 384–1024),`heapMbFor()` = 配额 − 96(cap 256)。运行中实证:guest `NODE_OPTIONS=--max-old-space-size=256` / scope `MemoryMax=544 MiB`(= 160 + univer 384);**admin 也是 256**(不再是 160)。平台 env 里那个写死的 `160` 已被代码侧 `withHeap()` 覆盖、**不是生效值**。⇒ **T03 的 guest 启用不再被容量阻塞**(装上 mcn-suite 后配额会自动变 672 MiB)。原「须重启服务(R8)」的前提已不成立 | +| 🟡 **待排期** | **档案 81 重构总纲的 R2/R4/R5**(**R0/R1/R3 已完成**) | **R2** 管理面就地化 → 🚧 **代码已完成(2026-09-13 17:5x,档案 82)**:插件 `@dsh-local/business-plugins` **0.2.8→0.2.9**,新增原生「平台管理」只读分区(仅 admin;数据调平台只读 API;写操作回跳管理台);`node --check` 通过、tgz 已出;**待投放 + 启用 + 浏览器验收(卡在需 admin 登录态)**;`/admin` 路由族与删 3 桩页**仍未做**。⚠️ **形态已改定(2026-09-13 用户):用「原生弹窗」,不用 iframe**(原话「iframe 不如原生弹窗体验好」)⇒ 改为**在自研插件里原生渲染只读子集**(调平台 `/api/*`,非 iframe 嵌 `portal.html`);仍按 R5 只读优先,登录后 30 天内无需重做门户全量 UI ⇒ 见档案 81 §9.2 修正|**R3** 内部标识统一为 **`dshs`** → ✅ **已完成(2026-09-13 15:4x–16:0x)**:代码 89 文件 / 382 处 + 本机其余 135 文件 / 863 处 + **服务器原子切换**(单元真名 `dshs.service`、`/etc/dshs.env`、`/opt/dshs`、`/var/lib/dshs`、`dshs.db`;含 **WAL 归位**与 DB **VACUUM**);上游具名引用全清(含移除 `git remote upstream`);两仓 git 历史已删重建为单提交|**R4** 文档/代码同仓 → ⏸ **待用户选 a(并入代码仓)/ b(单向导出)**(用户 09-13 反馈「没看懂 R4 要做什么」⇒ 需先讲清动机再选)|**R5** **多语言 i18n** → ✅ **方案已定(2026-09-13 用户):「匹配 dsh 官方方案」= 走官方 locale 体系**(`ctx.locale.addLanguage()` + `register(ns)`,平台页/注入层/自研插件全走官方扩展点、**零官方改动**);⚠️ 覆盖率边界见档案 81 §10.7 ⑥ —— 官方**仍有 20+ 个 UI 包未迁 locale**,那些包切语言不变(属官方自己的待办,不为我们可改范围)| +| ✅ **已修复并验收**(2026-09-13 17:2x) | ~~**档案 76 · `/univer-api/state` 生产持续 400**~~ | **本会话独立复核**:近 2 小时 400 = **0**、近 30 分钟 6 次请求**无 400**;实例 env `UNIVER_DSH_GATEWAY_SOCKET=auto` 已生效(档案 76 追加节=真因+修复+验收)。原述 2026-09-13 07:36 实测:guest 用户在 1 秒内被连续 ~10 次 400(平台侧只记状态码)→ **根因待取响应体**;⚠️ 同时发现 **guest `ws` 下 0 个 `.univer` 文件**(目录在、文件不在)→ 需沿「MCN 生成 `.univer` → Univer 打开」链路查 → 档案 76 追加节 | +| ✅ **已处置** | ~~AnySearch 能力是否保留~~(档案 70 §八) | **用户选 B · 彻底放弃**(2026-09-12 18:56):候选池条目已下架(HTTP 200 / `audit` 175)、凭据无残留、无用户启用过;**顺带体检池内存量** → `dsh-univer-office` = `ok`(保留)、`@liustack/modlens` = `unknown` + 有 incident → 一并下架(`audit` 176,tgz 已备份)。**未重启服务、未中断用户** → 档案 70 §九 | +| ✅ 已完成 | **T05 · 插件兼容性预检**(导入/上传即判定兼容性) | 2026-09-12 16:35 落地(`8d19e89`):判据模块 + 上传/导入双入口接入 + build + 重启;验收全绿 → 方案 `04-71`,单子已归档 | +| **档案内挂起** | 档案 57 四项(picker store 隐患/dep spec 指向 `ws` 会被清理/`portal_ping` 失效工具/插件源码两处存放)|~~档案 59 `wake.html` 本机 4708B vs 服务器 4591B 待核对同步~~ ✅ **2026-09-13 09:4x 实测双端一致**(均 4962 字节、md5 `00b81728…`;变大是因档案 78 给 wake.html 加了熔断文案)|档案 66 三项(官方目录**批量导入**未接信任入口/信任状态未持久化→建议并入 migration v6/`ensure-anysearch-admin.cjs` 覆写段待退役,否则与档案 65 托管段互相覆盖)|档案 68 平台 setpriv 路径待用户会话自然验证|档案 42 三项(③ **已闭环并实测通过**:子代理派发 `bash -c 'echo subagent-ok'` 正常返回 ⇒ shell 可用、平台零改动;见档案 42 末「追加结论 + 实测结果」)/档案 38b 三项/档案 15(`admin.html` 缺页面层 role 拦截)/档案 32(`dsh-market` 可绕过第三层管控,**未成档**) | 详见各档案 §待办段 | +| ✅ **已修复并部署** | ~~**插件启停的两处平台缺陷(档案 79)**~~ | **2026-09-13 13:4x 逐条核实产物(档案 79 §七)**:**D1** `await uninstall(…)` ✅(产物 `:587`/`:617`)|**D2** `remove` 已改 `-w`(`:575`,全仓无效 flag 仅剩 `add` 一处=合法)✅|**加固 a** 路由外层 `.catch()` ✅(`:613`+`:718`)。**D3 已消解**:R1-④ 把堆限与 `MemoryMax` 改为按插件集合动态计算(实测 guest `heap 256` / `MemoryMax 544 MiB`)⇒ 不再需要窗口。⚠️ **仍缺端到端验收**(重现「注定失败的启用」→ 期望 任务 `failed` + profile 回滚 + 服务不退出)→ 建议与 T03 的 guest 启用合并验。**加固 b**(半应用态主动自愈)未做,可选 | +| 触发式 | dsh 升级回归(档案 26 六类耦合点)|会话 GC | 升级 / 容量触发 | +| P3 | 门户「浏览文件」补下载入口 | 档案 56 §七:门户 `#/files` 目前只能列表/上传,可复用 `/api/fs/download` 加"下载"按钮 | +| ✅ | ~~老会话权限档位对齐(检测 + 提示)~~(档案 56) | **已完成(2026-09-11)**:`GET /api/dsh/session-permission` + 实例页顶部提示条(说明原因 + 切档位/新建会话两条路);实测 `stale=true` 触发正常。**不自动改档位**(安全语义变更需用户知情) | 档案 55:档位是**会话级播种** —— 09-10 及更早的会话仍 `workspace-write` → 沙箱 fail-closed,**bash 被拒 11 次**(实测:同一会话 22:16 手动切 `danger-full-access` 后立刻恢复)。建议 `/api/dsh/enter` 检测会话 preset 与平台默认不一致 → **页面提示 + 一键切换**(不自动改) | +| ✅ | ~~实例能力清单(自检)~~(档案 56) | **已完成(2026-09-11)**:`07-scripts/gen-capabilities.cjs`(cron 每日)→ `/opt/dsh/state/capabilities.json` + 共享技能 `platform-capabilities`(agent 可读);实例页「🧭 能力」面板同源展示 | 档案 55:agent 在 16:48 / 18:21 / 22:14 **三轮重复现场探测**,每次撞同样 4 类墙(`/etc` 白名单 / `127.0.0.1` 被 SSRF 拒 / skill 不存在 / 写边界),用户随之三次追问"能力有变化吗" → 平台注入能力说明(可读可写范围、网络边界、工具与技能清单、档位含义) | +| ✅已定 | 平台技能投放现状对齐 | **结论(用户原话):「业务技能要打包进插件里一起安装和使用,不要分开管理」** → 投放方式 = **随功能插件包投放**(见 `05-交接单/T03` §4.1),**不走**门户技能管理单独投放;平台自描述技能 `platform-capabilities` 照旧共享投放。「是否投放待定」的旧表述作废(档案 55 当时的背景是 `bundled-skills` 为空 → agent 撞 `unknown skill`) | +| ❌复核 | ~~管理类插件化~~(档案 05 PoC-2 ①) | **复核结论:不建议做** —— 门户 `portal.html` 已有完整管理面(服务/密钥/用户/技能/插件/运行环境),admin 在会话内经 `portal-entry` 卡片**整页跳门户**;把 admin 能力搬进实例子域反而扩大权限执行面(与档案 39 收窄方向相反) | +| ✅复核 | ~~插件↔门户鉴权令牌~~(档案 05 PoC-2 ②) | **复核结论:已实现(非令牌方式)** —— 共享会话 Cookie(`Domain=.alotbuy.com`)+ `server.ts` 的 CORS 白名单(`isAllowedOrigin` 允许 baseDomain 及其子域 + `Allow-Credentials`),功能插件 v0.2.1 已生产跑通;再做独立令牌属重复建设 | +| ❌复核 | ~~`portal_ping` 端到端~~(档案 05 PoC-2 ③) | **复核结论:原验收项作废** —— 该工具 `fetch(127.0.0.1:3080)`,而档案 39 已封 `127.0.0.0/8`(实测 BLOCKED)→ 必然失败;若要验证"host 插件给 agent 注册工具",需**另立不依赖 loopback 的探针** | +| 暂缓 | **MCN 工作台插件平台化改造**(档案 27)—— **【暂缓】需先测试插件兼容性**(2026-09-11 决策) | 3 处 P0:① `homedir()/.dsh` → `process.env.DSH_HOME ?? …`(config.js 3 处;`mcp.js` 已是正确写法可对照)② 技能路径硬编码 13 处 → 只报技能名 ③ agent preset(`.agent-presets/mcn/`)随包投放;P1:MCP 预装(现在 `npx myai-mcp`)、DB 重建兜底、技能依赖声明 | +| ✅关闭 | ~~白名单源码安装支持~~(档案 29 §七) | **评估结论:不做(2026-09-11)**:源码安装需让第三方构建脚本以 root 在平台机执行(供应链 + 可复现性 + 性能三重风险),与"平台不执行第三方构建脚本"红线冲突。**替代路径**:admin 本地构建后走「上传 tgz」通道(已有 P0/P1 扫描);如需开启须先满足沙箱构建 + `--ignore-scripts` + 仅 admin + 审计等全部前提 | +| 降级 | B5:给 portal-entry / business-plugins 加加载标记 | **降级为"顺手做"(2026-09-11 决策)**:不解决当前问题、源码不在仓库、且仅提升"升级时排查速度";下次改这两个插件时顺手加(零边际成本) | +| ✅ | ~~熔断 live 实测~~(档案 20 附) | **已完成(2026-09-11)**:由真实故障用户(档案 52)的日志完成实测 —— 退避 1000→2000→4000→8000ms、`restartsInWindow` 1→4、第 **5** 次触发 `crash-loop-circuit-open`(窗口 600000ms / 5 of 5),当日 2 次开断;无需再人为 kill 复现 | +| ✅ **已完成**(2026-09-15) | ~~档案 16 阶段 3/4(实例内「我的技能」)~~ → **= 交接单 `T01`,已归档** | **档案 100**|插件 `business-plugins` **0.3.20 → 0.3.21** 已投放两实例。① **形态修正(09-15)**:由「新增 section」改为**并入既有「功能管理」section 内分组**(依据用户口径「不要分开管理」+ 档案 60 分区命名;**仅入口层合并、机制层分离**)→ 见 `T01 §四 决策 6` 与 **`T01 §九`**;② 实现:技能行(名称/来源/状态/事实/动作)+ zip 拖拽上传 + 同名两阶段替换 + 页内确认弹窗 + 锁定行(共享技能)无动作按钮 + zh/en 46 条词条;③ 验收:`npm run verify` 全绿(含新 `07-scripts/verify-my-skills.mjs` 34 条断言)、06 §7 三段式全绿、端到端 **19/19**(409 / 400 守卫通过);④ 阶段 4 收口 = 未新增 section + 未改角色补丁 ⇒ 可见性不变。⚠️ **后续(档案 101)**:该分区已改名「**能力管理**」并改为**页内 tab 分页**(功能插件 / 我的技能)。原述:门户 skills.html 仅 admin → 普通用户需实例内入口 | +| ✅ **已消解** | ~~摘除 guest 仍在启用的已下架插件 `@liustack/modlens`~~ | **09-13 07:0x 实测:已无需处理** —— guest `profile/web` 的 `bundles` **与 `node_modules` 均已无 `@liustack/modlens`**(原「已下架却仍启用」的不一致态已不复存在)。历史:该包 09-12 从候选池下架(档案 70 收尾)时曾留在 bundles | +| ❌已复核 | ~~Cookie 域收窄~~ | **不做(2026-09-12 用户决定:把这条待办删掉)** —— 共享 Cookie(`Domain=.alotbuy.com`)是**有意设计**:支撑「功能插件 ↔ 门户」**免令牌鉴权**(配合 `server.ts` 的 CORS 白名单),功能插件 v0.2.1 已生产跑通 → **收窄会破坏该链路,收益为零**;结论见档案 05 PoC-2 ② | + +| 🟡 **待排期** | **T08 集群化的收尾项**(主体与生产切换已完成) | ① `join-worker.sh` 一键装机(现在 join = 装 unit + 起 agent + 注册,三步手工)~~② 隧道服务化~~ ✅ 已收口(本地 20s 定时器自愈,实测 30s 内恢复)③ 集中日志 / metrics ④ `smoke-domain` 定性 ⑤ drop-in 里的 PG 口令建议进一步收权限(现 root 可读)|详见 `05-交接单/T08 §16.5` | +| 🟡 **待排期** | **覆盖网络 / 客户端化这条线**(**本轮零代码、零服务器改动**) | 已落 **档案 103–112**(10 份规划转正式档案;入口 = 工作区根 `接续入口_覆盖网络线_20260916.md`)。📌 **第一件实事 = 把会合 / 中继从 Manager 里拆成可独立部署的组件**(现为绑在 Manager 上的**单中心 SSH 反向隧道**;所有后续多区域 / 多中心 / 骨干层的前置)。**已定口径**:只做技术实现(跨境合规由使用者自负)· **单机自用 ≠ 不需互联** · 按**异构**设计(中继 45% 设计 / 55% 留余量 + **必须补 443/TCP 兜底**)· 权威状态(归属 / 租约 / 资格)**必须单点控制面** · 游戏重点 = **MMORPG(2D/2.5D) / MUD / 传奇类**。⚠️ **唯一待拍板 = 骨干节点服务范围**(A 只服务自己名下设备 / B 服务全网;**倾向 A→B 渐进**,见档案 105 §七)。之后按 **档案 112「只做三件事」** 开工 | + +### 历史决策记录(保留,均已落地) + +1. **域名决策**(2026-09-08):无域名 → 先解决域名再回来做多用户改造 → 已落地:dsh.alotbuy.com + CF 通配 + 源站 443。 +2. **选型决策**(2026-09-08):dshs 模式 A(account 硬隔离)优先,理由:现成密码注册/审核/网页桌面 + 每用户独立实例;替代(自研 Node 网关 + Docker 每用户、dsh-webui-auth 单实例锁)归档为备选。 +3. **内存边界**:1.8G 内存 / 2 核 → 1-3 人规模(每用户一个常驻 DSH 子进程),扩容前不超 3 用户。 +4. **镜像 v2 / std 技能库归档(2026-09-09)**:旧规划 19 章第十一节的「P1 镜像 v2:内置公共插件 + `std/agents/skills` 标准技能库(rank400 层)」「P1 系统标准技能库内容确认(MCN 脚本创作技能等随镜像/`std/` 分发)」——形态前提是「每用户一个 `dsh-web:` Docker 容器,nginx 按子域/端口分流 + 容器内挂载 `/opt/dsh/std/agents/skills` 兜底层」。**已被 2026-09-08 决策 2(dshs 多租户形态)取代**,无需再做: + - 公共插件分发路径:**profile 层 `dsh plugin --profile add/remove `** 官方机制(pnpm 转发,cordis 补丁自动入 bundles);portal-entry v0.1.0→v0.4.3 走的就是这条。 + - 标准技能库分发路径:**dsh 原生分层**(项目级 `.dsh/skills` rank100 / `.agents/skills` rank200 / 个人 `~/.dsh/skills` rank300) + 后续「管理员预置 profile-skel 与工作区种子」= 新形态下的 std 技能库等价物(原 P3「编导工作区模板」已于 2026-09-11 删除)。 + - 原 dsh-web Docker 容器与镜像(`dsh-web:0.1.2` + `/opt/dsh/dsh-web-0.1.2.tar`)**已全清(2026-09-09)**——确认不回退,回退路径由 `/opt/dsh/backups/` 承担(db.bak 20260909_162739 + profile-web-admin-poc tgz + src-pre-reap tgz)。 + - **结论**:原方案归档废弃,不再构建 dsh-web:0.1.x 增强镜像;后续若需"统一技能/插件基线"通过管理员对 profile 模板操作实现,不走镜像层。 + +--- + +## 附:红线(硬性,2026-09-09 版) + +1. 禁止启动 dsh 时自动获取最新版本;版本升级走独立"升级测试→评估→修复"流程(档案 07)。 +2. 不改官方 dsh 主程序与缓存(/usr/local/lib/node_modules/@deepseek-ai/dsh);扩展只走 profile 层 `dsh plugin` 机制。 +3. client bundle 严禁 `exports.default`(loader ESM interop 取函数 → 无 inject → 注册静默失败;v0.4.0→v0.4.1 实证)。 +4. 服务器 docs 只读归档(root 600);技能/文档同步类红线见各 skill MEMORY 约定。 diff --git a/dsh-server-docs/06-工作台UI规范.md b/dsh-server-docs/01-规范/06-工作台UI规范.md similarity index 97% rename from dsh-server-docs/06-工作台UI规范.md rename to dsh-server-docs/01-规范/06-工作台UI规范.md index ab435d6..9b84d4d 100644 --- a/dsh-server-docs/06-工作台UI规范.md +++ b/dsh-server-docs/01-规范/06-工作台UI规范.md @@ -7,11 +7,11 @@ > ⚠️ **该路径 2026-09-12 实测已不存在**(glob 无结果;`E:\ProgramData\AI技能\mcn-short-video\` 目录仍在,其下已无 `工作台UI规范.md`)→ **本副本当前即唯一有效版本**。源侧若恢复,需重新建立回拷关系并补同步。 > **UI skill(按场景选对,用错场景等于没用)**:开发/改前端页面时**必须加载并使用对应技能**做视觉判断 —— > - **`impeccable`** → **本工作区绝大多数页面**(工具类后台、设置面板、管理页、表单、空态)。**用 Operate 模式**。它的描述明确覆盖 settings / empty states / a11y / 信息层级 / 动效,与本规范同向。 -> ⚠️ 它自带的机械检测器**未随包提供**(`scripts/detect.mjs` 会报 `bundled detector not found`)→ 改为**人工逐条过 `reference/craft-floor.md`** 的 Verify 清单与 Refuse 禁令(2026-09-12 实测的确抓出了真问题,见下)。 +> ⚠️ 它自带的机械检测器**未随包提供**(`07-scripts/detect.mjs` 会报 `bundled detector not found`)→ 改为**人工逐条过 `reference/craft-floor.md`** 的 Verify 清单与 Refuse 禁令(2026-09-12 实测的确抓出了真问题,见下)。 > - **`taste-skill`**(frontmatter name 为 `design-taste-frontend`)→ **仅用于落地页 / 作品集 / 整站重设计**。 > ⚠️ 它**自述「Not dashboards, not data tables, not multi-step product UI」** → **本平台的管理页 / 设置面板不在其射程内**,不要拿它做这类页面。 > - **冲突时以本文档的实测 Token 数值为准**(本文档是实测基线,技能是通用审美)。 -> - 位置:`E:\ProgramData\.workbuddy\skills\\`(2026-09-12 起两个技能已装入全局技能目录,并已过安全审计)。Skill 工具加载不到时**直接读文件**即可。 +> - 位置:`E:\ProgramData\.workbuddy\08-skills\\`(2026-09-12 起两个技能已装入全局技能目录,并已过安全审计)。Skill 工具加载不到时**直接读文件**即可。 > **改文档规则**:UI 决策若伴随"为什么这样改",回写源侧文件并回拷,保持两端一致。 > **同步时间戳与回拷流程(档案 19 §D8)**:本文档是**回拷副本**,权威源见上文"权威源"路径。 @@ -19,7 +19,7 @@ > - **2026-09-12**:修订「UI skill」条款 —— 原表述要求"必须同时使用 impeccable + taste-skill",但 taste-skill 自述**不覆盖** dashboards / data tables / 多步产品 UI,与本工作区页面类型不符 → 改为**按场景分流**(impeccable 管本工作区绝大多数页面;taste-skill 仅限落地页 / 作品集 / 重设计)。 > - ⚠️ **本次未回写源侧**:权威源路径(`E:\ProgramData\AI技能\mcn-short-video\...\工作台UI规范.md`)在本机**不存在**(2026-09-12 实测 glob 无结果)→ 修订**只落在本副本**;源侧恢复后需补回拷。 > - 回拷流程:① 改源侧 → ② 回拷到本文件 → ③ 在上一行追加同步日期 → ④ 走文档库对账脚本确认双端一致 -> (`bash scripts/docs-sync-check.sh`,0 = 全绿)。 +> (`bash 07-scripts/docs-sync-check.sh`,0 = 全绿)。 > **定位**:本文件是前端 UI 的**设计规范基线**(配色 / 字体 / 圆角 / 布局 / 组件 / 交互),供新增页面、调整样式时对照,避免重复踩坑。 diff --git a/dsh-server-docs/07-实例UI分区登记表.md b/dsh-server-docs/01-规范/07-实例UI分区登记表.md similarity index 92% rename from dsh-server-docs/07-实例UI分区登记表.md rename to dsh-server-docs/01-规范/07-实例UI分区登记表.md index aa6ffb4..9d859d7 100644 --- a/dsh-server-docs/07-实例UI分区登记表.md +++ b/dsh-server-docs/01-规范/07-实例UI分区登记表.md @@ -13,7 +13,7 @@ ## 一、实例「设置」面板的分区总表(现行) -> **本表由 `node scripts/find-ui.mjs --md` 生成**(扫的是**活着的 profile + 官方包**,不是仓里快照)。 +> **本表由 `node 07-scripts/find-ui.mjs --md` 生成**(扫的是**活着的 profile + 官方包**,不是仓里快照)。 > ⛔ **改分区 ⇒ 同一次改动里刷新本表**。下表的 label 是从代码里**解码**出来的(含 `\uXXXX` 转义形态)。 | order | id | label | 提供者 | 来源 | 源码 / 可改性 | @@ -62,7 +62,7 @@ |---|---|---|---| | 1 | 标签用 **`\uXXXX` 转义**存(`"\u7528\u6237\u8bbe\u7f6e"`),按 UTF-8 搜永远 0 命中 | **代码侧小缺陷**(可维护性) | 记入本表 §二 第 3 条;⛔ 新增文案**建议直接写中文**(官方 bundle 亦混用,但可 grep 的优先) | | 2 | **源码没入仓**:`portal-entry` 不在 `poc/` 里,线上只有 tgz ⇒ 只能从产物反推 | **真结构缺口** | ✅ 已把 0.5.3 源码补进 `poc/portal-entry/`;**约定:产物只从仓内源码构建** | -| 3 | **没有"哪个包提供哪个 UI"的索引**,id/order/label 散在 9 个包里 | **真结构缺口** | ✅ 本文件(`07-实例UI分区登记表.md`) | +| 3 | **没有"哪个包提供哪个 UI"的索引**,id/order/label 散在 9 个包里 | **真结构缺口** | ✅ 本文件(`01-规范/07-实例UI分区登记表.md`) | | 4 | 集群切换后"实例侧实测"变难(新用户落 `w-106`、47→106 无 SSH 路由) | 环境复杂度 | 见 `PLAYBOOK §9.1`;新用户落点见 `03-路线图` | --- @@ -77,7 +77,7 @@ | 根因 | 机制 | 怎么用 | |---|---|---| -| 找不齐「谁提供哪个 UI 分区」 | **`node scripts/find-ui.mjs [关键词] [--md] [--grep]`** —— 扫活 profile + 官方包,一次给全:id / order / label(**含转义解码**)/ 提供者 / 源码路径 / **能不能改** | 改任何实例 UI 前**先跑它**(本来要 30 次 grep 的事 → 1 条命令) | +| 找不齐「谁提供哪个 UI 分区」 | **`node 07-scripts/find-ui.mjs [关键词] [--md] [--grep]`** —— 扫活 profile + 官方包,一次给全:id / order / label(**含转义解码**)/ 提供者 / 源码路径 / **能不能改** | 改任何实例 UI 前**先跑它**(本来要 30 次 grep 的事 → 1 条命令) | | 中文文案搜不到(`\uXXXX` 转义) | 由 `find-ui.mjs` 内建(**两种形态都搜**);`PLAYBOOK §9.2` 也记了反推转义的方法 | 手写 grep 时记得两种形态都试 | | 自研 bundle 源码不在仓里 | **约定:`poc//` 是唯一源码位置,产物只从仓内源码构建**;`portal-entry` 已补入 | 发现某自研包不在 `poc/` ⇒ 先从部署产物取出入仓,再改 | | 改完忘了同步断言 / 文档 | `npm run verify`(含 `verify-my-skills` / `verify-portal-entry` 的结构断言)+ **本表** | 每次改动收尾跑一次 | diff --git a/dsh-server-docs/01-规范/08-插件开发与对接规范.md b/dsh-server-docs/01-规范/08-插件开发与对接规范.md new file mode 100644 index 0000000..303d3d3 --- /dev/null +++ b/dsh-server-docs/01-规范/08-插件开发与对接规范.md @@ -0,0 +1,216 @@ +# 08 · 插件开发与对接规范 + +> **读者**:写插件的会话(自研 / 第三方 / 端侧同源)—— 提案、开发、交付时都按本文。 +> **维护方**:插件投放与分库线(平台侧)|**落文** 2026-09-23|**长期维护**(跟改本文件即可) +> **上位权威(冲突时以它们为准)**:`03-数据库/DB-03-插件数据面规范.md`(数据面)· `05-交接单/插件投放与分库线-01共享只读包库与插件数据面.md`(投放链路)· `DEPLOY-本部署.md §6.4`(建库权限) +> **本文只写「插件侧必须照做的契约」**;平台内部实现(怎么物化、怎么路由)不在此复制。 + +--- + +## §0 一句话 + +插件只做三件事:**把声明放对位置**(可选)、**用一个函数取目录**、**把包交上来**。 +其余全部在平台侧 —— 装到哪台机、怎么装、库怎么建、凭据怎么投,插件**一概不管、也不许假设**。 + +## §1 开工四问(先答,答案决定后面走哪条路) + +| # | 问题 | 怎么判 | 结论 | +|---|---|---|---| +| 1 | 有「使用中数据」要持久化吗? | 换个实例/重启后必须还在的数据 | **有** ⇒ 走 §3;**无** ⇒ **一个字都不写**(§3-0) | +| 2 | 有文件产物吗? | 缓存在实例本地、丢了可重建 | 落 §4 目录;⛔ 不用 `homedir()` | +| 3 | 需要凭据吗? | 连外部服务要 token / key | 与 §4 同目录、**平台或运维投递**,插件**只读** | +| 4 | 装到哪台机? | —— | **平台决定**(Manager / worker 都可能)⇒ ⛔ 插件不得写死本机路径 | + +## §2 包形态(硬要求) + +- **打包**:`npm pack` 出 `.tgz` 交付;包名用 scope 形态(现网为 `@dsh-local/`)。 +- **`package.json` 三块**(`dsh` 字段),缺哪块就没有对应能力: + +```json +{ + "name": "@dsh-local/dsh-plugin-", + "version": "0.1.0", + "type": "module", + "main": "lib/index.js", + "dsh": { + "bundle": { "patch": "./cordis.patch.yml" }, + "client": { "platform": "web", "inject": [] }, + "data": { "schema": "./dsh.data.yaml" } + }, + "peerDependencies": { "@deepseek-ai/dsh": "^0.1.5-rc.1" }, + "dependencies": {} +} +``` + +- `dsh.bundle.patch` / `dsh.client` 与既有插件同族;`dsh.data` **仅当有数据面时写**(§3)。 +- **依赖**:`dependencies` 尽量留空;⛔ 不许把数据库驱动(`pg` / `better-sqlite3` / `node:sqlite`)、Redis / MQ 客户端打进包。 +- **体积与内存**:实例**硬顶 1024 MiB**、V8 堆按配额推导 ⇒ ⛔ 不引入重依赖、不常驻大内存结构。 +- ⛔ **不许改 `@deepseek-ai/dsh-*`**(平台红线 R2,官方核心零改动);⛔ 不许要求平台"给某用户铺 profile"(投放只有候选池 → 用户自助启用一条路)。 + +## §3 数据面(**有持久化数据才看本节**) + +### §3-0 第一步:先判「有没有」 + +**没有使用中数据**(业务数据在远端系统 / 只做工具面转发 / 凭据由平台投递)⇒ **不要在 `package.json` 里加 `dsh.data`,也不要放 `dsh.data.yaml`**。 + +三条后果必须知道: + +1. 无 `dsh.data` ⇒ 内核判 `level:'none'` ⇒ **数据面门禁直接放行**,插件可正常发布与启用。 +2. 若写一个**空声明**(`tables: []`)⇒ 判 `invalid` ⇒ 状态**恒为 `blocked`**,**永远发不出去**。 +3. 只在包内放一个 `dsh.data.yaml` 但 `package.json` 里不引用它 ⇒ **根本不会被读取**(平台**不自动探测**该文件)。 + +### §3-1 声明放哪(唯一入口) + +**唯一入口 = `package.json` 的 `dsh.data.schema`**,两种写法等价: + +- **内联对象**:`"schema": { "schemaVersion": 1, "tables": [ … ] }` +- **包内路径**:`"schema": "./dsh.data.yaml"`(字符串,只接受包内 `.yaml` / `.yml` / `.json`;⛔ 不许 `../` 逃出包) + +> ⚠️ **口径澄清(2026-09-23)**:`DB-03 §三` 与平台回复里说的「插件在 `im.data` 上声明表」,指的是**运行时对象的措辞**;**声明的存放位置**只认 `dsh.data.schema`(源码 `src/db/plugin-data/schema.ts:701-746` 现读)。按本文写,不要按别的写法试。 + +### §3-2 声明模板(可复制) + +```yaml +schemaVersion: 1 # ≥1 的整数;库结构一变就 +1(只增不减) +tables: + - name: character_sheet # 实际表名 => p__character_sheet + scope: room # room | user(必填,归属列强制) + columns: + - { name: pc_name, type: text, notNull: true, maxBytes: 128 } + - { name: sheet, type: json, notNull: true, maxBytes: 16384 } + - { name: sheet_rev, type: integer, default: 1 } + indexes: + - { columns: [pc_name], unique: true } +``` + +### §3-3 校验器实际会拒的东西(照抄避免返工) + +| 项 | 规则 | +|---|---| +| `schemaVersion` | 必须 **≥1 的整数** | +| `tables` | **1 ~ 20** 张;表名 `^[a-z][a-z0-9_]{0,40}$`;⛔ 不许重名 | +| `scope` | **必填**,只能 `user` 或 `room`(缺 ⇒ 拒) | +| `columns` | 每表 **1 ~ 20** 列;列名同表名正则;⛔ 不许重名 | +| 类型 | 仅 9 种中性类型:`text` `bigtext` `integer` `bigint` `real` `boolean` `json` `timestamp` `uuid` | +| `json` 列 | `maxBytes` **必填** | +| `indexes` | 可选;`columns` 必须是**本表已声明**的列 | +| `pluginId` | ⛔ **不要在声明里写** —— 由**包名**推导(去 scope、小写、非 `[a-z0-9_]` 折 `_`),写了也不生效 | + +**内核自动补齐**(⛔ 不用也不能自己写):库名 `dshs_pl_`、表名前缀 `p__`、**归属列**(`user_id` / `room_id`)、审计列 `created_at` / `updated_at`。 + +### §3-4 运行时读写(SDK 面) + +- 读写一律走内核数据 API:`im.data.table(name).insert / update / delete / find / count`; +- 同库内多表原子:`im.data.tx([...])`(⛔ **跨库无事务**,PG 跨 database 没有原生事务); +- 归属由内核强制:`scope: user` 只能读写**自己**的行;`scope: room` 复用房间成员判定。 + +### §3-5 迁移:只增不减 + +- **允许**:加表 / 加列(带默认值)/ 加索引 ⇒ `schemaVersion` +1。 +- **⛔ 一律拒**:删列 / 改类型 / 重命名 ⇒ 迁移预演 `executable=false` ⇒ 执行返回 **`409 plan_blocked`**,库**一字不动**。 +- 🔴 **两个版本号是两件事**:包 `version`(代码版本)与 `schemaVersion`(库结构版本)—— 判「库该不该动」**只看 `schemaVersion`**。 +- 平台在**非首次建库**的迁移前会先做 `pg_dump --schema-only` 结构备份;备份失败 ⇒ **不执行迁移**(`500 schema_backup_failed`)。 + +### §3-6 红线(违反即不予启用) + +⛔ 不直连数据库|⛔ 不写原生 SQL(含触发器/存储过程)|⛔ 不访问别的插件库|⛔ 不自组跨库事务|⛔ 不绕过归属判定|⛔ **不以实例本地库 / 本地文件当数据权威**(可当缓存,权威必须在插件库)。 + +**机器判据**(提包前自己跑,必须零命中): + +```bash +grep -rE "CREATE TABLE|ALTER TABLE|DROP TABLE|better-sqlite3|from 'pg'|require\('pg'\)" <包目录> +``` + +## §4 文件落点(D4) + +**落点 = `$DSH_HOME/.dsh/plugins//`**(平台侧同一绝对路径 = `/var/lib/dshs/users//home/.dsh/plugins//`;实例内**不重映射**)。 + +**取目录只有一种姿势**: + +```js +const base = process.env.DSH_HOME // 平台注入,= /home,必存在 +if (!base) throw new Error('DSH_HOME 缺失:无法定位插件数据目录') // fail-fast +const dir = join(base, '.dsh', 'plugins', 'dsh_plugin_') // ,与库名同一个 token +``` + +- ⛔ **禁止 `homedir()` 回退**:实例内 `HOME` = `/ws`,而 `ws` 会被平台按产物清理 ⇒ 回退不是"降级可用",是**把数据写到会被清掉的位置**。 +- ⛔ **不要去找 `im.paths.pluginData()`** —— **该 API 不存在**(2026-09-23 实测:源仓 `src/`、宿主仓 `deepseek-harness`、`node_modules` 三处零命中)。 +- **目录自己建**:插件首次用时 `mkdir -p`(幂等)。平台侧**不碰**该目录(用户 home 的托管文件名白名单只有 3 个裸文件名)。 +- **凭据 / 投递类文件与插件数据同目录**(同一清理单元)、文件 `0600`、属主 = **该用户 uid**;插件**只读**,⛔ 不覆盖投递来的文件名。 +- ⚠️ **跨机**:`` 是**那台机上的**路径。用户实例在 worker 上时,在管理机写 `/home/…` = **静默空操作**(平台已踩过)⇒ 投递必须发生在**实例所在那台机**。 +- **实例内其他已知事实**:共享包库 `/var/lib/dshs/bundled-plugins/` 是**只读**挂载(`touch` ⇒ `Read-only file system`);`HOME` = `/ws`、`DSH_HOME` = `/home`。 + +**`pluginId` 速查**:`@dsh-local/dsh-plugin-carbon` ⇒ `dsh_plugin_carbon`(⛔ 不是 `carbon`)。目录名与库名后缀**同一个 token**。 + +## §5 投放链路(谁做什么) + +``` +admin 上传 tgz + └─ 平台侧物化:解包 → chown -R root:root → pnpm install --prod(store 在共享层内) + ⇒ /var/lib/dshs/bundled-plugins/<扁平包名>/ ← 节点级「共享只读包库」(D2) + └─ 检测三项:安全 + 兼容 + **数据面声明校验** ⇒ 通过 = pending + └─ admin 点 [建库](仅"有数据面"的插件需要)⇒ CREATE DATABASE dshs_pl_ + └─ [发布] ⇒ 用户才能启用 + +用户(任意登录用户)在实例「功能管理」点启用 + └─ POST /api/plugins/mine/apply ⇒ 路由到实例所在那台机 + └─ worker: POST /plugins/apply ⇒ profile 写依赖引用 "pkg": "link:<共享层绝对路径>" + └─ pnpm 建 node_modules/ 链接 ⇒ 共享层**实体一份**、用户侧只有链接 +``` + +**插件侧要做的只有**:把包交上来(并说明"有无数据面""是否需要凭据")。⛔ 上传 / 建库 / 发布 / 启用**都不是插件侧动作**。 + +**D2 判据**:启用后用户侧 `node_modules/` 必须是**链接**、用户目录下**零实体**(`link:` 协议,2026-09-23 落地)。 + +⚠️ **只进池不发布 ⇒ 用户启用必撞 `plugin_bundle_missing`**(装配目标就是共享层)—— 这是平台侧操作顺序问题,插件侧只需知道这个错误码含义。 + +## §6 错误码表(看到什么该判给谁) + +| 错误码 / 现象 | 含义 | 归属 | +|---|---|---| +| `plugin_bundle_missing` | 包不在共享层(没上传/没发布/该节点未同步) | **平台侧**(含跨节点内容分发缺口) | +| `datastore_not_ready` | 有声明但未建库 / 版本不满足。⚠️ **无数据面插件不该出现它** | 平台侧(admin 未建库) | +| `invalid_plugin_db_name`(400) | 库名不合规(正常不该发生,由包名推导) | 平台侧 | +| `409` 检测不过 | 安全 / 兼容 / 声明校验未过(**声明非法会在一次响应里列全**) | **插件侧**(改声明或改包) | +| `409 plan_blocked` | 迁移含删列 / 改类型 / 重命名 | **插件侧**(改 schemaVersion 策略) | +| `500 schema_backup_failed` | 迁移前结构备份失败 ⇒ 未执行迁移 | 平台侧(运维) | +| `PG_CREATEDB_MISSING`(503) | 平台未授 `CREATEDB` | 平台侧(运维,见 `DEPLOY-本部署.md §6.4`) | +| 插件在实例内 `EACCES` / `EROFS` | 写到只读区(共享层)或路径不对 | **插件侧**(照 §4 取目录) | + +## §7 提包前自测清单(插件侧自己跑完再交) + +1. `grep` 红线机器判据 **零命中**(§3-6)。 +2. 有数据面 ⇒ 声明通过 §3-3 全部规则;无数据面 ⇒ **确认 `package.json` 里没有 `dsh.data`**。 +3. `npm pack` 后 `tar -tzf | head` 确认包根有 `package.json`、`lib/`、`cordis.patch.yml`(有 client 的还有 `lib/client.js`)。 +4. `dsh` 三块字段形态与 §2 一致(`bundle.patch` / `client` / 按需 `data`)。 +5. 交付时**一并带上**:tgz + `sha256` + 声明快照(或"无数据面")+ 上述自测输出。 + +## §8 端侧边界 + +端侧(桌面壳 / 浏览器)访问**同一个平台 API**: + +- ✅ 只保证「能看见 + 能开通」;⛔ **不要求端侧下载或安装插件包**。 +- ⛔ **不下发平台级凭据到客户端**(客户端只持有该用户自己的会话)。 +- ⛔ 端侧**不落插件数据**;插件数据在服务端该用户的主目录与插件库。 + +## §9 回报格式(交给平台侧 / 对接方时) + +必须带齐,否则平台侧无法定位: + +1. 包名 + 版本 + `sha256`; +2. 「有数据面 / 无数据面」明确写一句;有则附声明快照; +3. 失败时**把错误码与文案原样贴回**(⛔ 不要转述、不要只写"失败了"); +4. 涉及的用户 / 实例标识(userId、hostId),以及**看到现象的时间点**; +5. 自测清单(§7)的输出。 + +## §10 别做清单(一次看全) + +⛔ 改 `@deepseek-ai/dsh-*` ・⛔ 要求平台铺 profile ・⛔ 自己上传 / 建库 / 发布(那不是插件侧动作)・⛔ 走 handoff / 打开 `DSHS_ENABLE_PATCH` ・⛔ 扩大权限面 ・⛔ 引入外部服务进程(Redis / MQ)・⛔ 写裸 SQL / 直连 DB ・⛔ 以本地库为权威 ・⛔ 用 `homedir()` 拼路径 ・⛔ 提交空数据面声明 ・⛔ 假设实例在哪台机。 + +--- + +## §11 变更记录 + +| 日期 | 变更 | 依据 | +|---|---|---| +| 2026-09-23 | 首版:包形态 / 数据面(含"零数据面不要写空声明"与声明唯一入口澄清)/ 文件落点 D4 / 投放链路 / 错误码 / 自测清单 / 边界 | 现读源码 `src/db/plugin-data/schema.ts`、`src/web/routes/business-plugins.ts`、`src/fs/user-fs.ts`、`src/supervisor/orchestrator.ts`;`DB-03`;carbon 线两问裁定(`dsh-plugin-carbon/对接文档_carbon插件-平台侧两处阻塞_20260922.md §L`) | diff --git a/dsh-server-docs/01-规范/09-IM插件SDK与扩展点契约.md b/dsh-server-docs/01-规范/09-IM插件SDK与扩展点契约.md new file mode 100644 index 0000000..30d47a7 --- /dev/null +++ b/dsh-server-docs/01-规范/09-IM插件SDK与扩展点契约.md @@ -0,0 +1,134 @@ +# IM 插件 SDK 与扩展点契约 + +> 面向要给 IM 房间加场景能力的插件作者。内核只提供通用地基,**场景差异全部下沉到这里**。 +> 依据:`05-交接单/IM群组-04-插件SDK与扩展点契约.md`|需求基线:`04-调整方案/142-IM群组对话-需求基线与方案.md §3.6` + +## 1. 这是什么 + +IM 房间内核(`src/im/{types,db,store,clock,hub,ws}.ts`)只定义**消息信封**(`id` / `seq` / `ts` / +作者 / `via` / `visibility`)与房间生命周期。办公群的任务卡、MUD 的指令流、跑团的回合制, +**都不写进内核** —— 写法是注册一个插件,声明它要用到哪些扩展点。 + +SDK = `src/im/sdk/`: + +| 文件 | 内容 | +|---|---| +| `types.ts` | 七个扩展点的契约类型 + 两档传输 + 数据面声明 | +| `host.ts` | 宿主实现:注册面、超时熔断、预算继承、声明式建表、审计 | +| `index.ts` | 对外 Facade(插件 `import` 这一个就够) | + +## 2. 七个扩展点 + +| # | 扩展点 | 契约 | 一句话 | +|---|---|---|---| +| 1 | 房间类型注册 | `RoomTypeContract` | 声明 `room_type` + 默认配置 + 走哪一档传输 | +| 2 | 消息载荷 | `PayloadContract` | 插件自己定义 `payload` 结构与校验 | +| 3 | 成员角色 | `RoleContract` | 业务角色(DM / 玩家 / 负责人);⛔ 不提升内核权限 | +| 4 | 发言规则 hook | `SpeakRuleContract` | 自由并发 / 回合制 / 限速 | +| 5 | 能力机器人 | `BotContract` | 注册 bot + @ 路由(形态 A 的来源) | +| 6 | 事件订阅 | `EventSubscriber` | **拨出式**:宿主调插件,⛔ 插件不开监听端口 | +| 7 | 房间内 UI | `PanelContract` | 声明面板并挂 dsh 既有 slots | + +**无插件 ⇒ 默认自由并发**:不注册发言规则时,房间行为与改造前一致,⛔ 不报错。 + +## 3. 三条平台保护约束 + +写在契约里、由宿主强制执行 —— 不是文档约定: + +1. **插件不直连 DB**:要读写数据只能经注入的 `ImDataPort`(`insert` / `update` / `remove` / + `find` / `count`)。插件侧代码里出现 SQL 即违约。 +2. **插件故障不拖垮房间**:每个扩展点**必须**声明 `timeoutMs` + `circuit`(缺一注册不通过)。 + 回调挂起 ⇒ 超时记失败并把该次交给内核默认行为;连续失败 ⇒ 开熔断,冷却期内不再调用插件。 +3. **插件能力吃预算**:发言判定走**内核**提供的 `ImBudgetPort`。插件只能申请,⛔ 不得自带一套限速。 + +## 4. 写一个插件 + +最小形状 = 一个 `manifest` 对象 + 一个 `apply(ctx, host)`: + +```js +export const manifest = { + pluginId: 'im_plugin_demo', // = 包名去 scope、小写、连字符转下划线 + label: '示例', + packageName: '@dsh-local/im-plugin-demo', + version: '0.1.0', + + roomTypes: [{ + type: 'demo', label: '示例房', + transport: 'chat', // 或 'realtime' + defaultConfig: { topic: '' }, + timeoutMs: 50, circuit: { threshold: 5, cooldownMs: 30_000 }, // 🔴 必填 + }], + + payloads: [{ + kind: 'ping', + validate: (p) => typeof p?.text === 'string', + timeoutMs: 50, circuit: { threshold: 5, cooldownMs: 30_000 }, + }], + + tables: [{ + name: 'pings', scope: 'room', schemaVersion: 1, + columns: [{ name: 'text', type: 'text' }], // 归属列 room_id / user_id 由内核自动补 + }], +} + +export function apply(ctx, host) { + host.register(manifest) + ctx?.on?.('dispose', () => host.unregister(manifest.pluginId)) +} +``` + +打包与投递**复用既有通道**(`cordis.patch.yml` + `package.json#dsh` → 门户候选池 → +实例「能力管理」自助启用)。⛔ SDK 不新造分发机制。 + +## 5. 两档传输 + +| 档 | `fanoutBatch` | `heartbeatMs` | `maxJitterMs` | `latencyBudgetMs` | +|---|---|---|---|---| +| `chat`(档案 107 口径) | 200 | 25 000 | 1 000 | 1 000 | +| `realtime`(档案 108 游戏口径) | 50 | 5 000 | **20** | 100 | + +房间类型声明自己走哪一档;**两档参数不同源**,⛔ 不是同一份参数改个名。 +未声明 ⇒ 回落 `chat`(⛔ 不默认给实时档)。 + +## 6. 数据面 + +**判落点,再谈建表**(`03-数据库/DB-03-插件数据面规范.md`): + +| 数据种类 | 落点 | 归属列 | +|---|---|---| +| 跟着**房间**走的(角色卡 / 地图 / 任务卡) | 该插件自己的库 `dshs_pl_`,表 `p__*` | `room_id` | +| 只属于**某用户自己**的 | 同一个库 | `user_id` | +| **缓存 / 临时**(丢了可重建) | 实例本地 | — ⛔ 不得当权威 | + +规则: + +- **建表 = 声明式**:字段只用中性类型(`text` / `integer` / `bigint` / `real` / `boolean` / + `json` / `timestamp` / `uuid`),内核翻译成 sqlite + pg 两套 DDL。⛔ 禁裸 `CREATE TABLE`、 + 厂商专有类型、触发器、存储过程、跨前缀 `JOIN`。 +- **迁移只增不减**:`schemaVersion` 必填;只允许加列(带默认值)/ 加表 / 加索引。 + 任一步失败 ⇒ **拒绝启用**(⛔ 不做半迁移)。 +- **访问必走内核 API**:房间维度**自动注入 ACL**(复用 `canSee`);跨前缀 / 内核表 + **看不到也查不了**;事务仅限同前缀多表。 +- **卸载数据默认保留 30 天**;`DROP TABLE` 不可逆 ⇒ 需 admin 显式确认 + 先出受影响清单。 + +## 7. 三个示例插件 + +`poc/business-plugins-im/` 下三类场景各一个,可直接照抄: + +| 目录 | 包名 | 类型 | 演示的扩展点 | +|---|---|---|---| +| `office-tasks/` | `@dsh-local/im-plugin-office` | `office`(聊天档) | 任务卡载荷 + 面板 + 事件落库 | +| `mud-rate/` | `@dsh-local/im-plugin-mud` | `mud`(**实时档**) | **限速**发言规则 + 指令流载荷 | +| `trpg-turn/` | `@dsh-local/im-plugin-trpg` | `trpg`(**实时档**) | **回合制**发言规则 + 业务角色 + 骰子载荷 | + +## 8. 怎么验 + +```bash +export PATH="E:/ProgramData/.workbuddy/binaries/node/versions/22.22.2-3:$PATH" +npm run build +node --test test/im-sdk.test.mjs # 期望:退出码 0(66 用例全过) +git diff -- src/im/store.ts src/im/hub.ts src/web/routes/im.ts # 期望:无输出 +``` + +最后一条是**硬判据**:非聊天场景跑通**不改内核**(142 §五-6)。若必须改内核 ⇒ 扩展点没设计对, +回头改扩展点设计,⛔ 不是改内核。 diff --git a/dsh-server-docs/02-架构设计/README.md b/dsh-server-docs/02-架构设计/README.md new file mode 100644 index 0000000..e41dea0 --- /dev/null +++ b/dsh-server-docs/02-架构设计/README.md @@ -0,0 +1,45 @@ +# 架构设计(定稿) + +> **一个目录装全部架构文档**,文件名标明是哪个架构。每份都是「**推演完成、结论收敛、可直接据此施工**」的成品。 + +## 与 `04-调整方案/` 的分工(🔴 别混) + +| | `04-调整方案/` | 本目录 `02-架构设计/` | +|---|---|---| +| **装什么** | **过程** —— 调研、推演、取证、权衡、待拍板 | **成品** —— 收敛后的架构设计 | +| **形态** | 逐个问题一档,编号递增(100–150…),记录「当时怎么想的」 | 按**架构**成篇,文件名=架构名 | +| **会不会过时** | **会** —— 需求变则结论变 | **不轻易变** —— 只有架构本身变了才改 | +| **读者** | 追根因、查证据、看权衡过程 | 照此施工、对外介绍、新人理解全局 | +| **可否删** | ⛔ 不可(是历史与判据来源) | ⛔ 不可(是当前唯一权威) | + +**判据一句话**:**想知道「为什么这么做」→ 查 `04-调整方案/`;想知道「现在该做成什么样」→ 查本目录。** + +## 命名约定 + +`<对象>-<架构名>.md`。例:`覆盖网络-顶层架构全貌.md`。 + +- **对象** = 覆盖网络 / 平台 / 插件体系 / 客户端 … +- **架构名** = 这份文档讲的那个架构,**看文件名就知道该不该打开** +- ⛔ 不用编号前缀(编号属过程档案),本目录内**按文件名排序**即可 + +## 当前内容 + +| 文档 | 讲什么 | 定稿 | +|---|---|---| +| `覆盖网络-顶层架构全貌.md` | 两图分离 · 三层权威 · 多根与角色升格 · 缺口与推进序 | 2026-09-21 | +| `数据-分库与权威存储架构.md` | 一插件一库 · 归属列与按用户迁移 · 文件入桶 · S0–S5 路线 | 2026-09-22 | + +## 来源(过程依据) + +本目录内容**不是新写的**,而是从过程档案收敛而来,每条结论都能回溯到带 `file:line` 的证据: + +- **103–117** —— 可行性、全球架构复盘、九大瓶颈、骨干层、百台/千台推演、游戏专项、答疑、补遗、传输方案取舍、应用场景、插件化判断、问题逐条推演、参数表 +- **118–120** —— 会合中继拆分、集群化 Manager/Worker、跨节点迁移 +- **133 / 136** —— 低熵块治理、控制面按两台中继取并集 +- **145 / 146 / 147** —— 会话并存与设备来源维度、中继 per-port 额度、多节点插件投放 +- **148** —— 多 Manager 多区域联邦形态(**首份提出元假设问题**) +- **149** —— 顶层架构全貌(信任图/数据图分离;修正 148 两处过强表述) +- **序③ 交接单** —— 四层密钥模型落地(离线根 → 在线签名者 → 每机节点密钥 → 会话) +- **数据分库** —— 用户拍板 2026-09-22(「分库 = 不同插件建不同库」+「用户数据入库以便备份与迁移,实例本地只留文件与缓存」);取数基线 = 服务器实测(47 / PG **13.23** / 单库 `dshs` / 13 张内核表 / 迁移 v13)+ `src/db/index.ts:19-23`(单库单适配器)+ `src/fs/user-fs.ts:113/126`(home 面无二进制通路) + +⚠️ **本目录与过程档案冲突时,以本目录为准**;若发现冲突,请在过程档案头部加状态块指向此处,⛔ 不要直接改过程档案正文。 diff --git a/dsh-server-docs/02-架构设计/数据-分库与权威存储架构.md b/dsh-server-docs/02-架构设计/数据-分库与权威存储架构.md new file mode 100644 index 0000000..18458ed --- /dev/null +++ b/dsh-server-docs/02-架构设计/数据-分库与权威存储架构.md @@ -0,0 +1,136 @@ +# 数据 · 分库与权威存储架构 + +> **状态**:✅ 定稿(2026-09-22)| 落点:`dsh-server-docs/02-架构设计/` +> **解决什么**:平台数据「怎么分库、谁是权威、怎么备份、怎么迁移」——每个插件一个独立库,用户数据全部入库,实例本地只留文件与缓存。 +> **依据(用户口径,2026-09-22 两次拍板)**: +> ① 「我认为数据需要分库,并且用户数据也需要保存到库中方便备份和用户迁移,用户本地只保存文件相关内容(后续除了缓存和临时文件,主要文件内容也需要储存在对象存储中)」; +> ② **澄清分库粒度**:「我说的分库是指**不同的插件建立不同的库**」,且这些库**统一跑在 PostgreSQL 13.23** 上。 +> **取数基线**:服务器实测 2026-09-22 07:4x(47 = PG **13.23**,单库 `dshs`,13 张表全为内核表,`p_*` 零命中,迁移 v13)| 代码 `src/db/index.ts:19-23`(单库单适配器)· `src/fs/user-fs.ts:113/126`(home 面无二进制写入通路)。 +> ⚠️ 与 `03-数据库/DB-00…DB-03` 的关系:**本文是架构定稿,那份是接入规范**;冲突时以本文为准,并回改规范。 + +## 〇、一句话 + +**一个插件一个库(`dshs_pl_`),全部建在同一个 PostgreSQL 13.23 实例上;用户数据靠「库=插件 + 表内归属列」两层承载,"按用户备份迁移"由统一迁移器遍历归属列实现。** + +## 一、分库的口径(先把定义钉死) + +| 项 | 取值 | +|---|---| +| **分库维度** | **插件** —— 不同插件建不同库(⛔ **不是**每用户一库) | +| **载体** | **同一个 PostgreSQL 13.23 实例**(47 上,`127.0.0.1:15432`);⛔ 不新建实例、不换版本、不按插件拆实例 | +| **库数量** | = 插件数量(十几到几十量级)⇒ **连接与 catalog 成本可控** | +| **用户维度** | 落在**库内**:每张业务表带归属列(`user_id` / `room_id`),⛔ 不作为分库维度 | +| **SQLite 形态** | 一个插件一个文件(命名空间与文件都能承载"库",双后端同构仍成立) | + +🔴 **一处更正**:旧顾虑「插件分库会导致连接池爆炸」**只在按用户分库时成立**(库数 = 用户数)。按插件分库时库数 = 插件数,量级小一个数量级 ⇒ **分库在插件维度上是可承受的**。本文按插件维度设计。 + +## 二、为什么这个粒度合适(与另两种分法对比) + +| 分法 | 库数量 | 连接成本 | 隔离 | 卸载插件 | 按用户迁移 | +|---|---|---|---|---|---| +| **按插件(本文)** | 十几~几十 | 可控(每库小池) | **硬**(跨库无原生 join/事务) | **干净**(`DROP DATABASE` 一库带走) | 需归属列 + 迁移器(§四) | +| 按用户 | 用户数(几十~上千) | 随用户数线性涨 | 硬 | 需逐库清理该插件表 | 天然 | +| 按归属三层(控制面/用户/共享) | 3 | 最低 | 弱(同库内靠前缀) | 需前缀扫描清理 | 天然 | + +⇒ 按插件分库的收益集中在**插件隔离与卸载**;代价是"按用户迁移"必须靠约定补偿 —— 这个补偿就是 §四,**它是本设计的前提,不是可选项**。 + +## 三、库清单 + +| # | 库 / 存储 | 装什么 | 谁能写 | +|---|---|---|---| +| ① | **控制面库 `dshs`** | 平台内核:账号 / 实例 / 主机 / 租约 / 域名 / 凭据索引 / 候选池 / 审计 / **平台的用户维度数据**(会话、用户设置、工作区文件索引) | 只有平台;沿用现有库名,**零迁移** | +| ② | **插件库 `dshs_pl_`** | 该插件的**全部**数据:`scope: user` 的行 + `scope: room` 的行,靠归属列区分 | 只有内核数据 API 代写;插件**永远不持有连接** | +| ③ | **对象存储** | 文件内容(工作区文件 / AI 产出 / 图片视频 Excel) | 平台;库里只存 `sha256` + `bucket_key` | + +**为什么插件数据不拆进"用户库"**:插件数据的两种 scope(跟用户走 / 跟房间走)若拆到两个维度,卸载一个插件要跨多库清理,且插件与插件之间无法独立备份/独立提升。⇒ 插件的数据以插件为单位成库,**用户维度靠库内归属列**。 + +## 四、两个诉求的正交(本设计的关键) + +用户有两条诉求,方向正交: +- 诉求 A:**按插件分库** +- 诉求 B:**用户数据要能整体备份与迁移** + +⇒ 用两层承载,各自解决一条: + +1. **层 1(库)= 插件** —— 满足 A。 +2. **层 2(行归属列)= 用户 / 房间** —— 满足 B。 + +🔴 **由此产生一条硬前提**:插件库内**每张业务表必须带归属列**(`user_id` 或 `room_id`),由内核建表时**自动补齐**,插件**不许自建表、不许省略归属列**。 +- 没有归属列 ⇒ 插件库里的行无法按用户切分 ⇒ **按用户迁移直接变成不可能**。 +- 所以「归属列强制」是分库的**前提条件**,写进建表契约(见 `DB-03 §三`)。 + +**统一迁移器**(按用户导出 / 迁移)由此可机械实现: +`控制面库(该 user_id 的行)→ 遍历全部插件库(各库该 user_id 的行)→ 桶(该用户的 blob 清单)` ⇒ 一次遍历、可重入、可校验。 + +## 五、物理形态:database 还是 schema + +| 档 | PG 实现 | 连接成本 | 采用 | +|---|---|---|---| +| **默认** | **独立 database** `dshs_pl_` | 每库一套小池(库数 = 插件数 ⇒ 可控) | ✅ **本文默认** | +| 备选 | 同 database 内 schema `pl_` | 1 套池共享 | 仅在插件数量很大或连接吃紧时降档 | + +**默认选 database 的理由**:① 字面与用户口径一致("不同的插件建立不同的库");② 隔离最硬(PG 原生不允许跨 database join/事务 ⇒ 越权在数据库层就被挡住,不靠代码自觉);③ 卸载干净(`DROP DATABASE` 一次带走全部表,不留残骸);④ 备份单元清晰(`pg_dump `);⑤ 库数 = 插件数,连接成本可控。 +**代价与对策**:连接池随插件数增长 ⇒ 每库池**小且可配**(默认 1–2 条)+ 空闲回收 + 全平台连接上限;需要时可降为 schema 档(同一套代码,只改"库名 → 物理位置"的映射)。 + +## 六、访问通路:插件怎么拿到自己库的数据 + +- 插件**不持有连接、不写 SQL、不建表**;一律走内核数据 API(`im.data` 等)。 +- **库路由由内核按"调用方插件身份"决定**:插件只声明表名,内核把它映射到 `dshs_pl_<该插件Id>`。⇒ 插件**看不到库名、也拿不到别的插件的库**(越权在路由层与数据库层双重阻断)。 +- 按 `scope` 注入 ACL:`user` ⇒ 只读写自己的行;`room` ⇒ 复用房间成员判定。 +- **跨节点**:worker 上的实例经覆盖网络(已建)调 Manager 侧数据面;实例侧保留**读缓存**抵消延迟。写一律回源,⛔ 不做"本地先写、之后补传"。 +- 🔴 **由本条关闭一条旧路**:「插件自带 SQLite 当权威数据」(现 MCN 形态)此后**不允许**。插件可以把本地当**缓存**,但权威必须在插件库。 + +## 七、备份 / 迁移 / 导出 / 注销 + +| 场景 | 动作 | +|---|---| +| 全平台备份 | 控制面库 + 各插件库(`pg_dump` 逐库,可并行)+ 桶 lifecycle | +| **单插件备份 / 回滚** | 只 dump 该插件库 | +| **单用户迁移(跨节点)** | 统一迁移器按归属列抽取(§四)→ 目标节点 restore → 更新控制面库归属记录(租约 / `epoch` fencing 照旧) | +| 用户导出(交付给用户) | 迁移器导出该用户全部行 + 桶内对象清单,转可读格式 | +| 用户注销 | 遍历删除该 `user_id` 的行(各插件库)+ 桶内对象回收;⛔ 不 DROP 插件库(库属插件、不属用户) | +| **卸载插件** | 先出受影响清单(库名 / 表 / 行数 / 体积)→ admin 显式确认 → `DROP DATABASE`(**不可逆**) | + +## 八、迁移路线(S0–S5,每步可独立验收与回滚) + +| 步 | 动作 | 验收 | 回滚 | +|---|---|---|---| +| **S0** | 连接层从单库改**多库路由**;控制面库**沿用现有 `dshs`**;建一个空插件库做样板 | 既有功能零回归(`npm test` 绿);`dshs` 表结构零变化 | 撤路由,回到单库 | +| **S1** | 建 `dshs_pl_` 骨架 + 声明式建表(含**归属列自动补齐**) | 同一声明能在 PG 与 SQLite 上都建库建成;归属列不缺失 | 逻辑删库即可 | +| **S2** | 现有插件数据入库:把实例内 SQLite(MCN 那份)导入对应插件库,**并给出可复用的导入通路**(替代 09-21 的 root `cp` 一次性例外) | 行数/内容对账一致;源文件对账通过后才归档 | 源文件保留 | +| **S3** | 文件索引入库 + 桶接入(内容入桶、库仅存元数据) | 写文件 = 1 桶 PUT + 1 行 UPSERT,幂等;同 `sha256` 不重复入桶 | 桶可关,退回本地 `ws/` | +| **S4** | 实例本地收窄为三类(文件内容 / 缓存 / 临时);实例启动从库 + 桶恢复视图 | 本地清空后实例仍恢复出正确视图 | 保留本地副本一版 | +| **S5** | 落**统一迁移器**(按用户导出/迁移,跨控制面库 + 各插件库 + 桶) | 导出一个用户 → 在干净环境恢复 → 数据对账一致 | 工具化,不动数据 | + +⚠️ 全部步骤**不改**现有 13 张控制面表、**不动** `dsh` 主程序与缓存(项目红线)。⚠️ S2 起涉及现有生产数据搬迁 ⇒ 属**不可逆操作**,动手前先出受影响清单。 + +## 九、风险与未决 + +| # | 风险 | 缓解 | +|---|---|---| +| 1 | **连接池随插件数增长** | 每库小池 + 空闲回收 + 全平台上限;必要时降为 schema 档 | +| 2 | **跨库无事务** ⇒ 一个动作涉及两个插件库时无法原子 | 归属判据前置(§四)+ 幂等重跑 + 补偿动作;⛔ 不靠双写 | +| 3 | **归属列一旦缺失,按用户迁移即失效** | 归属列由内核强制补,验收含断言(`DB-03 §九`);插件自建表 = 拒启用 | +| 4 | **Manager 成为数据面单点**(数据集中在一台 PG) | 实例侧读缓存;后续可支持插件库就近放置(依赖覆盖网络) | +| 5 | **写放大**:文件入桶 + 索引入库 = 两次远端操作 | 批量 + 异步 + 幂等;`sha256` 去重 | +| 6 | **未决**:每插件库的配额口径(行数 / 体积);桶端点与密钥落点(走 `config/platform.env`);连接池参数初值 | 压测标定;随 S1/S3 定稿 | + +## 十、与既有文档的衔接(改哪些) + +| 文档 | 变化 | +|---|---| +| `03-数据库/DB-00-专区入口.md` | 判据速查改为本文 §一/§三;指向本文 | +| `03-数据库/DB-01-接入指南.md` | 情形 2 落点改**插件库**;情形 3 由"实例 home 装数据"降级为**缓存/临时**,用户数据改进库 | +| `03-数据库/DB-02-表结构台账与迭代.md` | 台账按库分开记(控制面库 / 各插件库);新增"库归属"列 | +| `03-数据库/DB-03-插件数据面规范.md` | 落点判据改「一插件一库」;**新增归属列强制条款**;红线新增「不得以实例本地库作权威」;建表 `scope` 增 `user`;管理面按库备份/迁移/导出 | +| `02-架构设计/README.md` | 「当前内容」表新增一行 | + +## 十一、回溯(结论与证据) + +| 结论 | 证据 | +|---|---| +| 现状是单库单适配器 | `src/db/index.ts:19-23`(有 `dbUrl` 走 PG,否则本地 SQLite) | +| 控制面库现状(实测 2026-09-22) | 47 上 PG **13.23**、库 `dshs`、13 张表全为内核表、`p_*` 零命中、迁移 v13 | +| 旧模型是"文件为权威" | `DB-01 §情形 3`(用户数据落实例 home)· `DB-03 §一` 第一张表 | +| home 面无二进制写入通路 | `src/fs/user-fs.ts:126`(3 个白名单裸文件名)· `:113`(文本语义)· `:83`(`upload` 走 `ws/`) | +| 数据分散在多台机器 | 47 与 106 各自持有 `/users//`;实测 106 未承载数据库 | diff --git a/dsh-server-docs/02-架构设计/覆盖网络-顶层架构全貌.md b/dsh-server-docs/02-架构设计/覆盖网络-顶层架构全貌.md new file mode 100644 index 0000000..d2bc605 --- /dev/null +++ b/dsh-server-docs/02-架构设计/覆盖网络-顶层架构全貌.md @@ -0,0 +1,251 @@ +# 覆盖网络 · 顶层架构全貌 + +- 定稿:2026-09-21 +- 状态:✅ **定稿**(可据此施工) +- 来源:档案 148(多 Manager 多区域联邦)+ 149(顶层架构全貌)+ 序③ 四层密钥模型 +- 判据:本文与过程档案冲突时,**以本文为准** + +--- + +## 一、这是什么 + +一个**多区域自治、彼此联邦**的覆盖网络。每台骨干服务器是**自己区域的 Manager**,背后带自己的 Worker / 节点 / 设备;各区域之间是**网状的网**,几百到上千台骨干各自成区、互不隶属。 + +一句话概括形状:**签发是中心化的,校验是分布式的,数据是网状的。** + +## 二、先纠正一个最易犯的错:它是两张图,不是一个结构 + +把这两张图混着讲,怎么画都会画成一棵树。**必须分开。** + +### 2.1 信任图 —— 只在「谁能入网」这一刻生效 + +``` +离线根(可多把,any-of-N) + │ 签 SignerSet(签名者名单)· ⛔ 不签节点 + ▼ +在线签名者(各区 Manager,多把) + │ 签 NodeGrant(节点入网凭据) + ▼ +节点(每机一钥) + └─ 收到对端凭据 ⇒ 用**本机**受信签名者公钥验签 ⇒ 过则通,不过则拒 +``` + +**四个角色与边界**: + +| 角色 | 放哪 | 用途 | 丢失后果 | +|---|---|---|---| +| **根**(离线,可多把) | 用户手里(`0600` 文件 / 纸质恢复码 / 离线 U 盘) | **只**授权·撤销「签名者」 | 最严重 ⇒ 全网重建 | +| **签名者**(在线,多把) | 各区 Manager(`/etc/dshs/overlay-signer-key.pem`) | 签发本区节点凭据 | 换一把即可(根仍在) | +| **节点密钥**(每机一把) | 每台机器(`0600`,属主正确) | 设备身份;对接时证明握有私钥 | 该设备重签 | +| **会话密钥**(内存) | 隧道 | 传输加密 | 无感 | + +⚠️ **根密钥的用途边界**:它**只用于授权 / 撤销「签名者」** —— ⛔ 不签发节点、⛔ 不加密数据、⛔ 不参与会话。⇒ 「根在线」不带来任何性能问题,唯一影响是**安全**与**恢复**。 + +**根离线 ⇒ 不影响任何已入网节点运行**;只影响「新节点入网」与「换签名者」。 + +### 2.2 数据图 —— 运行时常态 + +- 节点之间**直接建隧道**:打洞优先、relay 兜底 +- relay **只转发密文**,不懂业务 +- **❌ 无根、❌ 无中心** —— 根不在数据路径上 +- 一台 relay **可同时承载多张网**(网是**每会话**声明的,不是整机属性) +- 跨网在「能不能拨」这一步就走不到:`dialers: Map>` **默认拒绝** ⇒ **结构性隔离,不是配置约定** + +**🔴 分水岭:不是「有没有根」,而是「根在不在数据路径上」。** 这是它既不是树、也不是纯网状的原因。 + +## 三、三层权威 + +| 权威 | 管什么 | +|---|---| +| **根** | 区域名录 · 用户全局唯一名 · 联邦吊销 | +| **区 Manager** | 本区归属 · 租约 · 资格 | +| **区内 Worker / 节点** | 执行面 · 本机运维库 | + +**判据三句(背下来)**: + +1. 「**跟着用户走**」的数据 ⇒ **留锚定区,⛔ 不跨区同步** +2. 「**全局唯一**」的标识 ⇒ **只在根** +3. 「**区域内唯一**」的状态(归属 / 租约 / 资格)⇒ **只在区 Manager,根 ⛔ 不镜像** + +**明确不做(写下来防止后人当 bug 修)**: + +- ⛔ 用户数据跨区同步 +- ⛔ 区域 PG 互联或双向复制 +- ⛔ 根当区域代理(根不转发用户业务流量) +- ⛔ 区域自取区名 / 自行扩区(**区名必须根分配**) + +## 四、多根与角色升格 + +### 4.1 多个根 —— 已支持 + +受信根是**一个列表**(`DSHS_OVERLAY_ROOT_PUBKEYS`,逗号分隔多值),验签时**遍历受信密钥、命中任意一把即通过**。 + +⇒ **any-of-N(任一即可),不是 M-of-N(多数共识)**。 + +| 面 | 含义 | +|---|---| +| ✅ 好处 | 根丢失 / 某把根保管出问题 ⇒ 其余根照样恢复。实打实的容灾。 | +| ⚠️ 代价 | N 把根里**坏一把,那一把单独就能授权一整批签名者**,其他根拦不住。 | +| 🔴 后果 | **每一把根的保管等级必须一致**(任一把沦陷 = 全网沦陷)。⛔ 不可「一把自留、几把随意外放」。 | + +### 4.2 Manager 可升级为根 —— 成立 + +**根密钥与签名者密钥「同形」** —— 生成函数是同一个(`generateAuthorityKey` 就是 `generateNodeKey` 的别名)。密码学上「根」与「签名者」**没有任何区别**。 + +⇒ 由此推出: + +- **角色不由密钥决定**,由「**公钥被放进哪个清单**」决定:进签名者名单 = 签名者;进受信根清单 = 根 +- **升级 = 只改授权关系,⛔ 不需换密钥** —— 把 Manager 已有的公钥加进根清单并重新下发即可 +- **可降级** —— 从根清单移除该公钥即退回签名者身份,密钥不变 + +**升级路径(三步)**:① Manager 已有签名者密钥与公钥 → ② 离线根把该公钥追加进受信根清单,重新下发到各节点 / relay → ③ 各节点重新装载后,它签出的名单开始被认,与原有根并列生效。 + +### 4.3 信任闭环 —— 解决了「起初没有根」 + +第 1 台**自己造根、自己签自己** ⇒ 第 2 台造自己的根、两把根**互相列入**对方清单 ⇒ **N 台 = N 根并列**。 + +⇒ **不需要提前存在一个「总根」**,联邦可以从任意一台开始自举,根集合随节点数增长。 + +**这正是「没有 ROOT」那个直觉的正确落地形态 —— 不是真的没有根,而是没有唯一的根。** + +### 4.4 🔴 但代价必须写清楚 + +| 风险 | 说明 | +|---|---| +| 🔴 **任一根沦陷 = 全网沦陷** | any-of-N 下,**新加入的根与原有根权限完全相同**。升格动作**把全网安全下限拉到最弱那把根** ⇒ 必须有**准入门槛**,⛔ 不能因「技术上只需加一行」就随意升格。 | +| 🔴 **升格当前不可逆** | **撤根无任何机制**(见 §六 G6)⇒ 一把根泄露**今天没有作废手段**。⇒ **撤根机制是「开放升格」的前置条件**。在此之前,升格只能是**人工、离线、低频**的动作。 | +| ⚠️ **根集合无收敛机制** | 多根 + 可升格 ⇒ 根数量单调增长,而根清单**无容量上限**。⇒ 需定上限与轮换规则。 | + +### 4.5 不建议引入区块链共识 + +信任根是**你自己的**(离线根 + 你自己授权的签名者),不存在需要达成共识的对立方。any-of-N 已经给了容灾,加共识只换来运维复杂度。 + +**真正要补的是「防重放序号」** —— 轻得多,且足够。 + +## 五、设备入网与跨区访问 + +### 5.1 设备入网六步 + +| 步 | 做什么 | 现状 | +|---|---|---| +| ① | 拿到入口(邀请码 / 扫码) | ✅ 已有 | +| ② | 知道有哪些区可加入 | 🔴 **缺**(无区域名录) | +| ③ | 选加入哪个区(需该区审批) | ⚠️ 半成 | +| ④ | 该区签发凭据 | ✅ 已有 | +| ⑤ | 节点**本地**校验 | ✅ 已有 | +| ⑥ | 建隧道(打洞 / relay) | ✅ 已有 | + +### 5.2 跨区访问 —— 数据不动,只有请求动 + +用户在外地登录 ⇒ **路由到锚定区** ⇒ 由锚定区的实例服务,**home 不迁移**。 + +### 5.3 四条入口路径(都要求覆盖) + +| 路径 | 说明 | 现状 | +|---|---|---| +| **A** 新服务器首启 | 显示首管理员设置页(**一次性**,建完管理员即永久关闭)⇒ 建本区或加入他区 | ⚠️ 今天只有 CLI,无 Web 通路 | +| **B** 设备加入已有区 | 邀请码 ⇒ 该区签发 ⇒ 本地校验 | ✅ 机制齐备 | +| **C** 自建独立区(不联联邦) | **不需要任何根**,自成一网 | ⚠️ 今天无通路 | +| **D** 自建区加入联邦 | 需根背书区身份 | 🔴 今天无通路 | + +⚠️ **首管理员设置页上的「有哪些 manager 可选」不可做成公开列表** —— 那是拓扑信息,公开即扩大暴露面。改法:**凭邀请码**才列出该区信息。 + +## 六、缺口登记(按修复顺序) + +### G1 🔴 签名清单无序号 ⇒ 旧名单可重放 + +`SignerSet` / `NodeGrant` **只有 `issuedAt`,没有序号**,而 `issuedAt` 只参与签名、**不参与任何判定**(唯一被读取处是「签发时刻容差」,只查「太新」)。 + +⇒ **一份旧的 `SignerSet`(已撤销的签名者还在名单里)重放给节点,验签会通过** —— 因为签名本身是真的。 + +**今天为何没爆**:签名者名单由管理员下发、节点不主动拉取。联邦化之后「各区名单要常态下发」一旦成立,这条路就敞开。 + +**修法**:加 `serial: number`,节点持久化「已见最大序号」,≤ 则拒。⛔ **不用 `issuedAt` 替代 —— 时钟不可信。** + +🔴 **必须在区域名录之前修** —— 名录本身就是一份待下发的签名清单,会成为重放载体。 + +### G2 🔴 区域名录缺失 + +新机器不知道有哪些区可加入;自建区无法进联邦;区身份无背书载体 —— **三个症状,一个缺口**。 + +**实现建议**:复用地址目录已验证的机制(签名下发 + 本地缓存 + 长 TTL + 失败关闭),**新建独立载荷标签**,独立条目上限。 + +⚠️ **必须与「受信根清单」用两把不同的密钥、两个不同的用途**:那把签「地址目录」(可轮换的下发物),这把签「签名者名单」(身份根)。**⛔ 不要合并成一个变量 —— 合并等于让「能换地址的人」顺带能加签名者。** + +### G3 🔴 用户无区字段 ⇒ 跨区路由无从判断 + +用户表**无区 / 归属 host 字段**,用户名只在单库内唯一 ⇒ 「路由到锚定区」这一步今天无判据。 + +### G4 🔴 四条入口路径中 C / D 无通路 + +见 §5.3。C 与 D 都依赖 G2(区域名录)。 + +### G5 🔴 骨干资格签发权(**待拍板**) + +现行红线是「骨干资格**只能由控制面签发**」,千区下须改写为「**本区 Manager 签发 + 根背书**」。三案见 §八。 + +### G6 🔴 撤根无任何机制 + +吊销清单只能撤**节点**(host 与节点公钥),**撤销签名者**靠「根签一份新名单」,而**撤销根本身无任何机制**。 + +⇒ 多根形态下,一把根沦陷无法被作废 —— 只能逐台改配置重新下发,且 any-of-N 语义下,被撤那把若仍留在任一节点的受信清单里就仍然有效。 + +🔴 **且与 G1 叠加**:撤根的唯一补救是换新名单,而名单无序号 ⇒ 「换新名单」这条路本身也不可靠。⇒ **G1 与 G6 必须一起解决**:序号机制既治重放,也是「作废一把根」的唯一载体。 + +### G7 🔴 根集合无治理规则 + +根集合**无容量上限、无轮换规则、无准入标准**。多根 + 可升格 ⇒ 根数量单调增长。⇒ 需定「上限 / 升级准入条件 / 轮换流程」,且**排在 G6 之后**。 + +## 七、推进顺序 + +| 序 | 项 | 前置 | +|---|---|---| +| **F1** | 区域身份 | — | +| **F1.5** | 🔴 **签名清单序号(G1 + G6)** | — | +| **F2** | 区域名录(G2) | **依赖 F1.5** | +| **F3** | 用户锚点(G3) | — | +| **F4** | 权威归属改写(G5 · **待拍板**) | 🔴 必须在既有红线文档之前拍板,否则返工 | +| **F5** | 跨区级联吊销 | 依赖 F1.5 | +| **F6** | 跨区可见性 | — | +| **F7** | 插件版本真源下沉到区域 | — | +| **F8** | 根集合治理(G7)+ 升格流程 | 🔴 **依赖 F1.5(G6 撤根机制)** | + +**🔴 一句话记法:F1.5 是一切的前置** —— 没有防重放序号,后面每一项下发的签名清单都是可重放的。 + +## 八、待拍板 + +### 8.1 骨干资格签发权(F4 之前必须定) + +**A 严格照现行红线(根签发所有骨干资格)** +优点:与既有约束一致、实现最直接、审计面单一。 +缺点:根成千区瓶颈;根离线时全区无法扩骨干;与「几百上千台」目标冲突。 + +**B 区签 + 根背书**(本文倾向) +优点:根不在关键路径、可扩展到千区、与去中心化方向一致。 +缺点:须改写现行红线、跨区信任传递多一跳、依赖区域名录先落地。 + +**C 折中(日常区签,骨干级仍须根签)** +优点:风险最小、可渐进。 +缺点:两套规则并存、判据分叉、长期维护成本高。 + +### 8.2 根清单落地 + +受信根该放几把、分别谁保管 —— 属凭据类,**须你决定**。 + +### 8.3 跨区可见性默认值 + +倾向**默认互不可见**。 + +## 九、未验证 / 待查 + +1. **G1 重放未实测** —— 「`issuedAt` 不参与判定」是**读码推断**,落地前须**构造真实重放实验**复现。 +2. **生产实际配了几把根** —— 未查(服务器侧)。 +3. **根密钥恢复演练是否在真机跑过** —— 未查。 +4. **区之间是否允许互相签发**(peer delegation)—— 未定。 +5. **升格是否必须离线参与** —— 倾向**是**;若开放自助升格需重新设计。 +6. **根清单上限**取值 —— 未定。 +7. 本文所有「现有实现」结论均来自**读码**(`src/net/relay/` 相关模块),⛔ 尚未全部做真机验证。 + +--- + +**相关**:过程依据见 `../04-调整方案/148`、`149`;密钥模型落地证据见序③ 交接单。 diff --git a/dsh-server-docs/03-数据库/DB-00-专区入口.md b/dsh-server-docs/03-数据库/DB-00-专区入口.md new file mode 100644 index 0000000..68e0b99 --- /dev/null +++ b/dsh-server-docs/03-数据库/DB-00-专区入口.md @@ -0,0 +1,57 @@ +# 数据库专区 · 00 专区入口 + +> 状态:✅ **已落文档库**(2026-09-20)| 专区:`03-数据库/` + +> 目标位置 = 文档库新目录 **`dsh-server-docs/03-数据库/`**(与本 README 同级放置 `01` / `02` / `03`)。 +> 立稿:2026-09-20 | 起草会话 `IM插件数据面-专门文档`## 这是什么 + +**「数据怎么存、存哪、怎么演进」的单一入口。** 平台里凡涉及"要持久化"的事(账号 / 房间 / 插件数据 / 用户文件 / 大对象 / 缓存队列),都先来这里对一遍判据,再动手。 + +**什么时候读**:① 要新增任何持久化结构;② 要接数据库(含插件);③ 要改表结构 / 加迁移;④ 判断"这份数据该放哪一层"。 + +## 目录 + +| 文档 | 回答什么问题 | +|---|---| +| `DB-00-专区入口.md`(本文件) | 专区怎么用、有哪几份、迁入清单 | +| `DB-01-接入指南.md` | **各种情况怎么接**:内核表 / 插件表 / 实例 home / Worker 本地库 / 大对象桶 / 缓存队列(6 种情形逐条给「判据 + 怎么做 + 禁止 + 验收」) | +| `DB-02-表结构台账与迭代.md` | **表结构持续迭代**:迁移台账(v1–v13 现状 + 规划 v14)· 内核表全集 · 三条硬纪律 · 四步跨版本 · 评审门禁 · 台账维护规则 | +| `DB-03-插件数据面规范.md` | 插件那一侧的完整契约(声明式建表 / 迁移 / 访问 API / 配额审计 / 卸载)—— 见本目录 `DB-03-插件数据面规范.md` | + +## 判据速查(2026-09-22 按「一插件一库」更新) + +1. **先判库**:跟平台全局走 ⇒ **控制面库 `dshs`**;跟某个用户走 ⇒ **该插件的库 `dshs_pl_` 内带 `user_id` 的行**;跨用户共享 ⇒ 同库内带 `room_id` 的行;是文件内容 ⇒ **对象存储**(库里只存索引)。 +2. **库名即归属**:控制面库 = `dshs`;插件库 = `dshs_pl_`(每插件一个,同一 PG 13.23 实例);看不到第三种。库内表名 `p__*` = 该插件的表。 +3. **不直连**:插件(乃至任何业务代码)**不持有连接、不写 SQL**,一律走内核 API;**库路由由内核按调用方插件身份决定**。 +4. **归属列强制**:插件库内每张业务表必须带 `user_id` 或 `room_id`(内核构建时自动补)—— 这是「按用户备份 / 迁移」能成立的前提。 +5. **双后端**:任何新结构都要在 sqlite 与 pg 上**都建得起来**(sqlite 下「一个插件 = 一个文件」)。 +6. **只增不减**:加表 / 加列(带默认值)/ 加索引;改类型与删列走"四步跨版本"。 + +## 与既有文档的关系 + +- **`02-架构设计/数据-分库与权威存储架构.md`(2026-09-22 定稿)** ⇒ 分库与权威存储的**架构定稿**(分库维度 = 插件);本专区是它的落地规范,冲突时**以它为准**。 +- **档案 142 §3.6**(三条平台保护:插件不直连 DB / 插件故障不拖垮房间 / 能力吃预算)⇒ 本专区是它的**落地规范**。 +- **`04-调整方案/140`(配置外置)** ⇒ 连接串 / 桶端点 / 密钥一律走 `config/platform.env`(不入库、默认值中性)。 +- **`05-交接单/IM群组-01-房间内核与DB.md §九`**(内核侧裁决)与 **`D-插件SDK与扩展点契约.md §九`**(插件侧契约)⇒ 条款**收拢进本专区**,冲突时**以本专区为准**并回改那两处。 +- 类比:**`01-规范/06-工作台UI规范.md` 之于界面** = **本专区之于数据**。 + +## 迁入记录(2026-09-20 执行完毕,保留作追溯) + +1. **抢锁**:`bash dsh-server-docs/07-scripts/handoff-guard.sh --claim-exec "<会话名>"`;抢不到 = 停手。 +2. **建目录并落文件**:`mkdir dsh-server-docs/03-数据库/` → 落 `DB-00-专区入口.md`(本文件改名)· `DB-01-接入指南.md` · `DB-02-表结构台账与迭代.md` · `DB-03-插件数据面规范.md`。⚠️ **新建目录不需要占号**(`04-调整方案/` 的 `.lock-` 规则只针对档案号)。 +3. **登记 `INDEX.md`**(按该文件既有章节追加,只做单行追加)。 +4. 🔴 **修正 A 单**(**最重要的一步,现状会误导执行者**):`05-交接单/IM群组-01-房间内核与DB.md` + - 「只读前置 #1」把「现有迁移到 v11 为止 / 新增 v12」改为「**最新已 v13(v12 = 序㊻ `overlay_devices`、v13 = 序㊼ 会话/设备类型);IM 三表取 v14**」; + - 同时改掉原判据里「**若已 >11 ⇒ 说明别人先做了 ⇒ 停手回报**」—— 这句现在会误判,应改为「**若目标版本已被占 ⇒ 取下一个空号,不要停**」; + - §五-1 的 `SQLITE_V12` / `PG_V12` 同步改 `V14`。 +5. **D 单 §九 顶部**加一行指针:「完整契约见 `03-数据库/DB-03-插件数据面规范.md`」。 +6. **删草案**(⛔ 不留双源):确认落库无误后删除 `tmp/数据库专区_草案_20260920/` 与 `tmp/插件数据面规范_草案_20260920.md`。 +7. **验收**:`python 07-scripts/docs-audit.py` **RC=0 / 无 P0**;新档**纯 LF**(`CR=0`);`git status` 只含本次声明文件。 + +## 待定 + +- 插件配额具体数值(总行数 / 单行字节 / 每房行数)⇒ 与档案 142 §六-2(房间上限与发言预算)同一批**压测标定**。 +- `im.data` 是否提供聚合查询(`groupBy` / `sum`)⇒ 先只给 `count`。 +- 插件**跨房共享表**(`scope: plugin`)默认关闭,需 admin 审核后生效(对齐档案 142 §六-4)。 +- 主从落地时机:**现在是单机 PG 13.23**;只读副本等"读成为瓶颈"再上。 +- ⚠️ 原「⛔ 不提前分库」**已作废**(2026-09-22 用户拍板分库):分库维度 = **插件**,见 `02-架构设计/数据-分库与权威存储架构.md`。 diff --git a/dsh-server-docs/03-数据库/DB-01-接入指南.md b/dsh-server-docs/03-数据库/DB-01-接入指南.md new file mode 100644 index 0000000..c479b2d --- /dev/null +++ b/dsh-server-docs/03-数据库/DB-01-接入指南.md @@ -0,0 +1,94 @@ +# 数据库专区 · 01 接入指南(各种情况怎么接) + +> 状态:✅ **已落文档库**(2026-09-20)| 专区:`03-数据库/` + +> 立稿:2026-09-20 | 起草会话 `IM插件数据面-专门文档`> 单一来源:代码 `src/db/{adapter,connection,schema,repo,sqlite,pg}.ts`;档案 142 §3.6;`04-调整方案/140`(配置外置)。 + +## 〇、本文怎么用 + +先看你要存的是**哪一类数据**,再照着对应那节做。**顺序不可颠倒:先判落点,再谈建表。** + +| # | 情形 | 落点 | 能不能建表 | 访问入口 | +|---|---|---|---|---| +| 1 | 平台内核数据(账号 / 实例 / 主机 / 租约) | **控制面库 `dshs`** | ✅ 内核自己建 | `src/db/**` | +| 2 | 插件业务数据(跟房间走 / 跟用户走) | **该插件自己的库 `dshs_pl_`** | ✅ **声明式声明**,内核翻译建表(含归属列) | 内核数据 API | +| 3 | 用户的业务数据(索引 / 导出产物 / 设置) | **库**(插件维度进插件库、平台维度进控制面库) | ✅ 由内核建 | 内核数据 API | +| 4 | 本机运维数据(健康 / 临时态) | **Worker 本地库** | ✅ Worker 本地 | 平台内部接口 | +| 5 | 大对象(图片 / 视频 / Excel / 代码包) | **对象存储桶** | ❌ 不进 DB | `files` 面(`sha256` + `bucket_key`) | +| 6 | 实例本地(**只剩三类**:文件内容 / 缓存 / 临时) | 实例 home | ✅ 只放可重建的东西 | 实例本地 | +| 7 | 缓存 / 队列 / 后处理任务 | 预留端口 | 端口先立、实现后置 | `im.cache` / `im.queue` | + +判据一句话:**跟平台全局走 ⇒ 控制面库 `dshs`;跟某个插件走 ⇒ 该插件的库 `dshs_pl_`(库内按归属列切分用户 / 房间);是文件内容 ⇒ 桶;本机运维 ⇒ Worker 本地库。** +⛔ **同一份数据不双写**(双写 = 脑裂)。 + +--- + +## 情形 1 · 平台内核数据 + +- **谁做**:平台自己(不是插件)。改 `src/db/schema.ts` 的 `MIGRATIONS`。 +- **硬纪律**:每条迁移**必须同时给 `sqlite` 与 `pg` 两份 SQL**(双后端可切是本平台的既有承诺);**只增不减**(加表 / 加列带默认值 / 加索引)。 +- **命名**:内核表用**保留名、不带前缀**(`users` / `sessions` / `workspaces` / `folder_plugins` / `dsh_instances` / `audit_log` / `domains` / `credential_vault` / `business_plugins` / `dsh_hosts` / `email_codes` / `overlay_devices` / `schema_migrations`)。 +- **验收**:新库跑到最新 version 且**重复执行版本不变**(幂等);`npm test` 的 `test/db.test.mjs` 绿。 + +## 情形 2 · 插件业务数据(**问得最多的那种**) + +- **判据**:**一个插件一个库** —— `dshs_pl_`,建在**同一个 PostgreSQL 13.23 实例**上;库内按归属分两类(`scope: user` 跟用户走 / `scope: room` 跟房间走),靠归属列区分。 +- **建表**:插件**不写 SQL** —— 用**声明式 schema**(中性类型 `text/integer/bigint/real/boolean/json/timestamp/uuid`),由内核翻译成 sqlite + pg 两套 DDL;内核自动补**库名**、表前缀 `p__`、**归属列**(`user_id` / `room_id`)、审计列。 +- **迁移**:只许加表 / 加列(带默认值)/ 加索引;在**启用插件时**执行;任一步失败 ⇒ **拒绝启用**(不做半迁移)。 +- **访问**:`im.data.table(name).insert/update/delete/find/count` + 游标分页;**库路由由内核按插件身份决定**(插件看不到库名);归属判定与房间 ACL **由内核强制注入**;同库内要原子 ⇒ 走内核事务口;写与"刚写就读"**走主库**。 +- **禁止**:直连 DB / 原生 SQL / 访问别的插件库 / 自组跨库事务 / 绕过归属判定 / **以实例本地库当权威**。 +- **验收**:插件包内 `CREATE TABLE`、`better-sqlite3`、`pg` **零命中**;`scope: user` 读他人行 = **0 行**;非成员读 room-scoped 行 = **0 行**。 +- 📄 **完整契约 = 本专区 `DB-03-插件数据面规范.md`**。 + +## 情形 3 · 用户自己的数据(**2026-09-22 改:入库;实例本地只留缓存 / 临时**) + +- 🔴 **用户数据一律入库**(这是"能备份、能迁移"的前提): + - 插件里的用户数据(`scope: user`)⇒ **该插件的库** `dshs_pl_`,带 `user_id`; + - 平台维度的用户数据(会话 / 设置 / **工作区文件索引**)⇒ **控制面库 `dshs`**,带归属列。 +- **实例本地(`/`)此后只剩三类**:① 文件内容(当前形态;目标形态改为落对象存储、本地只留按需缓存)② **缓存**(可重建)③ **临时文件**。 + 判据:**本地丢了不影响正确性,只影响延迟** —— 凡是"本地丢了就丢数据"的东西,一律不属于实例本地。 +- ⛔ **不得以实例本地库 / 本地文件作为数据权威**(旧形态「插件在实例里自建 SQLite 装业务数据」已作废;存量按架构定稿 S2 步骤导入插件库)。 +- **仍生效的写入门禁**:平台侧写实例 home **必须走 `UserFs`** —— 直接 `fs` 写 = **静默空操作**(用户卷在 worker 上时尤其如此)。 +- 🔴 **平台侧不得以 `cp` / `mv` / 直接 `fs` 写用户 home 下的任意文件**(2026-09-21 立,**仍有效**):这三条路径**都不是合规写入通路**,⛔ 一律不认。 + - **理由(实测证据)**:`UserFs` 的 home 面只有 **3 个白名单裸文件名** —— `settings.yaml` / `.credentials.yaml` / `.overlay-device.json`(`src/fs/user-fs.ts:126`,⛔ **不收路径**,所以 `home/.dsh/.db` 这类**根本进不了白名单**);且 `writeHomeFile` 签名是 `text: string` = **文本语义**(`src/fs/user-fs.ts:113`),`upload`(`src/fs/user-fs.ts:83`)是 **workspace 相对路径** ⇒ 只能写 `ws/`。 + - **⇒ 结论(2026-09-22 收窄)**:**平台侧对实例 home 下的二进制 / 任意文件没有合规写入通路**。⚠️ 但这**不再是"插件自有 DB 该落哪"的答案** —— 插件数据的正解是**入库**(§情形 2 的插件库);home 只放**缓存 / 临时**,对 home 内的文件平台侧只能**只读核对**,⛔ 不能代写。 +- **实例内插件**(2026-09-22 改):**数据入库** —— 插件数据进 `dshs_pl_`(`scope: user` 的行带 `user_id`),平台维度的用户数据进控制面库;⛔ **不得再把实例本地 SQLite / 本地文件当数据权威**(可以做**缓存**,丢了可重建)。存量(如 MCN)按架构定稿 S2 步骤导入。 +- **坑**:⛔ 用 `homedir()` 拼路径**必错**(实例内 `HOME=/ws`);正解 = `process.env.DSH_HOME ? join(DSH_HOME,'.dsh') : join(homedir(),'.dsh')`。 + +## 情形 4 · Worker 本地运维数据 + +- **落点**:Worker 本机库(该机器的健康 / 临时态 / 运维缓存)。 +- **判据**:**跟机器走、可以重建** ⇒ 放这里,别放平台库(否则每台机器都要往 Manager 写,形成热点)。 +- ⛔ 不放**归属 / 租约**类数据(那类**只有 Manager 能写**)。 + +## 情形 5 · 大对象 + +- **落点**:**S3 兼容桶**(自建 MinIO 或云 OSS/COS 兼容端点)⇒ 不锁厂商;端点与密钥走 `config/platform.env`(⛔ 不入库、默认值中性)。 +- **分工**:**桶存内容、DB 存元数据**(`files` 表:`sha256` / `bucket_key` / `size` / `mime` / `visibility`)。⛔ 大对象**不进 DB**(`bytea` 会把库与备份一起拖垮)。 +- **上传**:**预签名直传桶**(平台不经中转);**下载**:短 TTL 预签名(≤5 min),需审计时走平台代理。 +- **生命周期**:同 `sha256` 只存一份 + 桶 lifecycle;**删除 = 先软删元数据**,桶内回收交 lifecycle(⛔ 不做同步删,不可逆)。 +- **权限**:**复用房间 ACL**,⛔ 不新起一套。 + +## 情形 6 · 缓存 / 队列 / 后处理 + +- **队列先用 PG**:`SELECT … FOR UPDATE SKIP LOCKED` + `LISTEN/NOTIFY` ⇒ 零新增进程、零新增内存。 +- **Redis 后置**:仅在「跨机扇出 / presence 广播」**实测成热点**后引入;硬前提 = **单实例 ≤128–256 MiB**(实例硬顶 1024 MiB / 宿主 1870 MiB)。 +- **"预留"的正解**:先把**端口**立起来(`im.cache` / `im.queue`),本期实现 = 内存 Map / PG 表 ⇒ 将来换实现不改调用点。 +- ⛔ 插件不得自行引入 Redis / MQ 进程。 + +--- + +## 主从与扩展(四个情形共用) + +| 项 | 结论 | +|---|---| +| 选型 | **保留 PostgreSQL 13.23**(现有实例 + `db/` 双后端可切)> MySQL(无收益、要重迁移)> SQLite(⛔ 不支持主从,只留单机/测试) | +| 主从 | 流复制 + **只读副本** | +| 读写分离 | ⚠️ **按数据分类**:元信息读可走副本;**消息/"刚写就读"必须走主**(否则破坏读己所写) | +| 扩展顺序 | 先索引与游标分页 → 再只读副本 → 再分区/分片。⚠️ 原「不要提前分库」**已作废**(2026-09-22 拍板按插件分库) | + +## 验收速查(任何接入都跑这三条) + +1. **归属对**:表名能一眼判断属于谁(无前缀 = 内核;`p__` = 该插件)。 +2. **双后端**:新结构在 sqlite 与 pg 上**都建得起来**。 +3. **不越权**:非成员 / 跨前缀 / 跨插件 一律**拿不到数据**(不是"拿得到但看不见")。 diff --git a/dsh-server-docs/03-数据库/DB-02-表结构台账与迭代.md b/dsh-server-docs/03-数据库/DB-02-表结构台账与迭代.md new file mode 100644 index 0000000..d560298 --- /dev/null +++ b/dsh-server-docs/03-数据库/DB-02-表结构台账与迭代.md @@ -0,0 +1,100 @@ +# 数据库专区 · 02 表结构台账与迭代机制 + +> 状态:✅ **已落文档库**(2026-09-20)| 专区:`03-数据库/` + +> 立稿:2026-09-20 | **取数时间 2026-09-20 21:5x**;起草会话 `IM插件数据面-专门文档`> 🔴 **DDL 的单一来源永远是 `src/db/schema.ts`**;本台账只记「版本 / 名称 / 影响表 / 来源」,**不复制 DDL 全文**(复制必然漂移)。 + +## 〇、🔴 先看这条:版本号已推进,别再按旧结论动手 + +`src/db/schema.ts` 的迁移**已到 v13**(2026-09-20 实测): + +- **v12 = `overlay device ledger (序㊻ 步骤6 / S3)`** ⇒ 新表 `overlay_devices`(单账号多设备会话并存的设备授权台账); +- **v13 = `session/device kind + session cap index (序㊼ S0)`** ⇒ 会话/设备类型字段 + 会话上限索引。 + +✅ **已处理(2026-09-20)**:A 单已改为取 v14。原始提示留档::IM 房间内核三表(`rooms` / `room_members` / `messages`)**必须取 v14**。 +`05-交接单/IM群组-01-房间内核与DB.md` 里写的 "新增 v12" **已过时**,且该单「只读前置 #1」原写「若已 >11 ⇒ 说明别人先做了 ⇒ 停手回报」——**这句话现在会误判**:v12/v13 是**覆盖网络线(序㊻/序㊼)**占的,不是 IM。 +⇒ 该单要改成:**「最新已 v13;IM 三表取 v14;若 v14 已被占 ⇒ 取下一个空的」**。 + +## 一、迁移台账(按版本,权威 = `schema.ts`) + +| 版本 | 名称(原文) | 影响表 / 关键变更 | 来源 | +|---|---|---|---| +| 1 | initial schema | `users` `sessions` `workspaces` `folder_plugins` `dsh_instances` `audit_log` `domains` `credential_vault` | 平台初版 | +| 2 | credential vault enabled flag | `credential_vault` 加启用标志 | 平台 | +| 3 | per-user uid | `users` 加 `uid`(实例属主 uid) | 档案 25 一带 | +| 4 | instance desired state | `dsh_instances` 加期望态 | 档案 41 一带 | +| 5 | business plugin candidate pool | `business_plugins`(候选池) | 档案 16 / 36 | +| 6 | user model providers | 用户级模型提供方 | 档案 87 | +| 7 | cluster host registry + instance lease | `dsh_hosts`(主机注册 + 实例租约) | 档案 119 / T08 | +| 8 | host reachability via (覆盖网络 S2) | `dsh_hosts` 可达性 | 覆盖网络 S2 | +| 9 | host network id (覆盖网络 P0-1) | `dsh_hosts` 网络 id | 覆盖网络 P0-1 | +| 10 | user email + email verification codes | `users` 邮箱 + `email_codes` | 档案 134 | +| 11 | shared model admin grant (per-user, default off) | `users.shared_model_grant`(默认 0) | 档案 87 后续 | +| 12 | overlay device ledger (序㊻ 步骤6 / S3) | `overlay_devices`(设备授权台账) | 序㊻ | +| 13 | session/device kind + session cap index (序㊼ S0) | 会话/设备类型 + 会话上限索引 | 序㊼ | +| **14(规划)** | **IM 房间内核** | `rooms` / `room_members` / `messages` | **IM 线 A 单** | + +**复核命令(可复现,勿凭记忆)**: + +```bash +grep -nE "^ \{ version:" src/db/schema.ts # 版本清单(权威顺序) +grep -noE "CREATE TABLE( IF NOT EXISTS)? [a-z_]+" src/db/schema.ts | sort -u # 表全集 +``` + +> ⚠️ 台账里的「来源」列是**指回**用途,不是精确出处;需要精确出处时查业务档案。 + +## 二、全部内核表(保留名,**不带前缀**) + +- **账号与身份**:`users` · `sessions` · `email_codes` · `credential_vault` · `audit_log` +- **工作区与实例**:`workspaces` · `folder_plugins` · `dsh_instances` +- **平台配置**:`domains` · `business_plugins` +- **集群与网络**:`dsh_hosts` · `overlay_devices` +- **迁移元数据**:`schema_migrations` +- **(规划)IM**:`rooms` · `room_members` · `messages` · (后续)`files` · `plugin_data_audit` + +⚠️ 保留名清单是**硬约定**:插件表必须带 `p__` 前缀 ⇒ 只要看到**无前缀的表名**,就说明它是内核表(一眼可判归属)。 + +## 三、迭代机制(**表结构怎么持续演进**) + +**三条硬纪律(违反即不予合并)** + +1. **双后端齐**:每条迁移同时给 `sqlite` 与 `pg` 两份 SQL(`Migration = { version, name, sqlite, pg }`)。只写一种 = 另一后端起不来。 +2. **只增不减**:只允许 加表 / 加列(**带默认值**)/ 加索引。⛔ 不改列类型、⛔ 不删列、⛔ 不重命名。 +3. **幂等**:迁移按 `schema_migrations` 的 version 推进、可重复执行;任一步失败 ⇒ **停在该版本**(不做"半迁移")。 + +**改结构但"只增不减"做不到时,走四步跨版本**(这是"改类型 / 删列"的唯一合法路径): +① 加新列(带默认值,可空)→ ② 双写(新旧都写)+ 回填脚本 → ③ 切读(读新列)→ ④ 下一版本删除旧列。 + +**命名规范** + +- 内核表:**无前缀**保留名,`snake_case`。 +- 插件表:`p__`,`pluginId` = 包名去 scope、小写、连字符转下划线。 +- 列:`snake_case`;时间列统一 `*_at`(`created_at` / `updated_at` / `expires_at`)。 + +**索引与查询** + +- 新增大表索引必须在单里写明「**取数时间 + 复核命令 + 走向**」(不写死行数断言 —— 会并行改动打穿)。 +- 分页一律**游标**(`WHERE seq > ?`),⛔ 不用 `OFFSET`(pg 上大偏移退化)。 +- 热路径(消息投递 / 权限校验)必须能命中索引;权限判定要**缓存 + 版本化失效**。 + +**回填(backfill)脚本纪律**:可重入 · 可中断 · 有进度输出 · 有 dry-run · 出错**不回滚已写行**(靠幂等重跑)。 + +**回滚**:因为"只增不减",代码回滚后**库里的多出内容被旧代码忽略** ⇒ 回滚 = 回滚代码;⛔ 不提供"自动降级 DDL"(那是不可逆操作,需 admin 显式确认)。 + +## 四、评审门禁(合并前四条,缺一不可) + +| # | 门禁 | 判据 | +|---|---|---| +| 1 | 双后端齐 | `sqlite` 与 `pg` 两份都存在且都能建表 | +| 2 | 只增不减 | 无 `ALTER COLUMN TYPE` / `DROP COLUMN` / 重命名 | +| 3 | 有单有据 | 对应**交接单或档案号**(迁移 name 里能指回去) | +| 4 | 台账已更新 | **本节表格新增一行**(与迁移同一提交) | + +## 五、台账维护规则 + +- **谁改谁更新**:新增迁移的**同一提交**里更新本台账(列:版本 / 名称 / 影响表 / 来源)。 +- **冲突热点**:台账在多人并行时是**写热点** ⇒ 只做**单行追加**(Edit 精确替换),⛔ 不整文件重写。 +- **每季度体检**:`schema.ts` 的版本数与台账行数**必须相等** —— 不等即有未登记迁移。 + ```bash + grep -cE "^ \{ version:" src/db/schema.ts # 与台账行数比对 + ``` diff --git a/dsh-server-docs/03-数据库/DB-03-插件数据面规范.md b/dsh-server-docs/03-数据库/DB-03-插件数据面规范.md new file mode 100644 index 0000000..52eb359 --- /dev/null +++ b/dsh-server-docs/03-数据库/DB-03-插件数据面规范.md @@ -0,0 +1,278 @@ +# 插件数据面规范(插件如何接入数据库) + +> 状态:✅ **已落文档库,本文件即插件数据面的唯一正式来源** —— `dsh-server-docs/03-数据库/DB-03-插件数据面规范.md`(2026-09-20 落点,专项区 `03-数据库/`)。 +> ⚠️ **不再"待落点"**:`04-调整方案/143` 是**另一件事**(「MCN 工作台入口失效与语言切换显示修复」),与本文无关;⛔ 不要再把本文往 04-143 归位。 +> 立稿:2026-09-20 | 起草会话 `IM插件数据面-专门文档` +> 来源:档案 142 §3.6(三条平台保护)· `05-交接单/IM群组-01-房间内核与DB.md §九`(内核侧 DB 裁决)· `05-交接单/IM群组-04-插件SDK与扩展点契约.md §九`(插件侧契约)。 +> 本文是**专门文档**:把散在两处的条款收拢成一份可直接照着实现的规范;冲突时**以本文为准**,并回改上述两处。 + +> ✅ **执行状态(2026-09-23 · 插件投放与分库线第 4 棒)**:数据面已**开工并落地**,本规范除标注处外**可照着实现**。 +> - ✅ **已落地**:共享只读**包**库 `bundled-plugins`(D2,"包库"与本文件的"插件库"是两回事)+ admin 投放面(`POST /api/plugins/business/share`)。逐条读数见 `05-交接单/插件投放与分库线-01共享只读包库与插件数据面.md §十二`。 +> - ✅ **建库权限已解决(2026-09-23 · 用户拍板 B 档)**:`ALTER ROLE dshs CREATEDB;`(47 已落地 + 11 条验收)⇒ **§三"谁建库"不再受阻**。 +> - ✅ **建库口已接线**:`src/db/plugin-data/datastore.ts`(`CREATE DATABASE` 单语句非事务 / 台账 + 审计 / 台账 × `pg_database` 对账)+ 控制面 v15 迁移(两张台账表)。落地件 ⇒ `交付物/建库口接线-B档落地-20260923.md`。 +> - 🔴 **§六"跨节点:worker 经覆盖网络调 Manager 侧数据面"仍不通**:所需"实例→控制面"身份通道**今天为 0**(147 §三 G3 / 序47 §②.1)。 +> - ⚠️ **B 档的真实缺口(已由代码兜住)**:PG **无**"只允许 `dshs_pl_*` 前缀"的原生机制 ⇒ 白名单只能靠平台代码三点(库名点生成 + 正则复校 + 每次建库落台账审计)。详见 §六-1。 +> - ✅ 上表 `§十 差异清单` 的 **D1–D5 / D7 回改已落文**(本轮)。 + +## 〇、定位 + +回答一个问题:**业务插件要存数据,怎么接?** +一句话:**一个插件一个库**(`dshs_pl_`,建在同一 PostgreSQL 13.23 实例上)—— 插件**不建表、不写 SQL、不持有连接**,用**声明式 schema** 由内核翻译建表,数据读写一律**走内核数据 API**、由内核按插件身份路由到该插件自己的库。 + +- 适用:所有走「候选池 → 实例功能管理启用」通道的业务插件(自研 / 第三方)。 +- 不适用:平台自身内核表(`rooms` / `room_members` / `messages` / `files` …);那些归控制面库与 A 单。 +- 🟢 范围:只讲技术实现。| **架构依据(冲突时以它为准)**:`02-架构设计/数据-分库与权威存储架构.md`。 + +## 一、第一步:先判落点(再谈建表) + +**第一判据**:插件数据的落点**只有一处** —— **该插件自己的库** `dshs_pl_`。库在**同一个 PostgreSQL 13.23 实例**上,由内核按"调用方插件身份"路由;插件**看不到库名、也拿不到别的插件的库**。 + +库内再按**归属**分两类,两者**同库共存**,靠归属列区分: + +| 归属 | 谁的数据 | 归属列(内核自动补) | 读写权限 | +|---|---|---|---| +| `scope: user` | 某个用户自己的(插件里的个人数据 / 索引 / 产物) | `user_id` | 只能读写**自己**的行 | +| `scope: room` | 跨用户共享的房间维度数据(跑团角色卡 / MUD 地图 / 办公任务卡) | `room_id` | 复用房间成员判定(`canSee`) | + +🔴 **归属列强制**(分库的**前提**,不是可选项):每张业务表**必须**带 `user_id` 或 `room_id`,由内核建表时自动补齐;**插件不许自建表、不许省略归属列**。 +理由:库的维度是**插件**,用户维度的备份/迁移**只能靠归属列切分** —— 缺了归属列,按用户导出/迁移**直接不可能**。 + +⛔ **同一份数据不双写**(双写 = 脑裂)。 +🔴 **不得以实例本地库作为数据权威**(2026-09-22 新增):插件可以在实例本地放**缓存**(丢了可重建),但**权威数据必须在插件库**。旧形态「插件在实例里自建 SQLite 存业务数据」此后**不允许**;存量(如 MCN)按架构定稿的 S2 步骤导入插件库。 +⚠️ `pluginId` = 包名去掉 scope(如 `@dsh-local/trpg-kit` ⇒ `trpg_kit`):**小写、连字符转下划线**;库名 = `dshs_pl_`。 + +## 二、红线(违反即不予启用) + +1. ⛔ 插件**不得直连数据库**:不持有连接串 / 不 import 数据库驱动 / 不自建连接池。 +2. ⛔ 插件**不得写原生 SQL**:无 `CREATE TABLE` / `ALTER` / `DROP` / `SELECT`;无触发器、存储过程、厂商专有类型。 +3. ⛔ 插件**不得访问其他插件的库**:看不到也查不了别人的 `dshs_pl_*`、控制面库与共享数据。 +4. ⛔ 插件**不得自组跨库事务**(PG 跨 database 无原生事务)。 +5. ⛔ 插件**不得绕过归属判定**:`scope: user` 只能读写自己的行;`scope: room` 由内核强制注入成员校验。 + +6. ⛔ 插件**不得以实例本地库 / 本地文件作为数据权威**(可作为缓存,权威必须在插件库)。 + +> 判据(可机械验证):插件包内 `grep -rE "CREATE TABLE|ALTER TABLE|DROP TABLE|better-sqlite3|from 'pg'|require\('pg'\)"` **零命中**。 + +## 三、建表:声明式 schema + +插件在 `im.data` 上声明表;**字段类型仅限中性集合**,由内核翻译成 **sqlite + pg 两套 DDL**: + +| 中性类型 | sqlite | pg | 说明 | +|---|---|---|---| +| `text` | TEXT | TEXT | 短文本 | +| `bigtext` | TEXT | TEXT | 长文本(有长度上限) | +| `integer` | INTEGER | INTEGER | 32 位整数 | +| `bigint` | INTEGER | BIGINT | 计数 / 时间戳毫秒 | +| `real` | REAL | DOUBLE PRECISION | 浮点 | +| `boolean` | INTEGER | BOOLEAN | 布尔 | +| `json` | TEXT | JSONB | 结构化载荷 | +| `timestamp` | TEXT(ISO) | TIMESTAMPTZ | 时间 | +| `uuid` | TEXT | UUID | 标识 | + +内核在建表时**自动补齐**:**库名** `dshs_pl_`、表名前缀 `p__`、**归属列**(`user_id` 或 `room_id`,按 `scope` 定)、审计列(`created_at` / `updated_at`)。 + +### 三·补 —— 谁建库、什么时候建(2026-09-23 落文,原缺失项) + +**谁建库**:**平台侧**(⛔ 不是插件、⛔ 不是实例)。插件只声明、不建库。建库由 admin 在门户上触发: + +``` +上传 ─检测(安全 + 兼容 + 数据面声明校验)→ 通过 ⇒ 状态 pending + ⇒ admin 点 [建库] + ⇒ POST /api/plugins/business/:id/datastore + ⇒ CREATE DATABASE dshs_pl_ + 落台账 + ─建库成功→ [发布] → 用户才能启用 +``` + +- **权限前提**:`ALTER ROLE dshs CREATEDB;`(2026-09-23 拍板 **B 档**;47 已落地 · 11 条验收)。⛔ 不得给 `CREATEROLE` / `SUPERUSER`。重建步骤 ⇒ `DEPLOY-本部署.md §6.4`。 +- **语句姿势**:`CREATE DATABASE dshs_pl_ OWNER dshs ENCODING 'UTF8' TEMPLATE template0;` —— **单语句、⛔ 不走事务**(PG 语句级内核限制:`CREATE DATABASE cannot be executed from a function` / 不能进事务块)。 +- **库名两重防线**(B 档缺口只能靠代码兜):① `pluginDbNameOf` 从 `.dsh` 声明换算点生成,⛔ 不收请求体里的库名;② `assertPluginDbName` 在拼 SQL **之前**复校 `^dshs_pl_[a-z][a-z0-9_]{0,40}$` + `quoteIdent` 标准引用。非法 ⇒ **400 `invalid_plugin_db_name`**,⛔ 不下发 PG。 +- **幂等**:已存在 ⇒ 返 `exists`(捕获 PG `42P04 duplicate_database`),⛔ 不报错、⛔ 不重建。 +- **错误码**:`PG_CREATEDB_MISSING`(503 · 未授 `CREATEDB` 或已回滚)|`invalid_plugin_db_name`(400)。⚠️ D 档的 `PLUGIN_DB_HELPER_MISSING` **已作废**。 +- **对账**:`GET /api/plugins/business/datastores/reconcile` ⇒ 台账 × `pg_database` 双向差集(orphan = 绕过平台建的库;missing = 台账有而 PG 无)。 + +示例(跑团插件): + +```yaml +pluginId: trpg_kit +schemaVersion: 3 +tables: + - name: character_sheet # 实际表名 => p_trpg_kit_character_sheet + scope: room # room | user(room = 注入房间 ACL;user = 只能读写自己的行) + columns: + - { name: pc_name, type: text, notNull: true, maxBytes: 128 } + - { name: sheet, type: json, notNull: true, maxBytes: 16384 } + - { name: sheet_rev,type: integer, default: 1 } + indexes: + - { columns: [room_id, pc_name], unique: true } +``` + +约束:每表**至多 20 列**、`json` 列 `maxBytes` 必填、表与列名 `^[a-z][a-z0-9_]{0,40}$`、每插件**至多 20 张表**。 + +## 四、迁移:只增不减 + +- 插件声明 `schemaVersion`(整数,单调递增);内核按版本推进。 +- **只允许**:加表 · 加列(**必须带默认值**)· 加索引。 +- ⛔ **不允许**:删列、改列类型、重命名、加无默认值的非空列(会导致上线即失败)。 +- 执行时机 = 🔄 **2026-09-23 修订:分两步,且都是插件级一次性动作**(原写法"启用插件时"会退化成 per-user 动作 —— 同插件每个用户开通都跑一遍 DDL): + - **建库**:admin 点 `[建库]` 时(`POST …/:id/datastore`)⇒ 建库 + 建表 + 归属列 + 索引一次做完,落台账 `plugin_datastores`。 + - **迁移**(`schemaVersion` 推进):**插件级一次性**,全局只推进一次;**用户开通只读台账版本、⛔ 不触发 DDL**(`POST /api/plugins/mine/apply` 走门禁但不改结构)。 + - 任一用户启用前,平台先校验台账 `status = ready` 且版本满足 ⇒ 不满足 ⇒ 拒绝启用(`datastore_not_ready`)。 +- 任一步失败 ⇒ **拒绝启用**(⛔ 不做"半迁移"),旧结构保持可用、可回滚。 +- ⚠️ **表级 DDL 在同一事务内**(失败整体回退);但 **建库项不可回滚** ⇒ 建库失败**不 DROP**,留空库 + 台账 `status='error'` + `last_error`,等 `[重试]` 覆盖。 +- 迁移语句由内核生成并记入审计(`plugin_data_audit`)。 +- **TOCTOU 防护**:预演(`/datastore/plan`)产出 `planHash` ⇒ 执行时必须回传同一指纹才对;⛔ 建库 / 迁移路径一律**不传** `planHash`(omit ⇒ 保留旧值,`COALESCE` 语义),⚠️ 传 `null` 会清掉指纹 = 拆掉防护。 + +### 四·补 —— 兼容矩阵:包版本 × `schemaVersion` 怎么判(2026-09-23 落文,P2「要有定义」的兑现) + +> **两个版本号是两件事,⛔ 不混为一谈**:`version` = 插件**包**版本(`package.json` 的 `version`,可能因改文案/修 bug 而不动结构);`schemaVersion` = **数据面结构**版本(整数、单调递增)。**判"库该不该动"只看 `schemaVersion`**;包版本只用来判"该装哪个包"与 B 档回滚素材的指向。 + +**判据(按优先级从上到下取首个命中项)**: + +| 场景 | 包版本 | `schemaVersion` vs 库现值 | 允许的动作 | 依据 | +|---|---|---|---|---| +| 首次装 | 任意 | 库不存在 | **建库** → 迁移 → 可启用 | §三·补 | +| 升级 | 新 > 旧 | 新 **≥** 现值 | **升级**(只增:加表/加列带默认值/加索引) | §四 | +| 等价换包 | 新 > 旧 | 新 **=** 现值 | **只换代码,不碰库**(`drift` 不触发) | §四 | +| 降级 | 新 **<** 旧 | 新 **<** 现值 | ⛔ **拒绝启用** + 明确错误码;**不回滚数据** | §四 + 本表下注 | +| 同名同版本重传 | 相同 | 相同 | **幂等**(`tgz sha256` 相等 ⇒ 不动文件) | §六-2 / S2 | +| 同名同版本但内容不同 | 相同 | 相同 | **冲突** ⇒ 两段式确认后才替换(⚠️ 会波及所有已指向的用户) | S2 | + +**三条硬规则**: + +1. **降级不自动发生**:`schemaVersion` 回退 ⇒ 状态 `drift`,`executable=false` ⇒ 前端「确认执行」置灰、后端 `POST /datastore/migrate` 也拒。给用户的错误必须**可读**(原话:不写"未达预期"了事),且**库一个字节都不改** —— 旧版代码若认不出新列,忽略即可(与 §八 一致)。 +2. **⛔ `UPDATE ≠ DROP`**:更新路径**永不** `DROP DATABASE`;卸载才走 §六-4(清单 + admin 确认 + 默认保留 30 天)。 +3. **矩阵的机器判据 = `diffDecl()`**:上表是**给人看的口径**,机器侧唯一实现是 `src/db/plugin-data/diff.ts` 的 `diffDecl(decl, current, …)` + `pluginDbNameOf` 推导的 `plan.schemaVersion`。判据来源是**库的现实**(真读 `information_schema`),⛔ 不是台账里记的那个数 —— 台账只是**对账参照**(§六-2 的 `reconcile` 抓的正是二者不等)。 + +**B 档(只留最新一版)给本矩阵加的两条前置**(§五 S5-a-3,2026-09-23 既有裁决): + +- 每次上传**先落一份 tgz** 到 `/opt/dsh/backups/plugins//.tgz` —— 否则"降级到哪一版"没有素材; +- 台账 `plugin_datastores` 必须记**当前包版本** —— 否则判不出该回滚到哪个包。 + +⚠️ **无灰度是 B 档的已知代价**(已拍板接受):共享层同名替换后,**所有**已指向它的用户下一次生效即为新版本 ⇒ 执行回报里必须写清"影响 N 个用户、动作耗时"。 + +**验证(兼容矩阵的可机械复现判据)**: + +``` +① 装 v1(schemaVersion=1) → 写 3 行 → 升 v2(schemaVersion=2,加一列) + 期望:3 行仍在、新列有默认值;`plan.summary.columns >= 1`;退出码 0 +② 强行装回 v1 + 期望:状态 `drift`、`executable=false`、错误码可读、`select count(*)` 仍 = 3(库未被改) +③ 同名同版本、内容相同重传 + 期望:`idempotent:true`,共享层 mtime / sha256 不变 +④ 同名同版本、内容不同 + 期望:`conflict:true` + 双方 sha256;确认前**不动任何文件** +``` + +## 五、访问 API(唯一入口) + +```ts +im.data.table('character_sheet') + .insert(row) // 单行;返回 id + .update(id, patch) // 按主键 + .delete(id) // 软删标记(内核保留 30 天,见 §六-4) + .find({ where, order, limit }) // 游标分页:返回 { rows, cursor } + .count({ where }) +``` + +语义要点: + +1. **库路由由内核决定**:插件只报表名,内核按**调用方插件身份**路由到 `dshs_pl_`;插件看不到库名,也拿不到别人的库。 +2. **归属维度自动注入判定**:`scope: user` 的表每次读写附加「调用者 == `user_id`」校验;`scope: room` 附加「调用者是该房成员」(复用内核 `canSee`)⇒ 越权得到 **0 行 / 明确拒绝**,不是"查得到但看不到"。 +3. **同库内要原子 ⇒ 走内核事务口**:`im.data.tx([...])` 仅支持**同一个插件库内**的多表;跨库(如同时改房间属性 + 自己的数据)**没有事务** ⇒ 必须拆成幂等步骤 + 补偿,⛔ 不许靠双写。 +4. **读写路由**:写、以及"刚写就读"**一律走主库**;⛔ 不从只读副本读自己的新行(副本延迟会破坏读己所写)。 +5. **限流**:插件写操作吃**与 agent 同族的预算**,并受房间级速率上限约束(142 §3.6 保护约束 3)。 +6. **分页**:一律**游标**,不用 `OFFSET`(大偏移在 pg 上会退化)。 + +## 六、管理面 + +1. **配额**(防单插件拖垮库):每插件 **总行数** + **单行字节** + **每房行数** 三重上限(库维度天然对齐"一插件一库");超限返回**明确错误码**(⛔ 不静默丢、⛔ 不静默截断),并在插件卡片上可见。 +2. **审计**:DDL、迁移、超阈值批量写记录进内核表 `plugin_data_audit`(谁 / 何时 / 哪个插件 / 影响行数);空间占用进平台看板。 + - 🔴 **两张台账表 = 控制面 v15 迁移新增**(2026-09-23 落地): + | 表 | 键 | 作用 | + |---|---|---| + | `plugin_datastores` | `plugin_id` → `database_name` | **库台账**:当前 `schema_version` + 插件包 `version` + `status`(`created`/`ready`/`error`) + `plan_hash` + `last_error` + 时间戳。**B 档白名单的第三点兜底** —— 使"平台到底建过哪些库"可对账 | + | `plugin_data_audit` | 自增 id | **审计流水**:建库 / 迁移 / DDL 逐条留痕(谁 / 何时 / 哪个插件 / 动作 / 影响) | + - ⚠️ 两表**由平台启动时 `runPgMigrations` 自动落**(47 当前停在 v13,部署 v15 代码后随 `restart dshs` 自动补齐);⛔ 不需手工建表。 + - **对账**:`GET /api/plugins/business/datastores/reconcile` ⇒ 台账 × `pg_database` 双向差集(抓 orphan / missing)。 +3. **可观测**:每插件暴露「行数 / 体积 / 最近写入」三项,进"功能管理"页。 +4. **卸载**:停用插件 ⇒ **数据默认保留 30 天**(可配置);窗口内可恢复。 + 彻底卸载 = `DROP DATABASE dshs_pl_`(**一次带走该插件全部表**,比逐表 DROP 干净)⇒ 属**不可逆** ⇒ 必须 **admin 显式确认 + 先出受影响清单**(库名 / 表 / 行数 / 体积),⛔ 不做"卸载即删"。 +5. **备份 / 迁移**(2026-09-22 新增):单插件备份 = `pg_dump dshs_pl_`;**按用户导出/迁移** = 统一迁移器按**归属列**遍历(控制面库 + 各插件库 + 桶清单)⇒ 一次遍历、可重入、可对账。⛔ 用户注销**不** DROP 插件库(库属插件、不属用户),只删该 `user_id` 的行 + 回收桶内对象。 +6. **禁用后的行为**:插件无关;房间照常(数据面异常不得影响消息流,见 §七)。 + +### 六·补 —— 插件文件落点与凭据投递(2026-09-23 落文 · 对 carbon 线两问的裁定) + +> **一句话**:插件的**非数据库文件**一律落 `/home/.dsh/plugins//`(D4);该目录**由实例侧插件自己建**,平台侧不碰。 + +| 项 | 口径 | +|---|---| +| **落点** | `/home/.dsh/plugins//`(= `/var/lib/dshs/users//home/.dsh/plugins//`,**实例内是同一绝对路径**,bwrap `--bind ` 不重映射) | +| **`` 定义** | 去 scope、小写、非 `[a-z0-9_]` 折 `_`(`src/db/plugin-data/schema.ts:126` `pluginIdOf`)⇒ 与库名 `dshs_pl_` **同一个 token**。例:`@dsh-local/dsh-plugin-carbon` ⇒ `dsh_plugin_carbon`(⛔ 不是 `carbon`) | +| **谁建目录** | **实例侧插件首次用时 `mkdir -p`**(幂等);投递类文件由投递方 `install -d -m 700 -o -g ` 先建。⛔ **平台侧不碰** —— `src/fs/user-fs.ts:126` 的 home 白名单只有 3 个**裸文件名**,不含任何目录 | +| **目录内取数姿势** | `join(process.env.DSH_HOME, '.dsh', 'plugins', pluginId)`。`DSH_HOME` 由平台注入(`src/supervisor/orchestrator.ts:813`),实例内**必存在**,缺失 ⇒ **fail-fast 报错**,⛔ 不许回退 | +| **⛔ 禁 `homedir()`** | 实例内 `HOME` = `/ws`(`orchestrator.ts:811`),而 `ws` 会**被平台按产物清理**(`:857`)⇒ `homedir()` 回退不是「降级可用」,是**把数据写到会被清掉的位置** | +| **⛔ 不存在 `im.paths.pluginData()`** | 实测三处零命中(`dsh_shenxian/src` / 宿主仓 `deepseek-harness` / `node_modules`)。**本规范不再引用该 API**;实例内取目录按上行的 `DSH_HOME` 拼 | +| **凭据类文件** | 与插件数据**同目录**(同一清理单元):文件 `0600`、**属主 = 该用户 uid**(⛔ root 写的 0600 实例读不了,同 `user-fs.ts:110-111` 的教训);插件侧**只读**,⛔ 不覆盖投递来的文件名 | +| **⚠️ 跨机** | `` 是**那台机上的**路径。用户实例在 worker 上时,在管理机直写 `/home/…` = **静默空操作**(`src/fs/user-fs.ts:122-124`,档案 138 §五)⇒ 投递必须发生在 `hostIdFor(userId)` 指到的那台机 | +| **卸载清理单元** | 整个 `/` 目录(与库 `dshs_pl_` 同粒度:库走 §六-4,文件走本行) | + +⚠️ **现状缺口(⛔ 不许当已知项推断)**:平台侧**没有**「把插件凭据投给用户」的通路 ⇒ 凭据仍是插件线**带外投递**。要做成「用户点一下开通 ⇒ 凭据自动就位」,须在平台立项;建议**并进 worker 侧 `/plugins/apply` 同一条腿**(与装配搬到实例所在机同族,零新增入站口)。 + +## 七、故障与降级 + +| 情形 | 行为 | +|---|---| +| 插件回调超时 | 熔断该插件数据面调用;**降级为普通聊天**,房间消息流不受影响 | +| 超配额 | 返回明确错误码;插件功能不可用但状态可见(⛔ 不静默吞掉用户数据) | +| 迁移失败 | **拒绝启用**,保留旧结构 | +| 某插件库不可用 | 只影响该插件;其他插件与内核照常(一插件一库的天然收益) | +| DB 主库不可用 | 房间读取按内核既有降级;插件写入直接失败并提示(⛔ 不落本地再补 = 会造成脑裂) | + +## 八、与其他能力的衔接 + +- **房间 ACL**:房间维度表的权限**复用**内核判定,⛔ 不新起一套文件/数据权限模型(与 `files` 面同源)。 +- **对象存储**:大对象(图片 / 视频 / Excel)**不进 DB**,走 `files` 面(`sha256` + `bucket_key`)⇒ 插件只拿 `file_id`;插件表里**只存引用**。 +- **缓存 / 队列**:内核提供 `im.cache`(get/set/del + TTL)与 `im.queue`(enqueue/lease/ack)**端口**;本期实现 = 内存 Map / PG 表,⛔ 插件不得自行引入 Redis / MQ 进程(宿主内存紧:实例硬顶 1024 MiB)。 +- **版本兼容**:`schemaVersion` 与插件包版本一并记录;降级安装(装旧版插件)⇒ 若旧版不认新结构,**拒绝启用**并提示,⛔ 不回滚数据(完整矩阵见 §四·补)。 + +### 八·补 —— 端侧边界声明(2026-09-23 落文 · S6-3) + +> **一句话**:端侧设备访问的是**同一个平台 API**,不另起一套数据面通路。 + +| 项 | 口径 | +|---|---| +| **看见** | ✅ 端侧(桌面壳 / 浏览器)调 `GET /api/plugins/mine` 与 `GET /api/plugins/shared/catalog`,与网页端**同一份数据、同一套鉴权** | +| **开通** | ✅ 端侧调 `POST /api/plugins/mine/apply`,与网页端同一条路径、同一套后端门禁(数据面未就绪一律拒) | +| **本地装包** | ⛔ **不要求**。端侧**不下载、不安装**插件包实体 —— 包在服务端共享只读库(D2),端侧只发请求 | +| **凭据下发** | ⛔ **不下发平台级凭据到客户端**。端侧只持有**该用户自己的会话**;插件库连接由**内核代持**(§五-1),客户端拿不到库名、更拿不到连接串 | +| **数据落点** | 插件数据**一律落服务端库**(`dshs_pl_`);端侧本地**不落插件数据**(§六-5 的"跟用户走"指的是 `home/.dsh/plugins//`,那在**服务端该用户的主目录**里,不是端侧磁盘) | +| **离线** | ⚠️ 端侧离线时插件**不可用**(数据面在服务端)—— 这是"数据入库"口径的必然推论,⛔ 不是缺陷 | + +⚠️ **现状读数(⛔ 不许当已知项推断)**:端侧**插件投放通路 = 无**(`交接单 §9.4` 实测),且桌面线客户端载体本身尚未产出 +(序46 §⑬ 遗留 1 停在 `client-artifact-missing`)。**本声明只定义"端侧该看到什么、不该拿到什么"**; +⛔ 造端侧载体、做端侧本地包分发**不在本规范范围**(后者属"跨节点内容分发",需单独立项)。 + +## 九、验收清单(可机械复现) + +| # | 断言 | 期望 | +|---|---|---| +| 1 | 零裸 SQL / 零 DB 驱动 | 插件包内 `CREATE TABLE`、`better-sqlite3`、`pg` **零命中** | +| 2 | 双后端可建 | 同一声明在 sqlite 与 pg 上**都建库建成** | +| 3 | 库与前缀 | 实际库名 `dshs_pl_`、表名全部 `p__*`;不存在无前缀的插件表 | +| 4 | 越权 | 读别的插件库 / 控制面库 ⇒ **拒绝** | +| 5 | 归属判定 | `scope: user` 读他人行 ⇒ **0 行**;`scope: room` 非成员读 ⇒ **0 行** | +| 6 | 迁移 | 加列成功;声明删列或改类型 ⇒ **拒绝启用**(预期失败) | +| 7 | 配额 | 超限返回明确错误码,且**无静默丢行**(前后计数可对账) | +| 8 | 卸载 | 停用后数据仍在且可查;`DROP` 走确认流程 | +| 9 | 降级 | 插件数据面挂起 ⇒ 房间消息流照常、无 5xx | +| 10 | **库归属** | 新启用一个插件 ⇒ 只新增 `dshs_pl_` 一个库;插件触达不到其他库 | +| 11 | **归属列** | 每张业务表都有 `user_id` 或 `room_id`;缺列 ⇒ **拒绝启用** | +| 12 | **按用户可切分** | 用迁移器按某 `user_id` 导出 ⇒ 行数与"该用户在各库的行数之和"一致 | + +## 十、待定 + +- 配额具体数值(总行数 / 单行字节 / 每房行数)⇒ **待压测标定**,与档案 142 §六-2(房间上限与发言预算)同一批。 +- `im.data` 是否提供**聚合查询**(`groupBy` / `sum`)还是只给 `count` ⇒ 先只给 `count`(够用且不易被滥用),确有需要再评估。 +- 插件数据的**跨房共享表**(`scope: plugin`)默认关闭,**需 admin 审核后生效**(与档案 142 §六-4 同一条口径)。 +- **每插件库的连接池参数**(池大小 / 空闲回收 / 全平台上限)⇒ 随架构定稿 S1 定稿;⚠️ 默认档用独立 database(库数 = 插件数),若插件数量增长到连接吃紧,降为同 database 内 schema 档(同一套代码,只改路由映射)。 +- **用户注销时的归属行清理器**(跨控制面库 + 各插件库 + 桶)⇒ 与统一迁移器同一批实现(S5)。 diff --git a/dsh-server-docs/04-调整方案/08-实例常驻上限与单活跃会话.md b/dsh-server-docs/04-调整方案/08-实例常驻上限与单活跃会话.md index 2389e13..02a2569 100644 --- a/dsh-server-docs/04-调整方案/08-实例常驻上限与单活跃会话.md +++ b/dsh-server-docs/04-调整方案/08-实例常驻上限与单活跃会话.md @@ -1,5 +1,12 @@ # 08-实例常驻上限与单活跃会话(idle-reap + last-wins,2026-09-09 落地) +> 🔴 **2026-09-20 头注:本文的「last-wins」那一半已被取代。** +> 序47(`交接单/覆盖网络-47-单账号多设备会话并存.md` · 落地档案 `04-调整方案/145-…md`)已把 +> `POST /api/auth/login` 的**无条件驱逐**改成「**多会话并存 + 每用户上限 + 显式登出全部**」 +> ⇒ 本文下部「### A. 单活跃会话(last-wins)」描述的行为**不再是现网行为**,只作历史与回滚依据保留。 +> ⚠️ **仍然成立的部分**:**常驻上限 + idle-reap**(本文另一半)未被动过。 +> ⚠️ 因此凡**引用 last-wins 作为前提**的表述(如"登录会踢掉用户会话")**一律失效**,需按序47 重判。 + ## 背景与动机 - 平台:dshs(本地后端,单机)托管多租户 dsh 0.1.2-rc.1,每账号一个常驻 main 实例。 diff --git a/dsh-server-docs/04-调整方案/117-覆盖网络-参数表与观测口径.md b/dsh-server-docs/04-调整方案/117-覆盖网络-参数表与观测口径.md index c5c94bc..6d9a3b9 100644 --- a/dsh-server-docs/04-调整方案/117-覆盖网络-参数表与观测口径.md +++ b/dsh-server-docs/04-调整方案/117-覆盖网络-参数表与观测口径.md @@ -374,7 +374,8 @@ RELAY_MAX_HOSTS = floor(C_RELAY × DESIGN_MARGIN) = floor(16700 × 0.45) | ⑧ | **临时 UDP 入站面**(序⑥ S4 打洞探测的观察器) | **是(临时,已关闭)** | 入站面(UDP 高位口) | ✅ **已收窄**:对象 = 47 的 `0.0.0.0:21100/21101`(`overlay-holepunch.cjs --observer`),**开放时长 ≈ 75 s**、进程退出即释放;关闭证据 = `ss -lunp \| grep 2110[01]` ⇒ **空**。⚠️ 实测该口**零收包**(连同机发出的都收不到)⇒ 47 的**云安全组拦 UDP 入站**(⛔ 不是 nft:`input policy = accept`)⇒ S4 改走第三方 STUN | `ss -lunp`(空)|本单 §8.4 E6 | **维持** | | ⑨ | **106 的 443 relay 路由**(序⑥ S8 新增 `location /dshs-relay`) | **是(新增可路由路径,⛔ 未新增监听口)** | 入站**路径**面(不是新口) | ✅ 已收窄:只放行 `/dshs-relay` **一个端点**(⛔ 不写 `location /`、不复制站点任何路径;未知路径 404);relay 侧仍强制 HMAC +节点凭据(`identityRequired = true`) | 本单 §8.6 + `curl -H "Upgrade: websocket" … https://106.54.21.172/dshs-relay` = **101**|`/nope` = **404** | **维持(监听口零新增)** | | ⑤ | **443 兜底入口**(复核是否真零扩大) | **否** | — | ✅ **已收窄**(相对 sshd 时代**净减 1 个公网口**) | 实测:`32022` 已回收(`ss -lntp` 无该口);443 由 **nginx 复用**(`ss -lntp` 显示 `0.0.0.0:443` + `nginx` 4 个进程),**未新增监听**;`/etc/dshs.env` 的 `DSHS_OVERLAY_BOOTSTRAP_SEEDS` 含 `relay-direct.alotbuy.com/dshs-relay`,而 `DSHS_OVERLAY_ADDR_OVERRIDES=relay-direct.alotbuy.com=47.77.182.89` ⇒ 走的是**既有** 443 | **维持** | -| ⑥ | **relay 拨号白名单 `dialers`**(R5 引入) | **否** | — | ✅ **已收窄**:白名单是**准入收窄**(默认拒绝,⛔ 不是放开);且**构造时定型、运行期不可改** | `src/net/relay/server.ts:355` `private readonly dialers`;`:404` `normalizeDialers()` 在构造期;`:747` `const wantDialer = this.dialers.get(network)?.has(hostId) === true`(默认拒绝);drop-in `dialers.conf` = `Environment="DSHS_RELAY_DIALERS=ops:manager"`(只有 **1** 个拨号方) | **维持** | +| ⑥ | **relay 拨号白名单 `dialers`**(R5 引入) | **否** | — | ✅ **准入面零放宽**(默认拒绝 + 逐设备 grant);⚠️ **身份面在序㊻ 步骤6 起按"租户网"整网放宽**(见右栏,已具名登记) | `src/net/relay/server.ts:701` `private readonly dialers`;`:821` `this.dialers = normalizeDialers(opts.dialers)`(**仍在构造期定型**);`:1314` `const wantDialer = isAllowedDialer(...)`(默认拒绝,**唯一出口**在 `network.ts:433`);drop-in `dialers.conf` = `Environment="DSHS_RELAY_DIALERS=ops:manager,u:*/*"` —— ① `ops:manager` = 平台自己的机器(照旧只有 1 个)② `u:*/*` = **任何租户网**整网放行(序㊻ 步骤6,租户 id 运行时才生成 ⇒ 只能这么表达)。**放宽的确切范围**:只放宽「进来之后算哪种身份」(拨号方 vs 被连方),且**只对 `networkKindOf(...)==='tenant'` 的网**生效;「能否进入本网」仍由 `REQUIRE_IDENTITY=1` + 受信签名者签发的逐设备 grant 决定(⛔ 一字未动)⇒ 没有 grant 的设备连这一层都到不了。**代价(有意)**:租户网内不得有声明端口的节点(桶语义 = 拨号方名单)。**收窄手段**:删掉 `u:*/*` 即回到"逐设备/逐网列举 + 默认拒绝"。 | **维持(准入面)+ 已具名登记(身份面)** | +| ⑩ | **设备台账 `overlay_devices`**(序㊻ 步骤6 新增,v12) | **是(新增存储面,⛔ 未新增对外面)** | — | ✅ **对外零扩大**:表只在平台 PG 内(`127.0.0.1:15432`),⛔ 不参与 relay 线上判定;管理面两条端点均 `requireAdmin`,且**只回公钥指纹、⛔ 不回 nodeKey 原文** | `src/db/schema.ts` v12(双方言);端点 `GET /api/admin/overlay-devices` + `POST /api/admin/overlay-devices/revoke`;`overlay-device.ts` 的停发闸门(`403 device-revoked`) | **维持** | | ⑦ | **每机独立密钥与信任根保管** | **否** | — | ✅ **已收窄**:私钥**只在本机**、信任根在**离线**签发;relay 侧只有**公钥**与**签名者集合** | `/etc/dshs.env` `DSHS_OVERLAY_NODE_KEY_FILE=/etc/dshs/node-manager.key`(**私钥路径仅本机**);drop-in `identity.conf` 里**只有** `DSHS_OVERLAY_ROOT_PUBKEYS=<公钥>` + `DSHS_OVERLAY_SIGNER_SET_FILE` ⇒ **私钥从不出现在 relay 的配置面**;`:399` `this.trustedSignerKeys`/`:401` `this.requireIdentity` 均 `readonly` | **维持** | **两条附注** diff --git a/dsh-server-docs/04-调整方案/133-覆盖网络-低熵块治理方案-C域分离与D非确定性.md b/dsh-server-docs/04-调整方案/133-覆盖网络-低熵块治理方案-C域分离与D非确定性.md index 1b83d28..25c0749 100644 --- a/dsh-server-docs/04-调整方案/133-覆盖网络-低熵块治理方案-C域分离与D非确定性.md +++ b/dsh-server-docs/04-调整方案/133-覆盖网络-低熵块治理方案-C域分离与D非确定性.md @@ -7,7 +7,7 @@ - 工作区根 `调研_MLE加密去重最优方案_20260918.md`(**§3** 六方案族 / **§5** 结构性发现 / **§5.5** 复杂度与收益) - `.workbuddy/memory/2026-09-18.md` **21:1x / 21:5x / 21:2x** 三节(C / D / B 定义、量化读数、"**收敛加密下持组密钥 ≠ 能解密**"这一前提) - 阈值与判据面单一来源:工作区根 `参数表_覆盖网络_20260917.md`(⚠️ 现版 §10 指纹 = `d408d640246a980f702fe7b0a2895219`,**以现算为准**) -- 执行载体:`交接单/覆盖网络-序45-低熵块治理-测熵与实现.md` +- 执行载体:`交接单/覆盖网络-45-低熵块治理-测熵与实现.md` --- diff --git a/dsh-server-docs/04-调整方案/142-IM群组对话-需求基线与方案.md b/dsh-server-docs/04-调整方案/142-IM群组对话-需求基线与方案.md new file mode 100644 index 0000000..b670065 --- /dev/null +++ b/dsh-server-docs/04-调整方案/142-IM群组对话-需求基线与方案.md @@ -0,0 +1,211 @@ +# 142 · IM 群组对话(需求基线与方案) + +- 日期:2026-09-20 | 状态:📋 **规划态**(需求已拍板;**未实施**,零代码改动、未动服务器) +- 层级:新功能规划 | 承接档案 **107 / 108 / 109 / 110**(覆盖网络 · 群聊与 agent 入群相关调研) +- 🟢 范围:只考虑技术实现;跨境数据合规由使用者自行考虑。 + +> ## 🔴 架构对齐状态(2026-09-22 复核) +> +> 本文成稿于 **09-20**,**早于**两份架构定稿 ⇒ §3.4 / §3.5 / §4 的落点与「可复用能力清单」必须**按定稿读**。 +> 依过程档案纪律:**正文保留原文不改**,差异以本块与五份交接单的对齐节为准。 +> **定稿(冲突时以它们为准)**:`架构设计/数据-分库与权威存储架构.md`(数据面:**一插件一库** + 归属列强制 + **不得以实例本地库为权威**)| `架构设计/覆盖网络-顶层架构全貌.md`(形态:**多区域联邦**,三层权威,跨区「**数据不动、只有请求动**」)。 +> +> **被后续定稿修正的五条**: +> +> 1. **§3.4「权威数据 = PG(Manager 侧)」** ⇒ 补三句:库是**控制面库 `dshs`**(三表属**内核表**,见 `DB-03 §〇`);房间权威落**建房那个区(锚定区)的 Manager**;**本期只做区域内房间**(跨区依赖 149 §七 **F6 跨区可见性**,**待拍板**,倾向默认互不可见)。 +> 2. **§3.5「relay / DIAL / presence 可直接复用」—— 整体更正**:**relay 与 DIAL 不在 IM 的依赖链上**(IM 用户间通道 = 平台侧 HTTPS/WS,不经覆盖网络数据面;relay 只绑回环、只转发密文)。**presence 只复用「机制口径」**(1 s 批合并 · 只订阅可见成员),**通道必须由平台侧 IM hub 自建** —— 覆盖网络 presence 是**同用户多设备**的节点在线态,而网络层**跨用户隔离是结构性的**(`dialers` 默认拒绝),接不出跨用户在线态。⇒ §3.5「传输层已就绪」应读作**「平台侧 HTTP/WS 与账号体系就绪」**,**不是「网络层能力可复用」**。 +> 3. **§3.6 / §4 插件数据落点** ⇒ **插件自己的库** `dshs_pl_`(同一 PG 13.23),表名 `p__*` 由内核自动补,用户 / 房间维度靠**归属列**;插件**不建表、不写 SQL、不持连接**。细则 = `数据库/DB-03-插件数据面规范.md`。 +> 4. **§4「DB」行的迁移号** ⇒ 三表取 **v14**(现网最新 = **v13**,见 A 单 §二)。 +> 5. **§六「待定」的落地口径** ⇒ 五份交接单(A / B / C / D / E)已于 **2026-09-22** 按上述定稿对齐;**执行前以交接单为准**,本档只作需求基线。 + +## 🔴 现状复核(2026-09-22) + +- **实施进度 = 零**:`src/im/` 目录**不存在**;`src/db/schema.ts` 最新迁移 = **v13** ⇒ IM 三表取 **v14** 仍成立(原写 v12 系 09-20 时点的偏差,已在 A 单与本块更正)。 +- **五单状态**:A / B / C / D / E 全部 **⏳ 待执行**、**未登记执行棒**;串行建议 = **A → B →(C ∥ D)→ E**(`交接单/README.md` §一)。 +- **四个待拍板项未决**:① UI 落点(门户 / 独立页)② E2EE 本期做不做 ③ 首批场景优先级(办公群 / MUD / 跑团)④ 房间上限与发言预算的具体数值(待压测标定)。 + +> **TL;DR** +> 用户拍板 **B 方案**(人 + Agent 一次做齐),并给出两条**决定形态**的语义:① **群聊消息本身要能作为 agent 的上下文**; +> ② **agent 入群有两种形态** —— **能力机器人**(挂功能区,被 @ 才答)与 **组员代理**(组员开启「待应答」后,@ 组员 = 由对应 agent 代答, +> **组员与其 agent 是一个整体**)。现成度:**传输层已就绪**(relay / DIAL / presence),**应用层全部需新建**。 +> +> ➕ **2026-09-20 追加硬要求(平台化)**:IM 群组**不是单一场景功能** —— 「**不同插件都要能接入**,比如协作办公插件群、mud 游戏群、跑团群等」 +> ⇒ 内核只做**通用地基**,**房间类型 / 消息载荷 / 发言规则 / 事件订阅** 全部下沉为**插件扩展点**(见 §3.6)。 + +## 一、背景与动机 + +用户问「现在项目功能和网络架构支持 IM 单聊或多人对话吗」。2026-09-20 只读取证结论: + +| 项 | 实测结论 | +|---|---| +| 应用层 | `room` / `chat` 在 `src/` **全仓零命中**(唯一一处 = `net/relay/content/peer.ts:25` 注释「不做房间层」) | +| 数据模型 | `schema.ts` **11 个迁移,无任何消息 / 房间 / 成员表** | +| 用户间通道 | **不存在**,且属**故意隔离**:`src/net/relay/server.ts:462` 判据「`u:A` 的节点看不到也到不了 `u:B` 的节点」 | +| 平台侧实时端点 | 只有「用户 → 自己实例」的 WS 透传(`supervisor/proxy.ts` 的 upgrade),**无平台级 IM 端点** | +| 现有「对话」 | 「用户 ↔ 自己的 dsh 实例」,单人私有,数据在实例本机 `$DSH_HOME`,平台不参与 | + +⇒ 判定:**IM 单聊与群聊当前都没有**;但**传输与身份地基已就绪**(见 §3.5),群聊属**应用层新增能力**,不是开关。 + +## 二、用户决策(2026-09-20 · 原话级) + +| # | 决策 | 原话要点 | +|---|---|---| +| 1 | 采纳 **B 方案**:人 + Agent **一次做齐** | 「B 方案:需要考虑群聊天信息要能作为上下文agent可以根据聊天信息回答问题」 | +| 2 | **群聊消息 = agent 的上下文** | 「群聊天信息要能作为上下文,agent 可以根据聊天信息回答问题」 | +| 3 | **形态 A · 能力机器人** | 「agent入群是作为能力机器人放在功能区域被@后回答问题」 | +| 4 | **形态 B · 组员代理** | 「组员开启待应答后组员被@后对应agent带用户回答问题(**组员和对应agent 是一个整体**)」 | +| 5 | **插件可接入(平台能力)** | 「这个功能需要让不同插件都能接入,比如协作办公插件群了,mud游戏群了,跑团群了等」 | + +## 三、方案设计 + +### 3.1 成员模型 —— 两种形态的落点差异(本案关键判断) + +`room_members.kind` 取值 `human` / `bot`;另加两列 `auto_reply`(待应答开关)与 `agent_ref`(绑定的 agent)。 + +| 形态 | 是否占独立成员位 | 归属 | 触发条件 | +|---|---|---|---| +| **A · 能力机器人** | ✅ **占**(`kind=bot`) | 房间 / 平台,**不隶属任何用户** | 被 @ ∧ 房间允许它在场 | +| **B · 组员代理** | ❌ **不占** —— agent 是组员的**属性**(`auto_reply=1` + `agent_ref`) | 绑定到某个组员 | 该组员**被 @** ∧ `auto_reply=1` ∧ `agent_ref` 已绑定 | + +**为什么形态 B 不给 agent 一个独立成员位**(四条理由,缺一条都不成立): + +1. 用户原话是「**组员和对应 agent 是一个整体**」⇒ 模型上就该是**一个成员**,不是「一个成员 + 一个机器人」; +2. 独立占位 ⇒ 成员列表与人数统计出现「一人两票」,房间人数、发言配额、管理员席位全要重算; +3. @ 对象歧义:@ 组员时若 agent 是独立成员,就必须再定义一层「转发规则」,凭空多一层间接; +4. 权限判定变简单:agent 的发言权**继承其主人**(可再收窄),无需单独一套成员权限。 + +**形态 A 必须独立**:它是「房间的公共能力」,不属于任何组员 —— @ 它不应绑定到某个人的身份上。 + +### 3.2 上下文模型 + +agent 回答时的上下文 = **房间消息流的一段窗口**,不是「只看到 @ 它的那一条」。组装式: + +```text +[agent 职责说明] + [房间元信息(名称、成员表、各成员是否挂了 agent)] + + [最近窗口:最近 N 条 或 最近 T 分钟,按逻辑时钟排序] + + [被 @ 的那一条] ← 窗口边界可能把它排除在外 ⇒ 必须显式补上 +``` + +必须定的三个量(**未定,见 §六**): + +- **窗口上限**:条数与字节/token 取小; +- **超限策略**:更早的消息折叠为**摘要**(谁、何时、什么主题),**不是硬截断** —— 硬截断会让 agent 丢掉"刚才在聊什么"; +- **可见性**:agent 只能拿到**它主人有权看到**的那部分;私密/子频道消息不进上下文(与「接入 ≠ 可见」一致)。 + +获取方式 = **agent 侧按游标拉取**(同档案 107「大房切游标拉取」的纪律),**平台不主动推全文** ⇒ 避免「一条消息 × N 成员」的推送放大。 + +### 3.3 触发模型与四条硬约束 + +| 形态 | 触发条件 | 谁能触发 | +|---|---|---| +| A 能力机器人 | 被 @ ∧ 房间允许该 bot | **人的消息**(`kind=human`) | +| B 组员代理 | 组员被 @ ∧ `auto_reply=1` ∧ `agent_ref` 已绑定 | 同上 | + +四条硬约束(沿用档案 110 §1.3,逐条保留): + +1. **只有「人的消息」能触发 agent** —— agent 产出的消息**默认不触发**任何 agent(切断 `A 说 → B 回 → A 再说` 的自激环); +2. **每轮发言预算**:每个 agent 每 N 分钟最多 M 条; +3. **静默期**:发言后冷却; +4. **房间级全局速率上限 + 排队**:多 agent 同时发言时按序,不刷屏。 + +➕ 配套一条**显式例外开关**:若要允许 agent 互相 @(多 bot 协作),必须**房间级显式开启**且带**跳数上限**(默认 2 跳),**默认关闭**。 + +➕ **代答标识是产品语义,不是实现细节**:形态 B 的代答消息必须带可见标识(如「由 X 的助手代答」),否则群成员无法分辨**谁在说话**。 + +### 3.4 存储与传输 + +| 项 | 落点 | +|---|---| +| 权威数据 | PG(Manager 侧):`rooms` / `room_members` / `messages` | +| 消息语义四项 | 内容哈希 ID(去重)· 逻辑时钟(排序)· 送达确认 + **游标补拉**(拉取式)· 房间级权限(档案 110 §1.2) | +| 用户端通道 | **平台侧 IM WS 端点**(新),复用现有 WS 透传与 nginx 配置 | +| Agent 端 | 实例内**自研插件主动拨出**(对齐「worker 永远只拨出」)→ 收消息 → 组装上下文 → 本地 dsh 会话 → 回复回房间 | + +### 3.5 与现有架构的边界(唯一张力) + +跨用户互通 = **现有网络模型的反向**(现模型:同用户多设备互通 / 跨用户隔离)⇒ 群聊必须走 +**中心房间服务 + 应用层房间 ACL**,网络层**继续维持跨用户隔离**。⛔ **不放开跨网 P2P** —— 那会退化成一巨网靠 ACL 兜底。 + +可直接复用的既有能力(**无需改动**): + +| 能力 | 状态 | +|---|---| +| relay ×2 真机(47 + 106,只绑回环) | ✅ 常驻 | +| `DIAL` 点到点拨号到 `(hostId, port)` | ✅ 已落 | +| `SUB` / `UNSUB` / `PRESENCE` / `SNAP` 订阅式在线态 | ✅ 已上线(1 s 批合并) | +| 一机一钥 + 邀请凭据 | ✅ 已落 | +| 网抽象 `ops` / `u:` + 跨网结构性隔离 | ✅ 已落 | + +### 3.6 插件接入模型(**第二个关键判断**:群组是平台能力,不是单一场景功能) + +用户要求「不同插件都能接入,比如协作办公插件群、mud 游戏群、跑团群等」⇒ **内核只做通用地基,场景差异全部下沉到插件**。 +若把「办公群 / 游戏群 / 跑团群」的差异写进内核,第一类新场景进来就要改内核 —— 那就不是平台能力。 + +**七个扩展点**(内核给什么 / 插件定什么): + +| 扩展点 | 内核提供 | 插件定义 | +|---|---|---| +| 房间类型 | `room_type` 字段 + 默认配置装载 | 类型名与默认值(`office` / `mud` / `trpg` …) | +| 消息载荷 | **只定义 envelope**:ID / 逻辑时钟 / 作者 / 可见性 | payload 结构与校验(任务卡片 / 行动指令 / 骰子结果) | +| 成员角色 | 基础角色 `human` / `bot` | 业务角色(DM / 玩家 / 观众)及其权限 | +| **发言规则** | hook:发言前询问插件 | 自由并发 / **回合制**(跑团)/ 限速(MUD) | +| 能力机器人 | 注册与 @ 路由 | 机器人的名字、能力说明、回复逻辑(= 形态 A 的来源) | +| 事件订阅 | 房间事件:新消息 / 成员变更 | 插件自定消费,**拨出式**(与 agent 插件同一条通道) | +| 房间内 UI | dsh 既有 slots 面板机制 | 场景面板(骰子 / 角色卡 / 地图) | + +**三条平台保护约束**(否则插件会把房间拖垮): + +1. **插件不直连 DB** —— 一律走内核 API,⛔ 不许插件直接写 `messages` 表; +2. **插件故障不拖垮房间** —— 超时 + 熔断,插件逻辑挂掉时消息流照常(降级为普通聊天,不中断); +3. **能力有预算** —— 插件的发言与事件回调**吃与 agent 同族的预算**,房间级速率上限对插件同样生效。 + +**传输档位必须可选**(这是 MUD / 跑团与办公群的**真实差异**,不是参数微调): + +- **聊天档** —— 沿用档案 107 的容量口径(50 msg/s × 扇出 50); +- **实时档** —— MUD 这类按档案 108 的游戏侧口径(每玩家 2–20 KB/s、jitter < 20 ms); +- ⇒ **房间类型要能声明自己走哪一档**,两档**不是同一套参数**。 + +**与既有机制衔接**:插件的注册 / 分发 / 启停**直接复用**「admin 导入候选池 → 用户在实例「功能管理」里启用」这条既有通道,**不新造分发机制**。 + +## 四、改动范围(初判,未细化) + +| 层 | 落点 | 状态 | +|---|---|---| +| DB | `src/db/schema.ts` 新增 **v12**(rooms / room_members / messages) | 需新建 | +| 平台 API | `src/web/routes/im.ts` + `src/im/**`(房间 / 消息 / 成员 / 游标) | 需新建 | +| 用户端通道 | 平台侧 IM WS 端点(挂现有 fastify) | 需新建 | +| Agent 接入 | 实例内自研插件(拨出 + 上下文组装 + 回复) | 需新建 | +| **插件 SDK** | 七个扩展点的注册面 + 房间事件契约(见 §3.6) | 需新建 | +| UI | 会话界面 | 需新建(**落点待定,见 §六-1**) | +| 传输 | relay / DIAL / presence | ✅ 复用 | + +## 五、验收判据(草案) + +| # | 判据 | 期望 | +|---|---|---| +| 1 | 乱序与去重 | 乱序投递收敛到同一顺序;重复消息按内容哈希去重(档案 110 §五 A2) | +| 2 | **上下文真的进去了** | agent 的回答能引用**窗口内、非 @ 它**的消息(若只能引用 @ 那条 ⇒ 上下文没生效) | +| 3 | 防自激 | 构造「A 的 agent 回复 → 是否触发 B 的 agent」 ⇒ **必须不触发**;压测下四条预算均生效 | +| 4 | 大房 | 1000 人房**不出现**「每人每条广播给全体」 | +| 5 | 离线补拉 | 断线期间的消息在重连后按游标补齐 | +| 6 | **跨场景验证(扩展点够不够)** | 至少跑通**一个非聊天场景**(MUD 或跑团)且**不改内核** —— 若必须改内核,说明扩展点没设计对 | + +## 六、待定 / 待拍板(2026-09-22 更新拍板结果) + +1. **UI 落点** —— 🔴 **仍待拍板**。用户 2026-09-22 要求先讲清「UI 具体指什么」⇒ 议题**细化为三案**(原只列两案):**A 门户内嵌「会话」分区**(`web/portal.html`)/**B 独立聊天页**(`web/im.html`,可挂子域)/**C 实例内工作台面板**(走 dsh 既有 slots 机制,与 MCN 工作台同族)。详见 E 单 §四-4。 +2. 房间上限与发言预算的具体数值(M / N / 房间上限)—— ✅ **用户 2026-09-22「待定」**:维持压测标定,先用保守初值(A 单决策点 7)。 +3. E2EE —— ✅ **用户 2026-09-22 拍板「本期做」(一步到位)** = 原 D 单**候选 B**;判据见档案 110 §4.1(中继只见密文 / 控制面只见密文与身份)。 + ✅ **2026-09-23 档位已拍板 = 强度档**(用户原话「强度档是否影响性能,影响较小就用强度档」⇒ 采用强度档):每发送者一条链(Megolm 式)+ 每消息一把密钥 + 每设备持久化棘轮状态 + agent 升格一等成员设备。定量依据与 9 条连带重写清单见 `交接单/IM群组-06-稳定性机制与框架选型.md` **§十四-14.1~14.3**;契约面落点见 D 单 §四-6(已重写)。 + ⚠️ **旧表述「形态档位待确认 / 按工程务实档暂记」已作废**。 +4. **插件扩展点的开放边界** —— 已自定:新 `room_type` 需 **admin 审核后生效**(D 单 §四-5,可推翻)。 +5. **首批场景优先级** —— ✅ **用户 2026-09-22 拍板「都需要」**:办公群 / MUD / 跑团**三类都要** ⇒ 扩展点须**一次覆盖三类**,⛔ 不再是「先做一个」;D 单硬判据**升级为三类各跑通一个**。 + +## 七、回滚 + +本档**零改动** ⇒ 无回滚需要。执行期各层独立可回滚:新表加列带默认值 · 新路由独立挂载 · agent 插件走候选池可禁用。 + +## 八、下一步 + +1. 出**可执行交接单**,按 A 层(DB)/ B 层(平台 API + 用户端通道)/ C 层(agent 插件)/ D 层(UI)**分单**; +2. 每单自带验收命令与回滚步骤; +3. ⛔ §六 的未拍板项**不在执行单里替用户决定**。 diff --git a/dsh-server-docs/04-调整方案/143-MCN工作台入口失效与语言切换显示修复.md b/dsh-server-docs/04-调整方案/143-MCN工作台入口失效与语言切换显示修复.md new file mode 100644 index 0000000..9f6e715 --- /dev/null +++ b/dsh-server-docs/04-调整方案/143-MCN工作台入口失效与语言切换显示修复.md @@ -0,0 +1,101 @@ +# 143 · MCN工作台入口"点了没反应" + 用户设置里切语言"没反应" + +- 日期:2026-09-20 +- 触发:用户「admin账号dsh会话 点击左侧 MCN工作台入口 没有反应,找到原因修复后把MCN任务会话入口的位置改到MCN工作台 header区域(用图标即可)」+「设置选项中 用户设置里面切换 语言还是不起做用」 +- 对象:`dsh-plugin-mcn-suite` **0.3.10 → 0.3.11**(客户端源 = `dsh-plugin-mcn/lib/client.js`)· `@dsh-local/portal-entry` **0.5.5 → 0.5.6** +- 状态:✅ 已落地,**真机浏览器实测通过**(admin 实例,47) + +> **TL;DR**|两条都是 **dsh 0.1.5-rc.1 与插件里写死的老契约不一致**,不是用户操作问题: +> ① 对话区槽位名从 `conversation` 改成 **`main.conversation`** ⇒ 插件 `querySelector('[data-slot="conversation"]')` +> 实测**恒为 null** ⇒ 工作台面板**根本不挂载**(面板宿主 `pageHost` 一直 null,portal 不渲染)="点了没反应"。 +> ② `ctx.locale.getLocale()` 由"返回 id 字符串"变成**返回快照对象** `{active,locales,revision}` ⇒ +> `String(obj).split("-")[0]` 恒为 `"[object Object]"` ⇒ 受控 `