# 架构与数据契约 本文是 `dsh-research-kit` 的架构参考:模块职责、运行时数据流、DSH 宿主边界、目录数据契约与扩展点。需要改动代码或新增资产时以此为准。 文档索引见 [`README.md`](README.md);手工验收方式见 [`MANUAL-QA.md`](MANUAL-QA.md);方法工坊与草稿增强器的嵌入细节见 [`METHOD-WORKSHOP.md`](METHOD-WORKSHOP.md)。 ## 1. 架构决策 ### 1.1 一句话架构 `dsh-research-kit` 是一个 **浏览器侧 DSH 插件**:它维护科研资源目录、把工作流和用户参数组装成可编辑 Prompt,并由 DSH 当前会话负责实际发送与执行。 ```text ┌────────────────────────────────────────────────────────────────────┐ │ DSH 宿主 │ │ conversation.view conversation.input.* inputActions │ │ (视图槽位) (左入口/浮层/右增强) setDraft / submit │ └───────▲───────────────────────────▲───────────────────▲────────────┘ │ │ │ 注册统一视图 注册输入槽位 写入 / 发送最终 Prompt │ │ │ ┌───────┴───────────────────────────┴───────────────────┴────────────┐ │ DSH Research Kit(浏览器侧) │ │ │ │ ResearchConsole(统一容器 + 两级吸顶) │ │ ├─ ①「资源与工作流」 catalog.js ──► catalog/*.json │ │ ├─ ②「方法工坊」 vendored 工件 + prompt-studio-glue │ │ ├─ ③「研究资产库」 research-vault.js ◄─► vault-core.js │ │ └─ ④「研究证据图谱」 evidence-store.js ◄─► evidence-graph-core │ │ │ │ 所有分区共享同一出口:setDraft() / submit(),缺失时降级为复制 Prompt │ └───────▲─────────────────────────────────────────────────────────────┘ │ 仅两条受控路由(index.js,Node half) │ /query(公开数据源直查,经 ctx.web.fetch) · /semantic-enhance* └───────┴──── DSH 受控 web 服务 / 当前会话模型路由 ``` ![全局架构:插件只做 Prompt 组装,执行全部交回 DSH 宿主](assets/architecture.svg) ### 1.2 不引入独立后端的原因 第一期不需要沙箱、数据库代理或任务队列。它们会重复 DSH 已经承担的能力,并带来权限、保密、成本和状态同步问题。 **例外:Node half 只保留两条无法在浏览器侧完成的受控路由**(`index.js`,见 §2.1): 1. `/dsh-research-kit/query` —— 公开数据源直查。浏览器无法直接跨域访问这些 API,且需要进程内缓存与限流;出网一律经 DSH `ctx.web.fetch()`,插件自身不持有凭据。 2. `/dsh-research-kit/semantic-enhance`(含 `/stream`)—— 草稿语义增强。复用当前会话已建立的模型路由(`sessionId → provider/model`),不持有任何 API Key。 两者都不构成"插件自己的后端":无独立进程状态可持久化、无凭据、无模型选择权,宿主卸载插件后不残留。除此之外的一切仍在浏览器侧完成。 | 能力 | 所有者 | 本插件职责 | | --- | --- | --- | | 模型路由与调用 | DSH | 不读取密钥,不自行调用模型。 | | 当前对话与发送 | DSH | 通过 `inputActions` 写入或提交。 | | 文件、`@` 提及与文件内容注入 | DSH | 只在 UI 中提示用户使用原生能力。 | | MCP、Web、数据库工具 | DSH / 已安装插件 | 只如实标注工作流的接入前提。 | | 工作流资产与 Prompt 组装 | Research Kit | 维护 JSON 目录、表单、校验与预览。 | | 科研结论的责任 | 研究者 | 产物必须标为草案/待核验,不自动升级为事实。 | ### 1.3 已知宿主兼容性原则 同级 `dsh-promptkit` 已验证 DSH 的 `conversation.view` 与 `inputActions` 集成方式,但 DSH 的槽位 props 曾在版本间演变。因此: - 首次接入必须在真实 DSH profile 启动验证,而不是只依赖单元测试; - 所有访问 `inputActions` 的代码必须允许其暂时不存在,展示可理解的错误,不得使视图崩溃; - 仅在用户点击“发送到当前会话”时调用 `submit()`; - 不截获 Enter,不改变原生输入框行为; - 不重新实现 `@文件` 自动补全、上传或文件解析。 ## 2. 目录与模块职责 ```text dsh-research-kit/ ├── catalog/ # 人工审核的科研资产,纯数据 │ ├── workflows.json # 参数化 Prompt 工作流(65) │ ├── skills.json # Prompt 指导或宿主能力前提(8) │ ├── databases.json # 数据源说明与接入状态(55) │ └── database-metadata.json # 数据源分组等展示元数据 ├── src/ │ ├── catalog.js # 资源读取、搜索、查询、Prompt 组装(纯函数) │ ├── catalog-storage.js # 收藏与使用历史的 CatalogStorage 接口 │ ├── research-selection-store.js # 会话级资源选择(仅存于当前会话) │ ├── evidence-store.js # 本会话证据索引(只读汇总) │ ├── evidence-vault-store.js # 证据库持久化:IndexedDB 最小 schema + 内存降级 + 项目隔离 / 去重 / 备份 │ ├── theme.js # 视觉令牌单一真源(--rk-* 明暗双源 + 交互反馈) │ ├── ui.js # 统一基础组件层(按钮/卡片/输入/标签/空态/弹窗…) │ ├── research-console.js # 统一视图容器:分区调度 + 两级吸顶偏移实测 │ ├── research-workbench.js # 分区①「资源与工作流」:目录检索 + Prompt 组装 │ ├── research-vault.js # 分区③「研究资产库」:灵感资产增删改 / 版本 / 验证状态 + 证据库子模块切换 │ ├── research-evidence-vault.js # 证据库面板(项目切换 / 删除 / 导入导出)与保存表单 │ ├── research-evidence-graph.js # 分区④「研究证据图谱」:关系图渲染 + 已保存证据接入 + 方向性锚点 │ ├── database-query-panel.js # 公开数据源直查面板(工作台详情内嵌) │ ├── composer-launcher.js # 输入框工具行「资源/工作流程」入口 │ ├── composer-overlay.js # 输入框 overlay 资源选择器与启动弹窗 │ └── lib/ │ ├── icons.js # 图标 path │ ├── enhance-output.js # 模型输出协议解析(Node half 与浏览器共用) │ ├── vault-core.js # 灵感资产纯逻辑与隐私边界 │ ├── evidence-graph-core.js # 证据图谱节点与边纯逻辑(含已保存证据接入) │ ├── evidence-vault-core.js # 证据条目纯逻辑:标识符识别、规范化、隐私校验、去重键、备份格式 │ ├── console-sections.js # 统一容器的分区契约(名称/定位/用途/边界/独占数据) │ └── overlay-anchor.js # 输入卡片浮层的锚定与可用高度解算(纯函数) ├── dsh/ │ ├── standalone-glue.js # DSH 槽位注册唯一入口(view + input.left + input.overlay + input.right) │ ├── slot-registry.js # 四个槽位的 id/order/label 单一事实源(纯数据) │ ├── prompt-studio-glue.js # 分区②「方法工坊」宿主与 provider 实例化 │ ├── prompt-enhancer-glue.js # 草稿增强器宿主:研究上下文桥接 + SSE 客户端 │ ├── database-query.js # 公开数据源直查适配器、缓存与限流(Node half) │ └── semantic-enhance.js # 语义增强 system 指令与两条路由(Node half) ├── ui/ │ ├── package.json # 浏览器子包元数据 │ └── client.js # 构建生成的 DSH ModuleLoader 产物(勿手改) ├── vendor/ │ ├── promptkit-embed.js # vendored 界面工件(SHA-256 锁定,勿手改) │ └── vendor-manifest.json # 工件来源 commit 与校验和 ├── scripts/ │ ├── build-client.mjs # 内联目录数据并生成浏览器产物(含符号顺序断言) │ ├── check-vendor.mjs # 校验 vendored 工件未被篡改 │ └── validate-catalog*.mjs # 目录契约校验(CLI 与测试共用纯逻辑库) ├── test/ # 122 项测试(15 个测试文件 + helpers 下的 1 个 IndexedDB 桩:纯逻辑 + 源码/产物文本断言) ├── docs/ # 读者文档,索引见 docs/README.md ├── index.js # Node half:仅注册受控路由 ├── package.json └── cordis.patch.yml # DSH bundle 注册补丁 ``` ### 2.1 分层规则 | 层 | 可以做什么 | 不可以做什么 | | --- | --- | --- | | `catalog/` | 声明研究流程、文本、字段、关联、限制 | 放可执行 JS、密钥、未核验结论。 | | `src/catalog.js` | 纯函数、数据校验、搜索、Prompt 组装 | 访问 DOM、网络、`localStorage`。 | | `src/catalog-storage.js` | 收藏/历史读写、变更通知,隔离 localStorage | 存参数值或完整 Prompt;网络访问。 | | `src/research-selection-store.js` | 会话级资源选择的读写与广播 | 跨会话持久化。 | | `src/evidence-store.js` | 在当前页面内存中汇总本会话已选资源、已启动工作流与直查来源的**索引**,并向各视图实时广播 | 执行查询、持久化、保存原始文件或检索词、生成结论。 | | `src/evidence-vault-store.js` | 证据库持久化:IndexedDB 最小 schema、按项目隔离与去重、备份序列化,并提供内存降级 | 触碰 DOM;在降级时伪装成已持久化;保存未经用户确认的条目。 | | `src/theme.js` | 主题 CSS 变量与 GlobalStyle 注入 | 读取宿主私有主题 API。 | | `src/ui.js` | 无业务状态的基础组件与图标 | 持有业务逻辑或读取目录数据。 | | `src/research-console.js` | 分区调度、分区导航、两级吸顶偏移实测 | 持有任何分区的业务逻辑,或读写分区的数据。 | | `src/research-workbench.js` / `research-vault.js` / `research-evidence-graph.js` / `research-evidence-vault.js` | React 状态、渲染、调用注入的宿主动作 | 直接依赖 DSH 私有全局或发网络请求。 | | `src/lib/*` | 与 DOM 解耦的纯逻辑(协议解析、布局解算、隐私边界、稳定性标识符识别、备份格式、分区契约) | 触碰 DOM 或宿主 API。 | | `src/composer-launcher.js` / `composer-overlay.js` | 输入框入口、overlay 选择器、启动弹窗 | 绕过 `inputActions` 直接发送或读取文件。 | | `dsh/standalone-glue.js` | 将 DSH props 映射为组件 props、注册槽位(返回统一释放函数) | 处理领域业务、拼 Prompt。 | | `dsh/slot-registry.js` | 槽位 id / order / label 的纯数据声明 | 执行注册本身。 | | `dsh/prompt-studio-glue.js` / `prompt-enhancer-glue.js` | 为 vendored 组件与增强器提供宿主装配 | 修改 vendored 工件本身。 | | `dsh/database-query.js` / `semantic-enhance.js` | Node half 的两条受控路由 | 持久化状态、持有凭据、自行选择模型。 | | `vendor/` | 存放经审查、SHA 锁定的工件快照 | 手工编辑;运行时从相邻目录加载。 | | `ui/client.js` | 仅为构建产物 | 手工编辑。 | | `index.js` | 保持插件 Node half 可被加载;仅注册 §1.2 的两条受控路由 | 持久化状态、持有凭据、自行选择模型或注册未经需求确认的路由。 | ### 2.2 统一视图的分区契约 三个并列视图(科研工作台 / 研究方法工坊 / 研究灵感库——后者即今天的「研究资产库」)已合并为单一 `conversation.view`(`dsh-research-kit-console`),内部按「发现 → 构造 → 沉淀 → 证据」的科研闭环做四个二级分区。证据图谱是 Research Kit 自有分区,汇总本会话资源、工作流、查询来源、资产与**已保存证据**的关系;其余三个分区组件继续以 `embedded` 模式复用。 | 分区 | 定位 | 核心用途 | 职责边界 | 独占数据 | | --- | --- | --- | --- | --- | | 资源与工作流 | 发现层 | 目录检索、按参数与技能组装 Prompt、公开数据源直查 | 不生产知识、不沉淀资产、不直接出网 | 目录收藏与使用历史 | | 方法工坊 | 构造层 | 方法卡库、变量填充生成可编辑 Prompt、从当前对话提取草稿并写回输入框 | 不管理资产正文、不检索项目记忆或最近会话、不替工作流决定领域参数 | 方法卡与工坊资产 | | 研究资产库(含「灵感资产 / 证据库」子模块) | 沉淀层 | 灵感资产增删改、版本派生与对比、验证状态跟进;证据库逐条保存来源元数据与笔记、按项目隔离与去重、导出导入与彻底删除 | 不生成 Prompt、不存原始数据与完整查询结果、不自动入库、不静默注入 | 灵感资产(PromptKit asset provider);证据条目(IndexedDB `dsh-research-kit-evidence`) | | 研究证据图谱 | 证据层 | 可视化本会话已选资源、已启动工作流、直查来源、资产与已保存证据之间的关系(节点与边见 §3.5) | 不执行查询、不生成结论、不保存原始文件与检索词;对证据库只读接入,不写入、不携带笔记与全文 | 本会话证据索引(`evidence-store`);对证据库(IndexedDB `dsh-research-kit-evidence`)只读 | 契约由 `src/lib/console-sections.js` 声明、`test/research-console.test.js` 守护: - 每个分区必须声明名称、定位、核心用途、职责边界与独占数据五项; - 职责边界必须显式包含否定项("不做什么"),数据归属必须互斥,避免合并后功能重叠; - 分区 id 必须与 `src/research-console.js` 的组件映射表一一对应,不允许出现"有导航无内容"的空分区。 合并过渡期的「原「旧标签」」提示徽标已移除:三个分区的旧位置映射在合并后已稳定,徽标只是常驻噪声,且它会随窗口变窄折行、反过来改变导航高度(而导航高度正是二级吸顶的偏移量来源)。因此 `formerLabel` 一并不再作为契约字段保留,避免留下无消费方的死数据。 **分区宽度一致性**:四个分区必须铺满内容区,水平内边距统一引用流式令牌 `--rk-gutter: clamp(16px, 3vw, 34px)`(`src/theme.js` 唯一定义处,容器导航条与四个分区五处共同消费):视口 ≥1133px 时取上限 34px(与既有视觉刻度一致),其间随窗口线性收缩,880px 以下由媒体查询锁定 16px。宽度方向一律用 `100%` 相对父容器解算,不得出现固定像素宽度。方法工坊复用 vendored `PromptStudio`,其根 `
` 内联了 `width: min(1240px, max(100%, calc(100vw - 280px)))` 与 `margin: 0 auto`——这是「独立插件页 + 为宿主侧栏预留 280px」场景的写法。嵌入统一容器后父容器已是扣除侧栏后的可视区,叠加该上限会使本分区收成居中窄栏(宽屏留白、窄容器横向溢出),与另外三个铺满分区视觉割裂。 vendor 为 SHA 锁定工件不可改,因此由 `dsh/prompt-studio-glue.js` 的宿主包装提供锚点 `.rk-studio-host`,`src/theme.js` 以 author 级 `!important` 规则覆盖内联宽度(内联声明非 `!important` 时可被覆盖),只解绑宽度、内边距与 `overflow`,不改动组件其他样式。真实 Chromium 实测(视口 1280px): | 父容器宽 | 解绑前 main 宽 | 解绑后 main 宽 | | --- | --- | --- | | 1440px | 1240px(左右各留白 100px) | 1440px(铺满) | | 900px | 1000px(横向溢出 100px) | 900px(贴合) | 流式令牌四档视口实测(方法工坊分区,`--rk-gutter` 解算值): | 视口宽 | `--rk-gutter` 解算值 | main 宽 | 横向溢出 | | --- | --- | --- | --- | | 600px | 16px(媒体查询锁定) | 铺满 | 无 | | 880px | 16px(媒体查询锁定) | 铺满 | 无 | | 1000px | 30px(3vw) | 铺满 | 无 | | 1440px | 34px(上限) | 铺满 | 无 | 契约由 `test/research-console.test.js` 守护:vendor 宽度写法漂移检测 + 宿主锚点存在 + 覆盖规则齐备(宽度 / 居中边距 / `overflow`)+ 流式令牌定义(上下限与视口项)+ 四分区内边距同刻度。 其中 `overflow` 解绑是吸顶生效的前提,原因见 §2.3:`S.page` 里的 `overflow: auto` 会在嵌入场景下变成一个不再滚动的内层滚动盒,把 `
` 内所有 `position: sticky` 的参照系锁死在它自己身上。 ### 2.3 顶部吸顶分层 统一视图的可滚动内容很长(分区①详情栏、分区③资产列表都能把页面撑到数千像素),而检索、筛选与模式切换是复用频率最高的动作。顶部因此采用**两层吸顶 + 一条分层原则**,而不是把所有头部一把吸住: > **吸顶只放「随时要用的操作」,不放「读一次就够的内容」。** | 层级 | 元素 | 是否吸顶 | 理由 | | --- | --- | --- | --- | | 一级 | `.rk-console-nav`(分区导航 + 说明块) | 是 | 回答"我在哪个分区",跨分区切换不应需要回滚 | | — | 分区封面 `PageHead`(kicker / 标题 / 导语 / 低频动作) | 否 | 定位是"封面":标题与一级导航的当前标签重复,导语是读一次的介绍;吸住会白占约 87px 并放大重复感 | | 二级 | `.rk-sticky-toolbar`(该分区的常驻操作行) | 是 | 高频控件。资源 128 项、资产与方法库持续增长,滚走意味着每次操作都要先回顶部 | 四个分区的二级吸顶带按同一口径组装(检索 + 筛选 + 该分区的模式/主操作): | 分区 | 二级吸顶带内容 | 实现 | | --- | --- | --- | | 资源与工作流 | 科研模式 · 检索框 · 类型筛选(全部/工作流程/技能/数据库/收藏/历史) | `Toolbar sticky` | | 方法工坊 | 方法检索框 · 分类下拉 | vendored 组件的筛选块(宿主侧解绑,见下) | | 研究资产库 | 灵感资产:检索框 · 状态筛选 · 项目筛选 · 新建资产;证据库:检索框 · 核验状态筛选 · 项目选择 · 导出 / 导入 / 清空 | 两个子模块各一条 `Toolbar sticky` | | 研究证据图谱 | 节点计数 · 关系计数 · 图例 | `Toolbar sticky` | **操作下沉而非封面吸顶**:`科研模式` 原挂在分区封面右侧、`新建资产` 原挂在封面动作区,都会随页面滚走(实测 `科研模式` 滚 800px 后 top = −555),每次切换都要先回顶部。两者都是常驻控件,因此下沉进二级吸顶带;而导出/恢复备份是一次性维护动作,留在封面即可,不占用常驻高度。 **折行顺序有意为「模式 → 检索 → 筛选」**:三者放不下一行时按 DOM 顺序折行,把最宽的筛选项留在最后折行才能占满整行;若把「科研模式」放末尾,被挤到第二行的就是它一个窄控件,会留下一整行空白(实测 1180px 窗口即触发)。检索框的 flex 基准(`1 1 200px`)也是按"三者同占一行"倒推的:922px 内容宽下 模式 176 + 检索 200 + 筛选 495 + 间距 20 = 891 ≤ 922。 **偏移量必须实测,不得写死像素**:二级吸顶的 `top` 取 `var(--rk-console-nav-h)`,由 `ResearchConsole` 用 `ResizeObserver` 观察一级导航并写入;挂载时先写一次,卸载时清理。写死必然错位——导航高度随窗口变窄换行(说明块变 4–5 行)而变化,实测在 1280px 宽下为 149px、780px 宽下为 180px。 三个关键约束: - **观察 `box: 'border-box'`**:`ResizeObserver` 默认只观察 content-box,因此"内边距/边框把导航撑高而内容框不变"的变化不会触发回调,二级吸顶会停在旧位置、压进导航底下。此缺陷只有在真实宿主里扰动导航高度才暴露得出来。 - **吸顶带必须有背景**,且垂直节奏由内边距而非外边距承接——外边距区域不绘制背景,吸顶后滚动内容会从缝隙透出(故 `Toolbar` 在 `sticky` 态把 `margin` 置零,由 `.rk-sticky-toolbar` 的 `padding` 接管)。 - **`overflow` 必须解绑**(方法工坊专属,见下):任何祖先建立滚动盒都会夺走后代 sticky 的参照系。 **方法工坊的两条 vendor 解绑**(这是四个分区里唯一不由本仓库实现的区块,两条都只有真实宿主才暴露): 1. **vendored 左列的死 sticky**。组件把筛选块与整个方法列表放进同一列 `