@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 +13 -0
- package/README.md +2 -2
- package/client.js +72 -2
- package/index.js +1 -1
- package/lib/host/member-tools.js +28 -2
- package/lib/host/service.js +107 -0
- package/lib/host/waker.js +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
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'
|
|
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.
|
|
37
|
+
const VERSION = '0.3.20';
|
|
38
38
|
const PROJECT_ID = 'prj_main';
|
|
39
39
|
const HUMAN_ID = 'mem_human';
|
|
40
40
|
|
package/lib/host/member-tools.js
CHANGED
|
@@ -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.
|
|
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));
|
package/lib/host/service.js
CHANGED
|
@@ -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
|
|
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.
|
|
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",
|