@suifei/dsh-ai-team 0.3.10 → 0.3.12
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/README.md +4 -2
- package/client.js +149 -12
- package/index.js +57 -24
- package/lib/host/member-tools.js +40 -6
- package/lib/host/project-registry.js +2 -2
- package/lib/host/project-runtime.js +2 -2
- package/lib/host/service.js +19 -1
- package/lib/host/waker.js +5 -1
- package/lib/ledger/fold.js +13 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.12 — 2026-11-10
|
|
4
|
+
|
|
5
|
+
- 点击成员名字直接打开该 Agent 的 DSH 原生 Session,可与它直接对话;
|
|
6
|
+
- 没有 session 的空闲成员会先激活再打开;
|
|
7
|
+
- 「⋯」仍打开成员资料,资料内也增加「直接对话」按钮。
|
|
8
|
+
|
|
9
|
+
## 0.3.11 — 2026-11-10
|
|
10
|
+
|
|
11
|
+
- 修复空闲默认成员不接收 Main stream:owner 无 session 时先自动激活,再记录 `wake/delivered`;
|
|
12
|
+
- Project 增加工作目录:默认项目首次进入提示选择,新建项目必填,成员 cwd 使用项目目录;
|
|
13
|
+
- 多 Project 用 name + workspacePath 区分,切换时显示各自目录;
|
|
14
|
+
- `team` 工具新增 `members`(返回精确 id/name/role),创建 Task 时 ownerId/assigneeId 按 id 或名字解析并校验;
|
|
15
|
+
- Main stream wake prompt 注入成员 id 清单,避免模型把名字拼音当 ownerId。
|
|
16
|
+
|
|
3
17
|
## 0.3.10 — 2026-11-09
|
|
4
18
|
|
|
5
19
|
- 默认成员改为小说式虚拟名:墨渊(主理人)/ 青岚(执行)/ 白砚(验收);
|
package/README.md
CHANGED
|
@@ -64,6 +64,8 @@ dsh --profile ai-team --no-open --port 33271
|
|
|
64
64
|
|
|
65
65
|
打开终端输出的 URL,点侧栏 **文渊阁**。
|
|
66
66
|
|
|
67
|
+
第一次进入 Project 先设置 **工作目录**:默认 Project 会显示「这个项目还没有工作目录」,点「选择目录」或用输入框填路径;新建项目时也会要求填工作目录。每个 Project 独立一个目录,成员只在这个目录里执行 Task。
|
|
68
|
+
|
|
67
69
|
> 插件只应安装在可信机器 / 可信 Workspace:成员以 `danger-full-access` 运行,且 profile patch 会把 sandbox / approval / permission 固定为 `danger-full-access / never`。普通 `desktop`/`web` profile 不安装时保持原有 preset roster。
|
|
68
70
|
|
|
69
71
|
---
|
|
@@ -94,14 +96,14 @@ dsh --profile ai-team --no-open --port 33271
|
|
|
94
96
|
|
|
95
97
|
| 能力 | 说明 |
|
|
96
98
|
|---|---|
|
|
97
|
-
| Project | 具名 owner
|
|
99
|
+
| Project | 具名 owner;独立工作目录;并发 runtime;每 Project 独立账本、Waker、SSE |
|
|
98
100
|
| Task | 唯一 accountable owner;pending / in_progress / pending_acceptance / done / cancelled |
|
|
99
101
|
| Thread | append-only;回复带 `baseSeq`,旧基线会被拒并说明变化 |
|
|
100
102
|
| 验收 | maker 不能验收;QA 硬门禁;`qaChecklist` 逐项通过 |
|
|
101
103
|
| 收件箱 | Human/Agent 各自拉取;跨 Project 聚合与项目内 ack |
|
|
102
104
|
| 关注 | 关注 Task;完成后收到 `followed_task_update` |
|
|
103
105
|
| 默认成员 | 新 Project 初始化墨渊 / 青岚 / 白砚;可改名、改职责、改个性、改模型 |
|
|
104
|
-
| 成员 | 持久 session
|
|
106
|
+
| 成员 | 持久 session;点击名字直接打开该 Agent 的 DSH Session 对话;「⋯」编辑资料 |
|
|
105
107
|
| 模型 | Provider / Model 下拉读取 DSH `llm` 目录;可跟随系统默认 |
|
|
106
108
|
| 进化 | `team.evolve` 只改自己的 personality;验收后 reflection wake |
|
|
107
109
|
| 人才库 | leave/rejoin;遗留 Task 可被接管(taken-over) |
|
package/client.js
CHANGED
|
@@ -178,14 +178,30 @@ window.__ModuleLoader__.load({
|
|
|
178
178
|
projectSwitchMock: 'spike:项目切换只更新视图标题;按项目读取账本在 M1 接入。',
|
|
179
179
|
archivedMock: 'spike:归档项目只读;取消归档在 M3 接入。',
|
|
180
180
|
agentMock: 'spike:成员详情(打开会话 / 发消息 / 记忆与技能)在 M3 接入。',
|
|
181
|
-
agentOpen: '
|
|
181
|
+
agentOpen: '与 {name} 对话',
|
|
182
182
|
moreActions: '更多操作',
|
|
183
|
+
agentChat: '直接对话',
|
|
184
|
+
agentChatOpening: '正在打开对话…',
|
|
185
|
+
agentChatUnavailable: '宿主没有可用的 Session 打开能力。',
|
|
183
186
|
newProject: '新建项目',
|
|
184
187
|
addAgent: '添加成员',
|
|
185
188
|
newProjectMock: 'spike:新建项目属于 S-01 流程,M1 之后接入。',
|
|
186
189
|
newProjectTitle: '新建项目',
|
|
187
190
|
projectName: '项目名',
|
|
188
191
|
projectGoal: '目标',
|
|
192
|
+
workspace: '工作目录',
|
|
193
|
+
workspaceUnset: '这个项目还没有工作目录',
|
|
194
|
+
workspaceHint: '先选择代码目录再开工;每个 Project 使用独立目录。',
|
|
195
|
+
workspaceChoose: '选择目录',
|
|
196
|
+
workspaceChange: '更换目录',
|
|
197
|
+
workspaceRequired: '请先设置项目工作目录。',
|
|
198
|
+
workspaceSaved: '项目工作目录已保存。',
|
|
199
|
+
workspaceDialogTitle: '设置项目工作目录',
|
|
200
|
+
workspaceDialogHint: 'Project 的成员会在这个目录里执行 Task;多个 Project 用不同目录隔离。',
|
|
201
|
+
workspacePathPlaceholder: '例如 D:/work/my-project',
|
|
202
|
+
workspacePickUnavailable: '当前宿主不支持目录选择,请手动输入路径。',
|
|
203
|
+
workspacePickFailed: '目录选择失败,请手动输入路径。',
|
|
204
|
+
workspaceSave: '保存目录',
|
|
189
205
|
projectCreate: '创建项目',
|
|
190
206
|
projectCreated: '已创建项目 {name}。',
|
|
191
207
|
projectSwitched: '已切换项目。',
|
|
@@ -452,14 +468,30 @@ window.__ModuleLoader__.load({
|
|
|
452
468
|
projectSwitchMock: 'spike: switching projects only updates the header; per-project ledger reads land with M1.',
|
|
453
469
|
archivedMock: 'spike: archived projects are read-only; unarchive lands with M3.',
|
|
454
470
|
agentMock: 'spike: member detail (open session / message / memory & skills) lands with M3.',
|
|
455
|
-
agentOpen: '
|
|
471
|
+
agentOpen: 'Chat with {name}',
|
|
456
472
|
moreActions: 'More actions',
|
|
473
|
+
agentChat: 'Direct chat',
|
|
474
|
+
agentChatOpening: 'Opening conversation…',
|
|
475
|
+
agentChatUnavailable: 'This host cannot open a Session.',
|
|
457
476
|
newProject: 'New project',
|
|
458
477
|
addAgent: 'Add member',
|
|
459
478
|
newProjectMock: 'spike: project creation belongs to S-01 and lands after M1.',
|
|
460
479
|
newProjectTitle: 'New project',
|
|
461
480
|
projectName: 'Project name',
|
|
462
481
|
projectGoal: 'Goal',
|
|
482
|
+
workspace: 'Workspace',
|
|
483
|
+
workspaceUnset: 'This Project has no workspace yet',
|
|
484
|
+
workspaceHint: 'Choose a code directory before starting; each Project uses its own directory.',
|
|
485
|
+
workspaceChoose: 'Choose directory',
|
|
486
|
+
workspaceChange: 'Change directory',
|
|
487
|
+
workspaceRequired: 'Set the Project workspace first.',
|
|
488
|
+
workspaceSaved: 'Project workspace saved.',
|
|
489
|
+
workspaceDialogTitle: 'Set Project workspace',
|
|
490
|
+
workspaceDialogHint: 'Members run Tasks in this directory; separate Projects use separate directories.',
|
|
491
|
+
workspacePathPlaceholder: 'e.g. D:/work/my-project',
|
|
492
|
+
workspacePickUnavailable: 'This host has no directory picker; type a path.',
|
|
493
|
+
workspacePickFailed: 'Directory picker failed; type a path.',
|
|
494
|
+
workspaceSave: 'Save directory',
|
|
463
495
|
projectCreate: 'Create project',
|
|
464
496
|
projectCreated: 'Created project {name}.',
|
|
465
497
|
projectSwitched: 'Switched project.',
|
|
@@ -910,6 +942,10 @@ window.__ModuleLoader__.load({
|
|
|
910
942
|
.at-chead { flex: 0 0 auto; padding: 14px 18px 0; }
|
|
911
943
|
.at-title { display: flex; align-items: baseline; gap: 8px; min-width: 0; color: var(--dsw-alias-label-primary); font-size: 16px; font-weight: 600; line-height: 22px; letter-spacing: -.01em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
912
944
|
.at-goal { min-width: 0; margin-top: 2px; color: var(--dsw-alias-label-tertiary); font-size: 12px; line-height: 18px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
945
|
+
.at-workspacebar { display: flex; align-items: center; gap: 8px; min-width: 0; margin-top: 4px; }
|
|
946
|
+
.at-workspacepath { flex: 1 1 auto; min-width: 0; color: var(--dsw-alias-label-tertiary); font-size: 11px; line-height: 16px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
947
|
+
.at-workspacebtn { flex: 0 0 auto; padding: 2px 7px; border: 1px solid var(--dsw-alias-border-l2); border-radius: var(--dsw-radius-sm); background: transparent; color: var(--dsw-alias-label-secondary); font: inherit; font-size: 11px; line-height: 16px; cursor: pointer; }
|
|
948
|
+
.at-workspacebtn:hover { color: var(--dsw-alias-label-primary); border-color: var(--dsw-alias-label-tertiary); }
|
|
913
949
|
.at-tabs { display: flex; gap: 4px; margin-top: 8px; border-bottom: 1px solid var(--dsw-alias-border-l1); }
|
|
914
950
|
.at-tab { min-height: 36px; display: inline-flex; align-items: center; gap: 6px; padding: 0 12px; border: 0; border-bottom: 2px solid transparent; background: transparent; color: var(--dsw-alias-label-tertiary); font: inherit; font-size: 13px; font-weight: 500; cursor: pointer; }
|
|
915
951
|
.at-tab:hover { color: var(--dsw-alias-label-primary); }
|
|
@@ -1452,7 +1488,7 @@ window.__ModuleLoader__.load({
|
|
|
1452
1488
|
h('span', { className: 'at-prowtitle' }, project.name));
|
|
1453
1489
|
}
|
|
1454
1490
|
|
|
1455
|
-
function AgentRow({ t, agent, onAgent }) {
|
|
1491
|
+
function AgentRow({ t, agent, onAgent, onAgentMore }) {
|
|
1456
1492
|
const statusText = t(AGENT_STATUS[agent.status] ?? 'agentIdle');
|
|
1457
1493
|
const config = agent.config ?? null;
|
|
1458
1494
|
const model = config?.model ? config.model : t('agentDefaultModel');
|
|
@@ -1480,11 +1516,11 @@ window.__ModuleLoader__.load({
|
|
|
1480
1516
|
type: 'button',
|
|
1481
1517
|
'aria-label': tf(t, 'agentOpen', { name: agent.name }),
|
|
1482
1518
|
title: t('moreActions'),
|
|
1483
|
-
onClick: () =>
|
|
1519
|
+
onClick: () => onAgentMore(agent),
|
|
1484
1520
|
}, '⋯'));
|
|
1485
1521
|
}
|
|
1486
1522
|
|
|
1487
|
-
function LeftPanel({ t, data, projectId, scope, tab, inboxUnread, onBack, onInbox, onBoard, onProject, onNewProject, onAgent, onAddAgent, onRecruit }) {
|
|
1523
|
+
function LeftPanel({ t, data, projectId, scope, tab, inboxUnread, onBack, onInbox, onBoard, onProject, onNewProject, onAgent, onAgentMore, onAddAgent, onRecruit }) {
|
|
1488
1524
|
const projects = data.projects.filter((project) => !project.archived);
|
|
1489
1525
|
const activeAgents = data.agents.filter((agent) => agent.status !== 'retired');
|
|
1490
1526
|
const talentAgents = data.agents.filter((agent) => agent.status === 'retired');
|
|
@@ -1534,7 +1570,7 @@ window.__ModuleLoader__.load({
|
|
|
1534
1570
|
h('button', { className: 'at-mini', type: 'button', 'aria-label': t('addAgent'), title: t('addAgent'), onClick: onAddAgent }, h(Ico, { name: 'plus', size: 12 })),
|
|
1535
1571
|
),
|
|
1536
1572
|
activeAgents.length
|
|
1537
|
-
? h('div', { role: 'list', 'aria-label': t('agentsAria') }, activeAgents.map((agent) => h(AgentRow, { key: agent.id, t, agent, onAgent })))
|
|
1573
|
+
? h('div', { role: 'list', 'aria-label': t('agentsAria') }, activeAgents.map((agent) => h(AgentRow, { key: agent.id, t, agent, onAgent, onAgentMore })))
|
|
1538
1574
|
: h('div', { className: 'at-colempty' }, t('emptyColumn')),
|
|
1539
1575
|
talentAgents.length ? h('details', { className: 'at-archived' },
|
|
1540
1576
|
h('summary', null, tf(t, 'talentPoolCount', { n: talentAgents.length })),
|
|
@@ -1953,6 +1989,11 @@ window.__ModuleLoader__.load({
|
|
|
1953
1989
|
const [newOpen, setNewOpen] = React.useState(false);
|
|
1954
1990
|
const [nName, setNName] = React.useState('');
|
|
1955
1991
|
const [nGoal, setNGoal] = React.useState('');
|
|
1992
|
+
const [nWorkspace, setNWorkspace] = React.useState('');
|
|
1993
|
+
const [workspaceOpen, setWorkspaceOpen] = React.useState(false);
|
|
1994
|
+
const [workspaceDraft, setWorkspaceDraft] = React.useState('');
|
|
1995
|
+
const [workspaceError, setWorkspaceError] = React.useState('');
|
|
1996
|
+
const [workspaceBusy, setWorkspaceBusy] = React.useState(false);
|
|
1956
1997
|
const [nBusy, setNBusy] = React.useState(false);
|
|
1957
1998
|
const [nError, setNError] = React.useState('');
|
|
1958
1999
|
const [pSuccessor, setPSuccessor] = React.useState('');
|
|
@@ -2091,7 +2132,11 @@ window.__ModuleLoader__.load({
|
|
|
2091
2132
|
|
|
2092
2133
|
if (phase === 'loading' || !data) return h(Skeleton, { t });
|
|
2093
2134
|
|
|
2094
|
-
const
|
|
2135
|
+
const projectRecord = data.projects.find((item) => item.id === projectId) ?? data.projects[0] ?? null;
|
|
2136
|
+
const project = projectRecord === null ? null : {
|
|
2137
|
+
...projectRecord,
|
|
2138
|
+
workspacePath: projectRecord.workspacePath || (data.project?.id === projectRecord.id ? data.project.workspacePath ?? '' : ''),
|
|
2139
|
+
};
|
|
2095
2140
|
const tasks = data.tasks;
|
|
2096
2141
|
const boardTaskCount = tasks.filter((task) => ['pending', 'in_progress', 'pending_acceptance', 'done'].includes(task.status)).length;
|
|
2097
2142
|
const selectedTask = tasks.find((task) => task.id === selected) ?? tasks[0] ?? null;
|
|
@@ -2201,15 +2246,54 @@ window.__ModuleLoader__.load({
|
|
|
2201
2246
|
function openNewProject() {
|
|
2202
2247
|
setNName('');
|
|
2203
2248
|
setNGoal('');
|
|
2249
|
+
setNWorkspace('');
|
|
2204
2250
|
setNError('');
|
|
2205
2251
|
setNewOpen(true);
|
|
2206
2252
|
}
|
|
2253
|
+
async function pickWorkspacePath() {
|
|
2254
|
+
try {
|
|
2255
|
+
const picked = await ctx?.uiWorkspace?.pickDirectory?.();
|
|
2256
|
+
return picked ?? null;
|
|
2257
|
+
} catch {
|
|
2258
|
+
return null;
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
async function pickNewWorkspace() {
|
|
2262
|
+
const picked = await pickWorkspacePath();
|
|
2263
|
+
if (picked) setNWorkspace(picked);
|
|
2264
|
+
else setNError(t('workspacePickUnavailable'));
|
|
2265
|
+
}
|
|
2266
|
+
function openWorkspaceEditor() {
|
|
2267
|
+
setWorkspaceDraft(project?.workspacePath ?? '');
|
|
2268
|
+
setWorkspaceError('');
|
|
2269
|
+
setWorkspaceOpen(true);
|
|
2270
|
+
}
|
|
2271
|
+
async function pickProjectWorkspace() {
|
|
2272
|
+
const picked = await pickWorkspacePath();
|
|
2273
|
+
if (picked) setWorkspaceDraft(picked);
|
|
2274
|
+
else setWorkspaceError(t('workspacePickUnavailable'));
|
|
2275
|
+
}
|
|
2276
|
+
async function saveWorkspace() {
|
|
2277
|
+
if (!project || workspaceBusy || !workspaceDraft.trim()) return;
|
|
2278
|
+
setWorkspaceBusy(true);
|
|
2279
|
+
setWorkspaceError('');
|
|
2280
|
+
try {
|
|
2281
|
+
await runCommand('project.workspace.update', { workspacePath: workspaceDraft.trim() }, project.id);
|
|
2282
|
+
setWorkspaceOpen(false);
|
|
2283
|
+
say(t('workspaceSaved'));
|
|
2284
|
+
await reloadData();
|
|
2285
|
+
} catch (error) {
|
|
2286
|
+
setWorkspaceError(error?.message ?? String(error));
|
|
2287
|
+
} finally {
|
|
2288
|
+
setWorkspaceBusy(false);
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2207
2291
|
async function createProject() {
|
|
2208
|
-
if (!nName.trim() || nBusy) return;
|
|
2292
|
+
if (!nName.trim() || !nWorkspace.trim() || nBusy) return;
|
|
2209
2293
|
setNBusy(true);
|
|
2210
2294
|
setNError('');
|
|
2211
2295
|
try {
|
|
2212
|
-
const result = await runCommand('project.create', { name: nName.trim(), goal: nGoal.trim() });
|
|
2296
|
+
const result = await runCommand('project.create', { name: nName.trim(), goal: nGoal.trim(), workspacePath: nWorkspace.trim() });
|
|
2213
2297
|
const next = result.project;
|
|
2214
2298
|
setNewOpen(false);
|
|
2215
2299
|
setScope('project');
|
|
@@ -2311,6 +2395,30 @@ window.__ModuleLoader__.load({
|
|
|
2311
2395
|
setMBusy(false);
|
|
2312
2396
|
}
|
|
2313
2397
|
}
|
|
2398
|
+
async function openAgentChat(agent) {
|
|
2399
|
+
closeMobile();
|
|
2400
|
+
if (ctx?.uiWorkspace?.openSession === undefined) {
|
|
2401
|
+
say(t('agentChatUnavailable'));
|
|
2402
|
+
openProfile(agent);
|
|
2403
|
+
return;
|
|
2404
|
+
}
|
|
2405
|
+
if (agent.sessionId) {
|
|
2406
|
+
ctx.uiWorkspace.openSession(agent.sessionId);
|
|
2407
|
+
return;
|
|
2408
|
+
}
|
|
2409
|
+
try {
|
|
2410
|
+
say(t('agentChatOpening'));
|
|
2411
|
+
const activated = await runCommand('member.activate', {
|
|
2412
|
+
memberId: agent.id,
|
|
2413
|
+
prompt: '你正在与 Human 直接对话。先简要说明你是谁、当前负责什么;不要创建 Task,除非 Human 明确要求。',
|
|
2414
|
+
});
|
|
2415
|
+
await reloadData();
|
|
2416
|
+
if (activated?.sessionId) ctx.uiWorkspace.openSession(activated.sessionId);
|
|
2417
|
+
} catch (error) {
|
|
2418
|
+
say(tf(t, 'sendFailed', { message: error?.message ?? String(error) }));
|
|
2419
|
+
openProfile(agent);
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2314
2422
|
function openProfile(agent) {
|
|
2315
2423
|
setProfileAgent(agent);
|
|
2316
2424
|
setPName(agent.name ?? '');
|
|
@@ -2494,6 +2602,7 @@ window.__ModuleLoader__.load({
|
|
|
2494
2602
|
async function send() {
|
|
2495
2603
|
const text = draft.trim();
|
|
2496
2604
|
if (!text) return;
|
|
2605
|
+
if (!project?.workspacePath) { say(t('workspaceRequired')); return; }
|
|
2497
2606
|
try {
|
|
2498
2607
|
if (mode === 'task') {
|
|
2499
2608
|
await runCommand('task.create', { title: text, resultDefinition: text, qaRequired: qaTask });
|
|
@@ -2674,7 +2783,8 @@ window.__ModuleLoader__.load({
|
|
|
2674
2783
|
onBoard: switchBoard,
|
|
2675
2784
|
onProject: switchProject,
|
|
2676
2785
|
onNewProject: () => { closeMobile(); openNewProject(); },
|
|
2677
|
-
onAgent: (agent) => { closeMobile();
|
|
2786
|
+
onAgent: (agent) => { closeMobile(); void openAgentChat(agent); },
|
|
2787
|
+
onAgentMore: (agent) => { closeMobile(); openProfile(agent); },
|
|
2678
2788
|
onAddAgent: () => { closeMobile(); openAddAgent(); },
|
|
2679
2789
|
onRecruit: (agent) => { closeMobile(); recruit(agent); },
|
|
2680
2790
|
};
|
|
@@ -2760,6 +2870,10 @@ window.__ModuleLoader__.load({
|
|
|
2760
2870
|
h('header', { className: 'at-chead' },
|
|
2761
2871
|
h('div', { className: 'at-title', title: headerTitle }, headerTitle),
|
|
2762
2872
|
headerGoal ? h('div', { className: 'at-goal', title: headerGoal }, headerGoal) : null,
|
|
2873
|
+
!inInbox && project ? h('div', { className: 'at-workspacebar' },
|
|
2874
|
+
h('span', { className: 'at-workspacepath', title: project.workspacePath || t('workspaceHint') },
|
|
2875
|
+
project.workspacePath ? t('workspace') + ':' + project.workspacePath : t('workspaceUnset')),
|
|
2876
|
+
h('button', { className: 'at-workspacebtn', type: 'button', onClick: openWorkspaceEditor }, project.workspacePath ? t('workspaceChange') : t('workspaceChoose'))) : null,
|
|
2763
2877
|
inInbox ? null : h('div', { className: 'at-tabs', role: 'tablist', 'aria-label': t('tabsAria'), onKeyDown: tabKeyDown },
|
|
2764
2878
|
h('button', {
|
|
2765
2879
|
className: cx('at-tab', tab === 'main' && 'at-on'),
|
|
@@ -2804,7 +2918,7 @@ window.__ModuleLoader__.load({
|
|
|
2804
2918
|
t,
|
|
2805
2919
|
mode,
|
|
2806
2920
|
draft,
|
|
2807
|
-
disabled: !project,
|
|
2921
|
+
disabled: !project || !project.workspacePath,
|
|
2808
2922
|
qaRequired: qaTask,
|
|
2809
2923
|
onQaRequired: setQaTask,
|
|
2810
2924
|
onMode: setMode,
|
|
@@ -2947,10 +3061,29 @@ window.__ModuleLoader__.load({
|
|
|
2947
3061
|
pError ? h('div', { className: 'at-modalerror', role: 'alert' }, pError) : null,
|
|
2948
3062
|
h('div', { className: 'at-modalactions' },
|
|
2949
3063
|
h('button', { className: 'at-btn', type: 'button', onClick: () => setProfileAgent(null), disabled: pBusy }, t('cancel')),
|
|
3064
|
+
h('button', { className: 'at-btn', type: 'button', onClick: () => { const target = profileAgent; setProfileAgent(null); void openAgentChat(target); } }, t('agentChat')),
|
|
2950
3065
|
h('button', { className: 'at-btn', type: 'button', onClick: leaveProfile, disabled: pBusy }, t('leaveTeam')),
|
|
2951
3066
|
profileAgent.status === 'failed' ? h('button', { className: 'at-btn', type: 'button', onClick: retryProfile, disabled: pBusy }, t('retry')) : null,
|
|
2952
3067
|
h('button', { className: 'at-btn', type: 'button', onClick: distillProfile, disabled: pBusy }, t('redistill')),
|
|
2953
3068
|
h('button', { className: 'at-btn at-btn-primary', type: 'submit', disabled: pBusy || !pName.trim() }, pBusy ? t('saving') : t('save'))))) : null,
|
|
3069
|
+
workspaceOpen ? h('div', { className: 'at-modalback' },
|
|
3070
|
+
h('form', {
|
|
3071
|
+
className: 'at-modal',
|
|
3072
|
+
role: 'dialog',
|
|
3073
|
+
'aria-modal': 'true',
|
|
3074
|
+
'aria-label': t('workspaceDialogTitle'),
|
|
3075
|
+
onSubmit: (event) => { event.preventDefault(); saveWorkspace(); },
|
|
3076
|
+
},
|
|
3077
|
+
h('h2', { className: 'at-modaltitle' }, t('workspaceDialogTitle')),
|
|
3078
|
+
h('p', { className: 'at-modalmeta' }, t('workspaceDialogHint')),
|
|
3079
|
+
h('label', { className: 'at-field' }, h('span', null, t('workspace')),
|
|
3080
|
+
h('div', { className: 'at-inlineadd' },
|
|
3081
|
+
h('input', { value: workspaceDraft, onChange: (event) => setWorkspaceDraft(event.target.value), placeholder: t('workspacePathPlaceholder'), autoFocus: true }),
|
|
3082
|
+
h('button', { className: 'at-btn at-btn-small', type: 'button', onClick: pickProjectWorkspace }, t('workspaceChoose')))),
|
|
3083
|
+
workspaceError ? h('div', { className: 'at-modalerror', role: 'alert' }, workspaceError) : null,
|
|
3084
|
+
h('div', { className: 'at-modalactions' },
|
|
3085
|
+
h('button', { className: 'at-btn', type: 'button', onClick: () => setWorkspaceOpen(false), disabled: workspaceBusy }, t('cancel')),
|
|
3086
|
+
h('button', { className: 'at-btn at-btn-primary', type: 'submit', disabled: workspaceBusy || !workspaceDraft.trim() }, workspaceBusy ? t('saving') : t('workspaceSave'))))) : null,
|
|
2954
3087
|
newOpen ? h('div', { className: 'at-modalback' },
|
|
2955
3088
|
h('form', {
|
|
2956
3089
|
className: 'at-modal',
|
|
@@ -2964,10 +3097,14 @@ window.__ModuleLoader__.load({
|
|
|
2964
3097
|
h('input', { value: nName, onChange: (event) => setNName(event.target.value), autoFocus: true })),
|
|
2965
3098
|
h('label', { className: 'at-field' }, h('span', null, t('projectGoal')),
|
|
2966
3099
|
h('textarea', { value: nGoal, onChange: (event) => setNGoal(event.target.value) })),
|
|
3100
|
+
h('label', { className: 'at-field' }, h('span', null, t('workspace')),
|
|
3101
|
+
h('div', { className: 'at-inlineadd' },
|
|
3102
|
+
h('input', { value: nWorkspace, onChange: (event) => setNWorkspace(event.target.value), placeholder: t('workspacePathPlaceholder') }),
|
|
3103
|
+
h('button', { className: 'at-btn at-btn-small', type: 'button', onClick: pickNewWorkspace }, t('workspaceChoose')))),
|
|
2967
3104
|
nError ? h('div', { className: 'at-modalerror', role: 'alert' }, nError) : null,
|
|
2968
3105
|
h('div', { className: 'at-modalactions' },
|
|
2969
3106
|
h('button', { className: 'at-btn', type: 'button', onClick: () => setNewOpen(false), disabled: nBusy }, t('cancel')),
|
|
2970
|
-
h('button', { className: 'at-btn at-btn-primary', type: 'submit', disabled: nBusy || !nName.trim() }, nBusy ? t('saving') : t('projectCreate'))))) : null));
|
|
3107
|
+
h('button', { className: 'at-btn at-btn-primary', type: 'submit', disabled: nBusy || !nName.trim() || !nWorkspace.trim() }, nBusy ? t('saving') : t('projectCreate'))))) : null));
|
|
2971
3108
|
}
|
|
2972
3109
|
|
|
2973
3110
|
function PavilionMark({ size = 18 }) {
|
package/index.js
CHANGED
|
@@ -33,7 +33,7 @@ export const name = 'ai-team';
|
|
|
33
33
|
/** The workbench only makes sense where the Web carrier exists. */
|
|
34
34
|
export const inject = ['webServer', 'permissionPresets', 'sandboxPolicy'];
|
|
35
35
|
|
|
36
|
-
const VERSION = '0.3.
|
|
36
|
+
const VERSION = '0.3.12';
|
|
37
37
|
const PROJECT_ID = 'prj_main';
|
|
38
38
|
const HUMAN_ID = 'mem_human';
|
|
39
39
|
|
|
@@ -91,6 +91,7 @@ async function boot() {
|
|
|
91
91
|
projectId: project.id,
|
|
92
92
|
projectName: project.name,
|
|
93
93
|
projectGoal: project.goal,
|
|
94
|
+
workspacePath: project.workspacePath ?? '',
|
|
94
95
|
});
|
|
95
96
|
runtimes.set(project.id, runtime);
|
|
96
97
|
}
|
|
@@ -208,6 +209,11 @@ export function apply(ctx) {
|
|
|
208
209
|
let host = null;
|
|
209
210
|
let hostPromise = null;
|
|
210
211
|
const wakeKeys = new Set();
|
|
212
|
+
const activationTasks = new Map();
|
|
213
|
+
const projectCwd = (projectRuntime, member) => member?.workspace
|
|
214
|
+
|| projectRuntime?.service?.state?.projects?.[projectRuntime.id]?.workspacePath
|
|
215
|
+
|| projectRuntime?.dir
|
|
216
|
+
|| undefined;
|
|
211
217
|
const wakeFromResult = async (projectRuntime, result, actor = {}) => {
|
|
212
218
|
if (projectRuntime === null || projectRuntime === undefined) return;
|
|
213
219
|
const plans = planWakes(projectRuntime.service.state, result, actor);
|
|
@@ -220,31 +226,54 @@ export function apply(ctx) {
|
|
|
220
226
|
const providers = typeof subagents.list === 'function' ? subagents.list() : [];
|
|
221
227
|
if (providers.length === 0) return;
|
|
222
228
|
const parentInfo = await ensureParent();
|
|
229
|
+
const wakePrompt = async (plan, member) => describeWake(plan, projectRuntime.service.state)
|
|
230
|
+
+ await formatSkillCatalog(skillsService, { cwd: projectCwd(projectRuntime, member) })
|
|
231
|
+
+ await loadSkillContext(member, projectRuntime.dir);
|
|
223
232
|
for (const plan of plans) {
|
|
224
233
|
if (wakeKeys.has(plan.key)) continue;
|
|
225
|
-
|
|
226
|
-
if (member
|
|
234
|
+
let member = projectRuntime.service.state.members?.[plan.memberId];
|
|
235
|
+
if (member === undefined) continue;
|
|
227
236
|
// Reserve before the async delivery: the HTTP result and the ledger
|
|
228
237
|
// event both schedule the same plan, and only one may deliver it.
|
|
229
238
|
wakeKeys.add(plan.key);
|
|
230
|
-
const
|
|
231
|
-
subagents, sessions, permissionPresets, sandboxPolicy,
|
|
232
|
-
agents: ctx.get('agents'),
|
|
233
|
-
agentPresets: ctx.get('agentPresets'),
|
|
234
|
-
direct: true,
|
|
235
|
-
parent: parentInfo.agent,
|
|
236
|
-
provider: providers[0],
|
|
237
|
-
preset: 'standard',
|
|
238
|
-
cwd: member.workspace ?? projectRuntime.dataDir,
|
|
239
|
-
});
|
|
239
|
+
const cwd = projectCwd(projectRuntime, member);
|
|
240
240
|
try {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
241
|
+
// Idle default members have no Agent session yet. A wake must
|
|
242
|
+
// activate them first; otherwise the first Human message is
|
|
243
|
+
// silently dropped in a fresh Project.
|
|
244
|
+
let messageId = null;
|
|
245
|
+
if (member.sessionId === undefined || member.sessionId === null) {
|
|
246
|
+
const activationKey = `${projectRuntime.id}:${member.id}`;
|
|
247
|
+
let activation = activationTasks.get(activationKey);
|
|
248
|
+
if (activation === undefined) {
|
|
249
|
+
activation = activateMember({
|
|
250
|
+
payload: { memberId: member.id, prompt: await wakePrompt(plan, member) },
|
|
251
|
+
}, projectRuntime);
|
|
252
|
+
activationTasks.set(activationKey, activation);
|
|
253
|
+
activation.finally(() => activationTasks.delete(activationKey)).catch(() => {});
|
|
254
|
+
}
|
|
255
|
+
const activated = await activation;
|
|
256
|
+
if (activated.error !== undefined) {
|
|
257
|
+
throw Object.assign(new Error(activated.error.message), { code: activated.error.code });
|
|
258
|
+
}
|
|
259
|
+
messageId = activated.messageId ?? null;
|
|
260
|
+
member = projectRuntime.service.state.members?.[member.id] ?? member;
|
|
261
|
+
} else {
|
|
262
|
+
const runtime = new MemberRuntime({
|
|
263
|
+
subagents, sessions, permissionPresets, sandboxPolicy,
|
|
264
|
+
agents: ctx.get('agents'),
|
|
265
|
+
agentPresets: ctx.get('agentPresets'),
|
|
266
|
+
direct: true,
|
|
267
|
+
parent: parentInfo.agent,
|
|
268
|
+
provider: providers[0],
|
|
269
|
+
preset: 'standard',
|
|
270
|
+
cwd,
|
|
271
|
+
});
|
|
272
|
+
const delivered = await runtime.wake(member, { prompt: await wakePrompt(plan, member) });
|
|
273
|
+
messageId = delivered.messageId ?? null;
|
|
274
|
+
}
|
|
246
275
|
await projectRuntime.service.recordWake(plan.memberId, {
|
|
247
|
-
ok: true, taskId: plan.taskId, reason: plan.reason, messageId
|
|
276
|
+
ok: true, taskId: plan.taskId, reason: plan.reason, messageId,
|
|
248
277
|
});
|
|
249
278
|
} catch (error) {
|
|
250
279
|
wakeKeys.delete(plan.key);
|
|
@@ -432,12 +461,12 @@ export function apply(ctx) {
|
|
|
432
461
|
parent: parentInfo.agent,
|
|
433
462
|
provider: providers[0],
|
|
434
463
|
preset: 'standard',
|
|
435
|
-
cwd:
|
|
464
|
+
cwd: projectCwd(projectRuntime, member),
|
|
436
465
|
});
|
|
437
466
|
const result = await runtime.activate(activationMember, {
|
|
438
467
|
prompt: `${memberIdentity(activationMember)}\n\n${typeof body.payload?.prompt === 'string'
|
|
439
468
|
? body.payload.prompt
|
|
440
|
-
: '你被加入 AI Team。先读取项目简报与收件箱,再处理分配给你的任务。'}${await formatSkillCatalog(skillsService, { cwd: activationMember
|
|
469
|
+
: '你被加入 AI Team。先读取项目简报与收件箱,再处理分配给你的任务。'}${await formatSkillCatalog(skillsService, { cwd: projectCwd(projectRuntime, activationMember) })}${await loadSkillContext(activationMember, projectRuntime.dir)}`,
|
|
441
470
|
});
|
|
442
471
|
let bound;
|
|
443
472
|
try {
|
|
@@ -536,7 +565,7 @@ export function apply(ctx) {
|
|
|
536
565
|
}
|
|
537
566
|
};
|
|
538
567
|
|
|
539
|
-
async function activateProject(nextProjectId, projectName, projectGoal) {
|
|
568
|
+
async function activateProject(nextProjectId, projectName, projectGoal, workspacePath = '') {
|
|
540
569
|
await ready;
|
|
541
570
|
if (booted === null) throw new Error('team ledger is unavailable');
|
|
542
571
|
let next = projectRuntime(nextProjectId);
|
|
@@ -546,6 +575,7 @@ export function apply(ctx) {
|
|
|
546
575
|
projectId: nextProjectId,
|
|
547
576
|
projectName,
|
|
548
577
|
projectGoal,
|
|
578
|
+
workspacePath,
|
|
549
579
|
});
|
|
550
580
|
booted.runtimes.set(nextProjectId, next);
|
|
551
581
|
attachRuntime(next);
|
|
@@ -689,6 +719,7 @@ export function apply(ctx) {
|
|
|
689
719
|
...project,
|
|
690
720
|
owner: active?.owner ?? null,
|
|
691
721
|
goal: active?.goal ?? '',
|
|
722
|
+
workspacePath: active?.workspacePath ?? project.workspacePath ?? '',
|
|
692
723
|
archived: false,
|
|
693
724
|
active: project.id === booted.activeProjectId,
|
|
694
725
|
};
|
|
@@ -828,10 +859,12 @@ export function apply(ctx) {
|
|
|
828
859
|
if (body.type === 'project.create') {
|
|
829
860
|
const name = typeof body.payload?.name === 'string' ? body.payload.name.trim() : '';
|
|
830
861
|
const goal = typeof body.payload?.goal === 'string' ? body.payload.goal.trim() : '';
|
|
862
|
+
const workspacePath = typeof body.payload?.workspacePath === 'string' ? body.payload.workspacePath.trim() : '';
|
|
831
863
|
if (name === '') return send(res, { error: { code: 'E_INVALID_ARGUMENT', message: 'project name is required' } }, 400);
|
|
864
|
+
if (workspacePath === '') return send(res, { error: { code: 'E_INVALID_ARGUMENT', message: 'project workspacePath is required' } }, 400);
|
|
832
865
|
const id = ids.project();
|
|
833
|
-
booted.registry.add({ id, name, goal });
|
|
834
|
-
const next = await activateProject(id, name, goal);
|
|
866
|
+
booted.registry.add({ id, name, goal, workspacePath });
|
|
867
|
+
const next = await activateProject(id, name, goal, workspacePath);
|
|
835
868
|
return send(res, { ok: true, project: next.service.state.projects[id] ?? { id, name } });
|
|
836
869
|
}
|
|
837
870
|
if (body.type === 'project.activate') {
|
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', 'inbox', 'recall', 'create', 'member', 'evolve', 'distill', 'note', 'skill', 'claim', 'submit', 'thread', 'reply', 'accept', 'requestChanges'];
|
|
10
|
+
const ACTIONS = ['tasks', 'members', 'inbox', 'recall', 'create', 'member', 'evolve', 'distill', 'note', 'skill', 'claim', 'submit', 'thread', 'reply', 'accept', 'requestChanges'];
|
|
11
11
|
|
|
12
12
|
function failure(error) {
|
|
13
13
|
return {
|
|
@@ -29,6 +29,20 @@ function projectId(state, requested) {
|
|
|
29
29
|
return Object.keys(state.projects ?? {})[0] ?? null;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
function resolveMember(state, value) {
|
|
33
|
+
if (typeof value !== 'string' || value === '') return null;
|
|
34
|
+
const exact = state.members?.[value];
|
|
35
|
+
if (exact?.kind === 'agent') return exact;
|
|
36
|
+
return Object.values(state.members ?? {}).find((member) => member.kind === 'agent' && member.name === value) ?? null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function rosterText(state) {
|
|
40
|
+
return Object.values(state.members ?? {})
|
|
41
|
+
.filter((member) => member.kind === 'agent' && member.status !== 'retired')
|
|
42
|
+
.map((member) => `${member.name}=${member.id}`)
|
|
43
|
+
.join('、');
|
|
44
|
+
}
|
|
45
|
+
|
|
32
46
|
/**
|
|
33
47
|
* @param {{
|
|
34
48
|
* getState: () => any,
|
|
@@ -39,7 +53,7 @@ function projectId(state, requested) {
|
|
|
39
53
|
export function createTeamTool(options) {
|
|
40
54
|
return {
|
|
41
55
|
name: 'team',
|
|
42
|
-
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 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.
|
|
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.',
|
|
43
57
|
parameters: {
|
|
44
58
|
type: 'object',
|
|
45
59
|
properties: {
|
|
@@ -54,8 +68,8 @@ export function createTeamTool(options) {
|
|
|
54
68
|
body: { type: 'string', description: 'Thread reply body.' },
|
|
55
69
|
title: { type: 'string', description: 'Task title for create.' },
|
|
56
70
|
resultDefinition: { type: 'string', description: 'Acceptance definition for create.' },
|
|
57
|
-
ownerId: { type: 'string', description: 'Accountable owner for create; defaults to you.' },
|
|
58
|
-
assigneeId: { type: 'string', description: 'Optional assignee for create.' },
|
|
71
|
+
ownerId: { type: 'string', description: 'Accountable owner id from members for create; defaults to you.' },
|
|
72
|
+
assigneeId: { type: 'string', description: 'Optional assignee id from members for create.' },
|
|
59
73
|
qaRequired: { type: 'boolean', description: 'Require a QA member to accept this task.' },
|
|
60
74
|
qaChecklist: { type: 'array', items: { type: 'string' }, description: 'QA checklist items; all must pass before acceptance.' },
|
|
61
75
|
personality: { type: 'string', description: 'New working personality for evolve/distill.' },
|
|
@@ -89,6 +103,18 @@ export function createTeamTool(options) {
|
|
|
89
103
|
.map((task) => taskCard(state, task));
|
|
90
104
|
return { ok: true, projectId: pid, tasks };
|
|
91
105
|
}
|
|
106
|
+
if (args.action === 'members') {
|
|
107
|
+
const members = Object.values(state.members ?? {})
|
|
108
|
+
.filter((candidate) => candidate.kind === 'agent' && candidate.status !== 'retired')
|
|
109
|
+
.map((candidate) => ({
|
|
110
|
+
id: candidate.id,
|
|
111
|
+
name: candidate.name,
|
|
112
|
+
role: candidate.role,
|
|
113
|
+
status: candidate.status,
|
|
114
|
+
qa: /验收|qa|quality|test|verification/i.test(candidate.role ?? ''),
|
|
115
|
+
}));
|
|
116
|
+
return { ok: true, projectId: pid, members };
|
|
117
|
+
}
|
|
92
118
|
if (args.action === 'recall') {
|
|
93
119
|
const query = typeof args.query === 'string' ? args.query.trim().toLowerCase() : '';
|
|
94
120
|
if (query === '') return failure({ code: 'E_INVALID_ARGUMENT', message: 'query is required' });
|
|
@@ -172,11 +198,19 @@ export function createTeamTool(options) {
|
|
|
172
198
|
if (typeof args.resultDefinition !== 'string' || args.resultDefinition.trim() === '') {
|
|
173
199
|
return failure({ code: 'E_INVALID_ARGUMENT', message: 'resultDefinition is required' });
|
|
174
200
|
}
|
|
201
|
+
const owner = args.ownerId === undefined ? member : resolveMember(state, args.ownerId);
|
|
202
|
+
if (owner === null) {
|
|
203
|
+
return failure({ code: 'E_INVALID_ARGUMENT', message: `ownerId ${String(args.ownerId)} is not a Team member; use members to get ids (${rosterText(state)})` });
|
|
204
|
+
}
|
|
205
|
+
const assignee = args.assigneeId === undefined ? null : resolveMember(state, args.assigneeId);
|
|
206
|
+
if (args.assigneeId !== undefined && assignee === null) {
|
|
207
|
+
return failure({ code: 'E_INVALID_ARGUMENT', message: `assigneeId ${String(args.assigneeId)} is not a Team member; use members to get ids (${rosterText(state)})` });
|
|
208
|
+
}
|
|
175
209
|
const result = await command('task.create', {
|
|
176
210
|
title: args.title,
|
|
177
211
|
resultDefinition: args.resultDefinition,
|
|
178
|
-
ownerId:
|
|
179
|
-
...(
|
|
212
|
+
ownerId: owner.id,
|
|
213
|
+
...(assignee === null ? {} : { assigneeId: assignee.id }),
|
|
180
214
|
...(args.qaRequired === true ? { qaRequired: true } : {}),
|
|
181
215
|
...(Array.isArray(args.qaChecklist) ? { qaChecklist: args.qaChecklist } : {}),
|
|
182
216
|
});
|
|
@@ -41,10 +41,10 @@ export class ProjectRegistry {
|
|
|
41
41
|
return this.projects.map((project) => ({ ...project, active: project.id === this.activeProjectId }));
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
add({ id, name, createdAt, goal }) {
|
|
44
|
+
add({ id, name, createdAt, goal, workspacePath }) {
|
|
45
45
|
if (typeof id !== 'string' || id === '') throw new Error('project id is required');
|
|
46
46
|
if (!this.projects.some((project) => project.id === id)) {
|
|
47
|
-
this.projects.push({ id, name: name ?? id, goal: goal ?? '', createdAt: createdAt ?? Date.now() });
|
|
47
|
+
this.projects.push({ id, name: name ?? id, goal: goal ?? '', workspacePath: workspacePath ?? '', createdAt: createdAt ?? Date.now() });
|
|
48
48
|
this.save();
|
|
49
49
|
}
|
|
50
50
|
return this.projects.find((project) => project.id === id);
|
|
@@ -25,7 +25,7 @@ export class ProjectRuntime {
|
|
|
25
25
|
this.service = service;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
static async open({ rootDir, projectId = DEFAULT_PROJECT_ID, now = undefined, seed = true, projectName = '我的项目', projectGoal = '把事交给团队' }) {
|
|
28
|
+
static async open({ rootDir, projectId = DEFAULT_PROJECT_ID, now = undefined, seed = true, projectName = '我的项目', projectGoal = '把事交给团队', workspacePath = '' }) {
|
|
29
29
|
const dir = join(rootDir, 'projects', projectId);
|
|
30
30
|
mkdirSync(dir, { recursive: true });
|
|
31
31
|
const ledger = openLedger({ dir, projectId, fsync: true });
|
|
@@ -57,7 +57,7 @@ export class ProjectRuntime {
|
|
|
57
57
|
type: 'project.create',
|
|
58
58
|
actor: { kind: 'human', memberId: HUMAN_ID },
|
|
59
59
|
projectId,
|
|
60
|
-
payload: { id: projectId, name: projectName, goal: projectGoal },
|
|
60
|
+
payload: { id: projectId, name: projectName, goal: projectGoal, workspacePath },
|
|
61
61
|
});
|
|
62
62
|
}
|
|
63
63
|
await seedDefaultRoster(service, projectId, { humanId: HUMAN_ID });
|
package/lib/host/service.js
CHANGED
|
@@ -67,7 +67,7 @@ export class TeamService {
|
|
|
67
67
|
bootstrap(projectId, threadTaskId) {
|
|
68
68
|
const state = this.state;
|
|
69
69
|
const projects = Object.values(state.projects ?? {}).map((p) => ({
|
|
70
|
-
id: p.id, name: p.name, goal: p.goal ?? '', owner: p.ownerName ?? p.ownerMemberId ?? p.ownerId, active: p.status !== 'archived',
|
|
70
|
+
id: p.id, name: p.name, goal: p.goal ?? '', owner: p.ownerName ?? p.ownerMemberId ?? p.ownerId, workspacePath: p.workspacePath ?? '', active: p.status !== 'archived',
|
|
71
71
|
}));
|
|
72
72
|
const members = Object.values(state.members ?? {}).map((m) => ({
|
|
73
73
|
id: m.id,
|
|
@@ -143,6 +143,7 @@ export class TeamService {
|
|
|
143
143
|
switch (command.type) {
|
|
144
144
|
case 'project.create': return await this.createProject(command, payload);
|
|
145
145
|
case 'project.owner.update': return await this.changeProjectOwner(command, payload);
|
|
146
|
+
case 'project.workspace.update': return await this.changeProjectWorkspace(command, payload);
|
|
146
147
|
case 'stream.post': return await this.postStream(command, payload);
|
|
147
148
|
case 'member.add': return await this.addMember(command, payload);
|
|
148
149
|
case 'member.bind': return await this.bindMemberSession(command, payload);
|
|
@@ -213,6 +214,23 @@ export class TeamService {
|
|
|
213
214
|
return { project };
|
|
214
215
|
}
|
|
215
216
|
|
|
217
|
+
async changeProjectWorkspace(command, payload) {
|
|
218
|
+
if (command.actor.kind !== 'human') fail(ERRORS.UNAUTHORIZED, 'only Human may change the Project workspace');
|
|
219
|
+
const projectId = command.projectId;
|
|
220
|
+
const project = this.state.projects?.[projectId];
|
|
221
|
+
if (project === undefined) fail(ERRORS.NOT_FOUND, `project ${String(projectId)} not found`);
|
|
222
|
+
if (typeof payload.workspacePath !== 'string' || payload.workspacePath.trim() === '') {
|
|
223
|
+
fail(ERRORS.INVALID_ARGUMENT, 'workspacePath is required');
|
|
224
|
+
}
|
|
225
|
+
const workspacePath = payload.workspacePath.trim();
|
|
226
|
+
await this.append(command, 'project/workspace-changed', {
|
|
227
|
+
projectId,
|
|
228
|
+
workspacePath,
|
|
229
|
+
reason: payload.reason ?? '',
|
|
230
|
+
});
|
|
231
|
+
return { project: { ...project, workspacePath } };
|
|
232
|
+
}
|
|
233
|
+
|
|
216
234
|
async changeProjectOwner(command, payload) {
|
|
217
235
|
if (command.actor.kind !== 'human') fail(ERRORS.UNAUTHORIZED, 'only Human may change the Project owner');
|
|
218
236
|
const projectId = command.projectId;
|
package/lib/host/waker.js
CHANGED
|
@@ -156,12 +156,16 @@ export function planWakes(state, result, actor = {}) {
|
|
|
156
156
|
if (event?.type === 'stream/posted' && event.payload?.kind === 'human_message') {
|
|
157
157
|
const project = Object.values(state.projects ?? {})[0];
|
|
158
158
|
const body = typeof event.payload?.body === 'string' ? event.payload.body : '';
|
|
159
|
+
const roster = Object.values(state.members ?? {})
|
|
160
|
+
.filter((member) => member.kind === 'agent' && member.status !== 'retired')
|
|
161
|
+
.map((member) => `${member.name}=${member.id}(${member.role || '成员'})`)
|
|
162
|
+
.join('、');
|
|
159
163
|
add(
|
|
160
164
|
project?.ownerMemberId,
|
|
161
165
|
null,
|
|
162
166
|
'human_message',
|
|
163
167
|
`human_message:${event.seq ?? event.payload?.seq}`,
|
|
164
|
-
`你是 Project owner。Human 在 Main stream 说:“${body}
|
|
168
|
+
`你是 Project owner。Human 在 Main stream 说:“${body}”。本轮必须先用 team action=members 查看成员,再用 action=create 创建至少一个 Task;ownerId 必须使用成员 id,写明可验证的 resultDefinition。可用成员:${roster || '无'}。如果需求很大或外部链接只是参考,先创建“调研/澄清/拆解”Task,不要直接开始实现;如果确实无法创建,必须明确说明原因。resultDefinition 只写验收标准,不要写“等待 Human 验收”;不要只读文件或只回一段话结束。`,
|
|
165
169
|
);
|
|
166
170
|
}
|
|
167
171
|
if (event?.type === 'inbox/item-created') {
|
package/lib/ledger/fold.js
CHANGED
|
@@ -637,6 +637,19 @@ export function applyEvent(state, event) {
|
|
|
637
637
|
return advance(state, event, { projects: { ...state.projects, [project.id]: project } });
|
|
638
638
|
}
|
|
639
639
|
|
|
640
|
+
case 'project/workspace-changed': {
|
|
641
|
+
const projectId = textOr(payload.projectId, event.projectId);
|
|
642
|
+
const workspacePath = textOr(payload.workspacePath, null);
|
|
643
|
+
const project = state.projects?.[projectId];
|
|
644
|
+
if (project === undefined || workspacePath === null) return advance(state, event);
|
|
645
|
+
return advance(state, event, {
|
|
646
|
+
projects: {
|
|
647
|
+
...state.projects,
|
|
648
|
+
[projectId]: { ...project, workspacePath },
|
|
649
|
+
},
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
|
|
640
653
|
case 'project/owner-changed': {
|
|
641
654
|
const projectId = textOr(payload.projectId, event.projectId);
|
|
642
655
|
const ownerMemberId = textOr(payload.ownerMemberId, null);
|
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.12",
|
|
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",
|