Bundle
dsh-aloof
把 Aloof 的团队资料库和办公审批接成 DeepSeek Harness 的原生工具:搜团队知识、读写共享文档、查审批待办、发起单子。写操作先过 dsh 的审批闸门。
- Source
- gaochonggeng
- License
- MIT
- Updated
- Updated 2 days ago
Readme
# dsh-aloof
把公司在 [Aloof](https://inside.aloof-ai.cn) 上的**团队资料库和办公审批**,接成你自己电脑上 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(dsh)的原生工具。
装上之后,你在本机的 dsh 里可以直接说:
- 「这个客户的电价方案,我们以前是怎么算的?」→ 它去团队资料库里搜,读出同事写过的那份,照着来
- 「把刚才这套踩坑记追加到《电价踩坑记》里」→ 写回同一份共享文档,同事在网页上立刻看得见
- 「我这个月的差旅报销提一下」→ 按模板发起审批,走公司的审批链
**分工是清楚的**:Aloof 是团队那一份——权限、数据范围、审批链、审计留痕都在它那边,它是权威源;你本机这台 dsh 是你自己的手脚,快、私密、想怎么折腾都行。这个插件是把两者接起来的**受控通路**:一张能单独吊销的票、一份写操作白名单、加上本机的确认闸。不是把公司数据拷到本地,也不是把本机的 AI 放进公司系统随便动手。
它同时是一份**可运行的 dsh 插件样板**——插件机制的每一层都用到了(工具注册、凭据服务、写操作审批闸门、结果渲染),单文件、没有一句 `import`(为什么见 `index.js` 顶部)。
## 装
```sh
dsh plugin --profile web add dsh-aloof
```
然后把票粘进来就完事了(下一节说票从哪拿):
```sh
export ALOOF_TOKEN='alf_xxxx@https://aloof.你的公司.com'
dsh web
```
**只有这一个东西要配,没有第二项。** 票里 `@` 后面那截就是你公司那台 Aloof 的地址——密钥和它该发去哪台**绑在一起**,所以不存在「地址填串了、票发到别人服务器上」这回事。整串复制,别只粘前半截。
也可以直接从 GitHub 装,**建议钉住某个提交**,免得哪天一次推送悄悄改了你机器上跑的东西:
```sh
dsh plugin --profile web add 'github:gaochonggeng/dsh-aloof#<commit-sha>'
```
(这个插件是纯 JS、没有构建步骤,所以 git 安装不需要 `allowBuilds` 那道构建授权。)
卸载 `dsh plugin --profile web remove dsh-aloof`,profile 的 `bundles` 列表会自动摘掉这一层。
## 工具
### 团队资料库(六个)
这组是「团队共享知识」落到本机的通路。**你在网页上进得去的空间,这里进得去;进不去的,拿这张票一样进不去**——空间 ACL 是同一套,插件不放宽任何东西。
| 工具 | 干什么 | 读/写 |
|---|---|---|
| `kb_spaces` | 我能进哪些空间,返回 `spaceId` 和我在里面的角色 | 读 |
| `kb_search` | 按名字、用途说明和**正文全文**搜,返回 `nodeId` 和命中的那句话 | 读 |
| `kb_list` | 列一个目录下的一层,用来摸结构 / 挑 `parentId` | 读 |
| `kb_read` | 读一份的全文(太长会截断并说明) | 读 |
| `kb_write` | 新建一份,或往已有的那份**追加 / 覆盖** | **写,过闸门** |
| `kb_delete` | 删一个文件或目录 | **写,过闸门;不可恢复** |
几个刻意的设计:
- **`kb_write` 默认是追加不是覆盖**(`mode` 默认 `append`)。「把这条记下来」的常态是往一份活文档后面接一段,不是把整篇换掉;要覆盖得显式 `mode: replace`,确认文案里也会写明「原文会没了」。
- **版本号(`rev`)插件自己处理**,不让模型编。改一份文档时它先读一遍拿到 `rev` 再提交,中间有人改过就会被后端拒——这是乐观锁,撞上了重读一遍再写就行。要是把 `rev` 交给模型,它迟早会随便填一个数把同事的修改盖掉。
- **`kb_read` 碰到读不了的东西会明说**。Word / Excel / PPT 是压缩包,按文本读只会读出一堆乱码;PDF、图片同理。这种情况返回「读不出来,去网页看」,**不把乱码当正文喂给模型**——喂了它会照着乱码瞎猜。
- **`kb_delete` 的确认文案是服务端问出来的**,不是模型自己报的。删目录之前先查出这个目录叫什么、底下直接挂着几项,写进确认里。让模型自己报的话,它报错一次,人就是在给一句假话点同意。资料库**没有回收站**,删了就是没了,所以这一句必须准。
### 办公审批(五个)
| 工具 | 干什么 | 读/写 |
|---|---|---|
| `oa_todo` | 查等我处理的单子,返回 `taskId` | 读 |
| `oa_templates` | 我能发起哪些审批,返回 `templateId` | 读 |
| `oa_form` | 某模板要填哪些字段(key / 类型 / 必填 / 选项) | 读 |
| `oa_submit` | 发起一单 | **写,过闸门** |
| `oa_decide` | 同意 / 驳回一条待办 | **写;接入令牌下后端直接拒**,见下 |
刻意分成四步(列模板 → 读字段 → 填 → 提交):模型不该猜表单字段名,`oa_form` 就是把「猜」换成「查」。
## 票据:用「dsh 接入令牌」,不要用登录票
在 Aloof 里点左下角自己的名字 → **dsh 接入** → 生成一张。复制出来的是这么一整串:
```
alf_kQ3xN…7Bv@https://aloof.你的公司.com
```
前半截是密钥,`@` 后面是这台 Aloof 的地址(就是你访问它用的那个地址)。**明文只显示一次,当场抄走**;丢了不要紧,回来吊销掉再生成一张。
为什么把地址塞进票里:这样 dsh 那头只有一个东西要配。地址和票各自是一个可填字段时,「填串了、票发到别处去」就永远可能发生;合成一串之后这件事在物理上就不成立了。
为什么不直接把网页的登录票复制过来:那张票带着这个人的**全部**权限、三十天有效、没法单独作废——放在笔记本上被捞走的人能替他批审批。接入令牌反过来长:
| | 登录票 | 接入令牌(`alf_` 开头) |
|---|---|---|
| 查数据 | ✅ | ✅ 范围完全一样(该看见什么就看见什么) |
| 读写资料库内容 | ✅ | ✅ 共享知识要能双向流动,只读的话「共享」只剩一半 |
| 提审批单 / 撤回 | ✅ | ✅ 提单是「请人来批」,不是终局动作 |
| 同意 / 驳回 | ✅ | ❌ **403**,只能本人在网页上点 |
| 拉人进空间 / 发邀请链接 | ✅ | ❌ 403,那是**授权**动作,得在网页上当面点 |
| 建空间 / 删整个空间 | ✅ | ❌ 403 |
| 改别的业务数据 | ✅ | ❌ 403 |
| 单独吊销 | ❌ | ✅ 按设备,管理员那边还看得见它最后一次从哪连的 |
分界线是:**内容可以从 dsh 写,权限和终局动作不行**。写一份文档是干活,谁能看这个空间、这单批不批,是要人当面拍的。
所以 `oa_decide` 在接入令牌下必然 403。**这是设计如此,不是配置错了**——工具描述里写了这句话,模型不会改参数重试。
## 配
**正常情况下什么都不用配**——地址跟着票来,装完粘一串票就能用。下面这些是给需要拧的人看的。
默认值在包自带的 `cordis.patch.yml` 里。**不要改那个文件**(升级会覆盖),要改就在 profile 自己的 `cordis.patch.yml` 里按 id 覆盖。patch 是**整块替换** `config` 而不是深合并,所以覆盖时键要写全:
```yaml
- id: aloof
name: 'dsh-aloof'
config:
tokenEnv: ALOOF_TOKEN
timeoutMs: 20000
requireApproval: true
```
| 键 | 说明 |
|---|---|
| `tokenEnv` | 令牌的**引用名**(POSIX 标识符),不是令牌本身 |
| `timeoutMs` | 单次 HTTP 超时 |
| `requireApproval` | 写操作是否必须先问人。`true` = fail closed |
| `baseUrl` | **默认没有这一项,通常也不该加。** 填了就以它为准(盖掉票里带的地址) |
`baseUrl` 只有一种情形该填:**网页地址和 dsh 能到达的地址确实不是同一个**——反向代理、内外网双入口,网页在 `https://aloof.corp.com`,而你的 dsh 只能走 `http://10.0.0.5:8000`。除此之外填它只是给自己留一个填错的机会,而包里之所以不带这一项,是因为一旦带了具体域名,装完没改的人的票就会发到那个域名去(那边只会回 401,但票已经出网了)。
令牌的值走 dsh 的 credentials(进程环境变量或 `$DSH_HOME/.credentials.yaml`),配置里只留引用名——这样配置可以随便同步、随便渲染到界面上,换令牌也不用碰文件,而且**每次调用现取不缓存**,换完(甚至换成另一家公司的实例)下一次请求就生效,不用重启 dsh。
票里带不带地址,取决于**发票的那台 Aloof**(是它的网页拼上去的),和插件版本无关。老票不带 `@`,还能用,但得自己在 `baseUrl` 里补地址——去网页上重新生成一张就不用管了。
## 团队里多个人一起用
一人一张票,各自在自己机器上生成,互不相干。想在**同一台机器上模拟多个人**(测试、或者一台共用的机器),靠 `DSH_HOME` 隔开就行——profile、插件、凭据全在那个目录里:
```sh
DSH_HOME=~/.dsh-alice ALOOF_TOKEN='alf_甲的票@https://aloof.你的公司.com' dsh web --port 3081
DSH_HOME=~/.dsh-bob ALOOF_TOKEN='alf_乙的票@https://aloof.你的公司.com' dsh web --port 3082
```
两边搜到的资料库内容按各自账号的空间权限算,写进去的东西对方立刻搜得到——**共享的是同一份,不是各存一份**。
管理员那边(Aloof 的 系统管理 → dsh 接入)能看到全公司谁的哪台机器连着、最后一次什么时候从哪个 IP 连的、用的什么客户端,也能替人吊销(人离职了、电脑丢了)。**但看不到明文、也发不了票**——票只能本人自己生成,不然审计里「这是他干的」就不成立了。
## 写操作的两道闸
别搞混,这是两道独立的闸,分别在两个地方:
**本机这道**:所有写工具(`kb_write` / `kb_delete` / `oa_submit` / `oa_decide`)在发请求之前先走 dsh 的 `ctx.approval.request()`,把「要改什么」写进 `reason` 交给人,只有拿到 `allowed-once` 才继续。三条拒绝路径都是不落库的——人点拒绝(`rejected`)、人关掉提示或会话被取消(`cancelled`)、这台 dsh 根本没有审批服务(直接拒,**不是**默认放行)。
`approval` 故意没写进模块级 `inject`:写进去会让整个插件在没有审批服务的装配里根本不挂载,连查资料都用不了。改成运行时探测 + fail closed,是能力降级而不是整体消失。
**服务端那道**:Aloof 后端按令牌类型和一张 **(方法, 路由) 白名单**拦——不在名单里的写请求一律 403,**以后新加的写端点默认也进不来**。所以 `oa_decide` 就算在本机被人点了同意,到了后端照样 403:「批准」这个动作不接受来自一张笔记本上的票。
两道闸管的是不同的事:本机那道防「模型自己悄悄干了」,服务端那道防「这张票压根不该能干这个」。前者可以关(`requireApproval: false`,你自己的机器你负责),后者关不掉。
## 有问题
提 [issue](https://github.com/gaochonggeng/dsh-aloof/issues)。这个插件只是个转发壳,业务逻辑(权限、数据范围、审批链、审计)全在 Aloof 后端——所以「它不让我批审批」「它进不去某个空间」这类不是插件的问题,见上面「票据」那节。
## 还没做
- **没有设置页**:`baseUrl` / 令牌目前只能写 YAML 或环境变量。做成设置里的表单需要 `installSettingsSection` + schemastery,那就得引入运行时依赖。
- **没有专用 UI 卡片**:`output.render` 只给了文本。要把同意/驳回按钮、资料 diff 画在对话里,得写浏览器那半边。
- **传不了文件**:`kb_write` 只写文本文档(md / csv / html / txt / json)。上传二进制要走 multipart,那是另一套写法。
- **没做转办 / 催办 / 抄送**:后端接口都有,照现有工具复制即可。
## License
MIT
Install
dsh plugin --profile web add github:gaochonggeng/dsh-aloof
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-aloof from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.