@chenkai114/dsh-team-admin 0.1.9 → 0.2.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chenkai114/dsh-team-admin",
3
- "version": "0.1.9",
3
+ "version": "0.2.1",
4
4
  "description": "DSH 插件:GUI 管理任意会话的智能体团队 —— 查看成员、删除角色并清理其持久会话(进回收站,可恢复)",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -55,4 +55,4 @@
55
55
  "test:real": "node tests/e2e/real-data.mjs",
56
56
  "test:pack": "npm pack --dry-run"
57
57
  }
58
- }
58
+ }
package/src/api.mjs CHANGED
@@ -79,7 +79,8 @@ export function createApi({ ctx, getStore, version, logSystem, services }) {
79
79
 
80
80
  if (path === '/api/team-admin/health') {
81
81
  if (method !== 'GET') { sendJson(res, 405, { error: '请求方法不支持' }); return }
82
- sendJson(res, 200, { ok: true, name: 'team-admin', version })
82
+ const budget = typeof services.readRosterBudget === 'function' ? services.readRosterBudget() : null
83
+ sendJson(res, 200, { ok: true, name: 'team-admin', version, maxMembers: budget === null ? null : budget.maxMembers })
83
84
  return
84
85
  }
85
86
 
@@ -132,6 +133,7 @@ export function createApi({ ctx, getStore, version, logSystem, services }) {
132
133
  { sessionId, name, deleteSession: body.deleteSession !== false },
133
134
  { store, logSystem },
134
135
  )
136
+ if (typeof services.onMemberDismissed === 'function') services.onMemberDismissed()
135
137
  sendJson(res, 200, { ok: true, ...result })
136
138
  return
137
139
  }
package/src/index.js CHANGED
@@ -21,12 +21,15 @@ const ROUTE_PREFIX = '/api/team-admin'
21
21
  const DISMISSED_TTL_MS = 2000
22
22
  /** 包装标记:apply 与延迟注入可能都执行一次,避免重复包装。 */
23
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')
24
27
  /** 单值出口:一次请求/一次流的快照。 */
25
28
  const UNARY_METHODS = ['list', 'projections']
26
29
  /** 流出口:control 基线携带全部投影,follow 携带单个会话的开场快照。 */
27
30
  const STREAM_METHODS = ['control', 'follow']
28
31
  /** 与 package.json 版本保持一致(发布前由测试校验)。 */
29
- const PLUGIN_VERSION = '0.1.9'
32
+ const PLUGIN_VERSION = '0.2.1'
30
33
 
31
34
  /**
32
35
  * 解析插件数据目录。
@@ -41,7 +44,7 @@ export function resolveDataDir(env = process.env, home = homedir()) {
41
44
  }
42
45
 
43
46
  /**
44
- * 装配插件:惰性建 store,注册一条 prefix 路由,并给投影出口加过滤。
47
+ * 装配插件:惰性建 store,注册一条 prefix 路由;再给投影出口与成员名额加补丁。
45
48
  * @param {object} ctx - Cordis 插件上下文。
46
49
  */
47
50
  export function apply(ctx) {
@@ -59,23 +62,40 @@ export function apply(ctx) {
59
62
  const logSystem = (line) => {
60
63
  if (typeof ctx.logger?.warn === 'function') ctx.logger.warn(line)
61
64
  }
65
+ const ensureBudget = () => installRosterBudget(ctx, { getStore, logSystem, silent: true })
66
+ // 诊断用:读当前生效的成员上限(配置上限 + 已回收名额)。
67
+ const readRosterBudget = () => {
68
+ const service = typeof ctx.get === 'function' ? ctx.get('agentTeams') : undefined
69
+ const roster = service === null || service === undefined ? undefined : service.roster
70
+ return roster !== undefined && roster !== null && typeof roster.maxMembers === 'number' ? { maxMembers: roster.maxMembers } : null
71
+ }
62
72
  const handler = createApi({
63
73
  ctx,
64
74
  getStore,
65
75
  version: PLUGIN_VERSION,
66
76
  logSystem,
67
- services: { listTeamSessions, readTeam, dismissMember },
77
+ services: {
78
+ listTeamSessions,
79
+ readTeam,
80
+ dismissMember,
81
+ // 删除成功后释放名额:名额外加数 = 墓碑数。
82
+ onMemberDismissed: () => { void ensureBudget()?.() },
83
+ readRosterBudget,
84
+ },
68
85
  })
69
86
  ctx.effect(
70
87
  () => ctx.webServer.register({ kind: 'prefix', path: ROUTE_PREFIX, handler }),
71
88
  'team-admin routes',
72
89
  )
90
+ ensureBudget()
91
+ if (typeof ctx.inject === 'function') {
92
+ ctx.inject(['agentTeams'], (sub) => installRosterBudget(sub, { getStore, logSystem }))
93
+ }
73
94
  if (!installSessionListFilter(ctx, { getStore, logSystem, silent: true }) && typeof ctx.inject === 'function') {
74
95
  // 插件可能早于 sessionController 注册;等服务可用后再包一次。
75
96
  ctx.inject(['sessionController'], (sub) => installSessionListFilter(sub, { getStore, logSystem }))
76
97
  }
77
98
  }
78
-
79
99
  /**
80
100
  * 建一个带短缓存的墓碑 id 读取器。
81
101
  * @param {() => Promise<object>} getStore - 惰性打开的状态存储。
@@ -148,3 +168,40 @@ export function installSessionListFilter(ctx, { getStore, logSystem, silent = fa
148
168
  service[FILTER_MARK] = true
149
169
  return true
150
170
  }
171
+
172
+ /**
173
+ * 把「已删除成员」占掉的名额还给 roster:宿主用 roster 的 append-only 成员数判断 maxMembers,
174
+ * 删除只写墓碑不删记录,所以这里把上限动态抬到 `配置上限 + 墓碑数`。
175
+ * @param {object} ctx - Cordis 插件上下文。
176
+ * @param {{ getStore: () => Promise<object>, logSystem: (line: string) => void, silent?: boolean }} deps - 存储与日志。
177
+ * @returns {null | (() => Promise<number>)} 刷新预算的函数;宿主没有 agentTeams 时返回 null。
178
+ */
179
+ export function installRosterBudget(ctx, { getStore, logSystem, silent = false }) {
180
+ const get = ctx !== null && typeof ctx?.get === 'function' ? ctx.get.bind(ctx) : null
181
+ const service = get === null ? undefined : get('agentTeams')
182
+ const roster = service === null || service === undefined ? undefined : service.roster
183
+ if (roster === undefined || roster === null || typeof roster.maxMembers !== 'number') {
184
+ if (silent !== true) logSystem('team-admin:未找到 agentTeams.roster,跳过名额回收')
185
+ return null
186
+ }
187
+ if (roster[ROSTER_MARK] === true) return roster[ROSTER_REFRESH]
188
+ const base = roster.maxMembers
189
+ const budget = { extra: 0 }
190
+ Object.defineProperty(roster, 'maxMembers', {
191
+ configurable: true,
192
+ enumerable: true,
193
+ get: () => base + budget.extra,
194
+ })
195
+ roster[ROSTER_MARK] = true
196
+ roster[ROSTER_REFRESH] = async () => {
197
+ try {
198
+ const store = await getStore()
199
+ budget.extra = collectDismissedIds(await store.readAll()).size
200
+ } catch (error) {
201
+ logSystem(`team-admin:读取墓碑失败,名额不回收(${error instanceof Error ? error.message : String(error)})`)
202
+ }
203
+ return budget.extra
204
+ }
205
+ void roster[ROSTER_REFRESH]()
206
+ return roster[ROSTER_REFRESH]
207
+ }