@suifei/dsh-ai-team 0.3.18 → 0.3.20

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,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.20 — 2026-11-10
4
+
5
+ - 收件箱决策项可直接选择/填写并提交,Human 决策会回写 Task Thread 并唤醒 owner;
6
+ - 顶部「待你处理」入口保持一键可达;
7
+ - 决策提交后收件箱状态 resolved,不再需要先 ack 再回复。
8
+
9
+ ## 0.3.19 — 2026-11-10
10
+
11
+ - 新增 `team askHuman`:Agent 必须把每个 Human 决策问题、选项、推荐项逐条提交;
12
+ - 收件箱决策项直接展开问题/选项/推荐,不再要求翻报告或 Thread;
13
+ - 顶部增加「待你处理 · N」一键入口;
14
+ - Thread 中 @Human 的普通消息仍自动生成决策项。
15
+
3
16
  ## 0.3.18 — 2026-11-10
4
17
 
5
18
  - 打开 Project 时回填已有 Thread 中对 Human 的拍板请求到收件箱;
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 聚合;失败、需要你决定、关注 Task 的完成报告,以及 Agent 在 Thread 里明确请你拍板的消息都在这里。
90
+ - **收件箱**:跨 Project 聚合;顶部「待你处理 · N」一键进入;决策项直接展开全部问题与选项,可当场选择/填写并提交,不用再翻 Thread/报告。
91
91
  - **直接对话**:点击成员名字直接打开该 Agent 的 DSH 原生 Session;没有 session 会先激活;打不开时落到成员资料,资料里有「直接对话」按钮。
92
92
  - **成员任务**:成员资料里点「他的任务」,查看他 owner/assignee 的 Task。
93
93
  - **成员资料**:点「⋯」改名、职责、personality、provider/model、运行配方、notes、skills、re-distill、离队/人才库。
@@ -102,7 +102,7 @@ dsh --profile ai-team --no-open --port 33271
102
102
  | Task | 唯一 accountable owner;pending / in_progress / pending_acceptance / done / cancelled |
103
103
  | Thread | append-only;回复带 `baseSeq`,旧基线会被拒并说明变化 |
104
104
  | 验收 | maker 不能验收;QA 硬门禁;`qaChecklist` 逐项通过 |
105
- | 收件箱 | Human/Agent 各自拉取;跨 Project 聚合;Thread 中提到 Human 会自动生成决策项 |
105
+ | 收件箱 | Human/Agent 各自拉取;跨 Project 聚合;顶部待办入口;`askHuman` 结构化问题与选项直接展开并提交 |
106
106
  | 关注 | 关注 Task;完成后收到 `followed_task_update` |
107
107
  | 默认成员 | 新 Project 初始化墨渊 / 青岚 / 白砚;可改名、改职责、改个性、改模型 |
108
108
  | 成员 | 持久 session;点击名字直接对话;「⋯」资料里有任务视图与直接对话 |
package/client.js CHANGED
@@ -107,6 +107,11 @@ window.__ModuleLoader__.load({
107
107
  inboxOpenTask: '打开 Task',
108
108
  inboxAck: '知道了',
109
109
  inboxAcked: '已标记处理。',
110
+ decisionChoose: '请选择…',
111
+ decisionAnswer: '填写你的决定',
112
+ decisionRecommend: '推荐',
113
+ decisionSubmit: '提交决策',
114
+ decisionSubmitted: '决策已提交给团队。',
110
115
  inboxEmptyAction: '看任务看板',
111
116
  projectEmptyTitle: '还没有项目',
112
117
  projectEmptyBody: '创建项目属于 S-01;M1 之后接入。',
@@ -277,6 +282,7 @@ window.__ModuleLoader__.load({
277
282
  discussSent: '已发送给 Project owner。',
278
283
  taskSent: '已创建 Task。',
279
284
  sendFailed: '发送失败:{message}',
285
+ pendingHuman: '待你处理',
280
286
  inboxUnread: '收件箱,{n} 条未读',
281
287
  dismiss: '关闭提示',
282
288
  retry: '重试',
@@ -408,6 +414,11 @@ window.__ModuleLoader__.load({
408
414
  inboxOpenTask: 'Open Task',
409
415
  inboxAck: 'Acknowledge',
410
416
  inboxAcked: 'Marked handled.',
417
+ decisionChoose: 'Choose…',
418
+ decisionAnswer: 'Write your decision',
419
+ decisionRecommend: 'Recommended',
420
+ decisionSubmit: 'Submit decision',
421
+ decisionSubmitted: 'Decision sent to the team.',
411
422
  inboxEmptyAction: 'Open the board',
412
423
  projectEmptyTitle: 'No projects yet',
413
424
  projectEmptyBody: 'Project creation belongs to S-01; it lands after M1.',
@@ -578,6 +589,7 @@ window.__ModuleLoader__.load({
578
589
  discussSent: 'Sent to the Project owner.',
579
590
  taskSent: 'Task created.',
580
591
  sendFailed: 'Send failed: {message}',
592
+ pendingHuman: 'Needs you',
581
593
  inboxUnread: 'Inbox, {n} unread',
582
594
  dismiss: 'Dismiss',
583
595
  retry: 'Retry',
@@ -904,6 +916,7 @@ window.__ModuleLoader__.load({
904
916
  .at-top { height: 40px; flex: 0 0 auto; display: flex; align-items: center; gap: 8px; min-width: 0; padding: 0 12px; border-bottom: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-1); }
905
917
  .at-brand { color: var(--dsw-alias-label-primary); font-size: 12px; font-weight: 500; white-space: nowrap; }
906
918
  .at-spike { display: inline-flex; align-items: center; height: 20px; padding: 0 6px; border: 1px solid var(--dsw-alias-border-l3); border-radius: var(--dsw-radius-xs); color: var(--dsw-alias-label-tertiary); font-size: 10px; line-height: 16px; letter-spacing: .04em; text-transform: uppercase; white-space: nowrap; }
919
+ .at-pending { margin-left: auto; height: 24px; padding: 0 8px; border: 1px solid var(--dsw-alias-state-warning-primary); border-radius: var(--dsw-radius-sm); background: var(--dsw-alias-bg-base); color: var(--dsw-alias-state-warning-primary); font: inherit; font-size: 11px; line-height: 16px; cursor: pointer; white-space: nowrap; }
907
920
  .at-topproject { margin-left: auto; min-width: 0; color: var(--dsw-alias-label-tertiary); font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
908
921
  .at-mobile { display: none; position: relative; min-width: 0; }
909
922
  .at-mobilesummary { display: inline-flex; align-items: center; gap: 6px; min-height: 28px; padding: 0 8px; border: 1px solid var(--dsw-alias-border-l2); border-radius: var(--dsw-radius-sm); color: var(--dsw-alias-label-secondary); font-size: 11px; cursor: pointer; list-style: none; }
@@ -947,6 +960,14 @@ window.__ModuleLoader__.load({
947
960
  .at-inboxitem { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding: 11px 13px; border: 1px solid var(--dsw-alias-border-l2); border-radius: var(--dsw-radius-sm); background: var(--dsw-alias-bg-layer-2); }
948
961
  .at-inboxitem.at-inboxdone { opacity: 0.62; }
949
962
  .at-inboxbody { min-width: 0; flex: 1; }
963
+ .at-inboxquestions { margin: 6px 0 2px; padding-left: 20px; color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 19px; }
964
+ .at-inboxquestions li { margin: 3px 0; }
965
+ .at-inboxquestion { display: block; color: var(--dsw-alias-label-primary); }
966
+ .at-inboxoptions { display: block; color: var(--dsw-alias-label-tertiary); font-size: 11px; }
967
+ .at-inboxrecommend { display: block; color: var(--dsw-alias-state-business-primary); font-size: 11px; }
968
+ .at-decision { display: flex; flex-direction: column; gap: 8px; margin: 6px 0 4px; }
969
+ .at-decisionselect, .at-decisioninput { width: 100%; margin-top: 4px; padding: 4px 6px; border: 1px solid var(--dsw-alias-border-l2); border-radius: var(--dsw-radius-sm); background: var(--dsw-alias-bg-base); color: var(--dsw-alias-label-primary); font: inherit; font-size: 12px; line-height: 18px; }
970
+ .at-decision .at-btn { align-self: flex-start; }
950
971
  .at-inboxtitle { color: var(--dsw-alias-label-primary); font-size: 13px; line-height: 20px; }
951
972
  .at-inboxmeta { color: var(--dsw-alias-label-tertiary); font-size: 11px; line-height: 16px; }
952
973
  .at-inboxstate { color: var(--dsw-alias-label-caption); font-size: 10.5px; }
@@ -1607,6 +1628,33 @@ window.__ModuleLoader__.load({
1607
1628
  running.length ? h('div', { className: 'at-teampulseagents' }, t('teamPulseAgents').replace('{names}', running.map((agent) => agent.name).join('、'))) : null);
1608
1629
  }
1609
1630
 
1631
+ function InboxDecision({ t, item, onSubmit }) {
1632
+ const questions = Array.isArray(item.ref?.questions) ? item.ref.questions : [];
1633
+ const [answers, setAnswers] = React.useState(() => questions.map(() => ''));
1634
+ const [busy, setBusy] = React.useState(false);
1635
+ const setAnswer = (index, value) => setAnswers((prev) => prev.map((entry, i) => i === index ? value : entry));
1636
+ const complete = questions.every((question, index) => answers[index].trim() !== '');
1637
+ const submit = async () => {
1638
+ if (!complete || busy) return;
1639
+ setBusy(true);
1640
+ try {
1641
+ await onSubmit(item, questions.map((question, index) => ({ id: question.id, value: answers[index].trim() })));
1642
+ } finally {
1643
+ setBusy(false);
1644
+ }
1645
+ };
1646
+ return h('div', { className: 'at-decision' },
1647
+ h('ol', { className: 'at-inboxquestions' }, questions.map((question, index) => h('li', { key: question.id ?? index },
1648
+ h('span', { className: 'at-inboxquestion' }, question.text),
1649
+ Array.isArray(question.options) && question.options.length
1650
+ ? h('select', { className: 'at-decisionselect', value: answers[index], onChange: (event) => setAnswer(index, event.target.value) },
1651
+ h('option', { value: '' }, t('decisionChoose')),
1652
+ question.options.map((option) => h('option', { key: option, value: option }, option)))
1653
+ : h('input', { className: 'at-decisioninput', value: answers[index], placeholder: question.recommendation ?? t('decisionAnswer'), onChange: (event) => setAnswer(index, event.target.value) }),
1654
+ question.recommendation ? h('span', { className: 'at-inboxrecommend' }, t('decisionRecommend') + ':' + question.recommendation) : null))),
1655
+ h('button', { className: 'at-btn at-btn-primary at-btn-small', type: 'button', disabled: !complete || busy, onClick: submit }, busy ? t('saving') : t('decisionSubmit')));
1656
+ }
1657
+
1610
1658
  function MemberView({ t, agent, tasks, onBack, onOpenTask, onOpenChat, onOpenProfile }) {
1611
1659
  return h('div', { className: 'at-memberview' },
1612
1660
  h('div', { className: 'at-memberviewhead' },
@@ -2740,6 +2788,15 @@ window.__ModuleLoader__.load({
2740
2788
  setThreadOpen(true);
2741
2789
  setRightNote('');
2742
2790
  }
2791
+ async function answerDecision(item, answers) {
2792
+ try {
2793
+ await runCommand('decision.answer', { itemId: item.id, answers }, item.projectId ?? projectId);
2794
+ say(t('decisionSubmitted'));
2795
+ await reloadData();
2796
+ } catch (error) {
2797
+ say(tf(t, 'sendFailed', { message: error?.message ?? String(error) }));
2798
+ }
2799
+ }
2743
2800
  async function ackInbox(item) {
2744
2801
  try {
2745
2802
  await runCommand('inbox.ack', { itemId: item.id, disposition: 'read' }, item.projectId ?? projectId);
@@ -2950,10 +3007,20 @@ window.__ModuleLoader__.load({
2950
3007
  h('div', { className: 'at-inboxbody' },
2951
3008
  h('div', { className: 'at-inboxtitle' },
2952
3009
  item.type === 'decision_required'
2953
- ? t('inboxDecisionTitle')
3010
+ ? (item.ref?.title || t('inboxDecisionTitle'))
2954
3011
  : item.type === 'followed_task_update'
2955
3012
  ? tf(t, 'inboxCompletedTitle', { title: item.ref?.title ?? item.ref?.taskId ?? '' })
2956
3013
  : t('inboxItemTitle')),
3014
+ Array.isArray(item.ref?.questions) && item.ref.questions.length
3015
+ ? item.state === 'unread'
3016
+ ? h(InboxDecision, { t, item, onSubmit: answerDecision })
3017
+ : h('ol', { className: 'at-inboxquestions' }, item.ref.questions.map((question, index) => h('li', { key: question.id ?? index },
3018
+ h('span', { className: 'at-inboxquestion' }, question.text),
3019
+ Array.isArray(question.options) && question.options.length
3020
+ ? h('span', { className: 'at-inboxoptions' }, question.options.join(' / '))
3021
+ : null,
3022
+ question.recommendation ? h('span', { className: 'at-inboxrecommend' }, '推荐:' + question.recommendation) : null)))
3023
+ : null,
2957
3024
  h('div', { className: 'at-inboxmeta' },
2958
3025
  item.projectName ? item.projectName + ' · ' : '',
2959
3026
  item.ref?.category ? item.ref.category + ' · ' : '',
@@ -2963,7 +3030,9 @@ window.__ModuleLoader__.load({
2963
3030
  item.state !== 'unread' ? h('div', { className: 'at-inboxstate' }, item.state) : null),
2964
3031
  h('div', { className: 'at-inboxactions' },
2965
3032
  item.ref?.taskId ? h('button', { className: 'at-btn at-btn-small', type: 'button', onClick: () => openInboxTask(item) }, t('inboxOpenTask')) : null,
2966
- item.state === 'unread' ? h('button', { className: 'at-btn at-btn-small at-btn-primary', type: 'button', onClick: () => ackInbox(item) }, t('inboxAck')) : null)))
3033
+ item.state === 'unread' && !(item.type === 'decision_required' && Array.isArray(item.ref?.questions) && item.ref.questions.length)
3034
+ ? h('button', { className: 'at-btn at-btn-small at-btn-primary', type: 'button', onClick: () => ackInbox(item) }, t('inboxAck'))
3035
+ : null)))
2967
3036
  : h(EmptyState, {
2968
3037
  t,
2969
3038
  icon: 'inbox',
@@ -3024,6 +3093,7 @@ window.__ModuleLoader__.load({
3024
3093
  h('div', { className: 'at-top' },
3025
3094
  h('span', { className: 'at-brand' }, t('panel')),
3026
3095
  h('span', { className: 'at-spike', title: t('spikeTitle') }, t('spike')),
3096
+ inboxUnread > 0 ? h('button', { className: 'at-pending', type: 'button', onClick: switchInbox }, t('pendingHuman') + ' · ' + unread) : null,
3027
3097
  h('details', { className: 'at-mobile', ref: mobileRef },
3028
3098
  h('summary', { className: 'at-mobilesummary', 'aria-label': t('menuAria') }, h(Ico, { name: 'menu', size: 14 }), h('span', null, t('menu'))),
3029
3099
  h('div', { className: 'at-mobilepop' }, h(LeftPanel, leftProps))),
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.18';
37
+ const VERSION = '0.3.20';
38
38
  const PROJECT_ID = 'prj_main';
39
39
  const HUMAN_ID = 'mem_human';
40
40
 
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { taskCard } from './views.js';
9
9
 
10
- const ACTIONS = ['tasks', 'members', 'inbox', 'recall', 'create', 'member', 'evolve', 'distill', 'note', 'skill', 'claim', 'submit', 'thread', 'reply', 'accept', 'requestChanges'];
10
+ const ACTIONS = ['tasks', 'members', 'inbox', 'recall', 'create', 'askHuman', 'member', 'evolve', 'distill', 'note', 'skill', 'claim', 'submit', 'thread', 'reply', 'accept', 'requestChanges'];
11
11
 
12
12
  function failure(error) {
13
13
  return {
@@ -53,7 +53,7 @@ function rosterText(state) {
53
53
  export function createTeamTool(options) {
54
54
  return {
55
55
  name: 'team',
56
- description: 'Read and act on the AI Team board. The Project owner can create tasks with action=create and must set resultDefinition and ownerId. Use members for the exact roster id/name/role; use tasks for ids/revisions. Use recall to search your notes/skills and your past Tasks/Threads. Use member to inspect a colleague, evolve for only your own personality, distill (Project owner/Human) or note/skill for durable memory. Claim before working; read a Thread before replying; submit evidence before acceptance.',
56
+ description: 'Read and act on the AI Team board. The Project owner can create tasks with action=create and must set resultDefinition and ownerId. Use members for the exact roster id/name/role; use tasks for ids/revisions. When Human must decide something, call askHuman with every question and options—do not bury questions in a report. Use recall to search your notes/skills and your past Tasks/Threads. Claim before working; read a Thread before replying; submit evidence before acceptance.',
57
57
  parameters: {
58
58
  type: 'object',
59
59
  properties: {
@@ -72,6 +72,20 @@ export function createTeamTool(options) {
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
74
  qaChecklist: { type: 'array', items: { type: 'string' }, description: 'QA checklist items; all must pass before acceptance.' },
75
+ questions: {
76
+ type: 'array',
77
+ description: 'Human decision questions for askHuman; include every question instead of pointing at a report.',
78
+ items: {
79
+ type: 'object',
80
+ properties: {
81
+ text: { type: 'string', description: 'The question.' },
82
+ options: { type: 'array', items: { type: 'string' }, description: 'Optional answer choices.' },
83
+ recommendation: { type: 'string', description: 'Optional recommended answer.' },
84
+ blocking: { type: 'string', description: 'What this decision blocks.' },
85
+ },
86
+ required: ['text'],
87
+ },
88
+ },
75
89
  personality: { type: 'string', description: 'New working personality for evolve/distill.' },
76
90
  reason: { type: 'string', description: 'Why the personality changed.' },
77
91
  memberId: { type: 'string', description: 'Target member for member/distill/note/skill.' },
@@ -216,6 +230,18 @@ export function createTeamTool(options) {
216
230
  });
217
231
  return { ok: true, task: taskCard(options.getState(), result.task) };
218
232
  }
233
+ if (args.action === 'askHuman') {
234
+ if (!Array.isArray(args.questions) || args.questions.length === 0) {
235
+ return failure({ code: 'E_INVALID_ARGUMENT', message: 'questions is required; list every question Human must answer' });
236
+ }
237
+ const result = await command('decision.request', {
238
+ title: typeof args.title === 'string' && args.title.trim() !== '' ? args.title.trim() : '需要你决策',
239
+ taskId: typeof args.taskId === 'string' && args.taskId !== '' ? args.taskId : null,
240
+ questions: args.questions,
241
+ source: 'team',
242
+ });
243
+ return { ok: true, item: result.item, duplicate: result.duplicate === true };
244
+ }
219
245
  if (args.action === 'inbox') {
220
246
  const items = Object.values(state.inbox ?? {})
221
247
  .filter((item) => item.memberId === member.id && !['resolved', 'dismissed', 'superseded'].includes(item.state));
@@ -232,6 +232,8 @@ export class TeamService {
232
232
  case 'task.follow': return await this.follow(command, payload);
233
233
  case 'thread.post': return await this.postThread(command, payload);
234
234
  case 'inbox.ack': return await this.ack(command, payload);
235
+ case 'decision.request': return await this.requestDecision(command, payload);
236
+ case 'decision.answer': return await this.answerDecision(command, payload);
235
237
  default: fail(ERRORS.INVALID_ARGUMENT, `unknown command: ${String(command.type)}`);
236
238
  }
237
239
  }
@@ -889,6 +891,111 @@ export class TeamService {
889
891
  return { thread: outcome.thread, entry: outcome.entry };
890
892
  }
891
893
 
894
+ /** Surface an explicit Human decision request with all questions inline. */
895
+ async requestDecision(command, payload) {
896
+ const humanId = this.state.humanMemberId
897
+ ?? Object.values(this.state.members ?? {}).find((member) => member.kind === 'human')?.id
898
+ ?? null;
899
+ if (humanId === null) fail(ERRORS.NOT_FOUND, 'Human member not found');
900
+ const raw = Array.isArray(payload.questions) ? payload.questions : [];
901
+ if (raw.length === 0 || raw.length > 20) fail(ERRORS.INVALID_ARGUMENT, 'questions must contain 1..20 items');
902
+ const questions = raw.map((question, index) => {
903
+ if (typeof question === 'string') return { id: `q${index + 1}`, text: question.trim() };
904
+ if (question === null || typeof question !== 'object') fail(ERRORS.INVALID_ARGUMENT, `questions[${index}] must be a string or object`);
905
+ const text = typeof question.text === 'string' ? question.text.trim() : '';
906
+ if (text === '') fail(ERRORS.INVALID_ARGUMENT, `questions[${index}].text is required`);
907
+ const options = Array.isArray(question.options) ? question.options.filter((option) => typeof option === 'string' && option.trim() !== '').slice(0, 12) : [];
908
+ return {
909
+ id: typeof question.id === 'string' && question.id !== '' ? question.id : `q${index + 1}`,
910
+ text,
911
+ ...(options.length ? { options } : {}),
912
+ ...(typeof question.recommendation === 'string' && question.recommendation !== '' ? { recommendation: question.recommendation } : {}),
913
+ ...(typeof question.blocking === 'string' && question.blocking !== '' ? { blocking: question.blocking } : {}),
914
+ };
915
+ });
916
+ const title = typeof payload.title === 'string' && payload.title.trim() !== '' ? payload.title.trim() : '需要你决策';
917
+ const taskId = typeof payload.taskId === 'string' ? payload.taskId : null;
918
+ const duplicate = Object.values(this.state.inbox ?? {}).find((item) =>
919
+ item.memberId === humanId
920
+ && item.type === 'decision_required'
921
+ && item.ref?.title === title
922
+ && item.ref?.taskId === taskId
923
+ && !['resolved', 'dismissed', 'superseded'].includes(item.state));
924
+ if (duplicate !== undefined) return { item: duplicate, duplicate: true };
925
+ const item = createInboxItem({
926
+ memberId: humanId,
927
+ type: 'decision_required',
928
+ ref: {
929
+ taskId,
930
+ title,
931
+ questions,
932
+ ...(typeof payload.source === 'string' && payload.source !== '' ? { source: payload.source } : {}),
933
+ },
934
+ requiresDecision: true,
935
+ }, { at: this.now() });
936
+ await this.ledger.append({
937
+ type: 'inbox/item-created',
938
+ payload: { itemId: item.id, ...item },
939
+ actor: command.actor,
940
+ correlationId: command.correlationId,
941
+ projectId: command.projectId,
942
+ at: this.now(),
943
+ });
944
+ return { item, duplicate: false };
945
+ }
946
+
947
+ async answerDecision(command, payload) {
948
+ if (command.actor.kind !== 'human') fail(ERRORS.UNAUTHORIZED, 'only Human may answer a decision');
949
+ const item = this.state.inbox?.[payload.itemId];
950
+ if (item === undefined) fail(ERRORS.NOT_FOUND, `inbox item ${String(payload.itemId)} not found`);
951
+ if (item.type !== 'decision_required' || item.requiresDecision !== true) {
952
+ fail(ERRORS.INVALID_STATE, 'this inbox item is not a Human decision');
953
+ }
954
+ const humanId = command.actor.memberId;
955
+ if (item.memberId !== humanId) fail(ERRORS.UNAUTHORIZED, 'decision belongs to another member');
956
+ const answers = (Array.isArray(payload.answers) ? payload.answers : [])
957
+ .map((answer, index) => ({
958
+ id: typeof answer?.id === 'string' && answer.id !== '' ? answer.id : `q${index + 1}`,
959
+ value: typeof answer?.value === 'string' ? answer.value.trim() : '',
960
+ }))
961
+ .filter((answer) => answer.value !== '');
962
+ if (answers.length === 0) fail(ERRORS.INVALID_ARGUMENT, 'at least one answer is required');
963
+ const questions = Array.isArray(item.ref?.questions) ? item.ref.questions : [];
964
+ const lines = answers.map((answer) => {
965
+ const question = questions.find((candidate) => candidate.id === answer.id);
966
+ return `- ${question?.text ?? answer.id}: ${answer.value}`;
967
+ });
968
+ const body = `Human 决策:\n${lines.join('\n')}${typeof payload.note === 'string' && payload.note.trim() !== '' ? `\n备注:${payload.note.trim()}` : ''}`;
969
+ let entry = null;
970
+ if (typeof item.ref?.taskId === 'string' && this.state.tasks?.[item.ref.taskId] !== undefined) {
971
+ const taskId = item.ref.taskId;
972
+ const thread = this.state.threads?.[taskId] ?? createThread(taskId);
973
+ const outcome = appendReply(thread, {
974
+ authorId: humanId,
975
+ actorKind: 'human',
976
+ baseSeq: thread.headSeq,
977
+ body,
978
+ }, { at: this.now() });
979
+ await this.ledger.append({
980
+ type: 'thread/posted',
981
+ payload: { taskId, ...outcome.entry },
982
+ actor: command.actor,
983
+ projectId: command.projectId,
984
+ at: this.now(),
985
+ });
986
+ entry = outcome.entry;
987
+ }
988
+ const next = ackInboxItem(item, { disposition: 'done', decision: { answers, note: payload.note ?? '', body } }, { at: this.now() });
989
+ await this.ledger.append({
990
+ type: 'inbox/acked',
991
+ payload: { itemId: next.id, memberId: next.memberId, state: next.state, disposition: next.disposition, at: next.ackedAt },
992
+ actor: command.actor,
993
+ projectId: command.projectId,
994
+ at: this.now(),
995
+ });
996
+ return { item: next, entry };
997
+ }
998
+
892
999
  async ack(command, payload) {
893
1000
  const state = this.state;
894
1001
  const item = state.inbox?.[payload.itemId];
package/lib/host/waker.js CHANGED
@@ -165,7 +165,7 @@ export function planWakes(state, result, actor = {}) {
165
165
  null,
166
166
  'human_message',
167
167
  `human_message:${event.seq ?? event.payload?.seq}`,
168
- `你是 Project owner。Human 在 Main stream 说:“${body}”。本轮必须先用 team action=members 查看成员,再用 action=create 创建至少一个 Task;ownerId 必须使用成员 id,写明可验证的 resultDefinition。可用成员:${roster || '无'}。如果需求很大或外部链接只是参考,先创建“调研/澄清/拆解”Task,不要直接开始实现;如果确实无法创建,必须明确说明原因。resultDefinition 只写验收标准,不要写“等待 Human 验收”;不要只读文件或只回一段话结束。`,
168
+ `你是 Project owner。Human 在 Main stream 说:“${body}”。本轮必须先用 team action=members 查看成员,再用 action=create 创建至少一个 Task;ownerId 必须使用成员 id,写明可验证的 resultDefinition。可用成员:${roster || '无'}。如果需求很大或外部链接只是参考,先创建“调研/澄清/拆解”Task,不要直接开始实现;如果确实无法创建,必须明确说明原因。需要 Human 拍板时,必须调用 team action=askHuman,把每个问题和选项逐条放进 questions,不要只写“见报告/§6/已提交 Human”。resultDefinition 只写验收标准,不要写“等待 Human 验收”;不要只读文件或只回一段话结束。`,
169
169
  );
170
170
  }
171
171
  if (event?.type === 'inbox/item-created') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suifei/dsh-ai-team",
3
- "version": "0.3.18",
3
+ "version": "0.3.20",
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",