bothouse-dsh 0.1.66 → 0.1.67
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/collab/cli-member.mjs +16 -3
- package/collab/dsh-member.mjs +2 -2
- package/collab/member-management.mjs +1 -1
- package/collab/plugin.mjs +14 -11
- package/collab/roster.mjs +13 -3
- package/collab/store.mjs +3 -3
- package/package.json +1 -1
package/collab/cli-member.mjs
CHANGED
|
@@ -226,7 +226,7 @@ export class CliMember {
|
|
|
226
226
|
if (prevId && this.canLoad) {
|
|
227
227
|
try {
|
|
228
228
|
const loaded = await Promise.race([this.conn.loadSession({ sessionId: prevId, cwd: this.cwd, mcpServers: [] }), this.exited])
|
|
229
|
-
s = { sessionId: prevId, configOptions: loaded?.configOptions }
|
|
229
|
+
s = { sessionId: prevId, configOptions: loaded?.configOptions, defaultModel: loaded?.models?.currentModelId }
|
|
230
230
|
this.sessions.set(room, s)
|
|
231
231
|
return { ...s, resumed: true, lost: false }
|
|
232
232
|
} catch (err) {
|
|
@@ -234,7 +234,7 @@ export class CliMember {
|
|
|
234
234
|
}
|
|
235
235
|
}
|
|
236
236
|
const r = await Promise.race([this.conn.newSession({ cwd: this.cwd, mcpServers: [] }), this.exited])
|
|
237
|
-
s = { sessionId: r.sessionId, configOptions: r.configOptions }
|
|
237
|
+
s = { sessionId: r.sessionId, configOptions: r.configOptions, defaultModel: r.models?.currentModelId }
|
|
238
238
|
this.sessions.set(room, s)
|
|
239
239
|
return { ...s, resumed: false, lost: Boolean(prevId) }
|
|
240
240
|
}
|
|
@@ -301,6 +301,18 @@ export class CliMember {
|
|
|
301
301
|
}
|
|
302
302
|
}
|
|
303
303
|
|
|
304
|
+
async _configureClaude(s, model) {
|
|
305
|
+
// Claude's ACP adapter exposes session/set_model, not Codex's config options.
|
|
306
|
+
// Remember its initial selection so clearing an override also affects a live session.
|
|
307
|
+
if (model == null && s.appliedModel === undefined) return
|
|
308
|
+
const target = model ?? s.defaultModel
|
|
309
|
+
if (!target) throw new Error('Claude default model is unavailable')
|
|
310
|
+
if (s.appliedModel === target) return
|
|
311
|
+
await Promise.race([this.conn.request('session/set_model', { sessionId: s.sessionId, modelId: target }), this.exited])
|
|
312
|
+
const stored = [...this.sessions.values()].find(x => x.sessionId === s.sessionId)
|
|
313
|
+
if (stored) stored.appliedModel = target
|
|
314
|
+
}
|
|
315
|
+
|
|
304
316
|
/**
|
|
305
317
|
* onText(textSoFar) 在每个公开文字分片到达时调用(#121);onTool(update) 在每个工具调用开始/更新和每个思考分片到达时调用。
|
|
306
318
|
* opts.resumeId:要续上的 sessionId;opts.onSession({ sessionId, resumed, lost }):会话就绪后、发提示词前调用。
|
|
@@ -314,9 +326,10 @@ export class CliMember {
|
|
|
314
326
|
return run
|
|
315
327
|
}
|
|
316
328
|
|
|
317
|
-
async _ask(text, room, onText, onTool, { resumeId, onSession, attachments = [], onPermission, cliConfig, cliConfigManaged } = {}) {
|
|
329
|
+
async _ask(text, room, onText, onTool, { resumeId, onSession, attachments = [], onPermission, model, cliConfig, cliConfigManaged } = {}) {
|
|
318
330
|
const s = await this._session(room, resumeId)
|
|
319
331
|
if (this.cli === 'codex' && cliConfigManaged) await this._configure(s, cliConfig)
|
|
332
|
+
if (this.cli === 'claude') await this._configureClaude(s, model)
|
|
320
333
|
await onSession?.({ sessionId: s.sessionId, resumed: s.resumed, lost: s.lost })
|
|
321
334
|
// persona 每轮都拼:续上的或新开的会话都不会丢人设。
|
|
322
335
|
const body = this.persona ? `${this.persona}\n\n${text}` : text
|
package/collab/dsh-member.mjs
CHANGED
|
@@ -171,7 +171,7 @@ export function promptText(room, member, messages, status = '', mode = DEFAULT_D
|
|
|
171
171
|
mode = DEFAULT_DISPATCH_MODE;
|
|
172
172
|
}
|
|
173
173
|
const lines = messages.map(m => `【${m.name}】${quotePrefix(store?.quote?.(m))}${m.text}`);
|
|
174
|
-
const model = member.model === undefined ? '' : `你底层用的模型是 ${member.model.provider}/${member.model.model}。`;
|
|
174
|
+
const model = member.model === undefined ? '' : `你底层用的模型是 ${typeof member.model === 'string' ? member.model : `${member.model.provider}/${member.model.model}`}。`;
|
|
175
175
|
const persona = member.persona?.trim() ? ['你的人设:', member.persona.trim(), ''] : [];
|
|
176
176
|
return [
|
|
177
177
|
`你是频道 #${room.name} 的成员「${member.name}」。${model}频道里其他人各有自己的身份,他们的自我介绍说的是他们自己,不是你。`,
|
|
@@ -851,7 +851,7 @@ export class DshMembers {
|
|
|
851
851
|
// 这两步失败时提前标记会让重试跳过 MEMORY.md。记忆目录是否真的送达由 onSession 里的调用决定。
|
|
852
852
|
// #197: a file over the bot connection cap is not inlined; the CLI reads it from disk.
|
|
853
853
|
const bigNote = oversizedNote(this.rt.files?.oversized(unseen.flatMap(messageAttachments)));
|
|
854
|
-
const text = (await entry.agent.ask(`${prompt}${bigNote ? `\n${bigNote}` : ''}`, String(room.id), onText, onTool, { resumeId, onSession, onPermission, cliConfig: member.cliConfig, cliConfigManaged: member.cliConfigManaged, attachments: attachmentBlocks(this.rt.files, unseen) }))?.trim();
|
|
854
|
+
const text = (await entry.agent.ask(`${prompt}${bigNote ? `\n${bigNote}` : ''}`, String(room.id), onText, onTool, { resumeId, onSession, onPermission, model: member.model, cliConfig: member.cliConfig, cliConfigManaged: member.cliConfigManaged, attachments: attachmentBlocks(this.rt.files, unseen) }))?.trim();
|
|
855
855
|
if (this.closed) return;
|
|
856
856
|
this.hub.quota?.observe(member.id, room.id, { error: text });
|
|
857
857
|
if (!text) { this.fail(job.id, `${member.name} 这一轮没有给出回复(${member.cli} 没有输出文字)`); return; }
|
|
@@ -12,7 +12,7 @@ function check(body, create = false) {
|
|
|
12
12
|
function view(m, source = m.source) {
|
|
13
13
|
return { id: m.id, name: m.name, source, can_edit: source === 'ui', can_delete: source === 'ui', persona: m.persona ?? '',
|
|
14
14
|
cli: m.cli ?? null, model: m.model ?? null, cliConfig: m.cliConfig ?? null,
|
|
15
|
-
apply: { timing: 'next_turn', status: m.cliConfigManaged ? 'pending' : 'not_configured' } };
|
|
15
|
+
apply: { timing: 'next_turn', status: m.cliConfigManaged || (m.cli === 'claude' && m.model) ? 'pending' : 'not_configured' } };
|
|
16
16
|
}
|
|
17
17
|
export function managementMembers(roster, hub) {
|
|
18
18
|
const rows = roster.store.members().map(rowToMember);
|
package/collab/plugin.mjs
CHANGED
|
@@ -74,8 +74,9 @@ export function resolveConfig(config, env = process.env) {
|
|
|
74
74
|
throw Error(`conest-collab: dshMembers id ${JSON.stringify(m?.id)} must be unique lowercase [a-z0-9_-]`);
|
|
75
75
|
}
|
|
76
76
|
if (typeof m.name !== 'string' || !m.name.trim()) throw Error(`conest-collab: dshMembers ${m.id} needs a name`);
|
|
77
|
-
if (m.model !== undefined && (
|
|
78
|
-
|
|
77
|
+
if (m.model !== undefined && (m.cli === 'claude' ? typeof m.model !== 'string' || !m.model.trim()
|
|
78
|
+
: typeof m.model?.provider !== 'string' || typeof m.model?.model !== 'string')) {
|
|
79
|
+
throw Error(`conest-collab: dshMembers ${m.id} model must be ${m.cli === 'claude' ? 'a non-empty model id' : '{ provider, model }'}`);
|
|
79
80
|
}
|
|
80
81
|
if (m.rooms !== undefined && (!Array.isArray(m.rooms) || m.rooms.some(id => !rooms.some(r => r.id === id)))) {
|
|
81
82
|
throw Error(`conest-collab: dshMembers ${m.id} rooms must contain configured chat channel ids`);
|
|
@@ -803,8 +804,8 @@ function ticketWorkspace(req, res, roomId, { store, hub, gateway, later, tasks,
|
|
|
803
804
|
/**
|
|
804
805
|
* 一句话拉团队 (user 11-04: DSH 用户一句话拉单模型多角色): the primary role of a channel, with its
|
|
805
806
|
* (member, room) later ticket, creates a new chat channel and its roles in one call.
|
|
806
|
-
* POST /collab/api/rooms/{id}/team { channel, members: [{ name, persona?, model? }], model? }
|
|
807
|
-
* model is
|
|
807
|
+
* POST /collab/api/rooms/{id}/team { channel, members: [{ name, persona?, cli?, model? }], model? }
|
|
808
|
+
* DSH model is an id or "provider/model"; Claude model is a CLI model id. Absent = backend default.
|
|
808
809
|
* The new roles join only the new channel; the caller becomes its primary role and joins it too.
|
|
809
810
|
*/
|
|
810
811
|
export function teamRoute(req, res, url, { store, hub, later, roster }) {
|
|
@@ -844,16 +845,18 @@ export async function buildTeam(body, leadId, { store, hub, roster }) {
|
|
|
844
845
|
if (wanted.length === 0 || wanted.length > MAX_TEAM) throw new CollabError('invalid', `members must list 1-${MAX_TEAM} roles`);
|
|
845
846
|
const names = wanted.map(x => String(x?.name ?? '').trim());
|
|
846
847
|
if (names.some(n => !n) || new Set(names.map(n => n.toLowerCase())).size !== names.length) throw new CollabError('invalid', 'every role needs a distinct name');
|
|
847
|
-
const shared = await resolveTeamModel(roster, body?.model);
|
|
848
848
|
const models = [];
|
|
849
|
-
for (const x of wanted)
|
|
849
|
+
for (const x of wanted) {
|
|
850
|
+
const wantedModel = x?.model === undefined || x?.model === '' ? body?.model : x.model;
|
|
851
|
+
models.push(x?.cli ? wantedModel : await resolveTeamModel(roster, wantedModel));
|
|
852
|
+
}
|
|
850
853
|
// Check every role before anything is written, so a bad name leaves no half-built channel behind.
|
|
851
|
-
for (const [i, x] of wanted.entries()) await roster.validate({ name: names[i], persona: String(x?.persona ?? ''), avatar: TEAM_AVATARS[i % TEAM_AVATARS.length] });
|
|
854
|
+
for (const [i, x] of wanted.entries()) await roster.validate({ name: names[i], persona: String(x?.persona ?? ''), cli: x?.cli, model: models[i], avatar: TEAM_AVATARS[i % TEAM_AVATARS.length] });
|
|
852
855
|
const room = store.createRoom(channel);
|
|
853
856
|
const created = [];
|
|
854
857
|
for (const [i, x] of wanted.entries()) {
|
|
855
858
|
const model = models[i];
|
|
856
|
-
created.push(await roster.create({ name: names[i], persona: String(x?.persona ?? ''), rooms: [room.id], avatar: TEAM_AVATARS[i % TEAM_AVATARS.length], ...(model ? { model } : {}) }));
|
|
859
|
+
created.push(await roster.create({ name: names[i], persona: String(x?.persona ?? ''), cli: x?.cli, rooms: [room.id], avatar: TEAM_AVATARS[i % TEAM_AVATARS.length], ...(model ? { model } : {}) }));
|
|
857
860
|
}
|
|
858
861
|
const lead = store.member(leadId);
|
|
859
862
|
if (lead?.source === 'ui' && lead.rooms && lead.rooms !== '[]') {
|
|
@@ -861,11 +864,11 @@ export async function buildTeam(body, leadId, { store, hub, roster }) {
|
|
|
861
864
|
}
|
|
862
865
|
store.setLead(room.id, leadId);
|
|
863
866
|
hub.emit({ type: 'rooms', rooms: store.roomsView() });
|
|
864
|
-
const label = model => (model ? model.model : 'DSH
|
|
867
|
+
const label = (model, i) => (model ? typeof model === 'string' ? model : model.model : `${wanted[i]?.cli ?? 'DSH'} 默认模型`);
|
|
865
868
|
const leadName = hub.members.get(leadId)?.name ?? leadId;
|
|
866
869
|
hub.post(room.id, { authorKind: 'system', author: 'system', name: '建团队',
|
|
867
|
-
text: `【已建团队】${leadName} 拉好了 #${channel}:${created.map((c, i) => `${c.name}(${label(models[i])})`).join('、')}。${leadName} 是本频道主角色,直接在这里说需求。` });
|
|
868
|
-
return { room, lead: leadId, members: created.map((c, i) => ({ id: c.id, name: c.name, model: label(models[i]) })) };
|
|
870
|
+
text: `【已建团队】${leadName} 拉好了 #${channel}:${created.map((c, i) => `${c.name}(${label(models[i], i)})`).join('、')}。${leadName} 是本频道主角色,直接在这里说需求。` });
|
|
871
|
+
return { room, lead: leadId, members: created.map((c, i) => ({ id: c.id, name: c.name, model: label(models[i], i) })) };
|
|
869
872
|
}
|
|
870
873
|
|
|
871
874
|
const ERROR_STATUS = { 'room-missing': 404, 'member-missing': 404, 'task-missing': 404, conflict: 409, 'read-only': 403, forbidden: 403, offline: 503, 'member-busy': 409 };
|
package/collab/roster.mjs
CHANGED
|
@@ -23,7 +23,8 @@ export function rowToMember(row) {
|
|
|
23
23
|
id: row.id,
|
|
24
24
|
name: row.name,
|
|
25
25
|
...(row.name_customized == null ? {} : { nameCustomized: Boolean(row.name_customized) }),
|
|
26
|
-
...(row.
|
|
26
|
+
...(row.cli === 'claude' && row.model ? { model: row.model }
|
|
27
|
+
: row.provider && row.model ? { model: { provider: row.provider, model: row.model } } : {}),
|
|
27
28
|
...(row.cli ? { cli: row.cli } : {}),
|
|
28
29
|
...(row.cli_config ? { cliConfig: JSON.parse(row.cli_config) } : {}),
|
|
29
30
|
...(row.cli_config_managed ? { cliConfigManaged: true } : {}),
|
|
@@ -185,6 +186,10 @@ export class Roster {
|
|
|
185
186
|
const existing = this.store.member(id);
|
|
186
187
|
if (existing === undefined || existing.removed) throw new CollabError('member-missing', `member ${id} does not exist`);
|
|
187
188
|
if (existing.source !== 'ui') throw new CollabError('read-only', `member ${id} comes from the config file and cannot be edited here`);
|
|
189
|
+
// Both PATCH endpoints preserve omitted fields. A backend change drops the old backend's model/config.
|
|
190
|
+
const old = rowToMember(existing);
|
|
191
|
+
const changedCli = Object.hasOwn(input, 'cli') && (input.cli ?? undefined) !== old.cli;
|
|
192
|
+
input = { ...old, ...(changedCli ? { model: undefined, cliConfig: undefined, permissionPreset: undefined } : {}), ...input };
|
|
188
193
|
const m = await this.validate({ ...input, rooms: input?.rooms === undefined ? JSON.parse(existing.rooms ?? '[]') : input.rooms }, id);
|
|
189
194
|
if (input?.cliConfig === undefined && existing.cli === m.cli) {
|
|
190
195
|
m.cliConfig = existing.cli_config ? JSON.parse(existing.cli_config) : undefined;
|
|
@@ -278,8 +283,13 @@ export class Roster {
|
|
|
278
283
|
}
|
|
279
284
|
if (!Object.keys(cliConfig).length) cliConfig = undefined;
|
|
280
285
|
}
|
|
281
|
-
|
|
282
|
-
|
|
286
|
+
let model;
|
|
287
|
+
if (input.model != null) {
|
|
288
|
+
if (cli !== 'claude') throw new CollabError('invalid', 'CLI members cannot use a DSH model route');
|
|
289
|
+
if (typeof input.model !== 'string' || !input.model.trim()) throw new CollabError('invalid', 'Claude model must be a non-empty model id');
|
|
290
|
+
model = input.model.trim();
|
|
291
|
+
}
|
|
292
|
+
return { name, persona, avatar, nameCustomized, rooms, cli, model, cliConfig, cliConfigManaged: input.cliConfig !== undefined, ...(preset === undefined ? {} : { permissionPreset: preset }) };
|
|
283
293
|
}
|
|
284
294
|
if (input?.cliConfig != null) throw new CollabError('invalid', 'cliConfig requires a Codex member');
|
|
285
295
|
let model;
|
package/collab/store.mjs
CHANGED
|
@@ -655,14 +655,14 @@ export class Store {
|
|
|
655
655
|
member(id) { return this.q.member.get(id); }
|
|
656
656
|
/** Upsert a config member; config always wins for its own ids. */
|
|
657
657
|
seedMember(m, now = Date.now() / 1000) {
|
|
658
|
-
this.q.seedMember.run(m.id, m.name, m.model?.provider ?? null, m.model?.model ?? null, m.cli ?? null, m.persona ?? '', m.avatar || null, m.permissionPreset ?? null, JSON.stringify(m.rooms ?? []), now);
|
|
658
|
+
this.q.seedMember.run(m.id, m.name, m.model?.provider ?? null, typeof m.model === 'string' ? m.model : m.model?.model ?? null, m.cli ?? null, m.persona ?? '', m.avatar || null, m.permissionPreset ?? null, JSON.stringify(m.rooms ?? []), now);
|
|
659
659
|
}
|
|
660
660
|
addMember(m, now = Date.now() / 1000) {
|
|
661
|
-
return this.q.addMember.get(m.id, m.name, m.model?.provider ?? null, m.model?.model ?? null, m.cli ?? null, m.persona ?? '', m.avatar || null, m.permissionPreset ?? null, m.nameCustomized == null ? null : Number(m.nameCustomized), JSON.stringify(m.rooms ?? []), m.cliConfig ? JSON.stringify(m.cliConfig) : null, Number(Boolean(m.cliConfigManaged)), now);
|
|
661
|
+
return this.q.addMember.get(m.id, m.name, m.model?.provider ?? null, typeof m.model === 'string' ? m.model : m.model?.model ?? null, m.cli ?? null, m.persona ?? '', m.avatar || null, m.permissionPreset ?? null, m.nameCustomized == null ? null : Number(m.nameCustomized), JSON.stringify(m.rooms ?? []), m.cliConfig ? JSON.stringify(m.cliConfig) : null, Number(Boolean(m.cliConfigManaged)), now);
|
|
662
662
|
}
|
|
663
663
|
/** Edit a UI-created member; config members and removed ones are refused (undefined). */
|
|
664
664
|
updateMember(id, m) {
|
|
665
|
-
return this.q.updateMember.get(m.name, m.model?.provider ?? null, m.model?.model ?? null, m.cli ?? null, m.persona ?? '', m.avatar || null, m.permissionPreset ?? null, m.nameCustomized == null ? null : Number(m.nameCustomized), JSON.stringify(m.rooms ?? []), m.cliConfig ? JSON.stringify(m.cliConfig) : null, Number(Boolean(m.cliConfigManaged)), id);
|
|
665
|
+
return this.q.updateMember.get(m.name, m.model?.provider ?? null, typeof m.model === 'string' ? m.model : m.model?.model ?? null, m.cli ?? null, m.persona ?? '', m.avatar || null, m.permissionPreset ?? null, m.nameCustomized == null ? null : Number(m.nameCustomized), JSON.stringify(m.rooms ?? []), m.cliConfig ? JSON.stringify(m.cliConfig) : null, Number(Boolean(m.cliConfigManaged)), id);
|
|
666
666
|
}
|
|
667
667
|
removeMember(id) { return this.q.removeMember.get(id); }
|
|
668
668
|
leadId(roomId) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bothouse-dsh",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.67",
|
|
4
4
|
"description": "BotHouse channels for DeepSeek Harness: Discord-style rooms where DSH models and local CLIs (Claude Code, Codex, Gemini) are members",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./collab/plugin.mjs",
|