@suifei/dsh-ai-team 0.3.24 → 0.3.26
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 +14 -0
- package/client.js +20 -3
- package/index.js +2 -2
- package/lib/domain/policy.js +6 -0
- package/lib/domain/task.js +7 -0
- package/lib/host/member-tools.js +3 -1
- package/lib/host/service.js +28 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.26 — 2026-11-10
|
|
4
|
+
|
|
5
|
+
- Task 创建增加重复保护:相同 title/resultDefinition/owner 的 active Task 会被拒绝并返回已有 Task;
|
|
6
|
+
- 主对话「任务」模式收到重复错误时自动打开已有 Task,不再产生 #3/#4 这类重复卡;
|
|
7
|
+
- `qaRequired=true` 但没有 qaChecklist 时拒绝创建,避免产生无法验收的 #4;
|
|
8
|
+
- 成员有 `pending_acceptance` Task 时不能再 claim 下一张,阻止 T2/T3 在 T1 验收前提前开工;
|
|
9
|
+
- `team() accept` 现在透传 checks,QA Agent 不再需要走 Human API。
|
|
10
|
+
|
|
11
|
+
## 0.3.25 — 2026-11-10
|
|
12
|
+
|
|
13
|
+
- 收件箱未读数按 `state=unread` 统计,决策提交后「待你处理」立即消失;
|
|
14
|
+
- 已答/已 dismiss 的报告六问不会在重启后再次导入;
|
|
15
|
+
- 收件箱决策项显示提问成员名字,不再显示 `mem_*`。
|
|
16
|
+
|
|
3
17
|
## 0.3.24 — 2026-11-10
|
|
4
18
|
|
|
5
19
|
- 修复 member `team` 工具 accept 未透传 `checks` 的缺口:agent QA 带 checklist 验收不再 E_INVALID_STATE;
|
package/client.js
CHANGED
|
@@ -588,6 +588,10 @@ window.__ModuleLoader__.load({
|
|
|
588
588
|
conversationMock: 'spike: this host has no working layout.selectPanel(conversation).',
|
|
589
589
|
discussSent: 'Sent to the Project owner.',
|
|
590
590
|
taskSent: 'Task created.',
|
|
591
|
+
duplicateTask: 'An identical Task already exists (#{number}); opened it instead.',
|
|
592
|
+
qaNeedsChecklist: 'A QA Task needs at least one checklist item.',
|
|
593
|
+
duplicateTask: '相同 Task 已存在(#{number}),已为你打开它。',
|
|
594
|
+
qaNeedsChecklist: 'QA Task 至少需要一条验收清单。',
|
|
591
595
|
sendFailed: 'Send failed: {message}',
|
|
592
596
|
pendingHuman: 'Needs you',
|
|
593
597
|
inboxUnread: 'Inbox, {n} unread',
|
|
@@ -2513,7 +2517,12 @@ window.__ModuleLoader__.load({
|
|
|
2513
2517
|
body: JSON.stringify({ type, payload }),
|
|
2514
2518
|
});
|
|
2515
2519
|
const body = await response.json().catch(() => null);
|
|
2516
|
-
if (!response.ok)
|
|
2520
|
+
if (!response.ok) {
|
|
2521
|
+
const error = new Error(body?.error?.message ?? ('HTTP ' + response.status));
|
|
2522
|
+
error.code = body?.error?.code;
|
|
2523
|
+
error.details = body?.error?.details;
|
|
2524
|
+
throw error;
|
|
2525
|
+
}
|
|
2517
2526
|
return body;
|
|
2518
2527
|
}
|
|
2519
2528
|
function openAddAgent() {
|
|
@@ -2820,7 +2829,15 @@ window.__ModuleLoader__.load({
|
|
|
2820
2829
|
say(mode === 'task' ? t('taskSent') : t('discussSent'));
|
|
2821
2830
|
await reloadData();
|
|
2822
2831
|
} catch (error) {
|
|
2823
|
-
|
|
2832
|
+
if (error?.code === 'E_CONFLICT' && error.details?.taskId) {
|
|
2833
|
+
setSelected(error.details.taskId);
|
|
2834
|
+
setThreadOpen(true);
|
|
2835
|
+
say(tf(t, 'duplicateTask', { number: error.details.number ?? '' }));
|
|
2836
|
+
} else if (error?.message?.includes('qaChecklist')) {
|
|
2837
|
+
say(t('qaNeedsChecklist'));
|
|
2838
|
+
} else {
|
|
2839
|
+
say(tf(t, 'sendFailed', { message: error?.message ?? String(error) }));
|
|
2840
|
+
}
|
|
2824
2841
|
}
|
|
2825
2842
|
}
|
|
2826
2843
|
async function sendReply() {
|
|
@@ -3024,7 +3041,7 @@ window.__ModuleLoader__.load({
|
|
|
3024
3041
|
h('div', { className: 'at-inboxmeta' },
|
|
3025
3042
|
item.projectName ? item.projectName + ' · ' : '',
|
|
3026
3043
|
item.ref?.category ? item.ref.category + ' · ' : '',
|
|
3027
|
-
item.ref?.memberId ? item.ref.memberId : '',
|
|
3044
|
+
item.ref?.memberName ?? (item.ref?.memberId ? item.ref.memberId : ''),
|
|
3028
3045
|
' · ',
|
|
3029
3046
|
fmtTime(item.createdAt)),
|
|
3030
3047
|
item.state !== 'unread' ? h('div', { className: 'at-inboxstate' }, item.state) : null),
|
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.26';
|
|
38
38
|
const PROJECT_ID = 'prj_main';
|
|
39
39
|
const HUMAN_ID = 'mem_human';
|
|
40
40
|
|
|
@@ -748,7 +748,7 @@ export function apply(ctx) {
|
|
|
748
748
|
return send(res, {
|
|
749
749
|
...bootstrap,
|
|
750
750
|
inbox,
|
|
751
|
-
inboxUnread: inbox.filter((item) => item.
|
|
751
|
+
inboxUnread: inbox.filter((item) => item.state === 'unread').length,
|
|
752
752
|
projects: booted.registry.list().map((project) => {
|
|
753
753
|
const active = bootstrap.projects?.find((entry) => entry.id === project.id);
|
|
754
754
|
return {
|
package/lib/domain/policy.js
CHANGED
|
@@ -48,6 +48,12 @@ export function memberHasInProgress(tasksById, memberId) {
|
|
|
48
48
|
return Object.values(tasksById ?? {}).find((candidate) => candidate.assigneeId === memberId && candidate.status === 'in_progress');
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/** A member must not start the next Task while their previous Task awaits QA. */
|
|
52
|
+
export function memberHasPendingAcceptance(tasksById, memberId) {
|
|
53
|
+
if (memberId === null) return undefined;
|
|
54
|
+
return Object.values(tasksById ?? {}).find((candidate) => candidate.assigneeId === memberId && candidate.status === 'pending_acceptance');
|
|
55
|
+
}
|
|
56
|
+
|
|
51
57
|
/**
|
|
52
58
|
* A reviewer is eligible when the task is in pending_acceptance and the
|
|
53
59
|
* reviewer never claimed it (Agent) or is Human. Human self-accept is allowed
|
package/lib/domain/task.js
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
assertRevision,
|
|
9
9
|
dependenciesDone,
|
|
10
10
|
memberHasInProgress,
|
|
11
|
+
memberHasPendingAcceptance,
|
|
11
12
|
} from './policy.js';
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -103,6 +104,12 @@ export function claimTask(task, actor, tasksById, options = {}) {
|
|
|
103
104
|
details: { taskId: task.id, blockedBy: task.blockedBy },
|
|
104
105
|
});
|
|
105
106
|
}
|
|
107
|
+
const awaiting = memberHasPendingAcceptance(tasksById, memberId);
|
|
108
|
+
if (awaiting !== undefined) {
|
|
109
|
+
fail(ERRORS.CONFLICT, 'member has a task awaiting acceptance; wait for QA before claiming the next one', {
|
|
110
|
+
details: { memberId, taskId: awaiting.id },
|
|
111
|
+
});
|
|
112
|
+
}
|
|
106
113
|
const busy = memberHasInProgress(tasksById, memberId);
|
|
107
114
|
if (busy !== undefined) {
|
|
108
115
|
fail(ERRORS.CONFLICT, 'member already has an in_progress task', {
|
package/lib/host/member-tools.js
CHANGED
|
@@ -71,7 +71,8 @@ export function createTeamTool(options) {
|
|
|
71
71
|
ownerId: { type: 'string', description: 'Accountable owner id from members for create; defaults to you.' },
|
|
72
72
|
assigneeId: { type: 'string', description: 'Optional assignee id from members for create.' },
|
|
73
73
|
qaRequired: { type: 'boolean', description: 'Require a QA member to accept this task.' },
|
|
74
|
-
qaChecklist: { type: 'array', items: { type: 'string' }, description: 'QA checklist items; all must pass before acceptance.' },
|
|
74
|
+
qaChecklist: { type: 'array', items: { type: 'string' }, description: 'QA checklist items; all must pass before acceptance; required when qaRequired is true.' },
|
|
75
|
+
allowDuplicate: { type: 'boolean', description: 'Set true only when an identical active Task is intentionally required.' },
|
|
75
76
|
checks: {
|
|
76
77
|
type: 'array',
|
|
77
78
|
description: 'Acceptance checks for accept; include every qaChecklist item with passed=true/false.',
|
|
@@ -237,6 +238,7 @@ export function createTeamTool(options) {
|
|
|
237
238
|
resultDefinition: args.resultDefinition,
|
|
238
239
|
ownerId: owner.id,
|
|
239
240
|
...(assignee === null ? {} : { assigneeId: assignee.id }),
|
|
241
|
+
...(args.allowDuplicate === true ? { allowDuplicate: true } : {}),
|
|
240
242
|
...(args.qaRequired === true ? { qaRequired: true } : {}),
|
|
241
243
|
...(Array.isArray(args.qaChecklist) ? { qaChecklist: args.qaChecklist } : {}),
|
|
242
244
|
});
|
package/lib/host/service.js
CHANGED
|
@@ -184,8 +184,7 @@ export class TeamService {
|
|
|
184
184
|
item.memberId === humanId
|
|
185
185
|
&& item.ref?.source === reportPath
|
|
186
186
|
&& Array.isArray(item.ref?.questions)
|
|
187
|
-
&& item.ref.questions.length > 0
|
|
188
|
-
&& !['resolved', 'dismissed', 'superseded'].includes(item.state));
|
|
187
|
+
&& item.ref.questions.length > 0);
|
|
189
188
|
if (alreadyStructured) return 0;
|
|
190
189
|
const chosen = candidates.find((candidate) => /§6|六问/.test(candidate.entry.body))
|
|
191
190
|
?? candidates.sort((left, right) => (right.entry.seq ?? 0) - (left.entry.seq ?? 0))[0];
|
|
@@ -196,6 +195,7 @@ export class TeamService {
|
|
|
196
195
|
taskId: chosen.task.id,
|
|
197
196
|
entrySeq: chosen.entry.seq,
|
|
198
197
|
memberId: chosen.entry.authorId,
|
|
198
|
+
memberName: this.state.members?.[chosen.entry.authorId]?.name ?? chosen.entry.authorId,
|
|
199
199
|
category: 'thread_mention',
|
|
200
200
|
title: `需 Human 决策 · ${reportQuestions.length} 问`,
|
|
201
201
|
questions: reportQuestions,
|
|
@@ -223,6 +223,7 @@ export class TeamService {
|
|
|
223
223
|
taskId: task.id,
|
|
224
224
|
entrySeq: entry.seq,
|
|
225
225
|
memberId: entry.authorId,
|
|
226
|
+
memberName: this.state.members?.[entry.authorId]?.name ?? entry.authorId,
|
|
226
227
|
category: 'thread_mention',
|
|
227
228
|
title: String(entry.body).slice(0, 180),
|
|
228
229
|
},
|
|
@@ -768,17 +769,35 @@ export class TeamService {
|
|
|
768
769
|
if (project === undefined) fail(ERRORS.NOT_FOUND, `project ${String(projectId)} not found`);
|
|
769
770
|
const number = Math.max(0, ...Object.values(state.tasks ?? {}).map((task) => task.number ?? 0)) + 1;
|
|
770
771
|
const ownerId = payload.ownerId ?? project.ownerMemberId ?? project.ownerId;
|
|
772
|
+
const qaRequired = payload.qaRequired === true;
|
|
773
|
+
const qaChecklist = Array.isArray(payload.qaChecklist) ? payload.qaChecklist.filter((item) => typeof item === 'string' && item.trim() !== '') : [];
|
|
774
|
+
if (qaRequired && qaChecklist.length === 0) {
|
|
775
|
+
fail(ERRORS.INVALID_ARGUMENT, 'qaRequired needs at least one qaChecklist item');
|
|
776
|
+
}
|
|
777
|
+
const title = typeof payload.title === 'string' ? payload.title.trim() : '';
|
|
778
|
+
const resultDefinition = typeof payload.resultDefinition === 'string' ? payload.resultDefinition.trim() : '';
|
|
779
|
+
const duplicate = Object.values(state.tasks ?? {}).find((candidate) =>
|
|
780
|
+
candidate.tombstone !== true
|
|
781
|
+
&& ['pending', 'in_progress', 'pending_acceptance'].includes(candidate.status)
|
|
782
|
+
&& candidate.ownerId === ownerId
|
|
783
|
+
&& candidate.title === title
|
|
784
|
+
&& candidate.resultDefinition === resultDefinition);
|
|
785
|
+
if (duplicate !== undefined && payload.allowDuplicate !== true) {
|
|
786
|
+
fail(ERRORS.CONFLICT, 'an active Task with the same title, result definition and owner already exists', {
|
|
787
|
+
details: { taskId: duplicate.id, number: duplicate.number, title: duplicate.title },
|
|
788
|
+
});
|
|
789
|
+
}
|
|
771
790
|
const { task, event } = createTask({
|
|
772
791
|
number,
|
|
773
|
-
title
|
|
774
|
-
resultDefinition
|
|
792
|
+
title,
|
|
793
|
+
resultDefinition,
|
|
775
794
|
ownerId,
|
|
776
795
|
assigneeId: payload.assigneeId,
|
|
777
796
|
blockedBy: payload.blockedBy,
|
|
778
797
|
writeScopes: payload.writeScopes,
|
|
779
798
|
irreversible: payload.irreversible,
|
|
780
|
-
qaRequired
|
|
781
|
-
qaChecklist
|
|
799
|
+
qaRequired,
|
|
800
|
+
qaChecklist,
|
|
782
801
|
originRefs: payload.originRefs,
|
|
783
802
|
at: this.now(),
|
|
784
803
|
});
|
|
@@ -928,6 +947,7 @@ export class TeamService {
|
|
|
928
947
|
taskId,
|
|
929
948
|
entrySeq: outcome.entry.seq,
|
|
930
949
|
memberId: authorId,
|
|
950
|
+
memberName: this.state.members?.[authorId]?.name ?? authorId,
|
|
931
951
|
category: 'thread_mention',
|
|
932
952
|
title: String(body).slice(0, 180),
|
|
933
953
|
},
|
|
@@ -982,6 +1002,8 @@ export class TeamService {
|
|
|
982
1002
|
ref: {
|
|
983
1003
|
taskId,
|
|
984
1004
|
title,
|
|
1005
|
+
memberId: command.actor.memberId ?? null,
|
|
1006
|
+
memberName: this.state.members?.[command.actor.memberId]?.name ?? command.actor.memberId ?? null,
|
|
985
1007
|
questions,
|
|
986
1008
|
...(typeof payload.source === 'string' && payload.source !== '' ? { source: payload.source } : {}),
|
|
987
1009
|
},
|
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.26",
|
|
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",
|