dsh-bots 0.2.27 → 0.2.28

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/lib/client.js CHANGED
@@ -484,7 +484,7 @@
484
484
  'chat.stop.noop': '当前没有进行中的生成',
485
485
  'media.openFailed': '打开失败,文件可能已移动或被删除',
486
486
  'chat.members.manage': '管理成员',
487
- 'chat.members.cap': '已达群成员上限 {cap} 人——可先在下方调大「成员上限」再添加',
487
+ 'chat.members.cap': '已达群成员上限 {cap} 人——可在会话设置里调高',
488
488
  'chat.settings': '设置',
489
489
  'chat.settings.title': '会话设置',
490
490
  'chat.settings.name': '名称',
@@ -495,9 +495,11 @@
495
495
  'chat.settings.paths': '额外可写路径(逗号分隔)',
496
496
  'chat.settings.pathsPh': '/tmp/xxx, /Users/you/shared',
497
497
  'chat.settings.hint': '写入被沙盒限制在工作区(含额外路径)内,读取不受限;同 slug 的 Bot 共享同一目录。',
498
+ 'chat.settings.cap': '成员上限(1–16)',
499
+ 'chat.settings.capHint': '保存在该群的配置里;调低会立即截断超出部分。',
500
+ 'chat.settings.capBad': '成员上限需为 1–16 的整数',
498
501
  'modal.members.title': '管理群成员',
499
502
  'modal.members.hint': '勾选的 Bot 为群成员;保存后立即生效(可随时再改)。',
500
- 'modal.members.cap': '成员上限(1–16)',
501
503
  'action.delete': '删除',
502
504
  'delete.title.bot': '删除 Bot',
503
505
  'delete.title.group': '删除群聊',
@@ -600,7 +602,7 @@
600
602
  'chat.stop.noop': 'No generation in progress',
601
603
  'media.openFailed': 'Open failed — the file may have moved or been deleted',
602
604
  'chat.members.manage': 'Manage members',
603
- 'chat.members.cap': 'Group cap is {cap} members — raise the cap below first',
605
+ 'chat.members.cap': 'Group cap is {cap} members — raise it in session settings',
604
606
  'chat.settings': 'Settings',
605
607
  'chat.settings.title': 'Session settings',
606
608
  'chat.settings.name': 'Name',
@@ -611,9 +613,11 @@
611
613
  'chat.settings.paths': 'Extra writable paths (comma-separated)',
612
614
  'chat.settings.pathsPh': '/tmp/xxx, /Users/you/shared',
613
615
  'chat.settings.hint': 'Writes are sandboxed to the workspace (plus extra paths); reads stay unrestricted. Bots sharing a slug share one directory.',
616
+ 'chat.settings.cap': 'Member cap (1–16)',
617
+ 'chat.settings.capHint': 'Stored in this group\'s config; lowering it drops members beyond the new cap right away.',
618
+ 'chat.settings.capBad': 'The cap must be an integer from 1 to 16',
614
619
  'modal.members.title': 'Manage group members',
615
620
  'modal.members.hint': 'Checked bots are members; changes apply immediately on save (editable again anytime).',
616
- 'modal.members.cap': 'Member cap (1–16)',
617
621
  'action.delete': 'Delete',
618
622
  'delete.title.bot': 'Delete bot',
619
623
  'delete.title.group': 'Delete group chat',
@@ -2224,7 +2228,6 @@
2224
2228
  const [picked, setPicked] = React.useState(null);
2225
2229
  const [err, setErr] = React.useState(null);
2226
2230
  const [working, setWorking] = React.useState(false);
2227
- const [capText, setCapText] = React.useState('8');
2228
2231
  // Seed once per opened group; a group vanishing mid-edit (deleted by
2229
2232
  // the swarm, say) just renders the modal inert until closed.
2230
2233
  React.useEffect(() => {
@@ -2234,18 +2237,17 @@
2234
2237
  for (const id of group.memberIds ?? [])
2235
2238
  init[id] = true;
2236
2239
  setPicked(init);
2237
- setCapText(String(group.maxMembers ?? 8));
2238
2240
  // eslint-disable-next-line react-hooks/exhaustive-deps
2239
2241
  }, [s.manageMembers]);
2240
2242
  if (group === null || group === undefined)
2241
2243
  return null;
2242
2244
  const singles = s.agents.filter((a) => !a.isGroup && a.isHiddenFromSidebar !== true);
2243
2245
  const pickedIds = picked === null ? [] : Object.keys(picked).filter((k) => picked[k]);
2244
- // Per-group cap (group.json maxMembers, default 8, engine hard max 16):
2245
- // the glue slice(0, maxMembers)-truncates the roster SILENTLY an
2246
- // over-cap save writes successfully and just drops whoever ranked past
2247
- // the cap. Surface it here instead of letting a save pretend to work.
2248
- const MEMBER_CAP = Math.min(16, Math.max(1, Math.floor(Number(capText)) || 8));
2246
+ // Per-group cap (group.json maxMembers, default 8, engine hard max 16;
2247
+ // the host enriches group rows with the real value). The glue slice(0,
2248
+ // maxMembers)-truncates the roster SILENTLY, so an over-cap pick is
2249
+ // blocked here the cap itself is edited in session settings.
2250
+ const MEMBER_CAP = group.maxMembers ?? 8;
2249
2251
  const overCap = pickedIds.length > MEMBER_CAP;
2250
2252
  function toggle(id) {
2251
2253
  if (picked === null)
@@ -2263,7 +2265,7 @@
2263
2265
  setWorking(true);
2264
2266
  setErr(null);
2265
2267
  try {
2266
- await botsCall('setGroupMembers', { id: group.id, memberIds: pickedIds, maxMembers: MEMBER_CAP });
2268
+ await botsCall('setGroupMembers', { id: group.id, memberIds: pickedIds });
2267
2269
  patch({ manageMembers: null });
2268
2270
  await refreshAgents();
2269
2271
  return;
@@ -2286,10 +2288,7 @@
2286
2288
  disabled: working,
2287
2289
  icon: Ico('IconCloseOutline16', { size: 16 }),
2288
2290
  onClick: () => patch({ manageMembers: null }),
2289
- })), e('div', { className: 'dbs-modalBody' }, e('div', { className: 'dbs-meta', style: { padding: '0 2px 6px' } }, t('modal.members.hint')), e('div', { className: 'dbs-setrow', style: { padding: '0 2px 8px', gap: 8 } }, e('span', { className: 'dbs-meta' }, t('modal.members.cap')), e(Input, {
2290
- value: capText, disabled: working, style: { width: 72 },
2291
- onChange: (ev) => setCapText(ev.target.value),
2292
- })), picked === null
2291
+ })), e('div', { className: 'dbs-modalBody' }, e('div', { className: 'dbs-meta', style: { padding: '0 2px 6px' } }, t('modal.members.hint')), picked === null
2293
2292
  ? e('div', { className: 'dbs-meta', style: { padding: '4px 6px' } }, t('list.loading'))
2294
2293
  : e('div', { className: 'dbs-modalMembers' }, singles.length === 0
2295
2294
  ? e('div', { className: 'dbs-meta', style: { padding: '4px 6px' } }, t('list.empty'))
@@ -2328,11 +2327,13 @@
2328
2327
  const [wsLoaded, setWsLoaded] = React.useState(false);
2329
2328
  const [working, setWorking] = React.useState(false);
2330
2329
  const [err, setErr] = React.useState(null);
2330
+ const [capText, setCapText] = React.useState('8');
2331
2331
  React.useEffect(() => {
2332
2332
  if (agent === null || agent === undefined)
2333
2333
  return;
2334
2334
  setName(String(agent.name ?? ''));
2335
2335
  setDesc(String(agent.description ?? ''));
2336
+ setCapText(String(agent.maxMembers ?? 8));
2336
2337
  if (agent.isGroup === true) {
2337
2338
  setWsLoaded(true);
2338
2339
  return;
@@ -2354,13 +2355,24 @@
2354
2355
  if (agent === null || agent === undefined)
2355
2356
  return null;
2356
2357
  const valid = name.trim() !== '';
2358
+ // Member cap lives here (session settings), not in the roster editor:
2359
+ // it is this group's own config field. Empty/unchanged = keep.
2360
+ const capNum = Math.floor(Number(capText));
2361
+ const capValid = capText.trim() === '' || (Number.isInteger(capNum) && capNum >= 1 && capNum <= 16);
2357
2362
  async function save() {
2358
2363
  if (working || valid === false)
2359
2364
  return;
2365
+ if (capValid === false) {
2366
+ setErr(t('chat.settings.capBad'));
2367
+ return;
2368
+ }
2360
2369
  setWorking(true);
2361
2370
  setErr(null);
2362
2371
  try {
2363
2372
  await botsCall('update', { id: agent.id, profile: { name: name.trim(), description: desc.trim() } });
2373
+ if (agent.isGroup === true && capText.trim() !== '') {
2374
+ await botsCall('setGroupMembers', { id: agent.id, memberIds: agent.memberIds ?? [], maxMembers: capNum });
2375
+ }
2364
2376
  if (agent.isGroup !== true && wsLoaded === true) {
2365
2377
  const list = paths.split(/[,,]/).map((x) => x.trim()).filter((x) => x !== '');
2366
2378
  await botsCall('workspaceSet', {
@@ -2394,7 +2406,12 @@
2394
2406
  })), e('div', { className: 'dbs-modalBody' }, e('div', { className: 'dbs-setLabel' }, t('chat.settings.name')), e(Input, {
2395
2407
  value: name, autoFocus: true, disabled: working,
2396
2408
  onChange: (ev) => setName(ev.target.value),
2397
- }), isGroup === false
2409
+ }), isGroup === true
2410
+ ? e('div', null, e('div', { className: 'dbs-setLabel' }, t('chat.settings.cap')), e(Input, {
2411
+ value: capText, disabled: working, inputMode: 'numeric', style: { width: 96 },
2412
+ onChange: (ev) => setCapText(ev.target.value),
2413
+ }), e('div', { className: 'dbs-meta', style: { marginTop: 6 } }, t('chat.settings.capHint')))
2414
+ : null, isGroup === false
2398
2415
  ? e('div', null, e('div', { className: 'dbs-setLabel' }, t('chat.settings.desc')), e(Input, {
2399
2416
  value: desc, disabled: working,
2400
2417
  onChange: (ev) => setDesc(ev.target.value),
package/lib/index.js CHANGED
@@ -214,7 +214,22 @@ export class BotsRemote extends TypertRemoteService {
214
214
  * would make the gateway reject the envelope with "unexpected request".
215
215
  */
216
216
  async list(request) {
217
- return normalizeAgents(await callGateway(this.cfg.dataDir, 'listAgents', {}));
217
+ const agents = normalizeAgents(await callGateway(this.cfg.dataDir, 'listAgents', {}));
218
+ // The gateway row omits group.json's maxMembers, so read it here — the
219
+ // settings editor needs the CURRENT cap, not a hardcoded default.
220
+ for (const a of agents) {
221
+ if (a.isGroup !== true)
222
+ continue;
223
+ try {
224
+ const cfg = JSON.parse(readFileSync(join(effectiveDataDir(this.cfg.dataDir), 'agents', a.id, 'group.json'), 'utf-8'));
225
+ const n = typeof cfg.maxMembers === 'number' ? Math.floor(cfg.maxMembers) : Number.NaN;
226
+ a.maxMembers = Number.isFinite(n) ? Math.min(16, Math.max(1, n)) : 8;
227
+ }
228
+ catch {
229
+ a.maxMembers = 8;
230
+ }
231
+ }
232
+ return agents;
218
233
  }
219
234
  async workspaces(request) {
220
235
  const registry = this.ctx.get('workspaceRegistry');
@@ -285,6 +300,7 @@ export class BotsRemote extends TypertRemoteService {
285
300
  const updated = await callGateway(this.cfg.dataDir, 'setGroupMembers', {
286
301
  id: request?.id,
287
302
  memberAgentIds: Array.isArray(request?.memberIds) ? request.memberIds : [],
303
+ ...(typeof request?.maxMembers === 'number' ? { maxMembers: request.maxMembers } : {}),
288
304
  });
289
305
  return trimAgent(updated?.agent ?? updated);
290
306
  }
@@ -91,6 +91,7 @@ export declare class BotsRemote extends TypertRemoteService {
91
91
  setGroupMembers(request: {
92
92
  id?: string;
93
93
  memberIds?: string[];
94
+ maxMembers?: number;
94
95
  } | null): Promise<AgentInfo | null>;
95
96
  /**
96
97
  * Resolve a bot-written path against the media allowlist: only files inside
@@ -20,6 +20,9 @@ export interface AgentInfo {
20
20
  title: string;
21
21
  isGroup: boolean;
22
22
  memberIds: string[];
23
+ /** Per-group roster cap (group.json maxMembers; 1-16, default 8). The
24
+ gateway row omits it, so the host enriches group rows from disk. */
25
+ maxMembers?: number;
23
26
  isRunning: boolean;
24
27
  isActive: boolean;
25
28
  lastMessagePreview: string | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-bots",
3
- "version": "0.2.27",
3
+ "version": "0.2.28",
4
4
  "description": "Multi-bot workbench for DeepSeek Harness: bridges the sdk-bots orchestration gateway (group chats, single bots) into the dsh web shell with official-styled UI.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",