bothouse-dsh 0.1.43 → 0.1.44
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/client/client.js +100 -47
- package/collab/dsh-member.mjs +4 -2
- package/collab/handoff-rules.mjs +31 -0
- package/collab/plugin.mjs +5 -2
- package/collab/store.mjs +37 -2
- package/collab/team-exit.mjs +2 -1
- package/package.json +1 -1
package/client/client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// dsh-client-source-sha256:
|
|
1
|
+
// dsh-client-source-sha256: 51732729e2bd1e81bb4d9c84a793292ba99e9ca0bbed95f72393ce083fbaeadd
|
|
2
2
|
// dsh-style-sha256: 8de64f320b70b31c6dba515e74187a98284317258d9cb57df3dc16f2eb345641
|
|
3
3
|
window.__ModuleLoader__.load({
|
|
4
4
|
id: "bothouse-dsh",
|
|
@@ -252,7 +252,8 @@ function createChannelSource(transport) {
|
|
|
252
252
|
...r,
|
|
253
253
|
lead_id: event.lead_id,
|
|
254
254
|
effective_lead_id: event.effective_lead_id,
|
|
255
|
-
workspace_path: event.workspace_path
|
|
255
|
+
workspace_path: event.workspace_path,
|
|
256
|
+
max_relay: event.max_relay ?? r.max_relay
|
|
256
257
|
} : r)
|
|
257
258
|
};
|
|
258
259
|
case "lead": return {
|
|
@@ -631,6 +632,15 @@ function createChannelSource(transport) {
|
|
|
631
632
|
...result
|
|
632
633
|
} : r) });
|
|
633
634
|
},
|
|
635
|
+
async setMaxRelay(roomId, maxRelay) {
|
|
636
|
+
const result = await request(`collab/api/rooms/${roomId}/settings`, json("PATCH", { max_relay: maxRelay }));
|
|
637
|
+
const chat = snapshot.rooms.find((r) => r.id === roomId)?.parent_id ?? roomId;
|
|
638
|
+
if (lifetime !== void 0) onEvent({
|
|
639
|
+
type: "room-settings",
|
|
640
|
+
room_id: chat,
|
|
641
|
+
...result
|
|
642
|
+
});
|
|
643
|
+
},
|
|
634
644
|
async setLead(id, roomId = snapshot.roomId) {
|
|
635
645
|
if (roomId === void 0) throw new Error("No channel is open");
|
|
636
646
|
const result = await request(`collab/api/rooms/${roomId}/settings`, json("PATCH", { lead_id: id }));
|
|
@@ -18272,6 +18282,8 @@ function MemberDialog({ member, members = NO_MEMBERS, listModels, listClis, list
|
|
|
18272
18282
|
});
|
|
18273
18283
|
}
|
|
18274
18284
|
// src/client/ChannelSettingsDialog.tsx
|
|
18285
|
+
const RELAY_CAP_MIN = 6;
|
|
18286
|
+
const RELAY_CAP_MAX = 20;
|
|
18275
18287
|
const tabs = [
|
|
18276
18288
|
"basic",
|
|
18277
18289
|
"dispatch",
|
|
@@ -18283,15 +18295,17 @@ const tabLabels = {
|
|
|
18283
18295
|
workspace: "settingsWorkspace"
|
|
18284
18296
|
};
|
|
18285
18297
|
/** Drafts and save outcomes live for the whole dialog, independently of the selected tab. */
|
|
18286
|
-
function ChannelSettingsDialog({ room, members, effectiveLead, renameRoom, setDispatchMode, setLead, setWorkspacePath, renewRoom, t, onClose }) {
|
|
18298
|
+
function ChannelSettingsDialog({ room, members, effectiveLead, renameRoom, setDispatchMode, setMaxRelay, setLead, setWorkspacePath, renewRoom, t, onClose }) {
|
|
18287
18299
|
const [tab, setTab] = (0, react.useState)("basic");
|
|
18288
18300
|
const [name, setName] = (0, react.useState)(room.name);
|
|
18289
18301
|
const [mode, setMode] = (0, react.useState)(room.dispatch_mode ?? "mixed");
|
|
18302
|
+
const [relay, setRelay] = (0, react.useState)(String(room.max_relay ?? RELAY_CAP_MIN));
|
|
18290
18303
|
const [lead, setLeadDraft] = (0, react.useState)(effectiveLead);
|
|
18291
18304
|
const [directory, setDirectory] = (0, react.useState)(room.workspace_path ?? "");
|
|
18292
18305
|
const [saved, setSaved] = (0, react.useState)({
|
|
18293
18306
|
name: room.name,
|
|
18294
18307
|
mode: room.dispatch_mode ?? "mixed",
|
|
18308
|
+
relay: String(room.max_relay ?? RELAY_CAP_MIN),
|
|
18295
18309
|
lead: effectiveLead,
|
|
18296
18310
|
directory: room.workspace_path ?? ""
|
|
18297
18311
|
});
|
|
@@ -18304,9 +18318,10 @@ function ChannelSettingsDialog({ room, members, effectiveLead, renameRoom, setDi
|
|
|
18304
18318
|
(0, react.useEffect)(() => {
|
|
18305
18319
|
dialogRef.current?.focus();
|
|
18306
18320
|
}, []);
|
|
18321
|
+
const relayValid = /^\d+$/.test(relay) && Number(relay) >= RELAY_CAP_MIN && Number(relay) <= RELAY_CAP_MAX;
|
|
18307
18322
|
const dirty = {
|
|
18308
18323
|
basic: room.kind !== "workspace" && name !== saved.name,
|
|
18309
|
-
dispatch: mode !== saved.mode || lead !== saved.lead,
|
|
18324
|
+
dispatch: mode !== saved.mode || relay !== saved.relay || lead !== saved.lead,
|
|
18310
18325
|
workspace: directory !== saved.directory
|
|
18311
18326
|
};
|
|
18312
18327
|
const close = () => {
|
|
@@ -18327,6 +18342,7 @@ function ChannelSettingsDialog({ room, members, effectiveLead, renameRoom, setDi
|
|
|
18327
18342
|
const save = async (target) => {
|
|
18328
18343
|
if (saving[target] || !dirty[target]) return;
|
|
18329
18344
|
if (target === "basic" && (!name || name.length > 32 || /\s/.test(name))) return;
|
|
18345
|
+
if (target === "dispatch" && !relayValid) return;
|
|
18330
18346
|
if (target === "workspace" && (!directory || renewing || renewPending)) return;
|
|
18331
18347
|
setSaving((previous) => ({
|
|
18332
18348
|
...previous,
|
|
@@ -18355,6 +18371,13 @@ function ChannelSettingsDialog({ room, members, effectiveLead, renameRoom, setDi
|
|
|
18355
18371
|
lead
|
|
18356
18372
|
}));
|
|
18357
18373
|
}
|
|
18374
|
+
if (relay !== saved.relay) {
|
|
18375
|
+
await setMaxRelay(room.id, Number(relay));
|
|
18376
|
+
setSaved((previous) => ({
|
|
18377
|
+
...previous,
|
|
18378
|
+
relay
|
|
18379
|
+
}));
|
|
18380
|
+
}
|
|
18358
18381
|
} else {
|
|
18359
18382
|
await setWorkspacePath(room.id, directory);
|
|
18360
18383
|
setSaved((previous) => ({
|
|
@@ -18473,51 +18496,75 @@ function ChannelSettingsDialog({ room, members, effectiveLead, renameRoom, setDi
|
|
|
18473
18496
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(room.kind === "workspace" ? "workspaceNameFixed" : "channelNameHint") })
|
|
18474
18497
|
]
|
|
18475
18498
|
}) }),
|
|
18476
|
-
tab === "dispatch" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
18477
|
-
|
|
18478
|
-
|
|
18479
|
-
|
|
18480
|
-
|
|
18481
|
-
|
|
18499
|
+
tab === "dispatch" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
18500
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
18501
|
+
className: ChannelsPage_module_default.field,
|
|
18502
|
+
role: "radiogroup",
|
|
18503
|
+
"aria-label": t("dispatchMode"),
|
|
18504
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("dispatchMode") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
18505
|
+
className: ChannelsPage_module_default.choiceColumn,
|
|
18506
|
+
children: [
|
|
18507
|
+
"solo",
|
|
18508
|
+
"team",
|
|
18509
|
+
"mixed"
|
|
18510
|
+
].map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
18511
|
+
className: ChannelsPage_module_default.choice,
|
|
18512
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
18513
|
+
type: "radio",
|
|
18514
|
+
name: "settings-dispatch-mode",
|
|
18515
|
+
disabled: saving.dispatch,
|
|
18516
|
+
checked: mode === value,
|
|
18517
|
+
onChange: () => {
|
|
18518
|
+
setMode(value);
|
|
18519
|
+
clearFeedback("dispatch");
|
|
18520
|
+
}
|
|
18521
|
+
}), t(value === "solo" ? "dispatchSolo" : value === "team" ? "dispatchTeam" : "dispatchMixed")]
|
|
18522
|
+
}, value))
|
|
18523
|
+
})]
|
|
18524
|
+
}),
|
|
18525
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
18526
|
+
className: ChannelsPage_module_default.field,
|
|
18527
|
+
children: [t("channelLead"), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
18528
|
+
className: ChannelsPage_module_default.fieldInput,
|
|
18529
|
+
"aria-label": t("channelLead"),
|
|
18530
|
+
value: lead,
|
|
18531
|
+
disabled: saving.dispatch,
|
|
18532
|
+
onChange: (event) => {
|
|
18533
|
+
setLeadDraft(event.target.value);
|
|
18534
|
+
clearFeedback("dispatch");
|
|
18535
|
+
},
|
|
18536
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
18537
|
+
value: "",
|
|
18538
|
+
disabled: true,
|
|
18539
|
+
children: t("leadNone")
|
|
18540
|
+
}), members.filter((member) => member.kind !== "human").map((member) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
18541
|
+
value: member.id,
|
|
18542
|
+
children: member.name
|
|
18543
|
+
}, member.id))]
|
|
18544
|
+
})]
|
|
18545
|
+
}),
|
|
18546
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
18547
|
+
className: ChannelsPage_module_default.field,
|
|
18482
18548
|
children: [
|
|
18483
|
-
"
|
|
18484
|
-
"
|
|
18485
|
-
|
|
18486
|
-
|
|
18487
|
-
|
|
18488
|
-
|
|
18489
|
-
|
|
18490
|
-
|
|
18549
|
+
t("settingsRelay"),
|
|
18550
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
18551
|
+
className: ChannelsPage_module_default.fieldInput,
|
|
18552
|
+
type: "number",
|
|
18553
|
+
min: RELAY_CAP_MIN,
|
|
18554
|
+
max: RELAY_CAP_MAX,
|
|
18555
|
+
step: 1,
|
|
18556
|
+
"aria-label": t("settingsRelay"),
|
|
18557
|
+
value: relay,
|
|
18491
18558
|
disabled: saving.dispatch,
|
|
18492
|
-
|
|
18493
|
-
|
|
18494
|
-
setMode(value);
|
|
18559
|
+
onChange: (event) => {
|
|
18560
|
+
setRelay(event.target.value);
|
|
18495
18561
|
clearFeedback("dispatch");
|
|
18496
18562
|
}
|
|
18497
|
-
}),
|
|
18498
|
-
|
|
18499
|
-
|
|
18500
|
-
|
|
18501
|
-
|
|
18502
|
-
children: [t("channelLead"), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
18503
|
-
className: ChannelsPage_module_default.fieldInput,
|
|
18504
|
-
"aria-label": t("channelLead"),
|
|
18505
|
-
value: lead,
|
|
18506
|
-
disabled: saving.dispatch,
|
|
18507
|
-
onChange: (event) => {
|
|
18508
|
-
setLeadDraft(event.target.value);
|
|
18509
|
-
clearFeedback("dispatch");
|
|
18510
|
-
},
|
|
18511
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
18512
|
-
value: "",
|
|
18513
|
-
disabled: true,
|
|
18514
|
-
children: t("leadNone")
|
|
18515
|
-
}), members.filter((member) => member.kind !== "human").map((member) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
18516
|
-
value: member.id,
|
|
18517
|
-
children: member.name
|
|
18518
|
-
}, member.id))]
|
|
18519
|
-
})]
|
|
18520
|
-
})] }),
|
|
18563
|
+
}),
|
|
18564
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("settingsRelayHint") })
|
|
18565
|
+
]
|
|
18566
|
+
})
|
|
18567
|
+
] }),
|
|
18521
18568
|
tab === "workspace" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
18522
18569
|
className: ChannelsPage_module_default.field,
|
|
18523
18570
|
children: [
|
|
@@ -18604,7 +18651,7 @@ function ChannelSettingsDialog({ room, members, effectiveLead, renameRoom, setDi
|
|
|
18604
18651
|
form: "channel-settings-form",
|
|
18605
18652
|
"aria-busy": Boolean(saving[tab]),
|
|
18606
18653
|
className: ChannelsPage_module_default.primaryButton,
|
|
18607
|
-
disabled: saving[tab] || !dirty[tab] || tab === "basic" && (!name || name.length > 32 || /\s/.test(name)) || tab === "workspace" && (!directory || renewing || renewPending),
|
|
18654
|
+
disabled: saving[tab] || !dirty[tab] || tab === "basic" && (!name || name.length > 32 || /\s/.test(name)) || tab === "dispatch" && !relayValid || tab === "workspace" && (!directory || renewing || renewPending),
|
|
18608
18655
|
children: t(saving[tab] ? "settingsSaving" : "messageSave")
|
|
18609
18656
|
})]
|
|
18610
18657
|
})
|
|
@@ -19052,7 +19099,7 @@ function MemberGroup({ title, members, busy, onPick, onEdit, onRemove, t }) {
|
|
|
19052
19099
|
* @param props - framework, injected, and locale shares.
|
|
19053
19100
|
* @returns the three-column channel view.
|
|
19054
19101
|
*/
|
|
19055
|
-
function ChannelsPage({ useChannels, selectRoom, send, reviewTask, createRoom, renameRoom, listModels, listClis, listPersonas, addMember, updateMember, removeMember, setLead, setWorkspacePath, renewRoom, setDispatchMode, decidePermission, retryMessage, editMessage, withdrawMessage, openModelSettings, t }) {
|
|
19102
|
+
function ChannelsPage({ useChannels, selectRoom, send, reviewTask, createRoom, renameRoom, listModels, listClis, listPersonas, addMember, updateMember, removeMember, setLead, setWorkspacePath, renewRoom, setDispatchMode, setMaxRelay, decidePermission, retryMessage, editMessage, withdrawMessage, openModelSettings, t }) {
|
|
19056
19103
|
const state = useChannels((snapshot) => snapshot);
|
|
19057
19104
|
const [editing, setEditing] = (0, react.useState)(void 0);
|
|
19058
19105
|
const [ownNotice, setOwnNotice] = (0, react.useState)(void 0);
|
|
@@ -19900,6 +19947,7 @@ function ChannelsPage({ useChannels, selectRoom, send, reviewTask, createRoom, r
|
|
|
19900
19947
|
effectiveLead: room.effective_lead_id === void 0 ? state.effectiveLeadId ?? state.leadId ?? "" : room.effective_lead_id ?? "",
|
|
19901
19948
|
renameRoom,
|
|
19902
19949
|
setDispatchMode,
|
|
19950
|
+
setMaxRelay,
|
|
19903
19951
|
setLead,
|
|
19904
19952
|
setWorkspacePath,
|
|
19905
19953
|
renewRoom,
|
|
@@ -19968,6 +20016,8 @@ const zh = {
|
|
|
19968
20016
|
settingsSaved: "已保存",
|
|
19969
20017
|
settingsSaving: "保存中…",
|
|
19970
20018
|
settingsDiscardConfirm: "还有未保存的改动。关闭将丢弃这些改动,确定关闭吗?",
|
|
20019
|
+
settingsRelay: "接力上限",
|
|
20020
|
+
settingsRelayHint: "Agent 连续接力达到上限后暂停自动接力,等人来 @。6–20 的整数,默认 6。",
|
|
19971
20021
|
workingDirectory: "工作目录",
|
|
19972
20022
|
workingDirectoryHint: "请输入已存在的绝对目录。只影响写入范围和相对路径,不影响读取;保存后下次新建会话生效。工作区继承所属频道。",
|
|
19973
20023
|
renewConfirm: "已更新工作目录,是否立即为该频道重建会话?重建会清空当前对话上下文。",
|
|
@@ -20218,6 +20268,8 @@ const en = {
|
|
|
20218
20268
|
settingsSaved: "Saved",
|
|
20219
20269
|
settingsSaving: "Saving…",
|
|
20220
20270
|
settingsDiscardConfirm: "You have unsaved changes. Closing will discard them. Close anyway?",
|
|
20271
|
+
settingsRelay: "Relay cap",
|
|
20272
|
+
settingsRelayHint: "Agent-to-agent relays stop at this cap until a person @-mentions again. Integer 6–20, default 6.",
|
|
20221
20273
|
workingDirectory: "Working directory",
|
|
20222
20274
|
workingDirectoryHint: "Enter an existing absolute directory. This affects write scope and relative paths, not reads. It applies to new sessions; workspaces inherit their channel.",
|
|
20223
20275
|
renewConfirm: "Working directory updated. Recreate this channel’s sessions now? This clears the current conversation context.",
|
|
@@ -20400,6 +20452,7 @@ function apply(ctx) {
|
|
|
20400
20452
|
updateMember: channels.actions.updateMember,
|
|
20401
20453
|
removeMember: channels.actions.removeMember,
|
|
20402
20454
|
setDispatchMode: channels.actions.setDispatchMode,
|
|
20455
|
+
setMaxRelay: channels.actions.setMaxRelay,
|
|
20403
20456
|
setLead: channels.actions.setLead,
|
|
20404
20457
|
setWorkspacePath: channels.actions.setWorkspacePath,
|
|
20405
20458
|
renewRoom: channels.actions.renewRoom,
|
package/collab/dsh-member.mjs
CHANGED
|
@@ -510,10 +510,12 @@ export class DshMembers {
|
|
|
510
510
|
for (const id of targets) this.enqueue({ member_id: id, room_id: msg.room_id }, () => this.renew(id, msg));
|
|
511
511
|
};
|
|
512
512
|
}
|
|
513
|
-
|
|
513
|
+
// #445: a channel's saved cap wins; otherwise the instance default (this.maxRelay).
|
|
514
|
+
const cap = this.store.savedMaxRelay?.(msg.room_id) ?? this.maxRelay;
|
|
515
|
+
if (msg.author_kind === 'role' && this.store.relayDepth(msg.room_id, msg.id) > cap) {
|
|
514
516
|
return () => this.hub.post(msg.room_id, {
|
|
515
517
|
authorKind: 'system', author: 'system', name: 'system', clientKey: `relay-cap-${msg.id}`,
|
|
516
|
-
text: `Agent 之间已经连续接力 ${
|
|
518
|
+
text: `Agent 之间已经连续接力 ${cap} 次以上,暂停自动接力;需要继续请人来 @。`,
|
|
517
519
|
});
|
|
518
520
|
}
|
|
519
521
|
const jobs = targets.map(id => this.store.addJob(id, msg.room_id, msg.id)).filter(Boolean);
|
package/collab/handoff-rules.mjs
CHANGED
|
@@ -79,6 +79,37 @@ function run(c, lines, lead, key = 'msg') {
|
|
|
79
79
|
const rx = new RegExp(`^${escape(c.field)}\\s*[::]\\s*\\S`);
|
|
80
80
|
return lines.some(l => rx.test(stripLead(l, lead))) ? null : c[key];
|
|
81
81
|
}
|
|
82
|
+
case 'art_ref': {
|
|
83
|
+
const trig = new RegExp(c.trigger_re);
|
|
84
|
+
if (!c.trigger_sections.some(s => sections(lines, s, lead).some(seg => seg.some(x => trig.test(x))))) return null;
|
|
85
|
+
// The scope changes code files: work on image-related code, not image work.
|
|
86
|
+
const code = new RegExp(c.code_line_re);
|
|
87
|
+
if (sections(lines, c.code_section, lead).some(seg => seg.slice(1).some(x => code.test(stripLead(x, lead))))) return null;
|
|
88
|
+
const refs = new Set(sections(lines, c.section, lead, Boolean(c.inline))
|
|
89
|
+
.flatMap(seg => seg.flatMap(x => [...x.matchAll(new RegExp(c.image_re, 'g'))].map(m => m[0]))));
|
|
90
|
+
if (refs.size < c.min) return c.msg.replaceAll('{min}', String(c.min)).replaceAll('{n}', String(refs.size));
|
|
91
|
+
const all = lines.join('\n');
|
|
92
|
+
if (new RegExp(c.original_re).test(all) && !new RegExp(c.sample_re).test(all)) return c.original_msg;
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
case 'section_needs_table': {
|
|
96
|
+
const body = sections(lines, c.trigger_section, lead).flatMap(seg => seg.slice(1)).join('\n');
|
|
97
|
+
if (!(new RegExp(c.trigger_re).test(body) && new RegExp(c.object_re).test(body))) return null;
|
|
98
|
+
const row = new RegExp(c.row_re);
|
|
99
|
+
for (const seg of sections(lines, c.section, lead)) {
|
|
100
|
+
const rest = seg.slice(1), text = rest.join('\n');
|
|
101
|
+
if (rest.filter(x => row.test(stripLead(x, lead))).length >= c.rows && c.need_res.every(r => new RegExp(r).test(text))) return null;
|
|
102
|
+
}
|
|
103
|
+
return c[key];
|
|
104
|
+
}
|
|
105
|
+
case 'forbid': {
|
|
106
|
+
const rx = new RegExp(c.re);
|
|
107
|
+
return lines.some(x => rx.test(x)) ? c[key] : null;
|
|
108
|
+
}
|
|
109
|
+
case 'needs_if': {
|
|
110
|
+
const text = lines.join('\n');
|
|
111
|
+
return new RegExp(c.trigger_re).test(text) && !new RegExp(c.need_re).test(text) ? c[key] : null;
|
|
112
|
+
}
|
|
82
113
|
default: throw new HandoffRulesError(`交接规则文件里有不认识的检查类型:${c.type}`);
|
|
83
114
|
}
|
|
84
115
|
}
|
package/collab/plugin.mjs
CHANGED
|
@@ -102,7 +102,8 @@ export async function apply(ctx, rawConfig) {
|
|
|
102
102
|
const { WebSocketServer } = require('ws');
|
|
103
103
|
if (typeof createUserMessage !== 'function' || typeof WebSocketServer !== 'function') throw Error('conest-collab: unsupported DSH runtime');
|
|
104
104
|
|
|
105
|
-
|
|
105
|
+
// #445: the instance maxRelay stays the default for channels that never set their own cap.
|
|
106
|
+
const store = new Store(join(config.dataDir, 'collab.sqlite3'), { maxRelay: config.maxRelay });
|
|
106
107
|
for (const room of config.rooms) {
|
|
107
108
|
store.ensureRoom(room.id, store.room(room.id)?.name ?? room.name.trim(), room.workspacePath);
|
|
108
109
|
store.ensureRoomSettings(room.id);
|
|
@@ -400,7 +401,7 @@ export function roomSettingsRoute(req, res, path, { store, hub, dsh, workspacePa
|
|
|
400
401
|
const roomId = Number(match[1]);
|
|
401
402
|
if (store.room(roomId) === undefined) return Promise.resolve(json(res, 404, { error: '频道不存在' }));
|
|
402
403
|
const view = () => ({ lead_id: store.leadId(roomId), effective_lead_id: hub.primaryIn(roomId)?.id ?? null,
|
|
403
|
-
workspace_path: store.workspacePath(roomId, workspacePath) });
|
|
404
|
+
workspace_path: store.workspacePath(roomId, workspacePath), max_relay: store.relaySetting(roomId).max_relay });
|
|
404
405
|
if (match[2] === 'renew' && req.method === 'POST') {
|
|
405
406
|
return withBody(req, res, async () => ({ members: await dsh.renewRoom(roomId) }), false);
|
|
406
407
|
}
|
|
@@ -421,6 +422,8 @@ export function roomSettingsRoute(req, res, path, { store, hub, dsh, workspacePa
|
|
|
421
422
|
store.transaction(() => {
|
|
422
423
|
if (body?.lead_id !== undefined) store.setLead(roomId, body.lead_id);
|
|
423
424
|
if (body?.workspace_path !== undefined) store.setWorkspacePath(roomId, body.workspace_path, workspacePath);
|
|
425
|
+
// #445: 6–20 only; an invalid value rolls the whole PATCH back.
|
|
426
|
+
if (body?.max_relay !== undefined) store.setMaxRelay(roomId, body.max_relay);
|
|
424
427
|
});
|
|
425
428
|
hub.emit({ type: 'room-settings', room_id: store.settingsRoom(roomId), ...view() });
|
|
426
429
|
return view();
|
package/collab/store.mjs
CHANGED
|
@@ -10,6 +10,11 @@ export const MAX_TEXT = 128 * 1024;
|
|
|
10
10
|
// 「默认混合模型」); the same names as the bridge's dispatch_mode config.
|
|
11
11
|
export const DISPATCH_MODES = ['solo', 'team', 'mixed'];
|
|
12
12
|
export const DEFAULT_DISPATCH_MODE = 'mixed';
|
|
13
|
+
// #445: a chat channel may set its own relay cap; the Issue's acceptance fixes the range
|
|
14
|
+
// (5/21 rejected, 6/20 accepted) and the default (6).
|
|
15
|
+
export const MIN_CHANNEL_MAX_RELAY = 6;
|
|
16
|
+
export const MAX_CHANNEL_MAX_RELAY = 20;
|
|
17
|
+
export const DEFAULT_CHANNEL_MAX_RELAY = 6;
|
|
13
18
|
// v7 is #133 (reply_to), v8 is #139 team mode A, v9 is #139 team mode B tasks, v10 is #177
|
|
14
19
|
// (room_settings.dispatch_explicit); columns are added by ensureSchema() on every open
|
|
15
20
|
// (checked with PRAGMA table_info), so either merge or release order ends with every column;
|
|
@@ -66,7 +71,13 @@ export const WORKSPACE_ID_MIN = 1000000;
|
|
|
66
71
|
export const QUOTE_CHARS = 40;
|
|
67
72
|
|
|
68
73
|
export class Store {
|
|
69
|
-
|
|
74
|
+
/**
|
|
75
|
+
* @param path - SQLite file, or ':memory:'.
|
|
76
|
+
* @param options - { maxRelay? } instance default for channels that never set one (#445);
|
|
77
|
+
* kept configurable so instances that run a tighter loop keep working.
|
|
78
|
+
*/
|
|
79
|
+
constructor(path, { maxRelay = DEFAULT_CHANNEL_MAX_RELAY } = {}) {
|
|
80
|
+
this.relayDefault = maxRelay;
|
|
70
81
|
if (path !== ':memory:') mkdirSync(dirname(path), { recursive: true });
|
|
71
82
|
this.db = new DatabaseSync(path);
|
|
72
83
|
this.closed = false;
|
|
@@ -188,6 +199,8 @@ export class Store {
|
|
|
188
199
|
}
|
|
189
200
|
if (!settingColumns.some(c => c.name === 'lead_id')) this.db.exec('ALTER TABLE room_settings ADD COLUMN lead_id TEXT');
|
|
190
201
|
if (!settingColumns.some(c => c.name === 'workspace_path')) this.db.exec('ALTER TABLE room_settings ADD COLUMN workspace_path TEXT');
|
|
202
|
+
// #445: NULL means the channel never set a cap, so the instance default applies.
|
|
203
|
+
if (!settingColumns.some(c => c.name === 'max_relay')) this.db.exec('ALTER TABLE room_settings ADD COLUMN max_relay INTEGER');
|
|
191
204
|
this.db.exec('DROP TABLE IF EXISTS collab_settings');
|
|
192
205
|
if (version < SCHEMA_VERSION) this.db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
|
|
193
206
|
});
|
|
@@ -540,6 +553,28 @@ export class Store {
|
|
|
540
553
|
return mode;
|
|
541
554
|
}
|
|
542
555
|
|
|
556
|
+
/** A channel's saved relay cap (a workspace reads its chat channel); null when it never set one (#445). */
|
|
557
|
+
savedMaxRelay(roomId) {
|
|
558
|
+
const row = this.db.prepare('SELECT max_relay FROM room_settings WHERE room_id = ?').get(this.settingsRoom(roomId));
|
|
559
|
+
return row?.max_relay ?? null;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/** The setting the client shows: the saved cap, else the instance default (#445). */
|
|
563
|
+
relaySetting(roomId) {
|
|
564
|
+
return { max_relay: this.savedMaxRelay(roomId) ?? this.relayDefault };
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/** Set a chat channel's relay cap; only 6–20 (Issue #445), stored on the channel so its workspaces share it. */
|
|
568
|
+
setMaxRelay(roomId, value) {
|
|
569
|
+
if (!Number.isInteger(value) || value < MIN_CHANNEL_MAX_RELAY || value > MAX_CHANNEL_MAX_RELAY) {
|
|
570
|
+
throw new CollabError('invalid', `接力上限必须是 ${MIN_CHANNEL_MAX_RELAY}–${MAX_CHANNEL_MAX_RELAY} 的整数`);
|
|
571
|
+
}
|
|
572
|
+
roomId = this.settingsRoom(roomId); // set on a workspace = set on its chat channel
|
|
573
|
+
this.ensureRoomSettings(roomId);
|
|
574
|
+
this.db.prepare('UPDATE room_settings SET max_relay = ? WHERE room_id = ?').run(value, roomId);
|
|
575
|
+
return value;
|
|
576
|
+
}
|
|
577
|
+
|
|
543
578
|
/** Keep legacy members visible in all channels, including future ones. */
|
|
544
579
|
migrateMemberRooms() {
|
|
545
580
|
return this.db.prepare("UPDATE members SET rooms = '[]' WHERE rooms IS NULL").run().changes > 0;
|
|
@@ -605,7 +640,7 @@ export class Store {
|
|
|
605
640
|
this.releaseIdleWorkspaces();
|
|
606
641
|
const counts = new Map(this.db.prepare(`SELECT room_id, COUNT(*) AS n FROM tasks
|
|
607
642
|
WHERE status IN ('assigned', 'accepted', 'asked', 'rework') GROUP BY room_id`).all().map(t => [t.room_id, t.n]));
|
|
608
|
-
return this.rooms().map(r => ({ ...r, active: counts.get(r.id) ?? 0, ...this.dispatchSetting(r.id) }));
|
|
643
|
+
return this.rooms().map(r => ({ ...r, active: counts.get(r.id) ?? 0, ...this.dispatchSetting(r.id), ...this.relaySetting(r.id) }));
|
|
609
644
|
}
|
|
610
645
|
|
|
611
646
|
/**
|
package/collab/team-exit.mjs
CHANGED
|
@@ -17,6 +17,7 @@ const SELF_CHECK = new RegExp(RULES.ask.self_check_re);
|
|
|
17
17
|
const CATEGORY = new RegExp(RULES.ask.category_re);
|
|
18
18
|
const EXEMPT = new RegExp(RULES.ask.exempt_re);
|
|
19
19
|
const NO_CHECK = new RegExp(RULES.ask.no_check_re);
|
|
20
|
+
const JARGON_URL = new RegExp(RULES.jargon.url_re, 'g');
|
|
20
21
|
const JARGON_LINE = new RegExp(RULES.jargon.line_re, 'g');
|
|
21
22
|
const JARGON_TABLE = new RegExp(RULES.jargon.table_re);
|
|
22
23
|
const BACKGROUND = new RegExp(RULES.summary.background_re);
|
|
@@ -102,7 +103,7 @@ export function summaryProblems(summary) {
|
|
|
102
103
|
/** The part for the user names numbers, workspaces, branches, paths or identifiers, or has a table (Go: jargonProblems). */
|
|
103
104
|
export function jargonProblems(line) {
|
|
104
105
|
const problems = [];
|
|
105
|
-
const hits = [...String(line).matchAll(JARGON_LINE)].map(m => m[0].replace(/^[^~/`#!_\-0-9A-Za-z#!工]+/u, ''));
|
|
106
|
+
const hits = [...String(line).replace(JARGON_URL, ' ').matchAll(JARGON_LINE)].map(m => m[0].replace(/^[^~/`#!_\-0-9A-Za-z#!工]+/u, ''));
|
|
106
107
|
if (hits.length) problems.push(RULES.jargon.line.replaceAll('{hits}', hits.join('、')));
|
|
107
108
|
if (JARGON_TABLE.test(line)) problems.push(RULES.jargon.table);
|
|
108
109
|
return problems;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bothouse-dsh",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.44",
|
|
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",
|