bothouse-dsh 0.1.60 → 0.1.62
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/README.en.md +5 -3
- package/README.md +5 -3
- package/collab/cloudweb.mjs +1 -1
- package/collab/desk/index.html +106 -29
- package/collab/desk.mjs +18 -1
- package/collab/dsh-member.mjs +7 -0
- package/collab/page.html +8 -1
- package/collab/roster.mjs +4 -1
- package/collab/store.mjs +5 -0
- package/collab/team-mode-rules.md +2 -2
- package/node_modules/bothouse-dsh-channels/client.js +134 -97
- package/package.json +1 -1
package/collab/desk.mjs
CHANGED
|
@@ -125,6 +125,19 @@ export function deskReplay({ store, boardDir, from, to, members = [] }) {
|
|
|
125
125
|
return { from: new Date(fromMs).toISOString(), to: new Date(toMs).toISOString(), events: kept.map(m => m.event) };
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
/**
|
|
129
|
+
* 现在还在进行中的任务:按任务 id 取最后一条板事件,最后一条是 created/assigned 就算在做。
|
|
130
|
+
* 页面刷新或第一次打开时用它把工位上的任务和「进行中」计数补回来,不必等下一个事件。
|
|
131
|
+
*/
|
|
132
|
+
export function deskActiveTasks({ boardDir }) {
|
|
133
|
+
const last = new Map();
|
|
134
|
+
for (const entry of readBoardEvents(boardDir)) {
|
|
135
|
+
const id = String(entry.id ?? '').trim();
|
|
136
|
+
if (id && entry.kind) last.set(id, entry);
|
|
137
|
+
}
|
|
138
|
+
return [...last.values()].filter(entry => entry.kind === 'created' || entry.kind === 'assigned');
|
|
139
|
+
}
|
|
140
|
+
|
|
128
141
|
/** 桌面页:C 路提供的 dsh/collab/desk/index.html;还没落地时给一个能看事件流的占位页。 */
|
|
129
142
|
export function deskPage() {
|
|
130
143
|
try { return readFileSync(new URL('./desk/index.html', import.meta.url), 'utf8'); } catch { return PLACEHOLDER_PAGE; }
|
|
@@ -177,9 +190,13 @@ async function namesRoute(req, res, namesFile) {
|
|
|
177
190
|
}
|
|
178
191
|
}
|
|
179
192
|
|
|
180
|
-
/** /collab/desk、/collab/api/desk/replay、/collab/api/desk/names;返回 true(或 Promise)表示已处理,未匹配返回 undefined。 */
|
|
193
|
+
/** /collab/desk、/collab/api/desk/replay、/collab/api/desk/state、/collab/api/desk/names;返回 true(或 Promise)表示已处理,未匹配返回 undefined。 */
|
|
181
194
|
export function deskRoute(req, res, url, { store, boardDir, members = [], namesFile } = {}) {
|
|
182
195
|
if (url.pathname.replace(/\/+$/, '') === '/collab/api/desk/names' && (req.method === 'GET' || req.method === 'PUT')) return namesRoute(req, res, namesFile);
|
|
196
|
+
if (url.pathname.replace(/\/+$/, '') === '/collab/api/desk/state' && req.method === 'GET') {
|
|
197
|
+
sendJson(res, 200, { tasks: deskActiveTasks({ boardDir }) });
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
183
200
|
if (url.pathname.replace(/\/+$/, '') !== '/collab/desk' && url.pathname.replace(/\/+$/, '') !== '/collab/api/desk/replay') return undefined;
|
|
184
201
|
if (req.method !== 'GET') return undefined;
|
|
185
202
|
if (url.pathname.replace(/\/+$/, '') === '/collab/desk') {
|
package/collab/dsh-member.mjs
CHANGED
|
@@ -308,6 +308,7 @@ export class TurnProgress {
|
|
|
308
308
|
this.id = undefined;
|
|
309
309
|
this.queued = false;
|
|
310
310
|
this.ended = false;
|
|
311
|
+
this.interrupted = false;
|
|
311
312
|
}
|
|
312
313
|
|
|
313
314
|
call(callId, name, args) {
|
|
@@ -346,10 +347,15 @@ export class TurnProgress {
|
|
|
346
347
|
end(state) {
|
|
347
348
|
if (this.ended) return;
|
|
348
349
|
this.ended = true;
|
|
350
|
+
// The owner withdrew the trigger, so this turn was stopped by hand, not broken: its card says 已停止.
|
|
351
|
+
if (this.interrupted) state = 'stopped';
|
|
349
352
|
// Only a turn that had events has a card to close.
|
|
350
353
|
if (this.steps > 0 || this.items.length > 0) this.write(state);
|
|
351
354
|
}
|
|
352
355
|
|
|
356
|
+
/** The owner withdrew the trigger message (#148): the card must not read as a failure. */
|
|
357
|
+
interrupt() { this.interrupted = true; }
|
|
358
|
+
|
|
353
359
|
write(state) {
|
|
354
360
|
const body = progressCardOf({ agent: this.member.name, state, steps: this.steps, elapsed_sec: Math.floor((this.now() - this.started) / 1000), items: this.items });
|
|
355
361
|
if (this.id === undefined) {
|
|
@@ -517,6 +523,7 @@ export class DshMembers {
|
|
|
517
523
|
this.approvals.expireWhere(p => p.meta?.sessionId === job.session_id);
|
|
518
524
|
if (w.turn === undefined && agent.inbox.remove(job.input_id)) continue;
|
|
519
525
|
this.watching.delete(job.session_id);
|
|
526
|
+
w.progress?.interrupt?.();
|
|
520
527
|
w.resolve(INTERRUPTED);
|
|
521
528
|
agent.cancel({ kind: 'user' }, { keepInbox: true });
|
|
522
529
|
stopped.push(job.member_id);
|
package/collab/page.html
CHANGED
|
@@ -247,6 +247,8 @@
|
|
|
247
247
|
被 40px 的头像列宽挤成一字一行(#112)。 */
|
|
248
248
|
.msg.cont .avatar { visibility: hidden; }
|
|
249
249
|
.msg.cont .head { display: none; }
|
|
250
|
+
/* 正文列固定落在第二列,即使头像被隐藏也不会顶到第一列。 */
|
|
251
|
+
.msg > :not(.avatar) { grid-column: 2; min-width: 0; }
|
|
250
252
|
.msg .stamp {
|
|
251
253
|
display: none;
|
|
252
254
|
position: absolute;
|
|
@@ -330,12 +332,14 @@
|
|
|
330
332
|
.card[data-state="completed"] { --card-accent: var(--ok); --card-tint: rgba(35, 165, 90, .16); --card-text: #7ee2a8; }
|
|
331
333
|
.card[data-state="failed"] { --card-accent: var(--danger); --card-tint: rgba(242, 63, 67, .16); --card-text: #ff9a9c; }
|
|
332
334
|
.card[data-state="cancelled"] { --card-accent: var(--muted); --card-tint: rgba(127, 127, 127, .14); --card-text: var(--muted); }
|
|
335
|
+
.card[data-state="stopped"] { --card-accent: var(--muted); --card-tint: rgba(127, 127, 127, .14); --card-text: var(--muted); }
|
|
333
336
|
@media (prefers-color-scheme: light) {
|
|
334
337
|
.card { --card-text: #3b46b8; --card-tint: rgba(74, 85, 214, .12); }
|
|
335
338
|
.card[data-state="running"] { --card-text: #6d4a00; --card-tint: rgba(240, 178, 50, .18); }
|
|
336
339
|
.card[data-state="completed"] { --card-text: #146c3a; --card-tint: rgba(26, 127, 67, .12); }
|
|
337
340
|
.card[data-state="failed"] { --card-text: #a52327; --card-tint: rgba(194, 43, 47, .12); }
|
|
338
341
|
.card[data-state="cancelled"] { --card-text: #545861; --card-tint: rgba(84, 88, 97, .12); }
|
|
342
|
+
.card[data-state="stopped"] { --card-text: #545861; --card-tint: rgba(84, 88, 97, .12); }
|
|
339
343
|
}
|
|
340
344
|
.card-head { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
|
|
341
345
|
.card .title { font-weight: 600; font-size: var(--fs-sm); color: var(--text-strong); }
|
|
@@ -650,7 +654,7 @@ const el = (tag, cls, text) => { const n = document.createElement(tag); if (cls)
|
|
|
650
654
|
const palette = ['#4F46E5', '#B45309', '#0F766E']; // taste-skill-ignore 靛蓝为 design-spec 唯一强调色(用户标杆),按 SKILL.md 已有品牌条款保留
|
|
651
655
|
const color = id => palette[[...id].reduce((h, c) => (h * 31 + c.charCodeAt(0)) >>> 0, 7) % palette.length];
|
|
652
656
|
const kindLabel = { dsh: 'DSH', bot: 'Agent', human: '' };
|
|
653
|
-
const cardStateLabel = { pending: '待确认', running: '进行中', completed: '已完成', failed: '失败', cancelled: '已取消' };
|
|
657
|
+
const cardStateLabel = { pending: '待确认', running: '进行中', completed: '已完成', failed: '失败', cancelled: '已取消', stopped: '已停止', retried: '已重试' };
|
|
654
658
|
// 5 分钟:同一人连续发言合并头像与昵称的窗口,沿用 Discord 的习惯(交接要求「按同一人连续发言分组」)
|
|
655
659
|
const GROUP_WINDOW_MS = 5 * 60 * 1000;
|
|
656
660
|
// 768px 是 U2 交接里定的窄屏阈值
|
|
@@ -1151,6 +1155,9 @@ function upsert(m) {
|
|
|
1151
1155
|
const after = [...log.children].find(n => n.dataset.id != null && Number(n.dataset.id) > m.id);
|
|
1152
1156
|
log.insertBefore(messageNode(m), after || null);
|
|
1153
1157
|
}
|
|
1158
|
+
// 「还没有消息」只是打开空频道时的占位:第一条消息(本地发的、SSE 推来的、重连补来的)一到就得收起,
|
|
1159
|
+
// 否则它会一直压在真实消息上面,直到手动切换频道或刷新。
|
|
1160
|
+
log.querySelector('.state.empty')?.remove();
|
|
1154
1161
|
regroup();
|
|
1155
1162
|
followBottom(log, stick);
|
|
1156
1163
|
}
|
package/collab/roster.mjs
CHANGED
|
@@ -248,7 +248,10 @@ export class Roster {
|
|
|
248
248
|
throw new CollabError('invalid', `name must be 1-${MAX_NAME} characters without spaces (except Claude Code), @ or <>`);
|
|
249
249
|
}
|
|
250
250
|
const rooms = input?.rooms ?? [];
|
|
251
|
-
|
|
251
|
+
// A private chat holds exactly its one member (hub.membersInRoom), so listing it here would
|
|
252
|
+
// store a membership the room never honours: the extra member shows nowhere, cannot be
|
|
253
|
+
// dispatched to, and the add-member dialog is the only thing that creates it.
|
|
254
|
+
if (!Array.isArray(rooms) || rooms.some(id => !Number.isSafeInteger(id) || this.store.room(id)?.kind !== 'chat' || this.store.dmMember(id) !== undefined)) {
|
|
252
255
|
throw new CollabError('invalid', 'rooms must contain existing chat channel ids');
|
|
253
256
|
}
|
|
254
257
|
const clash = this.hub.memberList().find(m => m.id !== selfId && (!rooms.length || !m.rooms?.length || rooms.some(id => m.rooms.includes(id))) && (m.name.toLowerCase() === name.toLowerCase() || m.id.toLowerCase() === name.toLowerCase()));
|
package/collab/store.mjs
CHANGED
|
@@ -880,6 +880,11 @@ export function publicMessage(row, store) {
|
|
|
880
880
|
id: row.id, room_id: row.room_id, author_kind: row.author_kind, author: row.author, name: row.name,
|
|
881
881
|
text: row.text, created_at: row.created_at, card: row.card, from_turn: row.from_turn,
|
|
882
882
|
...(row.edited_at != null ? { edited_at: row.edited_at } : {}),
|
|
883
|
+
// #1355: one message keeps its id for life; revision moves on every in-place edit or
|
|
884
|
+
// commit (changed_at), so a consumer can tell an update from a new message. `draft`
|
|
885
|
+
// marks a streamed reply that has not settled yet.
|
|
886
|
+
revision: row.changed_at ?? row.created_at,
|
|
887
|
+
...(row.draft ? { draft: true } : {}),
|
|
883
888
|
};
|
|
884
889
|
const quote = store?.quote(row);
|
|
885
890
|
if (quote !== undefined) out.reply_to = quote;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
[团队模式规矩 — 每个角色都适用]
|
|
2
|
-
-
|
|
2
|
+
- 频道分两种:聊天频道(用户聊天、看结果)和它下面的工作区(干活)。
|
|
3
3
|
- 多步骤的活先开公开任务卡(先发 --kind start);闲聊、问答不开。
|
|
4
4
|
- 聊天频道要保持畅通,大活交工作区:预计超过 10 分钟,或要改代码、跑测试、等命令的,谁接活谁这一轮写交接(副手被直接点名也一样)(至少写「用户原话」「总目标」「范围」三节和参照实现,范围每项写一行「改:…」;被退回按列出的一次补齐),跑 `ws-handoff <交接文件> "<任务>"`,不在聊天频道自己做;它不算开子 agent,不用另问用户;交出就结束本轮,不在本轮等或查进度,结果由工作区 [report] 转回。工作区里接到的活自己做。DSH 频道调 POST /collab/api/rooms/{聊天频道 id}/workspaces。一个工作区同一时间只做一件活,满 4 个就排队。
|
|
5
5
|
- 聊天频道里对真人说人话:讲场景、短段落,默认不用表格、不贴代码,细节进子话题。
|
|
6
6
|
- 同团队同平台成员在新频道默认可派;团队外成员才请用户发 `/bind <队友项目名>`,机器人自己发的 /bind 不生效。
|
|
7
7
|
- 在工作区里要问用户或交结果:最后一条回复第一行写 [ask] 或 [report] 加 #任务板编号和「背景:」,再一行做了什么,再一行「要你做的:」,再一行「我加的:」(用户原话里没有、你加的设定,没有写「无」);这四段贴到聊天频道,不写编号、代号、路径、表格;之后是细节,进子话题。截图用 ws-up --image 推;文字进度不推。
|
|
8
8
|
- 发 [ask] 后工作区让出、会话清空,用户回复后系统找空工作区带交接文件和答复接着做;交回最终 [report] 后系统自动标完成、清空会话、放回工作区。
|
|
9
|
-
-
|
|
9
|
+
- 聊天频道共用工作区1..n,各同时只归一个,挑空的,都占着就新建;用户要新频道用 `bothouse channel create <名字>`,不自己调 Discord API。
|
|
10
10
|
- 收口(每小时合结果、发汇报)时,没收完、这一小时也没在动的活由收口的人当场暂停(`board pause <编号>`):它让出工作区,那里没别的活就放回池子、清空会话;恢复时重新交活,先用空着的工作区。
|
|
11
11
|
- 会缩小或改变用户原话范围的决定(砍功能、换方向等),以及用户原话里没有、你拿不准的设定,不要自己定:在工作区用 [ask] 转到聊天频道问用户,等答复再做;等的时候先做不受影响的部分。主角色拿不准也一样,在聊天频道直接问用户。
|
|
12
12
|
- 转话必须指定哪个工作区,不默认发给最后一个。
|