🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

子页面网关 SJTUMalPan/dsh-subpages

DeepSeek Harness
SJTUMalPan37 天前有提交已提供中文文档扫描:含敏感能力

把独立网页挂到 DSH 端口下统一门户访问

30.7/100
综合分
1
GitHub 星标
—
npm 周下载
—/5
用户评分

数据截至 2026/9/28 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

自动兼容检查有告警:未发布到 npm registry,仅可从源码安装。
安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add SJTUMalPan/dsh-subpages
该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:已验证
是什么
dsh 原生插件 · ui
装得上吗
本站已在 dsh 0.2.1-rc.5 下真实安装成功
非静态推断
L5 · 运行时冒烟等级说明 →
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
更新放缓:最近一次提交在 37 天前
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →
社区实装还没有人打卡 · 装过的第一个就是你
安装兼容性检查⚠ 装前注意

未发布到 npm registry,仅可从源码安装

展开查看检查明细 ↓

以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。

✗npm 包dsh-subpages(未发布到 npm,仅可源码安装)
✓Node 引擎要求 >=22 · 基线 Node 22.19 满足
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明

验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/10/12 17:39:41

安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。

读写/删除本地文件
查看扫描证据(共 15 处,展示前 15 条)
  • 读取本地文件
    lib/index.js:64
    await access(path, constants.F_OK)
  • 读取本地文件
    lib/registry.mjs:33
    await access(path, constants.F_OK)
  • 读取本地文件
    lib/registry.mjs:213
    text = await readFile(file, 'utf8')
  • 读取本地文件
    lib/registry.mjs:361
    entries = await readdir(dir, { withFileTypes: true })
  • 读取本地文件
    lib/registry.mjs:382
    raw = JSON.parse(await readFile(manifestPath, 'utf8'))
  • 读取本地文件
    lib/proxy.mjs:464
    const real = await realpath(filePath)
  • 读取本地文件
    lib/proxy.mjs:465
    const baseReal = await realpath(base)
  • 读取本地文件
    lib/proxy.mjs:468
    info = await stat(filePath)
  • 读取本地文件
    lib/proxy.mjs:477
    info = await stat(indexPath)
  • 读取本地文件
    lib/proxy.mjs:492
    createReadStream(filePath).pipe(res)
  • 读取本地文件
    lib/assets.mjs:32
    const body = await readFile(target)
  • 读取本地文件
    lib/assets.mjs:47
    const template = await readFile(join(webDir, 'shell.html'), 'utf8')
  • 读取本地文件
    lib/startup-notice.mjs:57
    const info = await stat(logPath)
  • 读取本地文件
    lib/startup-notice.mjs:59
    const handle = await readFile(logPath)
  • 读取本地文件
    lib/startup-notice.mjs:74
    return (await stat(logPath)).size
扫描于 2026/11/2(1 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

功能亮点

  • 挂载独立网页
  • 统一门户访问
  • 提供子页面网关

DSH 子页面宿主插件:复用 DSH 端口提供 /subpages 网关与统一门户,把独立开发的下级页面一键挂载

README

由 DeepSeek 最新模型翻译生成
dsh-subpages DSH 看板宿主:把一个个独立开发的小网页应用,变成 DSH 里的「看板」——挂在 DSH 自己的端口下、共用 DSH 的登录态、用一个门户统一进入。不必为新页面单独开端口、单独配域名、单独做一套鉴权。 好处: - 不新开监听端口、不新增暴露面:网关只是 DSH webServer 上的一个前缀路由(默认 /subpages),与主界面共用端口;每个请求先过 DSH 自己的鉴权,浏览器侧看不到下游令牌,下游也拿不到 DSH 凭据。 - 看板各管各的:每块看板是独立仓库(自选技术栈、自己发版、自己鉴权),宿主只做转发与门户;git submodule add pages/ + 重启 dsh web 即挂上,不改宿主代码。 清单 subpage.json 放在看板自己的仓库根目录,pages/ 下一律是 submodule 指针——详见 pages/README.md。 - 风格统一:内置公共样式表(Pico classless + 深色 token + sp- 组件类),看板引一个 就跟门户一致,不必自己搭一套设计系统。 - 真 URL:/subpages/notify-hub/todos 可收藏、可深链、刷新不丢(DSH 客户端自身没有路由,这一点是它补上的)。
展开完整文档(README 剩余 1 节)
dsh-subpages

DSH 看板宿主:把一个个独立开发的小网页应用,变成 DSH 里的「看板」——挂在 DSH 自己的端口下、共用 DSH 的登录态、用一个门户统一进入。不必为新页面单独开端口、单独配域名、单独做一套鉴权。

好处:

- 不新开监听端口、不新增暴露面:网关只是 DSH webServer 上的一个前缀路由(默认 /subpages),与主界面共用端口;每个请求先过 DSH 自己的鉴权,浏览器侧看不到下游令牌,下游也拿不到 DSH 凭据。
- 看板各管各的:每块看板是独立仓库(自选技术栈、自己发版、自己鉴权),宿主只做转发与门户;git submodule add pages/ + 重启 dsh web 即挂上,不改宿主代码。
清单 subpage.json 放在看板自己的仓库根目录,pages/ 下一律是 submodule 指针——详见 pages/README.md。
- 风格统一:内置公共样式表(Pico classless + 深色 token + sp- 组件类),看板引一个 就跟门户一致,不必自己搭一套设计系统。
- 真 URL:/subpages/notify-hub/todos 可收藏、可深链、刷新不丢(DSH 客户端自身没有路由,这一点是它补上的)。

自带的两个看板(pages/,均为 submodule)

| 看板 | 独立仓库 | 挂载形态 | 是什么 |
| 待办 | SJTUMalPan/notify-hub | 代理:它自带 FastAPI 服务;宿主把浏览器带来的 ?token= 换成服务端持有的会话,浏览器全程看不到令牌 | 消息汇聚与待办跟踪:各进程投递消息 → 分类 → 落待办 → 飞书 / webhook / 邮件推送,超时未确认会提醒 |
| 用量 | SJTUMalPan/dsh-usage-stats | 静态:宿主直接投递生成好的单文件仪表盘 | DSH 各工程 / 会话 / 步骤的 token、缓存与花费统计(零依赖离线工具 + 单文件 HTML,standalone 样式) |

它们刚好覆盖宿主的两种形态(代理 / 静态)与两种样式策略(inherit / standalone),所以既是能用的页面,也是照抄即可的接入范例。

架构

浏览器 ──> DSH :3080 /subpages/ 门户壳(导航栏 + iframe + hash 路由) /subpages/_assets/ 公共前端库(subpage.css) /subpages/_api/pages 已挂载清单(壳用) /subpages//* ──► 看板自己的 http 服务(127.0.0.1:)每个请求先过 DSH 鉴权(connection.requestRejection),不自建凭据体系

安装

本机开发(符号链接,改完即生效)
dsh plugin --profile web add "$PWD" # 在仓库根目录执行

从 GitHub 装(别人/别的机器;插件市场里也能直接装)
dsh plugin --profile web add github:SJTUMalPan/dsh-subpages

锁 commit(可复现)
dsh plugin --profile web add "git+https://github.com/SJTUMalPan/dsh-subpages#"

装完重启 dsh web。之后侧边栏底部会出现「看板」按钮,点开即门户。

卸载:dsh plugin --profile web remove dsh-subpages

挂载一个看板

在 pages/ 下建一个目录,放 subpage.json:

{
"id": "notify-hub",
"title": "待办", "icon": "inbox", "order": 10, "target": "http://127.0.0.1:8000", "health": "/healthz", "style": "standalone",
"auth": {
"tokenFile": "~/notify-hub-run/config.yaml",
"tokenPath": "server.auth_token",
"queryParam": "token"
}
}

| 字段 | 必填 | 说明 |
| id | 否 | 缺省用目录名。必须 kebab-case(会进 URL 路径段) |
| title | 否 | 导航栏显示名,缺省用 id |
| order | 否 | 导航排序,缺省 100 |
| target | 是 | http://… 表示反向代理到该服务;相对/绝对路径表示静态目录 |
| index | 否 | 静态形态的默认文件,缺省 index.html |
| health | 否 | 代理形态的探活路径,缺省 /healthz |
| style | 否 | inherit(默认,宿主在导航类 HTML 响应里注入公共样式)或 standalone(完全不注入) |
| stripOwnStyle | 否 | 仅 inherit 有意义:压掉看板自带的 (前提是它已按公共约定重写标记) |
| hidden | 否 | 为 true 时不在导航栏显示(仍可直接访问) |
| auth | 否 | 下游凭据,见下 |

auth:令牌只留在服务端

宿主还会替浏览器完成「令牌 → 会话 cookie」的交换并持有该 cookie:下游用 Set-Cookie 开启会话时,宿主收下它、把跳转改写成「回到该看板根路径」,后续请求附带 cookie 而不再注入令牌;cookie 失效(401/403)时丢弃并重试一次。结果是浏览器*既看不到令牌、也拿不到下游 cookie,凭证全在服务端。下游把首页跳走(如 / → /todos)也由宿主在服务端跟掉,浏览器始终只看到一次 200。

"auth": { "tokenEnv": "MY_SERVICE_TOKEN", "header": "Authorization", "scheme": "Bearer" }
"auth": { "tokenFile": "/path/config.yaml", "tokenPath": "server.auth_token", "queryParam": "token" }

- tokenEnv:从环境变量取;
- tokenFile + tokenPath:直接从下游服务已有的 YAML 配置里按点号路径取——不需要你再抄一份令牌到环境变量;
- header + scheme(默认 Authorization: Bearer …)或 queryParam(拼进查询串,兼容只认 ?token= 的服务)。

令牌永远不会出现在浏览器可见的任何响应里:不进清单 JSON、不进 HTML、不进 URL。
这条有测试守着(test/gateway.test.mjs 里断言响应体不含令牌值与环境变量名)。

信任模型(挂载前必读)

一句话:挂一个看板 = 让它的前端代码在 DSH 的源(origin)里跑。看板目录是 submodule,等于「别人的代码」;这里把边界说清楚,免得把它当成沙箱。

1. iframe 是同源的,不是隔离层

门户壳里的 iframe 用
sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-downloads"。
allow-same-origin + allow-scripts 的组合意味着:框架内页面与 DSH 同源、且能执行脚本,于是它可以:

- 用 DSH 的凭据(Cookie 随请求自动带上)读写 DSH 自己的 HTTP 接口;
- 读 DSH 源下的 localStorage / sessionStorage / indexedDB;
- 反过来操作父窗口的 DOM。

因此:只挂你自己信任的看板(自己的仓库、自己审计过的 submodule)。
第三方看板应当部署在独立源上(另一个端口 + 另一个域名),以「跨源代理」形态接入——本插件不做跨源隔离,也不声称做到了。

2. 宿主替浏览器保管下游凭据(令牌不下发)

- 下游令牌只从服务端(环境变量或下游自己的配置文件)读,不进清单 JSON、不进 HTML、不进浏览器可见的 URL;浏览器带 ?token= 进来时,该参数在转发前被剥掉。
- 下游的会话 cookie 由宿主进程持有(按看板 id 存一份),浏览器拿不到它。
- 这份会话是进程级的:同一个看板的所有访问者共用同一个下游会话。
下游若把 cookie 当作「某个人已登录」的证据,本插件不满足该假设——下游自己的鉴权仍按「一个受信任客户端」看待。
- 转发头走白名单(FORWARD_ALLOW):cookie / authorization 等不会从浏览器透传给下游,避免把 DSH 侧凭据泄露给看板后端。

3. 静态形态的路径边界

静态看板的文件只能来自该看板自己的目录:路径解析后做 realpath 校验, ../ 与符号链接都逃不出去。但这不妨碍第 1 条——静态看板的 JS 依然是同源脚本。

4. 谁负责什么

| 边界 | 由谁负责 |
| 门户与网关路由要不要登录 | DSH 自己(connection.requestRejection),本插件不自建凭据体系 |
| 看板后端的鉴权与数据权限 | 看板自己(如 notify-hub 的 server.auth_token) |
| 看板前端能对 DSH 做什么 | 没有技术限制——靠「只挂可信代码」这条纪律 |
| 看板之间的相互影响 | 各自独立进程;同源脚本能力见第 1 条 |

看板的两种形态

代理形态(有后端服务):看板自己起服务监听本地端口,宿主转发。

静态形态(纯前端):target 指向一个目录,宿主直接投递文件(带路径穿越防护)。
静态看板引用公共样式即可风格统一:

公共样式 = Pico 2 classless(本地 vendor,10 KB gzip)+ 深色 token 层 + OS 字体栈(不内嵌任何字体文件)。可直接用的组件类名:.sp-page .sp-card .sp-list .sp-row .sp-toolbar .sp-btn .sp-empty .sp-badge。

运行时挂载(给其它 DSH 插件)

除了扫目录,宿主还通过 ctx.subPages 开放接口:

await ctx.subPages.register({ id: 'x', title: 'X', target: 'http://127.0.0.1:9000' })
ctx.subPages.list() // 浏览器可见投影
ctx.subPages.unregister('x')
ctx.subPages.mountPath // '/subpages'

配置

写在 profile 的 cordis.patch.yml 里(或安装后改插件行的 config):

- id: subpages
config:
mountPath: /subpages # 网关前缀
pagesDirs: [/abs/extra] # 额外扫描目录(默认只用包内 pages/)
requestTimeoutMs: 15000 # 转发超时 probeTimeoutMs: 2000 # 探活超时

启动通告(把访问链接发到飞书)

每次 DSH 启动时,把「带 token 的访问链接」投递到 notify-hub(它再转发到飞书/其它渠道),手机上点开即用。配置在 profile 的插件行里(本仓库 cordis.patch.yml 已给出可用样例):

- id: subpages
config:
startupNotice:yaml
enabled: true # 必须显式打开 publicHost: dsh.example.com # 对外主机(IP 或域名,按需替换) tokenFile: ~/notify-hub-run/config.yaml # 支持 ~ 与环境变量展开 tokenPath: server.auth_token # 凭据直接读下游自己的配置,不新增一份 logPath: /tmp/dsh-web.log # 启动横幅所在文件

要点与坑:

- token 的唯一来源是启动横幅(DSH 把它存在连接服务的进程私有状态里,插件取不到内存,也没有落盘文件),所以这里读启动日志——并且只认本次启动新增的字节,否则会把上一次启动的旧 token 发出去(一条死链)。
- 插件可能先于横幅打印完成装载,所以会等横幅(默认 30 秒),且这段等待不阻塞启动。
- 未配置 publicHost 时不投递:宁可漏发,也不发一条错链接。

手动重启(重启完成后同样会把新链接发到飞书): bash setsid nohup bash /workspace/dsh-restart-notify.sh >/dev/null 2>&1 /?debug=1 看诊断面板:fetch 状态码、iframe 的 load/error、内容长度 |
| 401 | DSH 登录态失效。刷新 DSH 页面重新登录即可——网关复用 DSH 鉴权,没有第二套凭据 |

开发

cd dsh-subpages && npm test # = node --test test/.test.mjs

测试用真实 HTTP:假下游是 node:http 服务端,宿主上下文是最小桩(可观测「注册了什么路由」「是否先过鉴权」)。覆盖:路由注册、鉴权拦截、清单投影不含令牌、反代路径改写与令牌注入、Location 重写、静态投递与路径穿越、未挂载 404、下游离线 502、探活。

文件

| 文件 | 作用 |
| lib/index.js | Cordis 插件:注册网关路由、扫描注册表、暴露 ctx.subPages |
| lib/registry.mjs | 清单校验/扫描/排序 + 令牌解析 + 浏览器可见投影 |
| lib/proxy.mjs | 反代、静态投递、请求头与响应头策略(令牌注入、Location 重写) |
| lib/assets.mjs | 公共资源投递与门户壳模板渲染 |
| lib/sessions.mjs | 下游会话代持 + Location 改写(幂等、根路径归一) |
| lib/startup-notice.mjs | 启动通告:等新增横幅 → 读凭据 → 投递到 notify-hub |
| lib/client.js | 客户端半(手写闭包工厂):侧边栏入口按钮 |
| web/shell.html | 门户壳:导航栏 + iframe + hash 路由 |
| web/subpage.css | 公共前端库(token 层 + 组件类名) |
| pages//subpage.json | 挂载一个看板 |
预览分享卡片长什么样 ↓SJTUMalPan/dsh-subpages
分享 · DeepSeek Harness Hub
SJTUMalPan/dsh-subpages

把独立网页挂到 DSH 端口下统一门户访问

这是你的仓库?认领条目

回到顶部