@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 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) throw new Error(body?.error?.message ?? ('HTTP ' + response.status));
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
- say(tf(t, 'sendFailed', { message: error?.message ?? String(error) }));
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.24';
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.read !== true).length,
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 {
@@ -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
@@ -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', {
@@ -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
  });
@@ -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: payload.title,
774
- resultDefinition: payload.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: payload.qaRequired === true,
781
- qaChecklist: Array.isArray(payload.qaChecklist) ? payload.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.24",
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",