@chenkai114/dsh-team-admin 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 chenkai2
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,86 @@
1
- # Temporary Holding Version
1
+ # dsh-team-admin
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 给 DeepSeek Harness 的「智能体团队(Agent Teams)」补一个**管理面**:在 GUI 里选任意会话,查看它的团队成员,**删除角色并清理该成员的持久会话**。
4
+
5
+ ## 为什么需要
6
+
7
+ v0.2.x 的内置 Team roster 是 **append-only**:只能 `spawn_teammate` 加人,没有删除 / 改名 / 名字复用(见 `dsh-experimental-agent-team` 的 Known Limitations)。成员上下文耗尽、任务结束或建错时,只能"停手不理会",名单和名额永久占着。
8
+
9
+ 本插件补上缺失的那一半:
10
+
11
+ - 列出任意根会话的团队成员(名字、状态、描述、模型、名下任务数)
12
+ - 删除成员:中断运行中的会话 → 成员会话目录**移入回收站** → 写墓碑,管理面标记「已删除」
13
+ - 已删除成员集中展示,回收站路径可复制
14
+
15
+ ## 安装
16
+
17
+ ```sh
18
+ dsh plugin --profile web add @chenkai114/dsh-team-admin
19
+ ```
20
+
21
+ 重启 / 重载后,打开 **设置 → 插件 → dsh-team-admin**(或插件管理页里本包的配置卡),即可看到「智能体团队管理」面板。
22
+
23
+ 未发布前用本地源码链接安装:
24
+
25
+ ```sh
26
+ dsh plugin --profile web add link:/Users/chenkai2/data1/www/htdocs/dsh-team-admin
27
+ ```
28
+
29
+ 或在 profile 的 `package.json` 里手动加:
30
+ ```json
31
+ {
32
+ "dependencies": { "@chenkai114/dsh-team-admin": "link:/Users/chenkai2/data1/www/htdocs/dsh-team-admin" },
33
+ "dsh": { "profile": { "bundles": ["...", "@chenkai114/dsh-team-admin"] } }
34
+ }
35
+ ```
36
+
37
+ ![团队管理卡片](docs/images/team-admin.png)
38
+
39
+ ## 用法
40
+
41
+ 1. 搜索并选中一个会话(只列根会话)
42
+ 2. 面板显示该会话的团队成员
43
+ 3. 点某个成员的「删除」→ 行内二次确认 → 确认
44
+ 4. 成员会话进回收站,管理面把它移到「已删除」区
45
+
46
+ ## 删除到底做了什么
47
+
48
+ | 步骤 | 行为 |
49
+ |---|---|
50
+ | 停止 | 运行中的成员会被中断,最多等 15s 变 inactive;超时则报错不动数据 |
51
+ | 清理会话 | 成员会话目录 `~/.dsh/sessions/.../<memberId>/` 移到 `~/.dsh/team-admin/trash/<leadSessionId>/<memberId>-<ms>/`(**移动,不是删除**,可手工恢复) |
52
+ | 墓碑 | 写入 `~/.dsh/team-admin/state.json`,管理面据此显示「已删除」 |
53
+
54
+ ## 边界(重要)
55
+
56
+ - **内置团队面板仍会显示该成员**:宿主 roster 是 append-only,本插件不(也无法)从 Lead 会话日志里抹掉记录。管理面才是"已删除"的真源。
57
+ - **名额不返还**:内置 `maxMembers` 统计的是历史成员记录,删了也占位;新角色请用新名字(`spawn_teammate`)。真需要重置名额时,fork / 新开会话换一个 Team。
58
+ - **只删成员,不删 Lead**:`lead` 与任意根会话都不受影响。
59
+ - **仅本机**:所有接口只接受回环地址;删除动作不会暴露到网络。
60
+ - **不做的事**:不改内置团队工具的语义、不写 Lead 会话日志、不碰任务板(成员名下的任务仍留在任务板上,需要时用 `team_task_update` reassign)。
61
+
62
+ ## 配置
63
+
64
+ | 变量 / 字段 | 默认 | 说明 |
65
+ |---|---|---|
66
+ | `DSH_TEAM_ADMIN_DATA_DIR` | `~/.dsh/team-admin` | 墓碑与回收站根目录 |
67
+
68
+ ## 开发
69
+
70
+ ```sh
71
+ npm test # node:test,纯本地,不依赖运行中的 dsh
72
+ ```
73
+
74
+ 模块划分与 HTTP 契约见 [docs/CONTRACT.md](docs/CONTRACT.md)。
75
+
76
+ ## 发布
77
+
78
+ 仓库走 GitHub Actions + npm Trusted Publishing(OIDC),与 dsh-daemon 同构:
79
+
80
+ 1. npm 包页面 → Settings → Trusted Publisher → GitHub Actions,填 `chenkai2` / `dsh-team-admin` / `publish.yml` / `npm-publish`
81
+ 2. 升 `package.json` 版本(同时改 `src/index.js` 的 `PLUGIN_VERSION`)→ 打 tag `vX.Y.Z` → 发 GitHub Release
82
+ 3. Actions 自动校验 tag/版本、跑测试、`npm publish --provenance --access public`
83
+
84
+ ## License
85
+
86
+ MIT
@@ -0,0 +1,6 @@
1
+ # dsh-team-admin 打包补丁:把团队管理插件作为单条目注入 profile 根。
2
+ # 宿主半区提供 /api/team-admin/* 路由;client 半区随宿主条目进入客户端模块表,
3
+ # 在「插件管理 → 本包」页渲染管理卡片。
4
+ - insert:
5
+ - id: team-admin
6
+ name: '@chenkai114/dsh-team-admin'
package/package.json CHANGED
@@ -1,6 +1,58 @@
1
1
  {
2
2
  "name": "@chenkai114/dsh-team-admin",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "DSH 插件:GUI 管理任意会话的智能体团队 —— 查看成员、删除角色并清理其持久会话(进回收站,可恢复)",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js",
9
+ "./client": "./src/client.js",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "files": [
13
+ "src",
14
+ "cordis.patch.yml",
15
+ "README.md"
16
+ ],
17
+ "dsh": {
18
+ "bundle": {
19
+ "patch": "./cordis.patch.yml"
20
+ },
21
+ "client": {
22
+ "platform": "web"
23
+ },
24
+ "requires": {
25
+ "dsh": ">=0.2.0-alpha.1 <0.3.0"
26
+ }
27
+ },
28
+ "keywords": [
29
+ "dsh",
30
+ "deepseek-harness",
31
+ "plugin",
32
+ "agent-team",
33
+ "team",
34
+ "admin"
35
+ ],
36
+ "license": "MIT",
37
+ "author": "chenkai2",
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "https://github.com/chenkai2/dsh-team-admin.git"
44
+ },
45
+ "bugs": {
46
+ "url": "https://github.com/chenkai2/dsh-team-admin/issues"
47
+ },
48
+ "homepage": "https://github.com/chenkai2/dsh-team-admin#readme",
49
+ "engines": {
50
+ "node": ">=22"
51
+ },
52
+ "scripts": {
53
+ "test": "node --test tests/*.test.mjs",
54
+ "test:e2e": "node tests/e2e/run.mjs",
55
+ "test:real": "node tests/e2e/real-data.mjs",
56
+ "test:pack": "npm pack --dry-run"
57
+ }
58
+ }
package/src/api.mjs ADDED
@@ -0,0 +1,133 @@
1
+ // /api/team-admin 路由分发:只做 HTTP 语义(回环校验、JSON 编解码、错误映射),
2
+ // 业务在 team-read.mjs / dismiss.mjs。
3
+ import { AdminError } from './error.mjs'
4
+
5
+ /** 请求体上限:64KB。 */
6
+ const BODY_MAX_BYTES = 64 * 1024
7
+ /** 默认会话列表条数 / 上限。 */
8
+ const DEFAULT_SESSIONS_LIMIT = 50
9
+ const MAX_SESSIONS_LIMIT = 200
10
+
11
+ /** 回环来源判定:只有本机可以操作(删除动作不可远程触发)。 */
12
+ export function isLoopbackAddress(address) {
13
+ if (typeof address !== 'string') return false
14
+ const normalized = address.startsWith('::ffff:') ? address.slice('::ffff:'.length) : address
15
+ return normalized === '::1' || normalized === '127.0.0.1' || normalized.startsWith('127.')
16
+ }
17
+
18
+ /** 写 JSON 响应。 */
19
+ function sendJson(res, status, payload) {
20
+ res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' })
21
+ res.end(JSON.stringify(payload))
22
+ }
23
+
24
+ /** 读完整请求体并解析 JSON 对象。 */
25
+ async function readJsonBody(req) {
26
+ const chunks = []
27
+ let size = 0
28
+ for await (const chunk of req) {
29
+ size += chunk.length
30
+ if (size > BODY_MAX_BYTES) {
31
+ throw new AdminError('请求体超过上限', 'body-too-large', 400)
32
+ }
33
+ chunks.push(chunk)
34
+ }
35
+ const text = Buffer.concat(chunks).toString('utf8')
36
+ if (text.trim() === '') return {}
37
+ let parsed
38
+ try {
39
+ parsed = JSON.parse(text)
40
+ } catch {
41
+ throw new AdminError('请求体不是合法 JSON', 'bad-json', 400)
42
+ }
43
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
44
+ throw new AdminError('请求体不是合法 JSON', 'bad-json', 400)
45
+ }
46
+ return parsed
47
+ }
48
+
49
+ /** 查询串取单值。 */
50
+ function queryValue(url, key) {
51
+ const value = url.searchParams.get(key)
52
+ return value === null || value.trim() === '' ? undefined : value.trim()
53
+ }
54
+
55
+ /** 整数查询参数(坏值回退默认)。 */
56
+ function queryInt(url, key, fallback) {
57
+ const raw = queryValue(url, key)
58
+ if (raw === undefined) return fallback
59
+ const parsed = Number.parseInt(raw, 10)
60
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback
61
+ }
62
+
63
+ /**
64
+ * 建路由处理器。
65
+ * @param {{ctx: object, getStore: () => Promise<object>, version: string, logSystem: (line: string) => void, services?: object}} deps
66
+ * @returns {(req: import('node:http').IncomingMessage, res: import('node:http').ServerResponse) => Promise<void>}
67
+ */
68
+ export function createApi({ ctx, getStore, version, logSystem, services }) {
69
+ if (services === undefined) throw new Error('createApi requires services { listTeamSessions, readTeam, dismissMember }')
70
+ return async function handle(req, res) {
71
+ try {
72
+ if (!isLoopbackAddress(req.socket?.remoteAddress)) {
73
+ sendJson(res, 403, { error: '仅允许本机访问' })
74
+ return
75
+ }
76
+ const url = new URL(req.url ?? '/', 'http://localhost')
77
+ const path = url.pathname
78
+ const method = req.method ?? 'GET'
79
+
80
+ if (path === '/api/team-admin/health') {
81
+ if (method !== 'GET') { sendJson(res, 405, { error: '请求方法不支持' }); return }
82
+ sendJson(res, 200, { ok: true, name: 'team-admin', version })
83
+ return
84
+ }
85
+
86
+ if (path === '/api/team-admin/sessions') {
87
+ if (method !== 'GET') { sendJson(res, 405, { error: '请求方法不支持' }); return }
88
+ const query = queryValue(url, 'query') ?? ''
89
+ const limit = Math.min(queryInt(url, 'limit', DEFAULT_SESSIONS_LIMIT), MAX_SESSIONS_LIMIT)
90
+ const sessions = await services.listTeamSessions(ctx, { query, limit })
91
+ sendJson(res, 200, { sessions })
92
+ return
93
+ }
94
+
95
+ if (path === '/api/team-admin/team') {
96
+ if (method !== 'GET') { sendJson(res, 405, { error: '请求方法不支持' }); return }
97
+ const sessionId = queryValue(url, 'sessionId')
98
+ if (sessionId === undefined) { sendJson(res, 400, { error: '缺少 sessionId' }); return }
99
+ const store = await getStore()
100
+ sendJson(res, 200, await services.readTeam(ctx, { sessionId, store }))
101
+ return
102
+ }
103
+
104
+ if (path === '/api/team-admin/dismiss') {
105
+ if (method !== 'POST') { sendJson(res, 405, { error: '请求方法不支持' }); return }
106
+ const body = await readJsonBody(req)
107
+ const sessionId = typeof body.sessionId === 'string' ? body.sessionId.trim() : ''
108
+ const name = typeof body.name === 'string' ? body.name.trim() : ''
109
+ if (sessionId === '' || name === '') {
110
+ sendJson(res, 400, { error: '缺少 sessionId 或 name' })
111
+ return
112
+ }
113
+ const store = await getStore()
114
+ const result = await services.dismissMember(
115
+ ctx,
116
+ { sessionId, name, deleteSession: body.deleteSession !== false },
117
+ { store, logSystem },
118
+ )
119
+ sendJson(res, 200, { ok: true, ...result })
120
+ return
121
+ }
122
+
123
+ sendJson(res, 404, { error: '接口不存在' })
124
+ } catch (error) {
125
+ const status = error instanceof AdminError ? error.status : 500
126
+ const message = error instanceof AdminError ? error.message : '操作失败(详见服务端日志)'
127
+ if (!(error instanceof AdminError)) {
128
+ logSystem('[team-admin] ' + String(error && error.stack || error))
129
+ }
130
+ if (!res.headersSent) sendJson(res, status, { error: message })
131
+ }
132
+ }
133
+ }