@suifei/dsh-ai-team 0.3.19 → 0.3.21
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 +12 -0
- package/README.md +2 -2
- package/client.js +60 -7
- package/index.js +1 -1
- package/lib/host/decision-report.js +48 -0
- package/lib/host/project-runtime.js +1 -1
- package/lib/host/service.js +73 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.21 — 2026-11-10
|
|
4
|
+
|
|
5
|
+
- 打开 Project 时,如果 Thread 里提到 Human 并登记了 Markdown 报告路径,会从报告的「6. 需 Human 决策的问题清单」提取全部问题;
|
|
6
|
+
- 现有项目无需重新问一遍:升级重启后,§6 六问直接进收件箱并可逐项填写提交;
|
|
7
|
+
- 收件箱决策标题显示问题数(如「需 Human 决策 · 6 问」)。
|
|
8
|
+
|
|
9
|
+
## 0.3.20 — 2026-11-10
|
|
10
|
+
|
|
11
|
+
- 收件箱决策项可直接选择/填写并提交,Human 决策会回写 Task Thread 并唤醒 owner;
|
|
12
|
+
- 顶部「待你处理」入口保持一键可达;
|
|
13
|
+
- 决策提交后收件箱状态 resolved,不再需要先 ack 再回复。
|
|
14
|
+
|
|
3
15
|
## 0.3.19 — 2026-11-10
|
|
4
16
|
|
|
5
17
|
- 新增 `team askHuman`:Agent 必须把每个 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 聚合;顶部「待你处理 · N
|
|
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 聚合;顶部待办入口;`askHuman`
|
|
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 之后接入。',
|
|
@@ -409,6 +414,11 @@ window.__ModuleLoader__.load({
|
|
|
409
414
|
inboxOpenTask: 'Open Task',
|
|
410
415
|
inboxAck: 'Acknowledge',
|
|
411
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.',
|
|
412
422
|
inboxEmptyAction: 'Open the board',
|
|
413
423
|
projectEmptyTitle: 'No projects yet',
|
|
414
424
|
projectEmptyBody: 'Project creation belongs to S-01; it lands after M1.',
|
|
@@ -955,6 +965,9 @@ window.__ModuleLoader__.load({
|
|
|
955
965
|
.at-inboxquestion { display: block; color: var(--dsw-alias-label-primary); }
|
|
956
966
|
.at-inboxoptions { display: block; color: var(--dsw-alias-label-tertiary); font-size: 11px; }
|
|
957
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; }
|
|
958
971
|
.at-inboxtitle { color: var(--dsw-alias-label-primary); font-size: 13px; line-height: 20px; }
|
|
959
972
|
.at-inboxmeta { color: var(--dsw-alias-label-tertiary); font-size: 11px; line-height: 16px; }
|
|
960
973
|
.at-inboxstate { color: var(--dsw-alias-label-caption); font-size: 10.5px; }
|
|
@@ -1615,6 +1628,33 @@ window.__ModuleLoader__.load({
|
|
|
1615
1628
|
running.length ? h('div', { className: 'at-teampulseagents' }, t('teamPulseAgents').replace('{names}', running.map((agent) => agent.name).join('、'))) : null);
|
|
1616
1629
|
}
|
|
1617
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
|
+
|
|
1618
1658
|
function MemberView({ t, agent, tasks, onBack, onOpenTask, onOpenChat, onOpenProfile }) {
|
|
1619
1659
|
return h('div', { className: 'at-memberview' },
|
|
1620
1660
|
h('div', { className: 'at-memberviewhead' },
|
|
@@ -2748,6 +2788,15 @@ window.__ModuleLoader__.load({
|
|
|
2748
2788
|
setThreadOpen(true);
|
|
2749
2789
|
setRightNote('');
|
|
2750
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
|
+
}
|
|
2751
2800
|
async function ackInbox(item) {
|
|
2752
2801
|
try {
|
|
2753
2802
|
await runCommand('inbox.ack', { itemId: item.id, disposition: 'read' }, item.projectId ?? projectId);
|
|
@@ -2963,12 +3012,14 @@ window.__ModuleLoader__.load({
|
|
|
2963
3012
|
? tf(t, 'inboxCompletedTitle', { title: item.ref?.title ?? item.ref?.taskId ?? '' })
|
|
2964
3013
|
: t('inboxItemTitle')),
|
|
2965
3014
|
Array.isArray(item.ref?.questions) && item.ref.questions.length
|
|
2966
|
-
?
|
|
2967
|
-
h(
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
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)))
|
|
2972
3023
|
: null,
|
|
2973
3024
|
h('div', { className: 'at-inboxmeta' },
|
|
2974
3025
|
item.projectName ? item.projectName + ' · ' : '',
|
|
@@ -2979,7 +3030,9 @@ window.__ModuleLoader__.load({
|
|
|
2979
3030
|
item.state !== 'unread' ? h('div', { className: 'at-inboxstate' }, item.state) : null),
|
|
2980
3031
|
h('div', { className: 'at-inboxactions' },
|
|
2981
3032
|
item.ref?.taskId ? h('button', { className: 'at-btn at-btn-small', type: 'button', onClick: () => openInboxTask(item) }, t('inboxOpenTask')) : null,
|
|
2982
|
-
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)))
|
|
2983
3036
|
: h(EmptyState, {
|
|
2984
3037
|
t,
|
|
2985
3038
|
icon: 'inbox',
|
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.21';
|
|
38
38
|
const PROJECT_ID = 'prj_main';
|
|
39
39
|
const HUMAN_ID = 'mem_human';
|
|
40
40
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read the Human-decision table from a linked Markdown report.
|
|
3
|
+
*
|
|
4
|
+
* This is deliberately narrow: only a report containing a section titled
|
|
5
|
+
* `6.` / `六` with a table gets converted. The Inbox still points back to the
|
|
6
|
+
* source Thread; the table is exposed so Human does not have to open the report
|
|
7
|
+
* just to discover the questions.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
function stripMarkdown(value) {
|
|
11
|
+
return String(value ?? '')
|
|
12
|
+
.replace(/\*\*/g, '')
|
|
13
|
+
.replace(/`/g, '')
|
|
14
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
|
15
|
+
.trim();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function findReportPath(text) {
|
|
19
|
+
if (typeof text !== 'string') return null;
|
|
20
|
+
const matches = text.match(/[A-Za-z]:\\[^\r\n"'<>|]+?\.md\b/g);
|
|
21
|
+
return matches === null || matches.length === 0 ? null : matches[matches.length - 1];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function parseDecisionQuestions(markdown) {
|
|
25
|
+
const lines = String(markdown ?? '').split(/\r?\n/);
|
|
26
|
+
const start = lines.findIndex((line) => /^##\s*6[.、\s]/.test(line) || /^##\s*6\s*$/.test(line));
|
|
27
|
+
if (start < 0) return [];
|
|
28
|
+
let end = lines.length;
|
|
29
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
30
|
+
if (/^##\s/.test(lines[index]) || /^---\s*$/.test(lines[index])) { end = index; break; }
|
|
31
|
+
}
|
|
32
|
+
const questions = [];
|
|
33
|
+
for (const line of lines.slice(start + 1, end)) {
|
|
34
|
+
if (!line.trim().startsWith('|')) continue;
|
|
35
|
+
const cells = line.split('|').slice(1, -1).map((cell) => cell.trim());
|
|
36
|
+
if (cells.length < 2) continue;
|
|
37
|
+
if (!/^\d+$/.test(stripMarkdown(cells[0]))) continue;
|
|
38
|
+
const text = stripMarkdown(cells[1]);
|
|
39
|
+
if (text === '') continue;
|
|
40
|
+
questions.push({
|
|
41
|
+
id: `q${questions.length + 1}`,
|
|
42
|
+
text,
|
|
43
|
+
...(cells[2] ? { recommendation: stripMarkdown(cells[2]) } : {}),
|
|
44
|
+
...(cells[3] ? { blocking: stripMarkdown(cells[3]) } : {}),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return questions;
|
|
48
|
+
}
|
|
@@ -61,7 +61,7 @@ export class ProjectRuntime {
|
|
|
61
61
|
});
|
|
62
62
|
}
|
|
63
63
|
await seedDefaultRoster(service, projectId, { humanId: HUMAN_ID });
|
|
64
|
-
await service.backfillHumanMentions();
|
|
64
|
+
await service.backfillHumanMentions(state.projects?.[projectId]?.workspacePath ?? '');
|
|
65
65
|
}
|
|
66
66
|
return runtime;
|
|
67
67
|
}
|
package/lib/host/service.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
1
2
|
import { ERRORS, fail } from '../domain/errors.js';
|
|
3
|
+
import { findReportPath, parseDecisionQuestions } from './decision-report.js';
|
|
2
4
|
import { ids } from '../domain/ids.js';
|
|
3
5
|
import { ackInboxItem, createInboxItem } from '../domain/inbox.js';
|
|
4
6
|
import {
|
|
@@ -141,7 +143,7 @@ export class TeamService {
|
|
|
141
143
|
* that explicitly asked Human to decide. Runs on Project open once; the
|
|
142
144
|
* per-entry dedupe makes it idempotent.
|
|
143
145
|
*/
|
|
144
|
-
async backfillHumanMentions() {
|
|
146
|
+
async backfillHumanMentions(workspacePath = '') {
|
|
145
147
|
const humanId = this.state.humanMemberId
|
|
146
148
|
?? Object.values(this.state.members ?? {}).find((member) => member.kind === 'human')?.id
|
|
147
149
|
?? null;
|
|
@@ -152,6 +154,20 @@ export class TeamService {
|
|
|
152
154
|
.filter((item) => item.memberId === humanId && item.ref?.entrySeq !== undefined)
|
|
153
155
|
.map((item) => `${item.ref?.taskId}:${item.ref?.entrySeq}`),
|
|
154
156
|
);
|
|
157
|
+
let reportPath = null;
|
|
158
|
+
for (const task of Object.values(this.state.tasks ?? {})) {
|
|
159
|
+
for (const entry of this.state.threads?.[task.id]?.entries ?? []) {
|
|
160
|
+
const found = findReportPath(entry.body);
|
|
161
|
+
if (found !== null && existsSync(found)) { reportPath = found; break; }
|
|
162
|
+
}
|
|
163
|
+
if (reportPath !== null) break;
|
|
164
|
+
}
|
|
165
|
+
let reportQuestions = [];
|
|
166
|
+
if (reportPath !== null) {
|
|
167
|
+
try {
|
|
168
|
+
reportQuestions = parseDecisionQuestions(readFileSync(reportPath, 'utf8'));
|
|
169
|
+
} catch { /* report may be unavailable; keep the Thread fallback */ }
|
|
170
|
+
}
|
|
155
171
|
let created = 0;
|
|
156
172
|
for (const task of Object.values(this.state.tasks ?? {})) {
|
|
157
173
|
if (task.tombstone === true) continue;
|
|
@@ -161,6 +177,7 @@ export class TeamService {
|
|
|
161
177
|
const key = `${task.id}:${entry.seq}`;
|
|
162
178
|
if (existing.has(key)) continue;
|
|
163
179
|
existing.add(key);
|
|
180
|
+
const useQuestions = reportQuestions.length > 0;
|
|
164
181
|
const item = createInboxItem({
|
|
165
182
|
memberId: humanId,
|
|
166
183
|
type: 'decision_required',
|
|
@@ -169,7 +186,8 @@ export class TeamService {
|
|
|
169
186
|
entrySeq: entry.seq,
|
|
170
187
|
memberId: entry.authorId,
|
|
171
188
|
category: 'thread_mention',
|
|
172
|
-
title: String(entry.body).slice(0, 180),
|
|
189
|
+
title: useQuestions ? `需 Human 决策 · ${reportQuestions.length} 问` : String(entry.body).slice(0, 180),
|
|
190
|
+
...(useQuestions ? { questions: reportQuestions, source: reportPath } : {}),
|
|
173
191
|
},
|
|
174
192
|
requiresDecision: true,
|
|
175
193
|
}, { at: this.now() });
|
|
@@ -233,6 +251,7 @@ export class TeamService {
|
|
|
233
251
|
case 'thread.post': return await this.postThread(command, payload);
|
|
234
252
|
case 'inbox.ack': return await this.ack(command, payload);
|
|
235
253
|
case 'decision.request': return await this.requestDecision(command, payload);
|
|
254
|
+
case 'decision.answer': return await this.answerDecision(command, payload);
|
|
236
255
|
default: fail(ERRORS.INVALID_ARGUMENT, `unknown command: ${String(command.type)}`);
|
|
237
256
|
}
|
|
238
257
|
}
|
|
@@ -943,6 +962,58 @@ export class TeamService {
|
|
|
943
962
|
return { item, duplicate: false };
|
|
944
963
|
}
|
|
945
964
|
|
|
965
|
+
async answerDecision(command, payload) {
|
|
966
|
+
if (command.actor.kind !== 'human') fail(ERRORS.UNAUTHORIZED, 'only Human may answer a decision');
|
|
967
|
+
const item = this.state.inbox?.[payload.itemId];
|
|
968
|
+
if (item === undefined) fail(ERRORS.NOT_FOUND, `inbox item ${String(payload.itemId)} not found`);
|
|
969
|
+
if (item.type !== 'decision_required' || item.requiresDecision !== true) {
|
|
970
|
+
fail(ERRORS.INVALID_STATE, 'this inbox item is not a Human decision');
|
|
971
|
+
}
|
|
972
|
+
const humanId = command.actor.memberId;
|
|
973
|
+
if (item.memberId !== humanId) fail(ERRORS.UNAUTHORIZED, 'decision belongs to another member');
|
|
974
|
+
const answers = (Array.isArray(payload.answers) ? payload.answers : [])
|
|
975
|
+
.map((answer, index) => ({
|
|
976
|
+
id: typeof answer?.id === 'string' && answer.id !== '' ? answer.id : `q${index + 1}`,
|
|
977
|
+
value: typeof answer?.value === 'string' ? answer.value.trim() : '',
|
|
978
|
+
}))
|
|
979
|
+
.filter((answer) => answer.value !== '');
|
|
980
|
+
if (answers.length === 0) fail(ERRORS.INVALID_ARGUMENT, 'at least one answer is required');
|
|
981
|
+
const questions = Array.isArray(item.ref?.questions) ? item.ref.questions : [];
|
|
982
|
+
const lines = answers.map((answer) => {
|
|
983
|
+
const question = questions.find((candidate) => candidate.id === answer.id);
|
|
984
|
+
return `- ${question?.text ?? answer.id}: ${answer.value}`;
|
|
985
|
+
});
|
|
986
|
+
const body = `Human 决策:\n${lines.join('\n')}${typeof payload.note === 'string' && payload.note.trim() !== '' ? `\n备注:${payload.note.trim()}` : ''}`;
|
|
987
|
+
let entry = null;
|
|
988
|
+
if (typeof item.ref?.taskId === 'string' && this.state.tasks?.[item.ref.taskId] !== undefined) {
|
|
989
|
+
const taskId = item.ref.taskId;
|
|
990
|
+
const thread = this.state.threads?.[taskId] ?? createThread(taskId);
|
|
991
|
+
const outcome = appendReply(thread, {
|
|
992
|
+
authorId: humanId,
|
|
993
|
+
actorKind: 'human',
|
|
994
|
+
baseSeq: thread.headSeq,
|
|
995
|
+
body,
|
|
996
|
+
}, { at: this.now() });
|
|
997
|
+
await this.ledger.append({
|
|
998
|
+
type: 'thread/posted',
|
|
999
|
+
payload: { taskId, ...outcome.entry },
|
|
1000
|
+
actor: command.actor,
|
|
1001
|
+
projectId: command.projectId,
|
|
1002
|
+
at: this.now(),
|
|
1003
|
+
});
|
|
1004
|
+
entry = outcome.entry;
|
|
1005
|
+
}
|
|
1006
|
+
const next = ackInboxItem(item, { disposition: 'done', decision: { answers, note: payload.note ?? '', body } }, { at: this.now() });
|
|
1007
|
+
await this.ledger.append({
|
|
1008
|
+
type: 'inbox/acked',
|
|
1009
|
+
payload: { itemId: next.id, memberId: next.memberId, state: next.state, disposition: next.disposition, at: next.ackedAt },
|
|
1010
|
+
actor: command.actor,
|
|
1011
|
+
projectId: command.projectId,
|
|
1012
|
+
at: this.now(),
|
|
1013
|
+
});
|
|
1014
|
+
return { item: next, entry };
|
|
1015
|
+
}
|
|
1016
|
+
|
|
946
1017
|
async ack(command, payload) {
|
|
947
1018
|
const state = this.state;
|
|
948
1019
|
const item = state.inbox?.[payload.itemId];
|
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.21",
|
|
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",
|