@chenkai114/dsh-team-admin 0.1.8 → 0.2.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/package.json +1 -1
- package/src/api.mjs +1 -0
- package/src/filter.mjs +7 -0
- package/src/index.js +114 -26
package/package.json
CHANGED
package/src/api.mjs
CHANGED
|
@@ -132,6 +132,7 @@ export function createApi({ ctx, getStore, version, logSystem, services }) {
|
|
|
132
132
|
{ sessionId, name, deleteSession: body.deleteSession !== false },
|
|
133
133
|
{ store, logSystem },
|
|
134
134
|
)
|
|
135
|
+
if (typeof services.onMemberDismissed === 'function') services.onMemberDismissed()
|
|
135
136
|
sendJson(res, 200, { ok: true, ...result })
|
|
136
137
|
return
|
|
137
138
|
}
|
package/src/filter.mjs
CHANGED
|
@@ -31,6 +31,13 @@ export function stripDismissedMembers(payload, dismissed, maxDepth = 6) {
|
|
|
31
31
|
if (node.agentTeam !== null && typeof node.agentTeam === 'object' && Array.isArray(node.agentTeam.members)) {
|
|
32
32
|
node.agentTeam.members = node.agentTeam.members.filter((entry) => !isDismissed(entry, dismissed))
|
|
33
33
|
}
|
|
34
|
+
// 投影帧形如 { key: 'agentTeam', value: { members } } / { key: 'subagentCatalog', value: [...] }
|
|
35
|
+
if (node.key === 'agentTeam' && node.value !== null && typeof node.value === 'object' && Array.isArray(node.value.members)) {
|
|
36
|
+
node.value.members = node.value.members.filter((entry) => !isDismissed(entry, dismissed))
|
|
37
|
+
}
|
|
38
|
+
if (node.key === 'subagentCatalog' && Array.isArray(node.value)) {
|
|
39
|
+
node.value = node.value.filter((entry) => !isDismissed(entry, dismissed))
|
|
40
|
+
}
|
|
34
41
|
for (const key of Object.keys(node)) {
|
|
35
42
|
if (key === 'subagentCatalog' || key === 'agentTeam') continue
|
|
36
43
|
visit(node[key], depth + 1)
|
package/src/index.js
CHANGED
|
@@ -5,8 +5,8 @@ import { homedir } from 'node:os'
|
|
|
5
5
|
import { join } from 'node:path'
|
|
6
6
|
|
|
7
7
|
import { createApi } from './api.mjs'
|
|
8
|
-
import { collectDismissedIds, stripDismissedMembers } from './filter.mjs'
|
|
9
8
|
import { dismissMember } from './dismiss.mjs'
|
|
9
|
+
import { collectDismissedIds, stripDismissedMembers } from './filter.mjs'
|
|
10
10
|
import { openStore } from './store.mjs'
|
|
11
11
|
import { listTeamSessions, readTeam } from './team-read.mjs'
|
|
12
12
|
|
|
@@ -17,10 +17,19 @@ export const inject = ['webServer']
|
|
|
17
17
|
const DATA_DIR_ENV = 'DSH_TEAM_ADMIN_DATA_DIR'
|
|
18
18
|
/** 路由前缀:webServer 只支持 exact/prefix 注册。 */
|
|
19
19
|
const ROUTE_PREFIX = '/api/team-admin'
|
|
20
|
-
/** 墓碑 id
|
|
20
|
+
/** 墓碑 id 缓存时长:过滤发生在每次投影响应/帧上,短缓存足够。 */
|
|
21
21
|
const DISMISSED_TTL_MS = 2000
|
|
22
|
+
/** 包装标记:apply 与延迟注入可能都执行一次,避免重复包装。 */
|
|
23
|
+
const FILTER_MARK = Symbol.for('dsh.team-admin.projection-filter')
|
|
24
|
+
/** roster 预算标记与刷新句柄:名额 = 配置上限 + 已删除成员数。 */
|
|
25
|
+
const ROSTER_MARK = Symbol.for('dsh.team-admin.roster-budget')
|
|
26
|
+
const ROSTER_REFRESH = Symbol.for('dsh.team-admin.roster-refresh')
|
|
27
|
+
/** 单值出口:一次请求/一次流的快照。 */
|
|
28
|
+
const UNARY_METHODS = ['list', 'projections']
|
|
29
|
+
/** 流出口:control 基线携带全部投影,follow 携带单个会话的开场快照。 */
|
|
30
|
+
const STREAM_METHODS = ['control', 'follow']
|
|
22
31
|
/** 与 package.json 版本保持一致(发布前由测试校验)。 */
|
|
23
|
-
const PLUGIN_VERSION = '0.
|
|
32
|
+
const PLUGIN_VERSION = '0.2.0'
|
|
24
33
|
|
|
25
34
|
/**
|
|
26
35
|
* 解析插件数据目录。
|
|
@@ -35,7 +44,7 @@ export function resolveDataDir(env = process.env, home = homedir()) {
|
|
|
35
44
|
}
|
|
36
45
|
|
|
37
46
|
/**
|
|
38
|
-
* 装配插件:惰性建 store,注册一条 prefix
|
|
47
|
+
* 装配插件:惰性建 store,注册一条 prefix 路由,并给投影出口加过滤。
|
|
39
48
|
* @param {object} ctx - Cordis 插件上下文。
|
|
40
49
|
*/
|
|
41
50
|
export function apply(ctx) {
|
|
@@ -58,12 +67,23 @@ export function apply(ctx) {
|
|
|
58
67
|
getStore,
|
|
59
68
|
version: PLUGIN_VERSION,
|
|
60
69
|
logSystem,
|
|
61
|
-
services: {
|
|
70
|
+
services: {
|
|
71
|
+
listTeamSessions,
|
|
72
|
+
readTeam,
|
|
73
|
+
dismissMember,
|
|
74
|
+
// 删除成功后释放名额:名额外加数 = 墓碑数。
|
|
75
|
+
onMemberDismissed: () => { void ensureBudget()?.() },
|
|
76
|
+
},
|
|
62
77
|
})
|
|
63
78
|
ctx.effect(
|
|
64
79
|
() => ctx.webServer.register({ kind: 'prefix', path: ROUTE_PREFIX, handler }),
|
|
65
80
|
'team-admin routes',
|
|
66
81
|
)
|
|
82
|
+
const ensureBudget = () => installRosterBudget(ctx, { getStore, logSystem, silent: true })
|
|
83
|
+
ensureBudget()
|
|
84
|
+
if (typeof ctx.inject === 'function') {
|
|
85
|
+
ctx.inject(['agentTeams'], (sub) => installRosterBudget(sub, { getStore, logSystem }))
|
|
86
|
+
}
|
|
67
87
|
if (!installSessionListFilter(ctx, { getStore, logSystem, silent: true }) && typeof ctx.inject === 'function') {
|
|
68
88
|
// 插件可能早于 sessionController 注册;等服务可用后再包一次。
|
|
69
89
|
ctx.inject(['sessionController'], (sub) => installSessionListFilter(sub, { getStore, logSystem }))
|
|
@@ -71,43 +91,111 @@ export function apply(ctx) {
|
|
|
71
91
|
}
|
|
72
92
|
|
|
73
93
|
/**
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* @
|
|
78
|
-
* @param {{ getStore: () => Promise<object>, logSystem: (line: string) => void }} deps - 数据目录与日志。
|
|
79
|
-
* @returns {boolean} 成功包上返回 true;宿主没有该服务时返回 false。
|
|
94
|
+
* 建一个带短缓存的墓碑 id 读取器。
|
|
95
|
+
* @param {() => Promise<object>} getStore - 惰性打开的状态存储。
|
|
96
|
+
* @param {(line: string) => void} logSystem - 系统日志。
|
|
97
|
+
* @returns {() => Promise<Set<string>>} 读取已删除成员 id。
|
|
80
98
|
*/
|
|
81
|
-
|
|
82
|
-
const get = ctx !== null && typeof ctx?.get === 'function' ? ctx.get.bind(ctx) : null
|
|
83
|
-
const service = get === null ? undefined : get('sessionController')
|
|
84
|
-
if (service === null || service === undefined || typeof service.list !== 'function') {
|
|
85
|
-
if (silent !== true) logSystem('team-admin:未找到 sessionController.list,跳过会话列表过滤')
|
|
86
|
-
return false
|
|
87
|
-
}
|
|
88
|
-
const original = service.list
|
|
99
|
+
function createDismissedIds(getStore, logSystem) {
|
|
89
100
|
let cached = { at: 0, ids: new Set() }
|
|
90
|
-
|
|
101
|
+
return async () => {
|
|
91
102
|
if (Date.now() - cached.at < DISMISSED_TTL_MS) return cached.ids
|
|
92
103
|
let ids = new Set()
|
|
93
104
|
try {
|
|
94
105
|
const store = await getStore()
|
|
95
106
|
ids = collectDismissedIds(await store.readAll())
|
|
96
107
|
} catch (error) {
|
|
97
|
-
logSystem(`team-admin
|
|
108
|
+
logSystem(`team-admin:读取墓碑失败,本次不过滤投影(${error instanceof Error ? error.message : String(error)})`)
|
|
98
109
|
}
|
|
99
110
|
cached = { at: Date.now(), ids }
|
|
100
111
|
return ids
|
|
101
112
|
}
|
|
102
|
-
|
|
103
|
-
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 给宿主的 sessionController 出口包一层:把已删除成员从 subagentCatalog / agentTeam.members 里摘掉。
|
|
117
|
+
* 顶部「子智能体」选择器读 `list` 与 `control` 基线,内置「智能体团队」面板读同一批投影,
|
|
118
|
+
* 因此在宿主出口过滤一次即可覆盖所有客户端,不需要改宿主源码,也不用重编译。
|
|
119
|
+
* @param {object} ctx - Cordis 插件上下文。
|
|
120
|
+
* @param {{ getStore: () => Promise<object>, logSystem: (line: string) => void, silent?: boolean }} deps - 存储与日志。
|
|
121
|
+
* @returns {boolean} 至少包上一个方法时返回 true。
|
|
122
|
+
*/
|
|
123
|
+
export function installSessionListFilter(ctx, { getStore, logSystem, silent = false }) {
|
|
124
|
+
const get = ctx !== null && typeof ctx?.get === 'function' ? ctx.get.bind(ctx) : null
|
|
125
|
+
const service = get === null ? undefined : get('sessionController')
|
|
126
|
+
if (service === null || service === undefined) {
|
|
127
|
+
if (silent !== true) logSystem('team-admin:未找到 sessionController,跳过投影过滤')
|
|
128
|
+
return false
|
|
129
|
+
}
|
|
130
|
+
if (service[FILTER_MARK] === true) return false
|
|
131
|
+
const dismissedIds = createDismissedIds(getStore, logSystem)
|
|
132
|
+
const strip = async (value) => {
|
|
104
133
|
try {
|
|
105
134
|
const ids = await dismissedIds()
|
|
106
|
-
return ids.size === 0 ? value : stripDismissedMembers(value, ids)
|
|
135
|
+
return ids.size === 0 ? value : stripDismissedMembers(value, ids, 10)
|
|
107
136
|
} catch (error) {
|
|
108
|
-
logSystem(`team-admin
|
|
137
|
+
logSystem(`team-admin:过滤投影失败(${error instanceof Error ? error.message : String(error)})`)
|
|
109
138
|
return value
|
|
110
139
|
}
|
|
111
140
|
}
|
|
141
|
+
let wrapped = 0
|
|
142
|
+
for (const method of UNARY_METHODS) {
|
|
143
|
+
const original = service[method]
|
|
144
|
+
if (typeof original !== 'function') continue
|
|
145
|
+
service[method] = async function teamAdminFilteredUnary(...args) {
|
|
146
|
+
return await strip(await original.apply(this, args))
|
|
147
|
+
}
|
|
148
|
+
wrapped += 1
|
|
149
|
+
}
|
|
150
|
+
for (const method of STREAM_METHODS) {
|
|
151
|
+
const original = service[method]
|
|
152
|
+
if (typeof original !== 'function') continue
|
|
153
|
+
service[method] = async function* teamAdminFilteredStream(...args) {
|
|
154
|
+
for await (const frame of original.apply(this, args)) yield await strip(frame)
|
|
155
|
+
}
|
|
156
|
+
wrapped += 1
|
|
157
|
+
}
|
|
158
|
+
if (wrapped === 0) {
|
|
159
|
+
if (silent !== true) logSystem('team-admin:sessionController 上没有可过滤的方法,跳过投影过滤')
|
|
160
|
+
return false
|
|
161
|
+
}
|
|
162
|
+
service[FILTER_MARK] = true
|
|
112
163
|
return true
|
|
113
|
-
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* 把「已删除成员」占掉的名额还给 roster:宿主用 roster 的 append-only 成员数判断 maxMembers,
|
|
168
|
+
* 删除只写墓碑不删记录,所以这里把上限动态抬到 `配置上限 + 墓碑数`。
|
|
169
|
+
* @param {object} ctx - Cordis 插件上下文。
|
|
170
|
+
* @param {{ getStore: () => Promise<object>, logSystem: (line: string) => void, silent?: boolean }} deps - 存储与日志。
|
|
171
|
+
* @returns {null | (() => Promise<number>)} 刷新预算的函数;宿主没有 agentTeams 时返回 null。
|
|
172
|
+
*/
|
|
173
|
+
export function installRosterBudget(ctx, { getStore, logSystem, silent = false }) {
|
|
174
|
+
const get = ctx !== null && typeof ctx?.get === 'function' ? ctx.get.bind(ctx) : null
|
|
175
|
+
const service = get === null ? undefined : get('agentTeams')
|
|
176
|
+
const roster = service === null || service === undefined ? undefined : service.roster
|
|
177
|
+
if (roster === undefined || roster === null || typeof roster.maxMembers !== 'number') {
|
|
178
|
+
if (silent !== true) logSystem('team-admin:未找到 agentTeams.roster,跳过名额回收')
|
|
179
|
+
return null
|
|
180
|
+
}
|
|
181
|
+
if (roster[ROSTER_MARK] === true) return roster[ROSTER_REFRESH]
|
|
182
|
+
const base = roster.maxMembers
|
|
183
|
+
const budget = { extra: 0 }
|
|
184
|
+
Object.defineProperty(roster, 'maxMembers', {
|
|
185
|
+
configurable: true,
|
|
186
|
+
enumerable: true,
|
|
187
|
+
get: () => base + budget.extra,
|
|
188
|
+
})
|
|
189
|
+
roster[ROSTER_MARK] = true
|
|
190
|
+
roster[ROSTER_REFRESH] = async () => {
|
|
191
|
+
try {
|
|
192
|
+
const store = await getStore()
|
|
193
|
+
budget.extra = collectDismissedIds(await store.readAll()).size
|
|
194
|
+
} catch (error) {
|
|
195
|
+
logSystem(`team-admin:读取墓碑失败,名额不回收(${error instanceof Error ? error.message : String(error)})`)
|
|
196
|
+
}
|
|
197
|
+
return budget.extra
|
|
198
|
+
}
|
|
199
|
+
void roster[ROSTER_REFRESH]()
|
|
200
|
+
return roster[ROSTER_REFRESH]
|
|
201
|
+
}
|