← 返回列表
需源码安装
一个多租户平台,在单个 Cloudflare Tunnel 后面为每个用户提供一个隔离的 DeepSeek…
暂不能直接安装(需源码编译或环境不满足):仓库缺少 package.json,无法用 dsh 插件安装命令安装。 · 最近上游提交 2026/8/18 · 已提供中文文档
一个多租户平台,用于配置和路由隔离的 DeepSeek Harness (dsh) 实例——每个用户一个容器。
综合分
33.4
GitHub 分
33.4
用户评分
—
★ Stars
2
周下载量
—
安装插件(需先安装 dsh CLI 引擎:npm install -g @deepseek-ai/dsh)
dsh plugin --profile web add vocsong/deepseek-harness-portal仓库缺少 package.json,无法用 dsh 插件安装命令安装,改用 GitHub 源安装
数据截至 2026/9/19(元数据每日更新 · 实装验证按队列轮转,单条结论的验证时间见上方)
安装兼容性检查需源码安装
以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。
✗npm 包deepseek-harness-portal(未发布到 npm,仅可源码安装)
✓Node 引擎未声明 engines.node
✓dsh CLI 依赖未声明 dsh 版本约束
✗入口文件缺少入口声明
仓库缺少 package.json,无法用 dsh 插件安装命令安装
验证方式:npm registry 存在性 + package.json 静态校验 · 最后验证 2026/9/19 03:09:59
用户评分
还没有人投票,来当第一个
订阅周报,不错过优质插件更新
每周一封 · 高评分插件 + 新用户活动
README
DeepSeek Harness Portal 一个多租户平台,在单个 Cloudflare Tunnel 后面为每个用户提供一个隔离的 DeepSeek Harness(dsh)实例——每个用户一个容器。 Internet │ (portal: login, admin, user dashboard) │ . (one instance per user) ▼ Cloudflare Tunnel (cloudflared, one named tunnel) │ ▼ Portal (Node + Fastify + SQLite) ── auth + reverse proxy + orchestrator │ ├─► container dsh- (dsh web on :3000 inside, published 127.0.0.1:18xxx) ├─► container dsh- ... └─► ... Portal 是唯一的认证入口。它负责注册、登录、角色以及每个实例的访问控制。 认证模型 - 注册:邮箱 + 一次性验证码(OTP),注册时可选设置用户名 + 密码(这样新用户可以立即使用凭据登录)。当管理员设置了邀请码时,会强制要求填写邀请码。 - 注册后:个人资料可以更改姓名、用户名、密码和邮箱。更换邮箱需要分别从当前邮箱和新邮箱获取验证码,并会撤销所有其他会话。 - 登录:用户名/密码或“给我发邮件验证码”(OTP 备用方式)。 - 管理员控制(Settings 标签页):邮箱域名白名单、邀请码、OTP 注册开关、密码登录开关。 OTP 发送使用 SMTP(nodemailer),在生产环境中为必需项。只有在明确仅限本地的开发组合 NODE_ENV=development、DOMAIN=localhost 和 OTP_DEV_MODE=true 下才可记录验证码日志;API 永远不会返回 OTP 值。 架构 | 组件 | 位置 | 作用 | |---|---|---| | Portal 应用 | portal/ | Fastify 服务器:认证(OTP + 密码、bcrypt、会话 cookie)、管理员/用户 JSON API、静态仪表盘、子域名反向代理 | | 编排器 | portal/src/orchestrator.js | podman CLI 封装:run/start/stop/rm/logs、端口分配、健康轮询、后台配置 | | 存储 | portal/data/portal.db | SQLite(better-sqlite3):users、otps、settings、instances、sessions | | dsh 镜像 | image/Dockerfile | 从已审核的上游提交、一个依赖安全补丁和两个有文档记录的源码补丁构建经过审查的 dsh 镜像;标记为 dsh:47f9438-node24(Debian 13 trixie 上的 Node 24 LTS),并通过其 sha256: 摘要进行部署 | | dsh 克隆 | dsh/ | 全新的 deepseek-harness 检出(构建上下文,已被 gitignore) | 每实例模型 每个用户恰好获得一个实例(1 user : 1 instance): - 容器 dsh-,CPU/内存/交换/PID 限制,只读根文件系统 + 有界 tmpfs/logs,无 capabilities,no-new-privileges,显式隔离的 pasta 网络,--restart unless-stopped - 两个卷:-home(挂载到 /home/dsh —— 用户的可写主目录,包括用于会话、设置和凭据的 $DSH_HOME=/home/dsh/.dsh)和 -workspace(挂载到 /workspace —— 代理的持久化 cwd) - 发布在 127.0.0.1: 上(绝不暴露到主机之外);门户会代理到它 - 代理将到实例的流量呈现为回环(changeOrigin 将 Host 重写为 127.0.0.1:,并且浏览器的 Origin 被丢弃),以便 dsh 仅限回环的设置/凭据方法可以工作;TRUSTED_HOST=. 仍作为回退传递 - 用户的 DeepSeek API 密钥在其实例内部输入(Settings → Models),并且仅存在于该实例的 home 卷中 对 dsh 的构建时更改(参见 image/Dockerfile) - image/dsh-security.patch 应用经过审查的传递依赖最低版本以及匹配的冻结 lockfile(生产审计:审查时零已知安全公告)。 - 身份开场白 → "You are an AI agent."(品牌化)。 - 允许 --host 0.0.0.0 → 这是必需的,以便发布的端口能够到达容器内的 dsh(出于 LAN 安全考虑,其 CLI 默认拒绝 0.0.0.0;在容器内,只有回环发布的端口可被访问)。 构建脚本要求使用确切批准的上游提交,并且如果源代码/补丁校验发生漂移就会失败。 使用子域,而非路径 实例使用一级子域(.),而不是 /。有两个原因: 1. dsh 的 SPA 调用 /api 并加载 /assets,这些绝对路径在构建时就被写死 —— 基于路径的路由需要重写 HTML 并拦截 SPA 的绝对 /api 调用(在多标签页情况下很脆弱)。 2. Cloudflare 免费套餐的 Universal SSL 通配符仅覆盖一个标签(.example.com),因此 .. 没有有效的 TLS 证书。-deepseek slug 后缀既保留了品牌,又保持为一个标签。 先决条件 - Podman 以及正在运行的虚拟机:podman machine start - Node.js 22+ 和 npm - cloudflared(cloudflared tunnel --version) - 你账户上的一个 Cloudflare 区域,并且隧道已经过身份验证 设置 1. 克隆 dsh(全新上游) git clone --depth 1 https://github.com/deepseek-ai/deepseek-harness.git dsh git -C dsh fetch --depth 1 origin 47f943859bef60e4160492346772ded9b24f765a git -C dsh checkout --detach 47f943859bef60e4160492346772ded9b24f765a 2. 构建 dsh 镜像 ./build-image.sh 始终使用该脚本:它会验证批准的提交和补丁,校验上下文策略,并从干净的 git archive 构建。直接运行 podman build 会绕过这些控制。将脚本打印出的 DSH_IMAGE=sha256:... 行复制到 .env 中;生产环境会拒绝可变镜像标签。 首次构建很慢(pnpm install + 完整 harness 构建);结果约为 2.5 GB。 3. 安装门户依赖 cd portal && npm install && cd .. 4. 配置 cp .env.example .env # 然后编辑 .env(DOMAIN、INSTANCE_DOMAIN、ADMIN_、SMTP_) 5. Cloudflare 路由(一次性) cloudflared tunnel route dns '.' cloudflared tunnel route dns 隧道配置(~/.cloudflared/.yml): tunnel: credentials-file: '~/.cloudflared/.json' ingress: - hostname: service: http://127.0.0.1:8080 - hostname: '.' service: http://127.0.0.1:8080 - service: http_status:404 cloudflared tunnel --config ~/.cloudflared/.yml run 6. 运行门户 ./run-portal.sh 首次启动时根据 ADMIN_EMAIL / ADMIN_NAME / ADMIN_PASSWORD 初始化管理员账户。密码必须显式设置、非占位符,且至少 16 个字符;启动时会拒绝不安全的引导配置。 run-portal.sh 首先应用租户出口防火墙(firewall/apply.sh),如果 Podman 虚拟机未运行则安全失败(fail closed)。 配置(环境变量) 参见 .env.example。重要的变量如下: | 变量 | 默认值 | 含义 | |---|---|---| | NODE_ENV | production | 仅在本地测试时使用 development | | DOMAIN | example.com | 门户顶级域名 | | PORTAL_ORIGIN | https:// | 用于变更/CSRF 校验的精确受信任浏览器来源 | | INSTANCE_DOMAIN | example.com | . 的基础域名 | | INSTANCE_SLUG_SUFFIX | -deepseek | 追加到 slug 后 | | COOKIE_DOMAIN | (空)* | 会话 cookie 域(必须覆盖顶级域名和实例) | | SESSION_ABSOLUTE_TTL_MS / SESSION_IDLE_TTL_MS | 7 天 / 24 小时 | 服务器强制执行的会话生命周期和空闲过期时间 | | PORT | 8080 | 门户监听端口(cloudflared 连接到此端口) | | ADMIN_EMAIL / ADMIN_NAME / ADMIN_PASSWORD | (空) | 首次启动管理员;密码必须显式设置且至少 16 个字符 | | SMTP_HOST / SMTP_PORT / SMTP_USER / SMTP_PASS / SMTP_FROM | (空) | 生产环境 OTP 投递;host/from 必填,认证值需成对提供 | | OTP_TTL_MS / OTP_MAX_ATTEMPTS | 10 分钟 / 5 | OTP 有效期和每个验证码的尝试上限 | | AUTH_RATE_WINDOW_MS / AUTH_RATE_BLOCK_MS | 15 分钟 / 15 分钟 | 持久化认证限流窗口/封禁时长 | | OTP_RESEND_COOLDOWN_MS | 60000 | 每个地址/用途之间 OTP 请求的最小间隔 | | PORT_RANGE_START / END | 18000 / 18100 | 主机回环端口池 | | DSH_IMAGE | 必填 | 由 ./build-image.sh 输出的不可变 sha256:... 镜像 ID;生产环境拒绝可变标签 | | PODMAN_COMMAND_TIMEOUT_MS | 60000 | 每次 podman 调用的子进程超时时间 | | INSTANCE_CPUS / INSTANCE_MEMORY / INSTANCE_MEMORY_SWAP | 2 / 2g / 2g | 每个实例的计算资源限制 | | INSTANCE_PIDS_LIMIT | 512 | 每个容器的进程数限制 | | INSTANCE_NETWORK | pasta | 必需的隔离无根网络模式 | | INSTANCE_LOG_SIZE / INSTANCE_TMPFS_SIZE | 10mb / 64m | 有界的运行时日志和临时存储 | | INSTANCE_READ_ONLY_ROOT | true | 以只读根文件系统运行租户容器 | 测试 cd portal && npm test 针对 portal/test/ 中的临时 SQLite 数据库运行隔离的事务性回归测试套件(电子邮件更改证明绑定、尝试次数统计以及唯一性/会话回滚)。它不会触碰实时数据、发送电子邮件,也不要求 Podman。 运维 - 管理员:以管理员身份登录 → 设置选项卡 → 电子邮件域白名单、邀请码、身份验证开关。实例选项卡 → 重新配置/删除、查看日志、使用情况(请求数 + 最后活跃时间)。用户选项卡 → 列出用户。 - 用户:注册(电子邮件 OTP,可选设置用户名/密码)→ 实例自动配置 → 仪表板显示 URL + 状态。实例在启动时自动启动,并在空闲超时后自动停止。 - 启动:https://. —— 门户进行身份验证 + 授权,然后代理到容器。 - API 密钥:在 dsh 内按实例设置(设置 → 模型)。 重启后重新启动 podman machine start # 1) 容器运行时 cloudflared tunnel --config ~/.cloudflared/.yml run ./run-portal.sh # 2) 门户(前台运行;用 nohup/& 包装以在后台运行) 容器带有 --restart unless-stopped;WSL 机器重启后的平台行为仍可能导致它们处于停止状态。门户在启动时会自动启动已授权用户的容器,并在启动时重新排队任何处于 provisioning 状态的实例。run-portal.sh 在每次启动时重新应用租户出口防火墙(幂等);如果你在门户已在运行时重启 Podman 机器,请手动运行 firewall/apply.sh。 故障排除 症状: 租户子域无法加载,容器在 podman ps 中显示 Up,但从 Windows 执行 curl http://127.0.0.1: 失败(而在发行版内部通过 podman machine ssh 'curl http://127.0.0.1:/' 返回 200)。 原因: WSL2 的 localhost 转发器(wslrelay.exe)在经过多次容器停止/启动循环(空闲停止 + 自动启动)后,可能会卡住一个过期的端口转发条目。过期的 wslrelay 进程和孤立的 pasta 进程会不断累积。 修复: 重启 WSL2 VM 并重新配置已停止的租户: wsl.exe --shutdown podman machine start 重新配置(或直接启动每个租户;门户会在访问时自动启动) ./run-portal.sh 将 WSL 引擎更新到最新版本(目前因 Windows Installer 重启而被阻止)是针对底层 wslrelay 过期问题的持久修复方案。 安全模型 - 会话 cookie:HttpOnly、SameSite=Lax,当设置了 COOKIE_DOMAIN 时为 Secure;持有者令牌在 SQLite 中以 SHA-256 摘要形式存储,并由服务器强制执行 7 天绝对过期和 24 小时空闲过期。 - 密码:bcrypt。OTP 验证码:加盐 SHA-256,10 分钟过期,尝试次数上限,恒定时间比较;登录/OTP/邀请流程使用持久的按 IP 和按账户限流。 - 每个子域请求在到达容器之前都要经过身份验证(会话)和授权(所有者或管理员);门户 cookie 和网关身份标头在 HTTP/WebSocket 转发之前会被剥离。 - 门户变更操作要求精确的门户 Origin 和每会话 CSRF 令牌;租户同级子域被视为不受信任。 - 实例相互隔离:私有主目录 + 工作区卷、CPU/内存上限、dsh 自带的沙箱。 - 租户出口防火墙:容器无法访问 RFC1918 私有地址范围(你的局域网、10/8、172.16/12);仅允许公共互联网出口。由 Podman 机器上的 nftables 强制执行(firewall/tenant-egress.nft),并由 run-portal.sh 自动应用。 - 门户是唯一的认证层(没有 Cloudflare Access)。 文件 deepseek-portal/ portal/ Fastify 应用 + 静态仪表盘 src/ 后端模块(auth、db、otp、mailer、proxy、orchestrator、email-change) test/ 事务性回归测试套件(npm test) image/ Dockerfile + start.sh + dsh-security.patch(dsh 镜像) firewall/ 租户出口防火墙(nftables)+ apply.sh dsh/ 全新上游克隆(构建上下文,已 gitignore) build-image.sh run-portal.sh 应用出口防火墙,然后启动门户 .env.example README.md SECURITY_AUDIT.md 完整审计、发现项和修复状态 SECURITY.md 漏洞披露政策
扫码进群