@suifei/dsh-ai-team 0.3.30 → 0.3.32
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/CHANGELOG.md +13 -0
- package/README.md +1 -1
- package/client.js +38 -14
- package/index.js +1 -1
- package/lib/domain/inbox.js +6 -0
- package/lib/host/service.js +9 -74
- package/lib/host/views.js +3 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.32 — 2026-11-10
|
|
4
|
+
|
|
5
|
+
- 四象限「需要你」与收件箱使用同一过滤:纯进度汇报不再计入 Q1;
|
|
6
|
+
- 看板「需要你」筛选无 Task 但有真实收件箱决策时,显示「需要你处理 N 条」并一键打开收件箱,不再显示空任务;
|
|
7
|
+
- Client 侧同步过滤旧 bootstrap 的 Q1,无需重启宿主也能消除截图里的 3 条假待办。
|
|
8
|
+
|
|
9
|
+
## 0.3.31 — 2026-11-10
|
|
10
|
+
|
|
11
|
+
- 信息性 Thread 汇报(“推进记录/备案/Human 决策收到”)不再生成 Human 决策收件箱项,避免“打开 Task 后无事可做”;
|
|
12
|
+
- 收件箱只保留真正的问题:`askHuman`、报告六问、失败/需要决定;
|
|
13
|
+
- 旧账本里的同类 `thread_mention` 项在 bootstrap/client 中隐藏,不再计入“待你处理”;
|
|
14
|
+
- 真正决策提交后仍自动回写 Thread 并唤醒 owner,不需要 Human 再发一句话。
|
|
15
|
+
|
|
3
16
|
## 0.3.30 — 2026-11-10
|
|
4
17
|
|
|
5
18
|
- 已提交的决策项增加「重新决策」:复用原六问/选项/建议重开一份未读项,可直接采纳建议后修改;
|
package/README.md
CHANGED
|
@@ -87,7 +87,7 @@ dsh --profile ai-team --no-open --port 33271
|
|
|
87
87
|
- **主对话**:Human 的一句话进入 Main stream,Project owner 拆 Task;顶部「团队动态」直接显示正在推进的 Task 和运行中的成员,点击 Task 可打开 Thread。
|
|
88
88
|
- **看板**:四列;进行中的卡片显示 accountable owner 与执行者;数量只统计 active Task。
|
|
89
89
|
- **态势**:四象限——需要你、团队在做、等待受阻、最近完成。点击条目打开对应 Task 的 Thread。
|
|
90
|
-
- **收件箱**:跨 Project 聚合;顶部「待你处理 · N
|
|
90
|
+
- **收件箱**:跨 Project 聚合;顶部「待你处理 · N」一键进入;只放真正需要 Human 的决策/失败,Agent 的进度汇报留在 Thread/动态,不制造“打开 Task 后无事可做”。
|
|
91
91
|
- **直接对话**:点击成员名字直接打开该 Agent 的 DSH 原生 Session;没有 session 会先激活;打不开时落到成员资料,资料里有「直接对话」按钮。
|
|
92
92
|
- **成员任务**:成员资料里点「他的任务」,查看他 owner/assignee 的 Task。
|
|
93
93
|
- **成员资料**:点「⋯」改名、职责、personality、provider/model、运行配方、notes、skills、re-distill、离队/人才库。
|
package/client.js
CHANGED
|
@@ -331,6 +331,9 @@ window.__ModuleLoader__.load({
|
|
|
331
331
|
filterLabel: '筛选:{name}',
|
|
332
332
|
filterClear: '清除筛选',
|
|
333
333
|
filterInboxExtra: '另有 {n} 条收件箱决策项',
|
|
334
|
+
filterInboxTitle: '需要你处理 {n} 条',
|
|
335
|
+
filterInboxBody: '这些是收件箱决策,不是 Task;去收件箱逐条处理。',
|
|
336
|
+
filterInboxAction: '打开收件箱',
|
|
334
337
|
filteredEmptyTitle: '这个筛选下没有任务',
|
|
335
338
|
filteredEmptyBody: '清除筛选后可看到全部任务。',
|
|
336
339
|
itemCount: '{n} 项',
|
|
@@ -653,6 +656,9 @@ window.__ModuleLoader__.load({
|
|
|
653
656
|
filterLabel: 'Filter: {name}',
|
|
654
657
|
filterClear: 'Clear filter',
|
|
655
658
|
filterInboxExtra: '{n} inbox decision item(s) not on the board',
|
|
659
|
+
filterInboxTitle: '{n} item(s) need you',
|
|
660
|
+
filterInboxBody: 'These are inbox decisions, not Tasks; open the inbox to handle them.',
|
|
661
|
+
filterInboxAction: 'Open inbox',
|
|
656
662
|
filteredEmptyTitle: 'No tasks under this filter',
|
|
657
663
|
filteredEmptyBody: 'Clear the filter to see all tasks.',
|
|
658
664
|
itemCount: '{n} items',
|
|
@@ -847,10 +853,20 @@ window.__ModuleLoader__.load({
|
|
|
847
853
|
};
|
|
848
854
|
};
|
|
849
855
|
|
|
856
|
+
function isInformationalInboxItem(item) {
|
|
857
|
+
return item?.ref?.category === 'thread_mention'
|
|
858
|
+
&& !(Array.isArray(item.ref?.questions) && item.ref.questions.length > 0);
|
|
859
|
+
}
|
|
860
|
+
|
|
850
861
|
/** Derive the read-only quadrants projection from the bootstrap DTO (spike fallback). */
|
|
851
862
|
function deriveQuadrants(data) {
|
|
852
863
|
const direct = data?.quadrants;
|
|
853
|
-
if (direct && direct.q1 && direct.q2 && direct.q3 && direct.q4)
|
|
864
|
+
if (direct && direct.q1 && direct.q2 && direct.q3 && direct.q4) {
|
|
865
|
+
const inboxById = new Map((data?.inbox ?? []).map((item) => [item.id, item]));
|
|
866
|
+
const q1Items = (direct.q1.items ?? []).filter((item) =>
|
|
867
|
+
item.kind !== 'inbox' || !isInformationalInboxItem(inboxById.get(item.id)));
|
|
868
|
+
return { ...direct, q1: { ...direct.q1, count: q1Items.length, items: q1Items } };
|
|
869
|
+
}
|
|
854
870
|
const tasks = data?.tasks ?? [];
|
|
855
871
|
const inbox = data?.inbox ?? [];
|
|
856
872
|
const q1 = [];
|
|
@@ -1520,7 +1536,7 @@ window.__ModuleLoader__.load({
|
|
|
1520
1536
|
})));
|
|
1521
1537
|
}
|
|
1522
1538
|
|
|
1523
|
-
function Board({ t, tasks, selected, follows, doneOpen, onToggleDone, onOpen, onToggleFollow, filter, onClearFilter, q1Extra, onBackMain }) {
|
|
1539
|
+
function Board({ t, tasks, selected, follows, doneOpen, onToggleDone, onOpen, onToggleFollow, filter, onClearFilter, q1Extra, onBackMain, onOpenInbox }) {
|
|
1524
1540
|
const filtered = (filter === 'all' ? tasks : tasks.filter((task) => matchesFilter(task, filter)))
|
|
1525
1541
|
.slice()
|
|
1526
1542
|
.sort((left, right) => (right.number ?? 0) - (left.number ?? 0));
|
|
@@ -1540,13 +1556,21 @@ window.__ModuleLoader__.load({
|
|
|
1540
1556
|
body: t('emptyBoardBody'),
|
|
1541
1557
|
action: { label: t('emptyBoardAction'), onClick: onBackMain },
|
|
1542
1558
|
})
|
|
1543
|
-
:
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1559
|
+
: filter === 'q1' && q1Extra > 0
|
|
1560
|
+
? h(EmptyState, {
|
|
1561
|
+
t,
|
|
1562
|
+
icon: 'inbox',
|
|
1563
|
+
title: tf(t, 'filterInboxTitle', { n: q1Extra }),
|
|
1564
|
+
body: t('filterInboxBody'),
|
|
1565
|
+
action: { label: t('filterInboxAction'), onClick: onOpenInbox },
|
|
1566
|
+
})
|
|
1567
|
+
: h(EmptyState, {
|
|
1568
|
+
t,
|
|
1569
|
+
icon: 'board',
|
|
1570
|
+
title: t('filteredEmptyTitle'),
|
|
1571
|
+
body: t('filteredEmptyBody'),
|
|
1572
|
+
action: { label: t('filterClear'), onClick: onClearFilter },
|
|
1573
|
+
}));
|
|
1550
1574
|
}
|
|
1551
1575
|
return h('div', { className: 'at-boardwrap', role: 'tabpanel', id: 'at-panel-board', 'aria-labelledby': 'at-tab-board' },
|
|
1552
1576
|
bar,
|
|
@@ -2374,9 +2398,8 @@ window.__ModuleLoader__.load({
|
|
|
2374
2398
|
const selectedTask = tasks.find((task) => task.id === selected) ?? tasks[0] ?? null;
|
|
2375
2399
|
const thread = threadFor(data, selectedTask?.id);
|
|
2376
2400
|
const quadrants = deriveQuadrants(data);
|
|
2377
|
-
const
|
|
2378
|
-
|
|
2379
|
-
: (data.inbox ?? []).filter((item) => !item.read).length;
|
|
2401
|
+
const inboxItems = (data.inbox ?? []).filter((item) => !isInformationalInboxItem(item));
|
|
2402
|
+
const inboxUnread = inboxItems.filter((item) => item.state === 'unread').length;
|
|
2380
2403
|
const q1Extra = (quadrants.q1.items ?? []).filter((item) => item.kind === 'inbox').length;
|
|
2381
2404
|
const threadHead = thread?.headSeq ?? thread?.entries?.length ?? 0;
|
|
2382
2405
|
const followingOf = (card) => (Object.prototype.hasOwnProperty.call(follows, card.id) ? follows[card.id] : Boolean(card.following));
|
|
@@ -3108,8 +3131,8 @@ window.__ModuleLoader__.load({
|
|
|
3108
3131
|
const body = inInbox
|
|
3109
3132
|
? h('div', { className: 'at-stream', role: 'region', 'aria-label': t('inboxTitle') },
|
|
3110
3133
|
h('div', { className: 'at-streaminner' },
|
|
3111
|
-
|
|
3112
|
-
?
|
|
3134
|
+
inboxItems.length
|
|
3135
|
+
? inboxItems.map((item) => h('div', { key: item.id, className: cx('at-inboxitem', item.state !== 'unread' && 'at-inboxdone') },
|
|
3113
3136
|
h('div', { className: 'at-inboxbody' },
|
|
3114
3137
|
h('div', { className: 'at-inboxtitle' },
|
|
3115
3138
|
item.type === 'decision_required'
|
|
@@ -3172,6 +3195,7 @@ window.__ModuleLoader__.load({
|
|
|
3172
3195
|
filter: boardFilter,
|
|
3173
3196
|
onClearFilter: () => setBoardFilter('all'),
|
|
3174
3197
|
q1Extra,
|
|
3198
|
+
onOpenInbox: switchInbox,
|
|
3175
3199
|
onBackMain: () => setTab('main'),
|
|
3176
3200
|
})
|
|
3177
3201
|
: tab === 'situation'
|
package/index.js
CHANGED
|
@@ -34,7 +34,7 @@ export const name = 'ai-team';
|
|
|
34
34
|
/** The workbench only makes sense where the Web carrier exists. */
|
|
35
35
|
export const inject = ['webServer', 'permissionPresets', 'sandboxPolicy'];
|
|
36
36
|
|
|
37
|
-
const VERSION = '0.3.
|
|
37
|
+
const VERSION = '0.3.32';
|
|
38
38
|
const PROJECT_ID = 'prj_main';
|
|
39
39
|
const HUMAN_ID = 'mem_human';
|
|
40
40
|
|
package/lib/domain/inbox.js
CHANGED
|
@@ -19,6 +19,12 @@ export const INBOX_TYPES = new Set([
|
|
|
19
19
|
|
|
20
20
|
export const DECISION_TYPES = new Set(['decision_required']);
|
|
21
21
|
|
|
22
|
+
/** Progress reports mentioning Human are not actionable decisions. */
|
|
23
|
+
export function isInformationalThreadMention(item) {
|
|
24
|
+
return item?.ref?.category === 'thread_mention'
|
|
25
|
+
&& !(Array.isArray(item.ref?.questions) && item.ref.questions.length > 0);
|
|
26
|
+
}
|
|
27
|
+
|
|
22
28
|
/**
|
|
23
29
|
* Create one inbox item. It points at a source object; it is not a copy of the
|
|
24
30
|
* message body (the reader pulls the source on demand).
|
package/lib/host/service.js
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
2
2
|
import { ERRORS, fail } from '../domain/errors.js';
|
|
3
3
|
import { findReportPath, parseDecisionQuestions } from './decision-report.js';
|
|
4
4
|
import { ids } from '../domain/ids.js';
|
|
5
|
-
import { ackInboxItem, createInboxItem } from '../domain/inbox.js';
|
|
5
|
+
import { ackInboxItem, createInboxItem, isInformationalThreadMention } from '../domain/inbox.js';
|
|
6
6
|
import {
|
|
7
7
|
acceptTask,
|
|
8
8
|
assignTask,
|
|
@@ -124,6 +124,9 @@ export class TeamService {
|
|
|
124
124
|
const stream = project === undefined ? [] : (state.stream ?? []).filter((entry) => entry.projectId === project.id);
|
|
125
125
|
const selected = threadTaskId ?? tasks.find((task) => task.status === 'in_progress')?.id ?? tasks[0]?.id;
|
|
126
126
|
const thread = selected === undefined ? null : (state.threads?.[selected] ?? { taskId: selected, headSeq: 0, entries: [] });
|
|
127
|
+
const inbox = Object.values(state.inbox ?? {}).filter((item) =>
|
|
128
|
+
(item.memberId === state.humanMemberId || item.memberId === 'h1')
|
|
129
|
+
&& !isInformationalThreadMention(item));
|
|
127
130
|
return {
|
|
128
131
|
project: project ?? null,
|
|
129
132
|
projects,
|
|
@@ -132,7 +135,7 @@ export class TeamService {
|
|
|
132
135
|
tasks,
|
|
133
136
|
stream,
|
|
134
137
|
thread,
|
|
135
|
-
inbox
|
|
138
|
+
inbox,
|
|
136
139
|
board: project === undefined ? null : this.board(project.id),
|
|
137
140
|
quadrants: project === undefined ? null : this.quadrants(project.id),
|
|
138
141
|
};
|
|
@@ -149,11 +152,6 @@ export class TeamService {
|
|
|
149
152
|
?? null;
|
|
150
153
|
if (humanId === null) return 0;
|
|
151
154
|
const projectId = Object.keys(this.state.projects ?? {})[0];
|
|
152
|
-
const existing = new Set(
|
|
153
|
-
Object.values(this.state.inbox ?? {})
|
|
154
|
-
.filter((item) => item.memberId === humanId && item.ref?.entrySeq !== undefined)
|
|
155
|
-
.map((item) => `${item.ref?.taskId}:${item.ref?.entrySeq}`),
|
|
156
|
-
);
|
|
157
155
|
let reportPath = null;
|
|
158
156
|
for (const task of Object.values(this.state.tasks ?? {})) {
|
|
159
157
|
for (const entry of this.state.threads?.[task.id]?.entries ?? []) {
|
|
@@ -168,18 +166,17 @@ export class TeamService {
|
|
|
168
166
|
reportQuestions = parseDecisionQuestions(readFileSync(reportPath, 'utf8'));
|
|
169
167
|
} catch { /* report may be unavailable; keep the Thread fallback */ }
|
|
170
168
|
}
|
|
169
|
+
if (reportQuestions.length === 0) return 0;
|
|
171
170
|
const candidates = [];
|
|
172
171
|
for (const task of Object.values(this.state.tasks ?? {})) {
|
|
173
172
|
if (task.tombstone === true) continue;
|
|
174
173
|
for (const entry of this.state.threads?.[task.id]?.entries ?? []) {
|
|
175
174
|
const author = this.state.members?.[entry.authorId];
|
|
176
175
|
if (author?.kind !== 'agent' || !mentionsHuman(entry.body)) continue;
|
|
177
|
-
|
|
178
|
-
if (existing.has(key) && reportQuestions.length === 0) continue;
|
|
179
|
-
candidates.push({ task, entry, key });
|
|
176
|
+
candidates.push({ task, entry, key: `${task.id}:${entry.seq}` });
|
|
180
177
|
}
|
|
181
178
|
}
|
|
182
|
-
if (
|
|
179
|
+
if (candidates.length > 0) {
|
|
183
180
|
const alreadyStructured = Object.values(this.state.inbox ?? {}).some((item) =>
|
|
184
181
|
item.memberId === humanId
|
|
185
182
|
&& item.ref?.source === reportPath
|
|
@@ -212,33 +209,7 @@ export class TeamService {
|
|
|
212
209
|
});
|
|
213
210
|
return 1;
|
|
214
211
|
}
|
|
215
|
-
|
|
216
|
-
for (const candidate of candidates) {
|
|
217
|
-
const { task, entry } = candidate;
|
|
218
|
-
existing.add(candidate.key);
|
|
219
|
-
const item = createInboxItem({
|
|
220
|
-
memberId: humanId,
|
|
221
|
-
type: 'decision_required',
|
|
222
|
-
ref: {
|
|
223
|
-
taskId: task.id,
|
|
224
|
-
entrySeq: entry.seq,
|
|
225
|
-
memberId: entry.authorId,
|
|
226
|
-
memberName: this.state.members?.[entry.authorId]?.name ?? entry.authorId,
|
|
227
|
-
category: 'thread_mention',
|
|
228
|
-
title: String(entry.body).slice(0, 180),
|
|
229
|
-
},
|
|
230
|
-
requiresDecision: true,
|
|
231
|
-
}, { at: this.now() });
|
|
232
|
-
await this.ledger.append({
|
|
233
|
-
type: 'inbox/item-created',
|
|
234
|
-
payload: { itemId: item.id, ...item },
|
|
235
|
-
actor: { kind: 'system' },
|
|
236
|
-
projectId,
|
|
237
|
-
at: this.now(),
|
|
238
|
-
});
|
|
239
|
-
created += 1;
|
|
240
|
-
}
|
|
241
|
-
return created;
|
|
212
|
+
return 0;
|
|
242
213
|
}
|
|
243
214
|
|
|
244
215
|
/**
|
|
@@ -926,42 +897,6 @@ export class TeamService {
|
|
|
926
897
|
projectId: command.projectId,
|
|
927
898
|
at: this.now(),
|
|
928
899
|
});
|
|
929
|
-
// A Thread message that explicitly asks Human to decide must land in the
|
|
930
|
-
// Human inbox; otherwise the board looks busy while the Human never sees
|
|
931
|
-
// the question (the exact failure mode behind “对话面板没有反馈”).
|
|
932
|
-
const body = typeof payload.body === 'string' ? payload.body : '';
|
|
933
|
-
if (actorKind === 'agent' && mentionsHuman(body)) {
|
|
934
|
-
const humanId = this.state.humanMemberId
|
|
935
|
-
?? Object.values(this.state.members ?? {}).find((candidate) => candidate.kind === 'human')?.id
|
|
936
|
-
?? null;
|
|
937
|
-
const duplicate = humanId === null ? undefined : Object.values(this.state.inbox ?? {}).find((item) =>
|
|
938
|
-
item.memberId === humanId
|
|
939
|
-
&& item.ref?.taskId === taskId
|
|
940
|
-
&& item.ref?.entrySeq === outcome.entry.seq
|
|
941
|
-
&& !['resolved', 'dismissed', 'superseded'].includes(item.state));
|
|
942
|
-
if (humanId !== null && duplicate === undefined) {
|
|
943
|
-
const item = createInboxItem({
|
|
944
|
-
memberId: humanId,
|
|
945
|
-
type: 'decision_required',
|
|
946
|
-
ref: {
|
|
947
|
-
taskId,
|
|
948
|
-
entrySeq: outcome.entry.seq,
|
|
949
|
-
memberId: authorId,
|
|
950
|
-
memberName: this.state.members?.[authorId]?.name ?? authorId,
|
|
951
|
-
category: 'thread_mention',
|
|
952
|
-
title: String(body).slice(0, 180),
|
|
953
|
-
},
|
|
954
|
-
requiresDecision: true,
|
|
955
|
-
}, { at: this.now() });
|
|
956
|
-
await this.ledger.append({
|
|
957
|
-
type: 'inbox/item-created',
|
|
958
|
-
payload: { itemId: item.id, ...item },
|
|
959
|
-
actor: { kind: 'system' },
|
|
960
|
-
projectId: command.projectId,
|
|
961
|
-
at: this.now(),
|
|
962
|
-
});
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
900
|
return { thread: outcome.thread, entry: outcome.entry };
|
|
966
901
|
}
|
|
967
902
|
|
package/lib/host/views.js
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* new events. The Host routes and the workbench both consume the same shapes.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { isInformationalThreadMention } from '../domain/inbox.js';
|
|
9
|
+
|
|
8
10
|
const ACTIVE_MEMBER_STATUSES = new Set(['provisioning', 'running', 'idle']);
|
|
9
11
|
|
|
10
12
|
function nameOf(state, memberId) {
|
|
@@ -146,6 +148,7 @@ export function quadrantsView(state, options = {}) {
|
|
|
146
148
|
|
|
147
149
|
for (const item of Object.values(state.inbox ?? {})) {
|
|
148
150
|
if (item.memberId !== humanId || item.requiresDecision !== true) continue;
|
|
151
|
+
if (isInformationalThreadMention(item)) continue;
|
|
149
152
|
if (item.state === 'resolved' || item.state === 'dismissed' || item.state === 'superseded') continue;
|
|
150
153
|
q1.push({
|
|
151
154
|
id: item.id,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@suifei/dsh-ai-team",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.32",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "文渊阁 (Wenyuan Pavilion) — AI Team mode for DeepSeek Harness: concurrent projects, owned tasks, threads, inbox and an append-only team ledger",
|
|
6
6
|
"license": "MIT",
|