dsh-collab-workbench 0.0.0-stage → 0.1.2
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.
Potentially problematic release.
This version of dsh-collab-workbench might be problematic. Click here for more details.
- package/LICENSE +202 -0
- package/README.md +112 -2
- package/client.js +2136 -0
- package/cordis.patch.yml +7 -0
- package/icon.svg +1 -0
- package/index.js +86 -0
- package/locale/en.json +1 -0
- package/locale/zh.json +1 -0
- package/package.json +60 -3
- package/scripts/build-approvals.mjs +15 -0
- package/scripts/check.mjs +17 -0
- package/src/client/approval-ui.js +117 -0
- package/src/client/update-ui.js +73 -0
- package/src/host/agents.js +235 -0
- package/src/host/api.js +264 -0
- package/src/host/approvals-fold.js +34 -0
- package/src/host/approvals.js +36 -0
- package/src/host/channels.js +509 -0
- package/src/host/delivery.js +95 -0
- package/src/host/execution-engine.js +417 -0
- package/src/host/execution-tools.js +65 -0
- package/src/host/execution.js +274 -0
- package/src/host/files.js +89 -0
- package/src/host/groups.js +501 -0
- package/src/host/ledger.js +177 -0
- package/src/host/memory.js +175 -0
- package/src/host/policy.js +43 -0
- package/src/host/tasks.js +117 -0
- package/src/host/tools.js +491 -0
- package/src/host/update.js +135 -0
- package/src/host/verify.js +44 -0
- package/src/host/work-item-sync.js +101 -0
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
// tools.js —— 工作台业务工具注册(P2 角色 + P3 群组)。
|
|
2
|
+
// 契约:@deepseek-ai/dsh-tools 的 defineTool(output 为 { schema, render })。
|
|
3
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
4
|
+
import { PRESET_ROLES } from './agents.js'
|
|
5
|
+
|
|
6
|
+
const OUTPUT = {
|
|
7
|
+
schema: { type: 'string' },
|
|
8
|
+
render: (_args, value) => [{ type: 'text', text: String(value) }],
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function install(rt) {
|
|
12
|
+
const { ctx } = rt
|
|
13
|
+
|
|
14
|
+
// ================= 角色工具(P2) =================
|
|
15
|
+
|
|
16
|
+
ctx.tools.register(defineTool({
|
|
17
|
+
name: 'dwb_create_role',
|
|
18
|
+
description: '创建一个工作台角色(Agent Session + 台账登记)。输入 name 角色名、type 预设类型或自定义、instructions 职责描述。',
|
|
19
|
+
parameters: {
|
|
20
|
+
name: { type: 'string', description: '角色名称,如 工程师A', required: true },
|
|
21
|
+
type: { type: 'string', description: '角色类型:预设或自定义,如 高级工程师' },
|
|
22
|
+
instructions: { type: 'string', description: '职责描述' },
|
|
23
|
+
},
|
|
24
|
+
output: OUTPUT,
|
|
25
|
+
async execute(args) {
|
|
26
|
+
await rt.loadAll()
|
|
27
|
+
const role = await rt.createRole({
|
|
28
|
+
name: args.name,
|
|
29
|
+
type: args.type,
|
|
30
|
+
instructions: args.instructions,
|
|
31
|
+
})
|
|
32
|
+
return [
|
|
33
|
+
'角色已创建:',
|
|
34
|
+
`- id: ${role.id}`,
|
|
35
|
+
`- 名称: ${role.name}`,
|
|
36
|
+
`- 类型: ${role.type}`,
|
|
37
|
+
`- Session: ${role.sessionId}`,
|
|
38
|
+
].join('\n')
|
|
39
|
+
},
|
|
40
|
+
}))
|
|
41
|
+
|
|
42
|
+
ctx.tools.register(defineTool({
|
|
43
|
+
name: 'dwb_list_roles',
|
|
44
|
+
description: '列出工作台全部角色:名称、类型、模型、状态、所属群组数。',
|
|
45
|
+
parameters: {},
|
|
46
|
+
output: OUTPUT,
|
|
47
|
+
async execute() {
|
|
48
|
+
await rt.loadAll()
|
|
49
|
+
const roles = rt.listRoles()
|
|
50
|
+
if (roles.length === 0) return '暂无角色。可用 dwb_create_role 新建。'
|
|
51
|
+
const lines = roles.map(role => {
|
|
52
|
+
const groups = (rt.listGroups() ?? []).filter(g => (g.members ?? []).includes(role.id)).length
|
|
53
|
+
return `- ${role.name}(${role.type})[${role.status}] ${role.model ?? '默认模型'} · 群组 ${groups} 个`
|
|
54
|
+
})
|
|
55
|
+
return ['角色列表:', ...lines].join('\n')
|
|
56
|
+
},
|
|
57
|
+
}))
|
|
58
|
+
|
|
59
|
+
ctx.tools.register(defineTool({
|
|
60
|
+
name: 'dwb_update_role',
|
|
61
|
+
description: '编辑角色:可改名称、职责描述、模型。',
|
|
62
|
+
parameters: {
|
|
63
|
+
id: { type: 'string', description: '角色 id(dwb_list_roles 可见)', required: true },
|
|
64
|
+
name: { type: 'string', description: '新角色名' },
|
|
65
|
+
instructions: { type: 'string', description: '新职责描述' },
|
|
66
|
+
model: { type: 'string', description: '新模型' },
|
|
67
|
+
},
|
|
68
|
+
output: OUTPUT,
|
|
69
|
+
async execute(args) {
|
|
70
|
+
await rt.loadAll()
|
|
71
|
+
const patch = {}
|
|
72
|
+
if (args.name !== undefined) patch.name = args.name
|
|
73
|
+
if (args.instructions !== undefined) patch.instructions = args.instructions
|
|
74
|
+
if (args.model !== undefined) patch.model = args.model
|
|
75
|
+
if (Object.keys(patch).length === 0) return '未提供任何要修改的字段。'
|
|
76
|
+
const updated = rt.updateRole(args.id, patch)
|
|
77
|
+
return `角色已更新:${updated.name}(${updated.type})[${updated.status}]`
|
|
78
|
+
},
|
|
79
|
+
}))
|
|
80
|
+
|
|
81
|
+
ctx.tools.register(defineTool({
|
|
82
|
+
name: 'dwb_set_role_status',
|
|
83
|
+
description: '停用或启用一个角色。停用后角色不参与新的群组与任务。',
|
|
84
|
+
parameters: {
|
|
85
|
+
id: { type: 'string', description: '角色 id', required: true },
|
|
86
|
+
status: { type: 'string', enum: ['active', 'disabled'], description: '目标状态', required: true },
|
|
87
|
+
},
|
|
88
|
+
output: OUTPUT,
|
|
89
|
+
async execute(args) {
|
|
90
|
+
await rt.loadAll()
|
|
91
|
+
const updated = await rt.setRoleStatus(args.id, args.status)
|
|
92
|
+
return `角色 ${updated.name} 状态已设为 ${updated.status}`
|
|
93
|
+
},
|
|
94
|
+
}))
|
|
95
|
+
|
|
96
|
+
ctx.tools.register(defineTool({
|
|
97
|
+
name: 'dwb_delete_role',
|
|
98
|
+
description: '删除一个角色。若角色仍在群组或任务中被引用,会返回拒绝原因。',
|
|
99
|
+
parameters: {
|
|
100
|
+
id: { type: 'string', description: '角色 id', required: true },
|
|
101
|
+
force: { type: 'boolean', description: '是否强制删除(会同时从群组/任务中移除引用)' },
|
|
102
|
+
},
|
|
103
|
+
output: OUTPUT,
|
|
104
|
+
async execute(args) {
|
|
105
|
+
await rt.loadAll()
|
|
106
|
+
const result = rt.deleteRole(args.id)
|
|
107
|
+
if (result.refused) {
|
|
108
|
+
if (args.force) {
|
|
109
|
+
const result = await rt.forceDeleteRole(args.id)
|
|
110
|
+
return `角色 ${result.role} 已强制删除,并已停止相关执行。`
|
|
111
|
+
}
|
|
112
|
+
return `删除被拒绝:${result.reason}。如需强制删除,请传入 force=true。`
|
|
113
|
+
}
|
|
114
|
+
return '角色已删除。'
|
|
115
|
+
},
|
|
116
|
+
}))
|
|
117
|
+
|
|
118
|
+
ctx.tools.register(defineTool({
|
|
119
|
+
name: 'dwb_list_presets',
|
|
120
|
+
description: '列出工作台内置预设角色模板(研发负责人/架构师/高级工程师/测试工程师/产品经理)。',
|
|
121
|
+
parameters: {},
|
|
122
|
+
output: OUTPUT,
|
|
123
|
+
async execute() {
|
|
124
|
+
const lines = PRESET_ROLES.map(item => `- ${item.type}:${item.defaultInstructions}`)
|
|
125
|
+
return ['预设角色模板:', ...lines].join('\n')
|
|
126
|
+
},
|
|
127
|
+
}))
|
|
128
|
+
|
|
129
|
+
// ================= 群组工具(P3) =================
|
|
130
|
+
|
|
131
|
+
ctx.tools.register(defineTool({
|
|
132
|
+
name: 'dwb_create_group',
|
|
133
|
+
description: '新建一个研发群组。name 为群组名称(原样使用);members 为成员角色 id 列表(至少 1 人);dir 为工作目录(必填:不存在会自动创建,成员在其中读取资料/存放成果物,群附件也存于此)。',
|
|
134
|
+
parameters: {
|
|
135
|
+
name: { type: 'string', description: '群组名称,如 ERP项目、登录系统', required: true },
|
|
136
|
+
members: { type: 'array', items: { type: 'string' }, description: '成员角色 id 列表,至少 1 人' },
|
|
137
|
+
dir: { type: 'string', description: '工作目录绝对路径(必填;不存在会自动创建;删除群组时该目录保留)', required: true },
|
|
138
|
+
admins: { type: 'array', items: { type: 'string' }, description: '管理员人员 id 列表(必须是 members 子集;普通消息先投递管理员)' },
|
|
139
|
+
notice: { type: 'string', description: '群公告(成员每次发言前都会读到)' },
|
|
140
|
+
},
|
|
141
|
+
output: OUTPUT,
|
|
142
|
+
async execute(args) {
|
|
143
|
+
await rt.loadAll()
|
|
144
|
+
if (!args.dir || !String(args.dir).trim()) {
|
|
145
|
+
return '创建失败:必须指定工作目录(dir 参数,绝对路径;不存在会自动创建)。'
|
|
146
|
+
}
|
|
147
|
+
const group = await rt.createGroup({
|
|
148
|
+
name: args.name ?? args.prefix,
|
|
149
|
+
members: args.members,
|
|
150
|
+
admins: args.admins ?? [],
|
|
151
|
+
notice: args.notice ?? '',
|
|
152
|
+
withDir: false,
|
|
153
|
+
customDir: String(args.dir).trim(),
|
|
154
|
+
})
|
|
155
|
+
return [
|
|
156
|
+
'群组已创建:',
|
|
157
|
+
`- id: ${group.id}`,
|
|
158
|
+
`- 名称: ${group.name}`,
|
|
159
|
+
`- 成员: ${group.members.map(id => rt.getRole(id)?.name ?? id).join(', ')}`,
|
|
160
|
+
`- 目录: ${group.dir}`,
|
|
161
|
+
].join('\n')
|
|
162
|
+
},
|
|
163
|
+
}))
|
|
164
|
+
|
|
165
|
+
ctx.tools.register(defineTool({
|
|
166
|
+
name: 'dwb_list_groups',
|
|
167
|
+
description: '列出全部群组:名称、成员、目录、状态。',
|
|
168
|
+
parameters: {},
|
|
169
|
+
output: OUTPUT,
|
|
170
|
+
async execute() {
|
|
171
|
+
await rt.loadAll()
|
|
172
|
+
const groups = rt.listGroups()
|
|
173
|
+
if (groups.length === 0) return '暂无群组。可用 dwb_create_group 新建。'
|
|
174
|
+
const lines = groups.map(group => {
|
|
175
|
+
const members = (group.members ?? []).map(id => rt.getRole(id)?.name ?? id).join(', ')
|
|
176
|
+
return `- ${group.name} [${group.status}] · 成员: ${members} · ${group.dir ?? '无目录'}`
|
|
177
|
+
})
|
|
178
|
+
return ['群组列表:', ...lines].join('\n')
|
|
179
|
+
},
|
|
180
|
+
}))
|
|
181
|
+
|
|
182
|
+
ctx.tools.register(defineTool({
|
|
183
|
+
name: 'dwb_rename_group',
|
|
184
|
+
description: '重命名群组(新名称原样使用)。同名工作目录会同步改名,保证一致。',
|
|
185
|
+
parameters: {
|
|
186
|
+
id: { type: 'string', description: '群组 id', required: true },
|
|
187
|
+
name: { type: 'string', description: '新群组名称', required: true },
|
|
188
|
+
},
|
|
189
|
+
output: OUTPUT,
|
|
190
|
+
async execute(args) {
|
|
191
|
+
await rt.loadAll()
|
|
192
|
+
const group = await rt.renameGroup(args.id, args.name ?? args.prefix)
|
|
193
|
+
return `群组已重命名为 ${group.name},目录已同步:${group.dir}`
|
|
194
|
+
},
|
|
195
|
+
}))
|
|
196
|
+
|
|
197
|
+
ctx.tools.register(defineTool({
|
|
198
|
+
name: 'dwb_delete_group',
|
|
199
|
+
description: '删除群组。同时删除同名工作目录及全部文件、群内所有任务。此操作不可撤销。',
|
|
200
|
+
parameters: {
|
|
201
|
+
id: { type: 'string', description: '群组 id', required: true },
|
|
202
|
+
},
|
|
203
|
+
output: OUTPUT,
|
|
204
|
+
async execute(args) {
|
|
205
|
+
const result = await rt.deleteGroup(args.id)
|
|
206
|
+
return `群组「${result.name}」已删除(目录 ${result.dir ?? '无'})`
|
|
207
|
+
},
|
|
208
|
+
}))
|
|
209
|
+
|
|
210
|
+
// ================= 群消息工具(P4) =================
|
|
211
|
+
|
|
212
|
+
ctx.tools.register(defineTool({
|
|
213
|
+
name: 'dwb_post_group_message',
|
|
214
|
+
description: '向群组发送需行动的 @协调或关键结论。负责人统一启动与交付,不发逐人开工、收到或无变化进度;独立执行结果用 dwb_report_execution 自动回执,不重复发帖。',
|
|
215
|
+
parameters: {
|
|
216
|
+
groupId: { type: 'string', description: '群组 id(dwb_list_groups 可见)', required: true },
|
|
217
|
+
text: { type: 'string', description: '必要结论或行动请求,最多150字;细节放文档,可含 @角色名', required: true },
|
|
218
|
+
fromRoleId: { type: 'string', description: '发送者角色 id(成员发言时填;用户发言留空)' },
|
|
219
|
+
taskId: { type: 'string', description: '关联任务 id;执行成员默认当前任务,后续消息直达其执行者' },
|
|
220
|
+
replyToSeq: { type: 'integer', description: '回复的群消息序号;继承该消息的任务归属' },
|
|
221
|
+
},
|
|
222
|
+
output: OUTPUT,
|
|
223
|
+
async execute(args) {
|
|
224
|
+
const result = await rt.postGroupMessage({
|
|
225
|
+
groupId: args.groupId,
|
|
226
|
+
text: args.text,
|
|
227
|
+
fromRoleId: args.fromRoleId,
|
|
228
|
+
taskId: args.taskId,
|
|
229
|
+
replyToSeq: args.replyToSeq,
|
|
230
|
+
})
|
|
231
|
+
const group = rt.getGroup(args.groupId)
|
|
232
|
+
const targetNames = result.delivered.map(id => rt.getRole(id)?.name ?? id).join(', ')
|
|
233
|
+
return [
|
|
234
|
+
`已在群组「${group.name}」发言(seq=${result.seq})`,
|
|
235
|
+
result.mentions.length > 0 ? `@ 到:${result.mentions.join(', ')}` : '按任务归属或群管理员路由',
|
|
236
|
+
`投递角色:${targetNames || '无'}`,
|
|
237
|
+
result.errors?.length ? `投递失败:${JSON.stringify(result.errors)}` : '',
|
|
238
|
+
].join('\n')
|
|
239
|
+
},
|
|
240
|
+
}))
|
|
241
|
+
|
|
242
|
+
ctx.tools.register(defineTool({
|
|
243
|
+
name: 'dwb_group_transcript',
|
|
244
|
+
description: '读取群组消息流(可用于查看群组历史)。',
|
|
245
|
+
parameters: {
|
|
246
|
+
groupId: { type: 'string', description: '群组 id', required: true },
|
|
247
|
+
sinceSeq: { type: 'integer', description: '从某个 seq 之后开始(默认 0=全部)' },
|
|
248
|
+
},
|
|
249
|
+
output: OUTPUT,
|
|
250
|
+
async execute(args) {
|
|
251
|
+
await rt.loadAll()
|
|
252
|
+
const lines = rt.groupTranscript(args.groupId, args.sinceSeq ?? 0)
|
|
253
|
+
if (lines.length === 0) return '群组暂无消息。'
|
|
254
|
+
return lines.map(line => `[seq ${line.seq}] ${line.fromName}: ${line.text}`).join('\n')
|
|
255
|
+
},
|
|
256
|
+
}))
|
|
257
|
+
|
|
258
|
+
ctx.tools.register(defineTool({
|
|
259
|
+
name: 'dwb_ask_user',
|
|
260
|
+
description: '向用户请求决策(唯一正确方式)。调用后决策卡会显示在群聊中,用户点击按钮回复。⚠️ 重要:调用此工具后你的本轮回复立即结束——不要再在你的会话输出中重复提问用户,用户不看你的会话,只看群聊。禁止使用任何其他方式(如直接输出问题文本、调用其他 ask 类工具)向用户提问。',
|
|
261
|
+
parameters: {
|
|
262
|
+
groupId: { type: 'string', description: '群组 id', required: true },
|
|
263
|
+
question: { type: 'string', description: '要问用户的问题(简明扼要,一条问题)', required: true },
|
|
264
|
+
options: { type: 'string', description: '可选项,用 | 分隔,如"A 单人无鉴权 | B 多用户+鉴权"。无固定选项可省略。' },
|
|
265
|
+
fromRoleId: { type: 'string', description: '你的角色 id' },
|
|
266
|
+
},
|
|
267
|
+
output: OUTPUT,
|
|
268
|
+
async execute(args, exec) {
|
|
269
|
+
await rt.loadAll()
|
|
270
|
+
await rt.executionReady
|
|
271
|
+
// 🔒 去重:已有未回复的决策卡时拒绝重复发送。
|
|
272
|
+
const grp = rt.getGroup(args.groupId)
|
|
273
|
+
if (grp?.pendingDecision) {
|
|
274
|
+
return `🛑 已有未回复的决策卡(seq ${grp.pendingDecision.seq}),不要重复发送。等待用户在群聊中回复那张卡即可。`
|
|
275
|
+
}
|
|
276
|
+
const opts = args.options ? `\n可选项:${args.options}` : ''
|
|
277
|
+
const fromName = args.fromRoleId ? (rt.getRole(args.fromRoleId)?.name ?? '成员') : '成员'
|
|
278
|
+
await rt.postGroupMessage({
|
|
279
|
+
groupId: args.groupId,
|
|
280
|
+
text: `⚠️ 需要您的决策(来自 ${fromName}):\n${args.question}${opts}`,
|
|
281
|
+
fromRoleId: args.fromRoleId,
|
|
282
|
+
noWake: true, // 🔒 决策卡不唤醒任何人——它是给用户看的,AI 成员不需要处理
|
|
283
|
+
})
|
|
284
|
+
const run = rt.currentExecution?.()
|
|
285
|
+
if (run) await rt.executionEngine.report(run.id, {status:'blocked',summary:`等待用户决策:${args.question}`})
|
|
286
|
+
exec?.concludeTurn?.()
|
|
287
|
+
return '决策卡已发到群聊。你的本轮回复到此结束——不要在会话中重复提问,等待用户在群聊中点击选项回复即可。'
|
|
288
|
+
},
|
|
289
|
+
}))
|
|
290
|
+
|
|
291
|
+
ctx.tools.register(defineTool({
|
|
292
|
+
name: 'dwb_report_progress',
|
|
293
|
+
description: '管理员仅在重要节点、状态变化或风险出现时汇报进度,不重复成员回执或无变化状态;最终交付用 dwb_notify_user。',
|
|
294
|
+
parameters: {
|
|
295
|
+
groupId: { type: 'string', description: '群组 id', required: true },
|
|
296
|
+
summary: { type: 'string', description: '最多150字,仅说明变化、影响及必要行动;无风险不凑风险栏', required: true },
|
|
297
|
+
fromRoleId: { type: 'string', description: '你的角色 id', required: true },
|
|
298
|
+
},
|
|
299
|
+
output: OUTPUT,
|
|
300
|
+
async execute(args) {
|
|
301
|
+
const fromName = rt.getRole(args.fromRoleId)?.name ?? '管理员'
|
|
302
|
+
// 写入群 log 但标记为 progress-report(不唤醒任何人)
|
|
303
|
+
await rt.postGroupMessage({
|
|
304
|
+
groupId: args.groupId,
|
|
305
|
+
text: `📊 进度汇报(${fromName}):\n${args.summary}`,
|
|
306
|
+
fromRoleId: args.fromRoleId,
|
|
307
|
+
noWake: true,
|
|
308
|
+
})
|
|
309
|
+
return '进度汇报已发送(仅用户可见,不影响群聊其他成员)。'
|
|
310
|
+
},
|
|
311
|
+
}))
|
|
312
|
+
|
|
313
|
+
ctx.tools.register(defineTool({
|
|
314
|
+
name: 'dwb_notify_user',
|
|
315
|
+
description: '负责人在全部完成或最终交付时统一通知一次,不复述成员工作过程。',
|
|
316
|
+
parameters: {
|
|
317
|
+
groupId: { type: 'string', description: '群组 id', required: true },
|
|
318
|
+
summary: { type: 'string', description: '结果、交付入口、验证情况及遗留问题,最多150字', required: true },
|
|
319
|
+
fromRoleId: { type: 'string', description: '你的角色 id', required: true },
|
|
320
|
+
},
|
|
321
|
+
output: OUTPUT,
|
|
322
|
+
async execute(args) {
|
|
323
|
+
const result = await rt.completeDelivery(args)
|
|
324
|
+
return result.duplicate ? '该任务版本已经交付,未重复通知用户。' : '已通知用户任务完成。'
|
|
325
|
+
},
|
|
326
|
+
}))
|
|
327
|
+
|
|
328
|
+
// ================= 任务工具(P5) =================
|
|
329
|
+
|
|
330
|
+
ctx.tools.register(defineTool({
|
|
331
|
+
name: 'dwb_create_task',
|
|
332
|
+
description: '创建任务并自动排队执行,按优先级和依赖调度;指派成员分别汇报结果,系统跟踪重试与完成。',
|
|
333
|
+
parameters: {
|
|
334
|
+
title: { type: 'string', description: '任务标题', required: true },
|
|
335
|
+
description: { type: 'string', description: '任务描述' },
|
|
336
|
+
groupId: { type: 'string', description: '所属群组 id', required: true },
|
|
337
|
+
assignees: { type: 'array', items: { type: 'string' }, description: '指派工程师角色 id 列表(可多个并行)', required: true },
|
|
338
|
+
priority: { type: 'string', enum: ['高', '中', '低'], description: '优先级,默认 中' },
|
|
339
|
+
dependencies: { type: 'array', items: { type: 'string' }, description: '同群前置任务 id;全部完成后才启动,默认无依赖' },
|
|
340
|
+
fromRoleId: { type: 'string', description: '创建者角色 id(任务通知显示创建者名)' },
|
|
341
|
+
},
|
|
342
|
+
output: OUTPUT,
|
|
343
|
+
async execute(args) {
|
|
344
|
+
// 🔒 决策阻塞:群内有未回复决策卡时,阻止建任务。
|
|
345
|
+
const grp = rt.getGroup(args.groupId)
|
|
346
|
+
if (grp?.pendingDecision) {
|
|
347
|
+
return `🛑【被系统阻止】群内有未回复的决策卡(seq ${grp.pendingDecision.seq}),用户尚未拍板。在用户回复决策卡之前,不能创建新任务——新任务可能依赖未决事项。请先用 dwb_group_transcript 查看决策卡内容,等用户回复后再创建任务。`
|
|
348
|
+
}
|
|
349
|
+
const task = await rt.createTask({
|
|
350
|
+
title: args.title,
|
|
351
|
+
description: args.description,
|
|
352
|
+
groupId: args.groupId,
|
|
353
|
+
assignees: args.assignees,
|
|
354
|
+
dependencies: args.dependencies,
|
|
355
|
+
priority: args.priority,
|
|
356
|
+
fromRoleId: args.fromRoleId,
|
|
357
|
+
})
|
|
358
|
+
const names = task.assignees.map(id => rt.getRole(id)?.name ?? id).join(', ')
|
|
359
|
+
return [
|
|
360
|
+
'任务已创建:',
|
|
361
|
+
`- id: ${task.id}`,
|
|
362
|
+
`- 标题: ${task.title}`,
|
|
363
|
+
`- 优先级: ${task.priority}`,
|
|
364
|
+
`- 指派: ${names}`,
|
|
365
|
+
`- 群组: ${rt.getGroup(task.groupId)?.name}`,
|
|
366
|
+
'- 已进入系统执行队列;用 dwb_task_execution 查看执行状态',
|
|
367
|
+
].join('\n')
|
|
368
|
+
},
|
|
369
|
+
}))
|
|
370
|
+
|
|
371
|
+
ctx.tools.register(defineTool({
|
|
372
|
+
name: 'dwb_list_tasks',
|
|
373
|
+
description: '列出任务(可按群组过滤):标题、优先级、状态、指派、群组。',
|
|
374
|
+
parameters: {
|
|
375
|
+
groupId: { type: 'string', description: '按群组过滤(可选)' },
|
|
376
|
+
},
|
|
377
|
+
output: OUTPUT,
|
|
378
|
+
async execute(args) {
|
|
379
|
+
await rt.loadAll()
|
|
380
|
+
const tasks = rt.listTasks(args.groupId)
|
|
381
|
+
if (tasks.length === 0) return '暂无任务。'
|
|
382
|
+
const lines = tasks.map(task => {
|
|
383
|
+
const names = (task.assignees ?? []).map(id => rt.getRole(id)?.name ?? id).join(', ')
|
|
384
|
+
const groupName = rt.getGroup(task.groupId)?.name ?? task.groupId
|
|
385
|
+
return `- ${task.id} [${task.priority}] ${task.title}(${task.status})· 指派: ${names} · ${groupName}${task.executionSummary ? ' · '+task.executionSummary : ''}`
|
|
386
|
+
})
|
|
387
|
+
return ['任务列表:', ...lines].join('\n')
|
|
388
|
+
},
|
|
389
|
+
}))
|
|
390
|
+
|
|
391
|
+
ctx.tools.register(defineTool({
|
|
392
|
+
name: 'dwb_update_task',
|
|
393
|
+
description: '修改任务范围、指派、优先级、依赖或人工状态;执行成员通过 dwb_report_execution 汇报自己的结果。',
|
|
394
|
+
parameters: {
|
|
395
|
+
id: { type: 'string', description: '任务 id', required: true },
|
|
396
|
+
title: { type: 'string', description: '新标题' },
|
|
397
|
+
description: { type: 'string', description: '新描述' },
|
|
398
|
+
assignees: { type: 'array', items: { type: 'string' }, description: '新指派列表' },
|
|
399
|
+
priority: { type: 'string', enum: ['高', '中', '低'], description: '新优先级' },
|
|
400
|
+
status: { type: 'string', enum: ['待办', '进行中', '已完成'], description: '新状态' },
|
|
401
|
+
dependencies: { type: 'array', items: { type: 'string' }, description: '同群前置任务 id;空数组移除依赖' },
|
|
402
|
+
fromRoleId: { type: 'string', description: '操作者角色 id(通知显示操作者名)' },
|
|
403
|
+
},
|
|
404
|
+
output: OUTPUT,
|
|
405
|
+
async execute(args) {
|
|
406
|
+
const patch = {}
|
|
407
|
+
if (args.title !== undefined) patch.title = args.title
|
|
408
|
+
if (args.description !== undefined) patch.description = args.description
|
|
409
|
+
if (args.assignees !== undefined) patch.assignees = args.assignees
|
|
410
|
+
if (args.priority !== undefined) patch.priority = args.priority
|
|
411
|
+
if (args.status !== undefined) patch.status = args.status
|
|
412
|
+
if (args.dependencies !== undefined) patch.dependencies = args.dependencies
|
|
413
|
+
if (Object.keys(patch).length === 0) return '未提供任何要修改的字段。'
|
|
414
|
+
const updated = await rt.updateTask(args.id, patch, { fromRoleId: args.fromRoleId })
|
|
415
|
+
return `任务「${updated.title}」已更新 [${updated.status}]`
|
|
416
|
+
},
|
|
417
|
+
}))
|
|
418
|
+
|
|
419
|
+
// ================= 命令工具(P8 辅助入口) =================
|
|
420
|
+
|
|
421
|
+
ctx.tools.register(defineTool({
|
|
422
|
+
name: 'dwb_command_new_project',
|
|
423
|
+
description: '快捷建项目:输入项目名称与成员,一步完成「建群 + 同名目录脚手架 + 首个任务」。',
|
|
424
|
+
parameters: {
|
|
425
|
+
name: { type: 'string', description: '项目名称(直接作为群组名与目录名)', required: true },
|
|
426
|
+
members: { type: 'array', items: { type: 'string' }, description: '成员角色 id 列表', required: true },
|
|
427
|
+
firstTask: { type: 'string', description: '首个任务标题(可选)' },
|
|
428
|
+
},
|
|
429
|
+
output: OUTPUT,
|
|
430
|
+
async execute(args) {
|
|
431
|
+
await rt.loadAll()
|
|
432
|
+
const group = await rt.createGroup({ name: args.name ?? args.prefix, members: args.members })
|
|
433
|
+
const lines = [
|
|
434
|
+
`项目已建立:${group.name}`,
|
|
435
|
+
`- 群组: ${group.id}`,
|
|
436
|
+
`- 目录: ${group.dir}`,
|
|
437
|
+
]
|
|
438
|
+
if (args.firstTask) {
|
|
439
|
+
const task = await rt.createTask({
|
|
440
|
+
title: args.firstTask,
|
|
441
|
+
groupId: group.id,
|
|
442
|
+
assignees: args.members,
|
|
443
|
+
})
|
|
444
|
+
lines.push(`- 首个任务: ${task.title}(已自动 @ 成员)`)
|
|
445
|
+
}
|
|
446
|
+
return lines.join('\n')
|
|
447
|
+
},
|
|
448
|
+
}))
|
|
449
|
+
|
|
450
|
+
ctx.tools.register(defineTool({
|
|
451
|
+
name: 'dwb_command_status',
|
|
452
|
+
description: '工作台状态概览:角色/群组/任务/消息计数。',
|
|
453
|
+
parameters: {},
|
|
454
|
+
output: OUTPUT,
|
|
455
|
+
async execute() {
|
|
456
|
+
await rt.loadAll()
|
|
457
|
+
const roles = rt.listRoles()
|
|
458
|
+
const groups = rt.listGroups()
|
|
459
|
+
const tasks = rt.listTasks()
|
|
460
|
+
const messages = (groups ?? []).reduce((sum, g) => sum + rt.groupMessageCount(g.id), 0)
|
|
461
|
+
return [
|
|
462
|
+
'工作台状态:',
|
|
463
|
+
`- 角色: ${roles.length}(活跃 ${roles.filter(r => r.status === 'active').length})`,
|
|
464
|
+
`- 群组: ${groups.length}`,
|
|
465
|
+
`- 任务: ${tasks.length}(待办 ${tasks.filter(t => t.status === '待办').length} / 进行中 ${tasks.filter(t => t.status === '进行中').length} / 已完成 ${tasks.filter(t => t.status === '已完成').length})`,
|
|
466
|
+
`- 群消息: ${messages} 条`,
|
|
467
|
+
].join('\n')
|
|
468
|
+
},
|
|
469
|
+
}))
|
|
470
|
+
|
|
471
|
+
ctx.tools.register(defineTool({
|
|
472
|
+
name: 'dwb_command_archive',
|
|
473
|
+
description: '列出群组任务完成度(可据此判断归档候选)。',
|
|
474
|
+
parameters: {
|
|
475
|
+
groupId: { type: 'string', description: '按群组过滤(可选)' },
|
|
476
|
+
},
|
|
477
|
+
output: OUTPUT,
|
|
478
|
+
async execute(args) {
|
|
479
|
+
await rt.loadAll()
|
|
480
|
+
const groups = (rt.listGroups() ?? []).filter(g => !args.groupId || g.id === args.groupId)
|
|
481
|
+
if (groups.length === 0) return '无符合条件的群组。'
|
|
482
|
+
const lines = groups.map(group => {
|
|
483
|
+
const tasks = rt.listTasks(group.id)
|
|
484
|
+
const done = tasks.filter(t => t.status === '已完成').length
|
|
485
|
+
const rate = tasks.length ? Math.round(done / tasks.length * 100) : 100
|
|
486
|
+
return `- ${group.name}:任务 ${tasks.length} · 已完成 ${done}(${rate}%)`
|
|
487
|
+
})
|
|
488
|
+
return ['群组完成度:', ...lines].join('\n')
|
|
489
|
+
},
|
|
490
|
+
}))
|
|
491
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Update checks are read-only until a user invokes updateStart from the UI.
|
|
2
|
+
// Keep npm identity separate from the historical workbench data and RPC paths.
|
|
3
|
+
import { readFileSync } from 'node:fs'
|
|
4
|
+
|
|
5
|
+
const PKG = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'))
|
|
6
|
+
export const CHECK_TTL_MS = 10 * 60_000
|
|
7
|
+
export const FETCH_TIMEOUT_MS = 8_000
|
|
8
|
+
const OFFICIAL = 'https://registry.npmjs.org/'
|
|
9
|
+
const MIRROR = 'https://registry.npmmirror.com/'
|
|
10
|
+
const STABLE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/
|
|
11
|
+
|
|
12
|
+
const stable = version => typeof version === 'string' && STABLE.test(version) ? version : null
|
|
13
|
+
const newer = (a, b) => {
|
|
14
|
+
const next = stable(a)?.split('.').map(BigInt)
|
|
15
|
+
const current = stable(b)?.split('.').map(BigInt)
|
|
16
|
+
if (!next || !current) return false
|
|
17
|
+
for (let i = 0; i < 3; i++) if (next[i] !== current[i]) return next[i] > current[i]
|
|
18
|
+
return false
|
|
19
|
+
}
|
|
20
|
+
const normalize = value => {
|
|
21
|
+
if (typeof value !== 'string') return null
|
|
22
|
+
try {
|
|
23
|
+
const url = new URL(value)
|
|
24
|
+
return ['https:', 'http:'].includes(url.protocol) && !url.username && !url.password && !url.search && !url.hash
|
|
25
|
+
? `${url.href.replace(/\/+$/, '')}/` : null
|
|
26
|
+
} catch { return null }
|
|
27
|
+
}
|
|
28
|
+
const encodeName = name => name.startsWith('@') ? `@${encodeURIComponent(name.slice(1))}` : encodeURIComponent(name)
|
|
29
|
+
|
|
30
|
+
// Reject file:, link:, tarballs and git even when prefixed by the npm package name.
|
|
31
|
+
function sourceKind(source) {
|
|
32
|
+
if (typeof source !== 'string' || !source.trim()) return 'unknown'
|
|
33
|
+
const text = source.trim()
|
|
34
|
+
const registrySpec = text === PKG.name || new RegExp(`^${PKG.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}@(?:[~^]?\\d+\\.\\d+\\.\\d+(?:-[\\w.-]+)?|latest|\\*)$`).test(text)
|
|
35
|
+
if (registrySpec) return 'registry'
|
|
36
|
+
if (/^(?:link:|file:|https?:\/\/|\/|\.\/|\.\.\/|~\/)/i.test(text) || /\.(?:tgz|tar\.gz)(?:$|[?#])/i.test(text)
|
|
37
|
+
|| text.startsWith(`${PKG.name}@file:`) || text.startsWith(`${PKG.name}@link:`)) return 'local'
|
|
38
|
+
return 'unknown'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function registries(manager) {
|
|
42
|
+
const configured = await Promise.resolve(manager.registries?.()).catch(() => null)
|
|
43
|
+
if (!configured) return [OFFICIAL, MIRROR]
|
|
44
|
+
const own = normalize(configured.registry ?? configured.resolved) ?? OFFICIAL
|
|
45
|
+
const fallback = (Array.isArray(configured.fallbackRegistries) ? configured.fallbackRegistries : []).map(normalize).filter(Boolean)
|
|
46
|
+
const publicRegistry = own === OFFICIAL || own === MIRROR || fallback.includes(own)
|
|
47
|
+
return publicRegistry ? [...new Set([own, ...fallback])] : [own]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Install into the existing runtime in index.js; no tool or automatic installer is registered. */
|
|
51
|
+
export function install(rt, { fetchImpl = globalThis.fetch, now = Date.now } = {}) {
|
|
52
|
+
const manager = () => rt.ctx.get?.('pluginManager')
|
|
53
|
+
const status = {
|
|
54
|
+
name: PKG.name, current: PKG.version, latest: null, source: 'unknown',
|
|
55
|
+
checkedAt: 0, phase: 'idle', installed: null, error: null,
|
|
56
|
+
}
|
|
57
|
+
let target = null
|
|
58
|
+
let checking = null
|
|
59
|
+
const view = () => ({ ...status, available: target !== null && newer(status.latest, status.current)
|
|
60
|
+
&& status.phase !== 'installing' && status.phase !== 'installed' })
|
|
61
|
+
|
|
62
|
+
async function runCheck() {
|
|
63
|
+
const service = manager()
|
|
64
|
+
const bundles = await Promise.resolve(service?.listBundles?.())
|
|
65
|
+
const own = Array.isArray(bundles) ? bundles.find(bundle => bundle.name === PKG.name && bundle.installed !== false) : undefined
|
|
66
|
+
status.source = sourceKind(own?.source)
|
|
67
|
+
status.checkedAt = now()
|
|
68
|
+
target = null
|
|
69
|
+
status.latest = null
|
|
70
|
+
status.error = null
|
|
71
|
+
if (status.source !== 'registry') { status.phase = 'idle'; return }
|
|
72
|
+
if (!service || typeof fetchImpl !== 'function') throw new Error('Update registry is unavailable')
|
|
73
|
+
let failed = false
|
|
74
|
+
for (const registry of await registries(service)) {
|
|
75
|
+
try {
|
|
76
|
+
const response = await fetchImpl(`${registry}${encodeName(PKG.name)}/latest`, {
|
|
77
|
+
headers: { accept: 'application/json' }, signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
78
|
+
})
|
|
79
|
+
if (response.status === 404) continue
|
|
80
|
+
if (!response.ok) throw new Error(`Registry answered ${response.status}`)
|
|
81
|
+
const found = await response.json()
|
|
82
|
+
const version = stable(found?.version)
|
|
83
|
+
if (found?.name !== PKG.name || !version) throw new Error('Invalid registry release')
|
|
84
|
+
status.latest = version
|
|
85
|
+
if (newer(version, status.current)) target = { spec: `${PKG.name}@${version}`, registry, version }
|
|
86
|
+
status.phase = 'idle'
|
|
87
|
+
return
|
|
88
|
+
} catch { failed = true }
|
|
89
|
+
}
|
|
90
|
+
status.phase = failed ? 'failed' : 'idle'
|
|
91
|
+
if (failed) status.error = 'update-check-failed'
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function check({ fresh = false } = {}) {
|
|
95
|
+
if (status.phase === 'installing' || status.phase === 'installed') return Promise.resolve(view())
|
|
96
|
+
if (checking) return checking
|
|
97
|
+
if (!fresh && status.checkedAt && now() - status.checkedAt < CHECK_TTL_MS) return Promise.resolve(view())
|
|
98
|
+
status.phase = 'checking'
|
|
99
|
+
checking = runCheck().catch(() => {
|
|
100
|
+
target = null
|
|
101
|
+
status.latest = null
|
|
102
|
+
status.checkedAt = now()
|
|
103
|
+
status.phase = 'failed'
|
|
104
|
+
status.error = 'update-check-failed'
|
|
105
|
+
}).then(view).finally(() => { checking = null })
|
|
106
|
+
return checking
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function start() {
|
|
110
|
+
if (checking || status.phase === 'installing') throw new Error('Update operation is already in progress')
|
|
111
|
+
if (!target || status.source !== 'registry' || now() - status.checkedAt >= CHECK_TTL_MS) throw new Error('No checked npm update is available')
|
|
112
|
+
const service = manager()
|
|
113
|
+
if (typeof service?.installBundle !== 'function') throw new Error('The plugin manager is unavailable')
|
|
114
|
+
const chosen = target
|
|
115
|
+
status.phase = 'installing'
|
|
116
|
+
status.error = null
|
|
117
|
+
// The plugin can unload while installing itself; never let that reject a detached promise.
|
|
118
|
+
void Promise.resolve().then(() => service.installBundle(chosen.spec, { registry: chosen.registry }))
|
|
119
|
+
.then(result => {
|
|
120
|
+
if (result?.error || !['applied', 'restart-required'].includes(result?.application)
|
|
121
|
+
|| (result.bundle && result.bundle !== PKG.name)
|
|
122
|
+
|| (result.version && result.version !== chosen.version)) {
|
|
123
|
+
throw new Error(result?.error?.diagnostic ?? result?.error?.code ?? `Install ${result?.application ?? 'failed'}`)
|
|
124
|
+
}
|
|
125
|
+
status.phase = 'installed'
|
|
126
|
+
status.installed = chosen.version
|
|
127
|
+
})
|
|
128
|
+
.catch(error => {
|
|
129
|
+
status.phase = 'failed'
|
|
130
|
+
status.error = String(error?.message ?? error).slice(0, 300)
|
|
131
|
+
})
|
|
132
|
+
return view()
|
|
133
|
+
}
|
|
134
|
+
Object.assign(rt, { updateView: view, updateCheck: check, updateStart: start })
|
|
135
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// verify.js —— P1 健康检查工具。
|
|
2
|
+
// 仅保留 dwb_verify(业务工具集中在 tools.js)。
|
|
3
|
+
import { readFile } from 'node:fs/promises'
|
|
4
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
5
|
+
import { PRESET_ROLES } from './agents.js'
|
|
6
|
+
|
|
7
|
+
const OUTPUT = {
|
|
8
|
+
schema: { type: 'string' },
|
|
9
|
+
render: (_args, value) => [{ type: 'text', text: String(value) }],
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function install(rt) {
|
|
13
|
+
const { ctx } = rt
|
|
14
|
+
|
|
15
|
+
ctx.tools.register(defineTool({
|
|
16
|
+
name: 'dwb_verify',
|
|
17
|
+
description: '协作工作台插件健康检查:报告插件版本、台账文件、角色/群组/任务计数。',
|
|
18
|
+
parameters: {},
|
|
19
|
+
output: OUTPUT,
|
|
20
|
+
async execute() {
|
|
21
|
+
await rt.loadAll()
|
|
22
|
+
const roles = rt.listRoles()
|
|
23
|
+
const groups = rt.listGroups()
|
|
24
|
+
const tasks = rt.listTasks()
|
|
25
|
+
// 版本从 package.json 读取(运行中进程实际加载的副本),
|
|
26
|
+
// 用于确认重启后新版本已生效。
|
|
27
|
+
let version = 'unknown'
|
|
28
|
+
try {
|
|
29
|
+
const pkg = JSON.parse(await readFile(new URL('../../package.json', import.meta.url), 'utf8'))
|
|
30
|
+
version = pkg.version
|
|
31
|
+
} catch { /* 读取失败不影响健康检查 */ }
|
|
32
|
+
return [
|
|
33
|
+
'协作工作台健康检查',
|
|
34
|
+
`- 插件版本:${version}`,
|
|
35
|
+
`- 插件已加载:是`,
|
|
36
|
+
`- 台账路径:${rt.ledgerPath()}`,
|
|
37
|
+
`- 角色数:${roles.length}`,
|
|
38
|
+
`- 群组数:${groups.length}`,
|
|
39
|
+
`- 任务数:${tasks.length}`,
|
|
40
|
+
`- 预设角色模板:${PRESET_ROLES.map(item => item.type).join(' / ')}`,
|
|
41
|
+
].join('\n')
|
|
42
|
+
},
|
|
43
|
+
}))
|
|
44
|
+
}
|