@chenkai114/dsh-team-admin 0.1.3 → 0.1.5

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.3",
3
+ "version": "0.1.5",
4
4
  "description": "DSH 插件:GUI 管理任意会话的智能体团队 —— 查看成员、删除角色并清理其持久会话(进回收站,可恢复)",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/api.mjs CHANGED
@@ -92,6 +92,22 @@ export function createApi({ ctx, getStore, version, logSystem, services }) {
92
92
  return
93
93
  }
94
94
 
95
+ if (path === '/api/team-admin/dismissed') {
96
+ if (method !== 'GET') { sendJson(res, 405, { error: '请求方法不支持' }); return }
97
+ const store = await getStore()
98
+ const state = await store.readAll()
99
+ const buckets = state !== null && typeof state === 'object' && typeof state.dismissed === 'object' && state.dismissed !== null
100
+ ? state.dismissed
101
+ : {}
102
+ const ids = []
103
+ for (const bucket of Object.values(buckets)) {
104
+ if (bucket === null || typeof bucket !== 'object') continue
105
+ for (const memberId of Object.keys(bucket)) ids.push(memberId)
106
+ }
107
+ sendJson(res, 200, { ids })
108
+ return
109
+ }
110
+
95
111
  if (path === '/api/team-admin/team') {
96
112
  if (method !== 'GET') { sendJson(res, 405, { error: '请求方法不支持' }); return }
97
113
  const sessionId = queryValue(url, 'sessionId')
package/src/client.js CHANGED
@@ -170,11 +170,77 @@
170
170
  }
171
171
 
172
172
  const DISMISS_URL = API_BASE + '/dismiss'
173
+ const DISMISSED_URL = API_BASE + '/dismissed'
174
+ const LIST_FILTER_FLAG = '__dshTeamAdminSessionListFilter__'
173
175
 
174
176
  function dismissPayload(sessionId, name) {
175
177
  return { sessionId: sessionId, name: name, deleteSession: true }
176
178
  }
177
179
 
180
+ // —— 宿主会话列表过滤:把已删除成员从 subagentCatalog 投影里摘掉 ——
181
+ // 宿主 catalog 与 roster 都是 append-only,删会话不会撤销 catalog 事件;
182
+ // 这里在本插件内拦截 /api/session/list 的响应,按墓碑把失效条目过滤掉,
183
+ // 不修改宿主源码,也不需要重编译。
184
+ let dismissedIds = new Set()
185
+ let dismissedReady = null
186
+
187
+ function stripDismissedCatalogs(payload, dismissed) {
188
+ if (payload === null || typeof payload !== 'object' || dismissed === undefined || dismissed.size === 0) return payload
189
+ const visit = (node, depth) => {
190
+ if (node === null || typeof node !== 'object' || depth > 6) return
191
+ if (Array.isArray(node.subagentCatalog)) {
192
+ node.subagentCatalog = node.subagentCatalog.filter((entry) => !(entry !== null && typeof entry === 'object' && typeof entry.id === 'string' && dismissed.has(entry.id)))
193
+ }
194
+ for (const key of Object.keys(node)) {
195
+ if (key === 'subagentCatalog') continue
196
+ visit(node[key], depth + 1)
197
+ }
198
+ }
199
+ visit(payload, 0)
200
+ return payload
201
+ }
202
+
203
+ async function refreshDismissedIds() {
204
+ try {
205
+ const response = await fetch(DISMISSED_URL, { headers: { accept: 'application/json' } })
206
+ if (response.ok !== true) return dismissedIds
207
+ const payload = await response.json()
208
+ const ids = isRecord(payload) && Array.isArray(payload.ids) ? payload.ids : []
209
+ dismissedIds = new Set(ids.filter((id) => typeof id === 'string' && id !== ''))
210
+ } catch (error) {
211
+ // 墓碑拿不到就放行原始列表:过滤是增强,不是前置条件。
212
+ }
213
+ return dismissedIds
214
+ }
215
+
216
+ function installSessionListFilter(win) {
217
+ const scope = win !== undefined ? win : (typeof window === 'undefined' ? null : window)
218
+ if (scope === null || scope === undefined) return false
219
+ if (scope[LIST_FILTER_FLAG] === true) {
220
+ dismissedReady = refreshDismissedIds()
221
+ return false
222
+ }
223
+ const original = scope.fetch
224
+ if (typeof original !== 'function') return false
225
+ scope[LIST_FILTER_FLAG] = true
226
+ dismissedReady = refreshDismissedIds()
227
+ scope.fetch = async function teamAdminAwareFetch(input, init) {
228
+ const url = typeof input === 'string' ? input : (isRecord(input) && typeof input.url === 'string' ? input.url : '')
229
+ if (url.indexOf('/api/session/list') < 0) return await original(input, init)
230
+ if (dismissedReady !== null) await dismissedReady
231
+ const response = await original(input, init)
232
+ if (dismissedIds.size === 0 || response.ok !== true) return response
233
+ try {
234
+ const body = await response.clone().json()
235
+ stripDismissedCatalogs(body, dismissedIds)
236
+ return new Response(JSON.stringify(body), { status: response.status, statusText: response.statusText, headers: response.headers })
237
+ } catch (error) {
238
+ return response
239
+ }
240
+ }
241
+ return true
242
+ }
243
+
178
244
  // —— 响应校验:缺字段回退默认值,形态不对返回可展示的错误 ——
179
245
  function normalizeSessionRow(raw) {
180
246
  const row = isRecord(raw) ? raw : {}
@@ -788,6 +854,7 @@
788
854
  return
789
855
  }
790
856
  state.confirm = null
857
+ dismissedReady = refreshDismissedIds()
791
858
  state.notice = { text: dismissResultText(readDismissResult(outcome.data, target)) }
792
859
  render()
793
860
  await loadTeam(state.selectedId)
@@ -847,6 +914,7 @@
847
914
  throw new Error('mount(container) 需要一个可挂载的 DOM 元素')
848
915
  }
849
916
  ensureStyle(typeof document === 'undefined' ? null : document)
917
+ installSessionListFilter()
850
918
  const controller = createPanelController()
851
919
  container.appendChild(controller.root)
852
920
  controller.start()
@@ -922,6 +990,7 @@
922
990
  return {
923
991
  inject: ['slots'],
924
992
  apply(ctx) {
993
+ installSessionListFilter()
925
994
  const slots = ctx !== null && ctx !== undefined && ctx.slots !== undefined ? ctx.slots : null
926
995
  if (slots === null || typeof slots.inject !== 'function' || typeof slots.register !== 'function') {
927
996
  console.warn('[team-admin] 宿主未提供 slots 服务,配置卡片未注册')
@@ -979,6 +1048,10 @@
979
1048
  PANEL_LABEL: PANEL_LABEL,
980
1049
  PANEL_ORDER: PANEL_ORDER,
981
1050
  API_BASE: API_BASE,
1051
+ DISMISSED_URL: DISMISSED_URL,
1052
+ stripDismissedCatalogs: stripDismissedCatalogs,
1053
+ installSessionListFilter: installSessionListFilter,
1054
+ refreshDismissedIds: refreshDismissedIds,
982
1055
  DEBOUNCE_MS: DEBOUNCE_MS,
983
1056
  CARD_TITLE: CARD_TITLE,
984
1057
  CARD_SUMMARY: CARD_SUMMARY,
@@ -1011,4 +1084,4 @@
1011
1084
  && typeof window.__ModuleLoader__.load === 'function') {
1012
1085
  window.__ModuleLoader__.load({ id: PKG_ID, factory: factory })
1013
1086
  }
1014
- })()
1087
+ })()
package/src/dismiss.mjs CHANGED
@@ -148,7 +148,30 @@ async function stopMember(ctx, { leadSessionId, memberId, timeoutMs, logSystem }
148
148
  }
149
149
 
150
150
  /**
151
- * 删除一个团队成员:停会话 → 子会话目录进回收站 → 写墓碑。
151
+ * 释放一个子会话的 Activation 注册:宿主 GUI 的子智能体选择器按注册列表渲染,
152
+ * 只删会话文件不会摘掉该条目(点进去报 subagent/not-found)。
153
+ * 宿主没有该方法、Lead Agent 不在线或释放失败都只记日志——删除本身已经生效。
154
+ * @param {object} ctx - 宿主上下文。
155
+ * @param {{leadSessionId: string, memberId: string, logSystem: (line: string) => void}} request - 根会话、成员、日志函数。
156
+ * @returns {Promise<void>} 释放结束(失败已吞并记录)。
157
+ */
158
+ async function releaseActivation(ctx, { leadSessionId, memberId, logSystem }) {
159
+ const subagents = service(ctx, 'subagents')
160
+ if (subagents === undefined || typeof subagents.drainChildren !== 'function') return
161
+ const leadAgent = liveAgent(ctx, leadSessionId)
162
+ if (leadAgent === undefined) {
163
+ logSystem('[team-admin] 无法释放 ' + memberId + ' 的子会话激活:Lead Agent 未加载')
164
+ return
165
+ }
166
+ try {
167
+ await subagents.drainChildren(leadAgent, [memberId])
168
+ } catch (error) {
169
+ logSystem('[team-admin] 释放子会话激活 ' + memberId + ' 失败:' + String(error?.message ?? error))
170
+ }
171
+ }
172
+
173
+ /**
174
+ * 删除一个团队成员:停会话 → 子会话目录进回收站 → 释放子会话激活 → 写墓碑。
152
175
  * @param {object} ctx - 宿主上下文(sessionPersistence / agents / subagents 均可选)。
153
176
  * @param {{sessionId: string, name: string, deleteSession?: boolean}} request - 根会话、成员名、是否清理子会话(默认 true)。
154
177
  * @param {{store?: object, now?: number|(() => number), logSystem?: (line: string) => void, interruptTimeoutMs?: number}} [options]
@@ -235,6 +258,9 @@ export async function dismissMember(ctx, request, options = {}) {
235
258
  }
236
259
  }
237
260
 
261
+ // 移动成功后释放该子会话的 Activation 注册;失败只记日志,删除与墓碑照常。
262
+ if (sessionDeleted) await releaseActivation(ctx, { leadSessionId: sessionId, memberId, logSystem })
263
+
238
264
  const record = await store.record(sessionId, memberId, {
239
265
  name: isRecord(member) && typeof member.name === 'string' ? member.name : name,
240
266
  dismissedAt: stamp,
package/src/index.js CHANGED
@@ -17,7 +17,7 @@ const DATA_DIR_ENV = 'DSH_TEAM_ADMIN_DATA_DIR'
17
17
  /** 路由前缀:webServer 只支持 exact/prefix 注册。 */
18
18
  const ROUTE_PREFIX = '/api/team-admin'
19
19
  /** 与 package.json 版本保持一致(发布前由测试校验)。 */
20
- const PLUGIN_VERSION = '0.1.3'
20
+ const PLUGIN_VERSION = '0.1.5'
21
21
 
22
22
  /**
23
23
  * 解析插件数据目录。