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

团队多用户服务 sunbigfly/dsh-multi-user-service

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

让团队成员用浏览器登录共享一套服务,各自拥有独立会话与文件

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

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

安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add sunbigfly/dsh-multi-user-service
该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)

状态速览

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

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

验证判据与等级说明 →
社区实装还没有人打卡 · 装过的第一个就是你
安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

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

读写/删除本地文件写入剪贴板(不计风险)发起外部网络请求
查看扫描证据(共 119 处,展示前 89 条)
  • 发起网络请求
    client.js:35
    const send = init => (transportFetch ? transportFetch(ENDPOINT, init) : fetch(target, init));
  • 写入剪贴板
    client.js:1128
    if (globalThis.navigator?.clipboard?.writeText) await globalThis.navigator.clipboard.writeText(url);
  • 发起网络请求
    member-host.mjs:87
    const response = await contentApi.fetch(new Request(new URL('/api/' + method, ownerOrigin), { method: 'POST', headers: { 'content-type': 'application/
  • 读取本地文件
    carrier/backup.mjs:36
    if (!this.path.startsWith('/') || Buffer.byteLength(this.path) > 100 || await realpath(dirname(this.path)) !== dirname(this.path)) throw new Error('Ba
  • 读取本地文件
    carrier/backup.mjs:38
    const stat = await lstat(this.path);
  • 删除本地文件
    carrier/backup.mjs:42
    await unlink(this.path);
  • 读取本地文件
    carrier/backup.mjs:58
    await chmod(this.path, 0o600); this.stat = await lstat(this.path);
  • 写入本地文件
    carrier/backup.mjs:58
    await chmod(this.path, 0o600); this.stat = await lstat(this.path);
  • 读取本地文件
    carrier/backup.mjs:64
    try { const current = await lstat(this.path); if (current.ino === this.stat?.ino) await unlink(this.path); } catch (error) { if (error.code !== 'ENOEN
  • 删除本地文件
    carrier/backup.mjs:64
    try { const current = await lstat(this.path); if (current.ino === this.stat?.ino) await unlink(this.path); } catch (error) { if (error.code !== 'ENOEN
  • 读取本地文件
    carrier/administration.mjs:19
    const exists = async path => { try { return await lstat(path); } catch (error) { if (error.code === 'ENOENT') return null; throw error; } };
  • 读取本地文件
    carrier/administration.mjs:27
    if (await realpath(root) !== root) reject('ADMIN_FILE_DENIED', 'The member directory is not canonical.', 403);
  • 读取本地文件
    carrier/administration.mjs:48
    const entries = await opendir(at(child));
  • 读取本地文件
    carrier/administration.mjs:54
    const stat = await lstat(at(handle, name));
  • 删除本地文件
    carrier/administration.mjs:55
    if (!stat.isDirectory()) return unlink(at(handle, name));
  • 读取本地文件
    carrier/administration.mjs:57
    try { for (const entry of await readdir(at(child))) await removeEntry(child, entry); }
  • 删除本地文件
    carrier/administration.mjs:59
    await rmdir(at(handle, name));
  • 写入本地文件
    carrier/administration.mjs:64
    try { await mkdir(at(held, RECYCLE), { mode: 0o700 }); } catch (error) { if (error.code !== 'EEXIST') throw error; }
  • 读取本地文件
    carrier/administration.mjs:71
    const stat = await lstat(at(source, sourceName));
  • 写入本地文件
    carrier/administration.mjs:74
    await mkdir(at(target, targetName), { mode: stat.mode & 0o777 });
  • 写入本地文件
    carrier/administration.mjs:181
    await rename(at(location.handle, location.name), at(bin, id));
  • 删除本地文件
    carrier/administration.mjs:374
    if (job.revision === null) { await link(at(job.parent, job.temporary), at(job.parent, job.name)); await unlink(at(job.parent, job.temporary)); }
  • 写入本地文件
    carrier/administration.mjs:375
    else await rename(at(job.parent, job.temporary), at(job.parent, job.name));
  • 写入本地文件
    carrier/administration.mjs:418
    if (r.operation === 'files.mkdir') { await mkdir(at(location.handle, location.name), { mode: 0o700 }); return { created: true }; }
  • 写入本地文件
    carrier/administration.mjs:426
    await rename(at(location.handle, location.name), at(target.handle, target.name)); return { renamed: true };
  • 删除本地文件
    carrier/administration.mjs:440
    try { await job.file.close(); await unlink(at(job.parent, job.temporary)).catch(error => { if (error.code !== 'ENOENT') throw error; }); }
  • 读取本地文件
    carrier/setup.mjs:63
    if ((await realpath(parent)).replaceAll('\\', '/') !== parent) reject('INVALID_INPUT', 'Recommended directory parent must be canonical.');
  • 写入本地文件
    carrier/setup.mjs:64
    try { await mkdir(this.recommendedRoot, { mode: 0o750 }); } catch (error) { if (error.code !== 'EEXIST') throw error; }
  • 读取本地文件
    carrier/setup.mjs:65
    const info = await lstat(this.recommendedRoot);
  • 读取本地文件
    carrier/member.mjs:82
    try { target = await realpath(lexical); }
  • 读取本地文件
    carrier/member.mjs:106
    try { realHome = memberPath(await realpath(home)); } catch { return false; }
  • 读取本地文件
    carrier/member.mjs:107
    try { realTarget = memberPath(await realpath(candidate)); } catch (error) { return ['ENOENT', 'ENOTDIR'].includes(error?.code); }
  • 读取本地文件
    carrier/member.mjs:137
    const root = await realpath(frontendDist);
  • 发起网络请求
    carrier/member.mjs:246
    const response = await api.fetch(new Request(target, { method: 'POST', headers: { 'content-type': 'application/json' }, body }), peer);
  • 读取本地文件
    carrier/member.mjs:307
    try { canonicalBase = await realpath(base); } catch { bad('MEMBER_FILE_DENIED', 'Member file base is unavailable', 403); }
  • 发起网络请求
    carrier/member.mjs:357
    const response = await api.fetch(new Request(new URL('/api/settings/describe', origin), { method: 'POST', headers: { 'content-type': 'application/json
  • 发起网络请求
    carrier/member.mjs:587
    const response = await api.fetch(new Request(target, init), authority);
  • 读取本地文件
    carrier/member.mjs:795
    const memberSource = await readFile(fileURLToPath(new URL('../native-client.js', import.meta.url)), 'utf8');
  • 读取本地文件
    carrier/member-boundary.mjs:45
    if (typeof home !== 'string' || !home.startsWith('/') || canonical(home) !== home || !lstatSync(home).isDirectory()) failure('MEMBER_BOUNDARY_DENIED',
  • 读取本地文件
    carrier/member-boundary.mjs:46
    const info = lstatSync(home);
  • 写入本地文件
    carrier/member-boundary.mjs:54
    try { mkdirSync(temp, { mode: 0o700 }); } catch (error) { if (error.code !== 'EEXIST') throw error; }
  • 读取本地文件
    carrier/member-boundary.mjs:55
    if (!lstatSync(temp).isDirectory() || lstatSync(temp).isSymbolicLink() || canonical(temp) !== temp) failure('MEMBER_BOUNDARY_DENIED', 'The private tem
  • 读取本地文件
    carrier/workspaces.mjs:39
    try { ids = JSON.parse(await readFile(posix.join(home, ARCHIVED), 'utf8')); if (!Array.isArray(ids)) throw new Error('Invalid archive list'); }
  • 读取本地文件
    carrier/workspaces.mjs:54
    const parsed = JSON.parse(await readFile(posix.join(home, META), 'utf8'));
  • 写入本地文件
    carrier/workspaces.mjs:62
    try { await writeFile(posix.join(home, META), body, 'utf8'); } catch { }
  • 读取本地文件
    carrier/workspaces.mjs:67
    try { entries = await readdir(home, { withFileTypes: true }); } catch { return []; }
  • 读取本地文件
    carrier/workspaces.mjs:114
    const parsed = JSON.parse(await readFile(posix.join(root, ARCHIVED), 'utf8'));
  • 删除本地文件
    carrier/workspaces.mjs:127
    catch (error) { await unlink(temporary).catch(() => {}); throw error; }
  • 写入本地文件
    carrier/workspaces.mjs:129
    try { await rename(temporary, posix.join(root, ARCHIVED)); }
  • 删除本地文件
    carrier/workspaces.mjs:130
    catch (error) { await unlink(temporary).catch(() => {}); throw error; }
  • 读取本地文件
    carrier/workspaces.mjs:139
    const parsed = JSON.parse(await readFile(posix.join(root, ATTACHMENTS), 'utf8'));
  • 写入本地文件
    carrier/workspaces.mjs:149
    try { await writeFile(posix.join(root, ATTACHMENTS), `${JSON.stringify(ids, null, 2)}\n`, 'utf8'); } catch { }
  • 写入本地文件
    carrier/workspaces.mjs:164
    await mkdir(root, { recursive: true });
  • 写入本地文件
    carrier/workspaces.mjs:171
    await mkdir(path);
  • 写入本地文件
    carrier/workspaces.mjs:191
    await mkdir(root, { recursive: true });
  • 读取本地文件
    carrier/workspaces.mjs:289
    const read = await readdir(target, { withFileTypes: true });
  • 写入本地文件
    carrier/access.mjs:21
    appendFileSync(TRACE, `${JSON.stringify({ at: new Date().toISOString(), action: request?.action, resources: request?.resources, args: request?.args ==
  • 读取本地文件
    carrier/file-ranges.mjs:26
    || realpathSync(path).replaceAll('\\', '/') !== path || !statSync(path).isDirectory()) invalid();
  • 读取本地文件
    carrier/home-runtime.mjs:21
    try { info = await lstat(current); }
  • 写入本地文件
    carrier/home-runtime.mjs:25
    await mkdir(current, { mode: 0o700 }).catch(error => { if (error.code !== 'EEXIST') throw error; });
  • 读取本地文件
    carrier/home-runtime.mjs:26
    info = await lstat(current);
  • 读取本地文件
    carrier/home-runtime.mjs:28
    if (!info.isDirectory() || info.isSymbolicLink() || (await realpath(current)).replaceAll('\\', '/') !== current) fail('UNSAFE_PROJECT_ROOT');
  • 读取本地文件
    carrier/home-runtime.mjs:30
    const info = await lstat(directory);
  • 删除本地文件递归
    carrier/home-runtime.mjs:64
    } finally { await rm(directory, { recursive: true, force: true }); }
  • 读取本地文件
    carrier/cli-management.mjs:73
    try { await access(path,constants.X_OK); if ((await stat(path)).isFile()) { command=await realpath(path); break; } } catch {}
  • 读取本地文件
    carrier/cli-management.mjs:73
    try { await access(path,constants.X_OK); if ((await stat(path)).isFile()) { command=await realpath(path); break; } } catch {}
  • 读取本地文件
    carrier/cli-management.mjs:73
    try { await access(path,constants.X_OK); if ((await stat(path)).isFile()) { command=await realpath(path); break; } } catch {}
  • 写入本地文件
    carrier/execution-tools.mjs:82
    const path = join(temp, 'arguments.json'); await writeFile(path, JSON.stringify(args.arguments), { mode: 0o600, flag: 'wx' });
  • 删除本地文件
    carrier/execution-tools.mjs:89
    await rm(join(temp, 'arguments.json'), { force: true });
  • 写入本地文件
    carrier/cli-tools.mjs:27
    await mkdir(path, { recursive: true, mode: 0o700 });
  • 读取本地文件
    carrier/cli-tools.mjs:28
    const info = await lstat(path);
  • 读取本地文件
    carrier/cli-tools.mjs:29
    if (!info.isDirectory() || info.isSymbolicLink() || await realpath(path) !== path || (info.mode & 0o077)) fail('CLI state requires a canonical, owner-
  • 读取本地文件
    carrier/cli-tools.mjs:33
    try { const info = await lstat(path); if (!info.isSymbolicLink() && (info.mode & 0o777) === mode && await readFile(path, 'utf8') === text) return; }
  • 读取本地文件
    carrier/cli-tools.mjs:33
    try { const info = await lstat(path); if (!info.isSymbolicLink() && (info.mode & 0o777) === mode && await readFile(path, 'utf8') === text) return; }
  • 写入本地文件
    carrier/cli-tools.mjs:36
    try { await writeFile(temporary, text, { mode, flag: 'wx' }); await rename(temporary, path); }
  • 写入本地文件
    carrier/cli-tools.mjs:36
    try { await writeFile(temporary, text, { mode, flag: 'wx' }); await rename(temporary, path); }
  • 删除本地文件
    carrier/cli-tools.mjs:37
    finally { await rm(temporary, { force: true }); }
  • 读取本地文件
    carrier/cli-tools.mjs:51
    if (!statSync(command).isFile() || !(statSync(command).mode & 0o111)) fail('CLI command is not an executable file.');
  • 读取本地文件
    carrier/cli-tools.mjs:97
    try { previous = JSON.parse(await readFile(join(home, 'commands.json'), 'utf8')); }
  • 删除本地文件
    carrier/cli-tools.mjs:100
    await rm(join(bin, id), { force: true }); await rm(join(configs, id + '.json'), { force: true });
  • 读取本地文件
    carrier/execution-catalog.mjs:24
    const info = await stat(path);
  • 读取本地文件
    carrier/execution-catalog.mjs:26
    return readFile(path, 'utf8');
  • 读取本地文件
    carrier/execution-catalog.mjs:55
    try { await access(command, constants.X_OK); await access(join(root, 'tools')); return [{ id: 'tool-runner', kind: 'tool-runner', root, command }]; }
  • 读取本地文件
    carrier/execution-catalog.mjs:62
    try { await access(join(path, 'package.json')); return path; } catch {}
  • 读取本地文件
    carrier/execution-catalog.mjs:63
    try { await access(join(path, 'pyvenv.cfg')); return path; } catch {}
  • 读取本地文件
    carrier/execution-catalog.mjs:74
    const interpreter = await realpath(match[1]);
  • 发起网络请求
    native-client.js:58
    function createMemberApi({ csrf, fetcher = (...args) => fetch(...args), onExpired = () => {} }) {
  • 发起网络请求
    native-client.js:202
    const snapshot = await api.request('snapshot');
  • 发起网络请求
    native-client.js:223
    await api.request('logout');
扫描于 2026/11/1(0 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库

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

依赖的 DSH / Cordis 模块
@deepseek-ai/dsh-api-gateway@deepseek-ai/cordis@deepseek-ai/schemastery
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

DSH 多用户服务:在公司或团队服务器上运行一套 DSH,供多人通过浏览器登录;成员拥有独立会话与文件,管理员统一管理账号、权限、用量与并发。服务端业务扩展,适配 DSH 0.2.1-rc.2,支持 Linux/WSL。

README

由 DeepSeek 最新模型翻译生成
DSH 多用户服务一台服务器,一套 DSH,为整个团队提供服务。 插件 v0.2.3 · DSH 0.2.1-rc.2 · Linux / WSL 服务器 在公司或团队的服务器上运行一套 DSH,让多人通过浏览器登录使用。每位成员拥有自己的会话和文件空间,管理员统一管理账号、权限与用量额度。 这是为 DSH 增加多用户管理能力的服务端业务扩展。模型、Skills、MCP、Agent 预设和工具插件由服务器统一配置,再按成员授权使用。 功能 - 账号管理:创建或邀请成员,管理账号状态与登录安全。 - 权限分配:按成员分配模型、工具、Skills、MCP、Agent 预设和目录访问范围。 - 独立空间:成员管理自己的会话与文件,管理员可统一管理成员内容。 - 用量控制:查看成员用量,设置并发任务数及每日、每月 tokens 额度。 - 服务维护:支持隔离执行、文件回收站和备份,界面沿用 DSH 原生风格。 可用于公司内网或已配置的公网入口;成员需要能访问服务器,并使用管理员创建或邀请的账号登录。 开始使用 1. 准备服务器 服务器使用 Linux 或 WSL,需已安装: - DSH 0.2.1-rc.2、Node.js 22+。 - Python 3、bubblewrap(bwrap)、用户级 systemd。 成员只需浏览器,无需安装 DSH。
展开完整文档(README 剩余 1 节)
DSH 多用户服务一台服务器,一套 DSH,为整个团队提供服务。
插件 v0.2.3 · DSH 0.2.1-rc.2 · Linux / WSL 服务器

在公司或团队的服务器上运行一套 DSH,让多人通过浏览器登录使用。每位成员拥有自己的会话和文件空间,管理员统一管理账号、权限与用量额度。

这是为 DSH 增加多用户管理能力的服务端业务扩展。模型、Skills、MCP、Agent 预设和工具插件由服务器统一配置,再按成员授权使用。

功能

- 账号管理:创建或邀请成员,管理账号状态与登录安全。
- 权限分配:按成员分配模型、工具、Skills、MCP、Agent 预设和目录访问范围。
- 独立空间:成员管理自己的会话与文件,管理员可统一管理成员内容。
- 用量控制:查看成员用量,设置并发任务数及每日、每月 tokens 额度。
- 服务维护:支持隔离执行、文件回收站和备份,界面沿用 DSH 原生风格。

可用于公司内网或已配置的公网入口;成员需要能访问服务器,并使用管理员创建或邀请的账号登录。

开始使用

1. 准备服务器

服务器使用 Linux 或 WSL,需已安装:

- DSH 0.2.1-rc.2、Node.js 22+。
- Python 3、bubblewrap(bwrap)、用户级 systemd。

成员只需浏览器,无需安装 DSH。当前版本已在 DSH 0.2.1-rc.2 验证,其他 DSH 版本尚未验证。

2. 安装

已有 DSH 服务器:从市场安装。

1. 在服务器本机的 DSH 插件页打开 实验性插件 → 自定义插件索引。
2. 在 发现 → 添加索引源 填入:

https://raw.githubusercontent.com/sunbigfly/dsh-multi-user-service/main/market/index.json

3. 搜索 DSH 多用户服务,安装并启用。
4. 按 已有服务接入 配置访问入口,继续使用这一份 DSH。

独立部署:从源码启动服务器。

git clone https://github.com/sunbigfly/dsh-multi-user-service.git
cd dsh-multi-user-service
npm ci --ignore-scripts
python3 ops/install-service.py
~/.local/bin/dsh-lan

安装脚本配置 lan profile 和后台服务;启动后输出访问地址,默认使用本机局域网 IP。

3. 初始化管理员

1. 在服务器本机已授权的 DSH 页面中操作。独立部署的本机启动链接位于 ~/.local/share/dsh-lan/dsh-lan.log。
2. 在 DSH 设置中配置可用模型。
3. 打开侧栏 多用户服务,初始化管理员账号,并在 共享环境 检查执行隔离。

首次初始化需要服务器本机的 DSH 授权。以后可直接使用管理员账号登录服务入口。

4. 添加成员

1. 打开 多用户服务 → 成员管理,选择 新建账号 或 邀请成员。
2. 先填写信息,再配置权限,最后创建账号或生成邀请。
3. 在 我的账号 中复制成员入口,发送给成员。成员登录后即可使用自己的会话与文件空间。

| 入口 | 用途 |
| http(s)://服务器地址/ | 管理员登录 |
| http(s)://服务器地址/lan/ | 成员登录、接受邀请 |

实际协议、地址和端口以部署配置为准;域名与反向代理配置见 配置与运维。

常用命令

dsh-lan status # 查看状态 dsh-lan restart # 重启 dsh-lan stop # 停止

安装包名为 @sunbigfly/dsh-multi-user-service。dsh-lan 命令和 /lan/ 路径沿用原标识。

旧包 @sunbigfly/dsh-lan-workspace 升级到新包:先停止服务,在原 profile 卸载旧包后安装新包,再启动服务;独立部署可重新运行 python3 ops/install-service.py 自动迁移安装引用。账号、会话与文件的存储位置不变。

MIT License
预览分享卡片长什么样 ↓sunbigfly/dsh-multi-user-service
分享 · DeepSeek Harness Hub
sunbigfly/dsh-multi-user-service

让团队成员用浏览器登录共享一套服务,各自拥有独立会话与文件

回到顶部