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.

@@ -0,0 +1,264 @@
1
+ // api.js —— host 侧 RPC API(P7)。
2
+ // 统一端点 /api/dsh-workbench(POST,buffered JSON):
3
+ // 请求体 { endpoint, payload }
4
+ // 响应 { ok, data } 或 { ok:false, error }
5
+ // client 半区经 fetch('api/dsh-workbench', {credentials:'same-origin'}) 调用。
6
+ // 契约:ctx.connection.fetch.register(DSH 核心,P0 已验证)。
7
+
8
+ export function install(rt) {
9
+ const { ctx } = rt
10
+
11
+ // 业务动作分发表:endpoint -> async (payload) => result
12
+ const actions = {
13
+ // ---- 概览 ----
14
+ 'overview': async () => ({
15
+ roles: rt.listRoles().length,
16
+ groups: (rt.listGroups() ?? []).length,
17
+ tasks: (rt.listTasks() ?? []).length,
18
+ messages: (rt.listGroups() ?? []).reduce((sum, g) => sum + rt.groupMessageCount(g.id), 0),
19
+ }),
20
+
21
+ // 可选模型目录(人员创建/编辑时选择;值格式 "provider/model")。
22
+ 'models.list': async () => {
23
+ const catalog = await ctx.sessionController.modelCatalog()
24
+ const models = []
25
+ for (const group of catalog?.groups ?? []) {
26
+ for (const m of group.models ?? []) {
27
+ models.push({ id: `${group.id}/${m.id}`, label: `${m.name || m.id} · ${group.name || group.id}` })
28
+ }
29
+ }
30
+ const def = catalog?.default
31
+ return { models, default: def ? `${def.provider}/${def.model}` : '' }
32
+ },
33
+
34
+ // ---- 角色 ----
35
+ 'roles.list': async () => ({
36
+ roles: rt.listRoles(),
37
+ presets: PRESETS_FOR_UI,
38
+ }),
39
+ 'roles.create': async (payload) => {
40
+ const role = await rt.createRole({
41
+ name: payload.name,
42
+ type: payload.type,
43
+ instructions: payload.instructions,
44
+ model: payload.model || null,
45
+ })
46
+ return { role }
47
+ },
48
+ 'roles.update': async (payload) => {
49
+ const patch = payload.patch ?? {}
50
+ const before = rt.getRole(payload.id)
51
+ const role = rt.updateRole(payload.id, patch)
52
+ // 模型或职责变化 → 同步到该人员的 Session(模型选择 + 职责刷新消息)。
53
+ if (role?.sessionId) {
54
+ if (patch.model !== undefined && patch.model !== before?.model) {
55
+ await rt.applySessionModel?.(role.sessionId, role.model)
56
+ }
57
+ if (patch.instructions !== undefined && patch.instructions !== before?.instructions) {
58
+ await rt.sendRoleContextMessage?.(role)
59
+ await rt.syncDutyToGroups?.(role)
60
+ }
61
+ }
62
+ return { role }
63
+ },
64
+ 'roles.setStatus': async (payload) => ({ role: await rt.setRoleStatus(payload.id, payload.status) }),
65
+ 'roles.delete': async (payload) => {
66
+ const result = rt.deleteRole(payload.id)
67
+ if (result.refused && payload.force) {
68
+ return rt.forceDeleteRole(payload.id)
69
+ }
70
+ return result
71
+ },
72
+
73
+ // ---- 群组 ----
74
+ // 返回精简字段(不含 log/seen),并附最后一条消息预览/时间/发送者与总 seq(供未读计算)。
75
+ 'groups.list': async () => {
76
+ const groups = (rt.listGroups() ?? []).map((group) => {
77
+ const lines = rt.groupTranscript(group.id)
78
+ const last = lines.length > 0 ? lines[lines.length - 1] : null
79
+ return {
80
+ id: group.id,
81
+ name: group.name,
82
+ prefix: group.prefix,
83
+ members: group.members ?? [],
84
+ memberNames: (group.members ?? []).map(id => rt.getRole(id)?.name ?? id),
85
+ admins: group.admins ?? [],
86
+ adminNames: (group.admins ?? []).map(id => rt.getRole(id)?.name ?? id),
87
+ notice: group.notice ?? '',
88
+ workspaceId: group.workspaceId ?? null,
89
+ dir: group.dir,
90
+ status: group.status,
91
+ createdAt: group.createdAt,
92
+ messageCount: rt.groupMessageCount(group.id),
93
+ seq: group.seq ?? 0,
94
+ lastMessage: String(last?.text ?? '').replace(/\s+/g, ' ').slice(0, 80)
95
+ || (last?.attachments?.length ? `📎 ${last.attachments[0].name}${last.attachments.length > 1 ? ` 等 ${last.attachments.length} 个文件` : ''}` : ''),
96
+ lastTime: last?.time ?? null,
97
+ lastFrom: last?.fromName ?? '',
98
+ }
99
+ })
100
+ return { groups }
101
+ },
102
+ 'groups.create': async (payload) => {
103
+ // 工作目录必选:不再提供"留空 = 静默创建默认目录"路径——
104
+ // 用户必须显式指定群文件(含附件)的存放位置。
105
+ if (!payload.customDir || !String(payload.customDir).trim()) {
106
+ throw new Error('必须选择工作目录:请指定已有目录,或输入新路径(不存在会自动创建)')
107
+ }
108
+ const group = await rt.createGroup({
109
+ name: payload.name ?? payload.prefix,
110
+ members: payload.members ?? [],
111
+ admins: payload.admins ?? [],
112
+ notice: payload.notice ?? '',
113
+ withDir: false,
114
+ customDir: String(payload.customDir).trim(),
115
+ })
116
+ return { group }
117
+ },
118
+ 'groups.update': async (payload) => ({
119
+ group: await rt.updateGroupMeta(payload.id, {
120
+ name: payload.name,
121
+ notice: payload.notice,
122
+ admins: payload.admins,
123
+ members: payload.members,
124
+ }),
125
+ }),
126
+ 'groups.rename': async (payload) => ({ group: await rt.renameGroup(payload.id, payload.name ?? payload.prefix) }),
127
+ 'groups.delete': async (payload) => rt.deleteGroup(payload.id),
128
+
129
+ // ---- 目录选择(代理 DSH 原生 directoryPickerController,供建群指定自定义工作目录) ----
130
+ 'directory.pick': async () => {
131
+ const picker = ctx.get?.('directoryPickerController') ?? ctx.directoryPickerController
132
+ if (!picker) throw new Error('目录选择器不可用')
133
+ const path = await picker.pick(AbortSignal.timeout(120000))
134
+ return { path }
135
+ },
136
+ 'directory.list': async (payload) => {
137
+ const picker = ctx.get?.('directoryPickerController') ?? ctx.directoryPickerController
138
+ if (!picker) throw new Error('目录选择器不可用')
139
+ const listing = await picker.list(payload.path, AbortSignal.timeout(10000))
140
+ return { listing }
141
+ },
142
+ 'directory.create': async (payload) => {
143
+ const picker = ctx.get?.('directoryPickerController') ?? ctx.directoryPickerController
144
+ if (!picker) throw new Error('目录选择器不可用')
145
+ const path = await picker.createDirectory(payload.parentPath, payload.name)
146
+ return { path }
147
+ },
148
+
149
+
150
+ // Read-only approval projection. No approval answer endpoint is exposed.
151
+ 'approvals.list': async (payload) => rt.groupApprovals(payload.groupId),
152
+
153
+ // ---- 运行态 / 停止 ----
154
+ 'activity.list': async (payload) => ({ activity: await rt.activityList(payload?.groupId) }),
155
+ 'people.stop': async (payload) => rt.stopPerson(payload.groupId, payload.roleId),
156
+
157
+ // ---- 记忆 ----
158
+ 'memory.list': async (payload) => rt.memoryList(payload.roleId),
159
+ 'memory.add': async (payload) => ({
160
+ count: await rt.memoryAdd(payload.roleId, payload.scope ?? 'me', payload.text),
161
+ }),
162
+ 'memory.remove': async (payload) => ({
163
+ count: await rt.memoryRemove(payload.roleId, payload.scope ?? 'me', payload.index),
164
+ }),
165
+
166
+ // ---- 任务 ----
167
+ 'tasks.list': async (payload) => {
168
+ const tasks = rt.listTasks(payload?.groupId).map(task => ({
169
+ ...task,
170
+ assigneeNames: (task.assignees ?? []).map(id => rt.getRole(id)?.name ?? id),
171
+ groupName: rt.getGroup(task.groupId)?.name ?? task.groupId,
172
+ }))
173
+ return { tasks }
174
+ },
175
+ 'tasks.create': async (payload) => ({
176
+ task: await rt.createTask({
177
+ title: payload.title,
178
+ description: payload.description,
179
+ groupId: payload.groupId,
180
+ assignees: payload.assignees ?? [],
181
+ priority: payload.priority,
182
+ dependencies: payload.dependencies,
183
+ fromRoleId: payload.fromRoleId,
184
+ }),
185
+ }),
186
+ 'tasks.update': async (payload) => ({ task: await rt.updateTask(payload.id, payload.patch ?? {}) }),
187
+
188
+ 'execution.status': async (payload) => ({error:rt.executionError??null,runs:rt.executionRuns(payload?.taskId).slice(-100),
189
+ projects:rt.listGroups().filter(g=>!payload?.groupId || g.id===payload.groupId).map(g=>({groupId:g.id,delivery:g.delivery??null,workItemSync:g.workItemSync??null})),
190
+ reviewOutbox:(rt.state.execution?.reviewOutbox??[]).filter(n=>!payload?.groupId || n.groupId===payload.groupId).slice(-100)}),
191
+ 'execution.retry': async (payload) => ({runs:await rt.retryExecution(payload.taskId)}),
192
+ 'execution.stop': async (payload) => ({runs:await rt.stopTaskExecution(payload.taskId)}),
193
+ 'decisions.resolve': async (payload) => ({decision:await rt.resolveDecision(payload.groupId,payload.answer,payload.seq)}),
194
+
195
+ // ---- 协作工作台版本更新(仅浏览器用户界面调用;不注册 Agent 工具) ----
196
+ 'updates.check': async (payload) => rt.updateCheck({ fresh: payload.fresh === true }),
197
+ 'updates.install': async () => rt.updateStart(),
198
+
199
+ // ---- 群消息 ----
200
+ 'messages.post': async (payload) => ({
201
+ result: await rt.postGroupMessage({
202
+ groupId: payload.groupId,
203
+ text: payload.text,
204
+ fromRoleId: payload.fromRoleId,
205
+ attachments: payload.attachments,
206
+ taskId: payload.taskId,
207
+ replyToSeq: payload.replyToSeq,
208
+ decisionSeq: payload.decisionSeq,
209
+ }),
210
+ }),
211
+ 'messages.transcript': async (payload) => ({
212
+ lines: rt.groupTranscript(payload.groupId, payload.sinceSeq ?? 0),
213
+ // 附带活跃成员(工作指示器数据源——跟随消息轮询,无需单独调用)
214
+ activity: await rt.activityList?.(payload.groupId) ?? [],
215
+ }),
216
+
217
+ // ---- 群附件(+ 号上传:落入群目录「附件/」,发送前可移除) ----
218
+ 'files.upload': async (payload) => ({
219
+ file: await rt.uploadGroupFile({
220
+ groupId: payload.groupId,
221
+ name: payload.name,
222
+ base64: payload.base64,
223
+ }),
224
+ }),
225
+ 'files.remove': async (payload) => rt.removeGroupFile({
226
+ groupId: payload.groupId,
227
+ path: payload.path,
228
+ }),
229
+ }
230
+
231
+ ctx.connection.fetch.register({
232
+ path: '/api/dsh-workbench',
233
+ methods: ['POST'],
234
+ requestBody: 'buffered',
235
+ fetch: async (request) => {
236
+ const respond = (status, body) => new Response(JSON.stringify(body), {
237
+ status,
238
+ headers: { 'content-type': 'application/json' },
239
+ })
240
+ try {
241
+ const raw = await request.text()
242
+ const body = raw ? JSON.parse(raw) : {}
243
+ const { endpoint, payload } = body
244
+ const handler = actions[endpoint]
245
+ if (!handler) return respond(404, { ok: false, error: `未知端点: ${endpoint}` })
246
+ await rt.loadAll()
247
+ await rt.executionReady
248
+ const data = await handler(payload ?? {})
249
+ return respond(200, { ok: true, data })
250
+ } catch (error) {
251
+ return respond(400, { ok: false, error: error?.message ?? String(error) })
252
+ }
253
+ },
254
+ })
255
+ }
256
+
257
+ // UI 展示用的预设模板(去内部字段)。
258
+ const PRESETS_FOR_UI = [
259
+ { type: '研发负责人', instructions: '接收需求、分解任务、协调进度、验收交付。' },
260
+ { type: '架构师', instructions: '技术方案设计、架构评审。' },
261
+ { type: '高级工程师', instructions: '代码实现、联调、自测。' },
262
+ { type: '测试工程师', instructions: '测试用例设计、质量验证。' },
263
+ { type: '产品经理', instructions: '需求澄清、验收标准定义、价值把关。' },
264
+ ]
@@ -0,0 +1,34 @@
1
+ export const APPROVAL_KEY = 'dwbApprovals'
2
+ export const emptyApprovals = () => ({calls: [], approvals: []})
3
+ export function approvalDisplayItems(items) {
4
+ const ordered=[...items].sort((a,b)=>a.askedAt-b.askedAt)
5
+ const history = new Set(ordered.filter(a=>a.outcome!=='pending').slice(-40))
6
+ return ordered.filter(a=>a.outcome==='pending'||history.has(a))
7
+ }
8
+ const settle = (items, id, outcome, time) => items.map(item => item.id === id && item.outcome === 'pending' ? {...item, outcome, decidedAt: time} : item)
9
+ const trim = approvalDisplayItems
10
+
11
+ /** Display cache only. No approval capability, resolver or authority is stored here. */
12
+ export function foldApprovals(state, event) {
13
+ const data = event.data ?? {}
14
+ if (event.type === 'tool/call') {
15
+ return {...state, calls: [...state.calls, {id: data.callId, name: data.name, arguments: data.arguments ?? ''}].slice(-60)}
16
+ }
17
+ if (event.type === 'approval/asked') {
18
+ if (state.approvals.some(a => a.id === data.id)) return state
19
+ const call = state.calls.findLast(c => c.id === data.callId)
20
+ let args = {}
21
+ try { args = JSON.parse(call?.arguments ?? '{}') } catch { /* retain safe text fallback */ }
22
+ const item = {id: data.id, toolName: data.toolName, callId: data.callId ?? '', reason: data.reason ?? '',
23
+ command: String(args.command ?? args.file_path ?? call?.arguments ?? '').slice(0, 12000),
24
+ permission: String(args.sandbox_permissions ?? ''), askedAt: event.time, outcome: 'pending'}
25
+ return {...state, approvals: trim([...state.approvals, item])}
26
+ }
27
+ if (event.type === 'approval/decided') return {...state, approvals: trim(settle(state.approvals, data.id, data.outcome, event.time))}
28
+ if (event.type === 'turn/end') {
29
+ if (!state.approvals.some(a => a.outcome === 'pending')) return state
30
+ return {...state, approvals: trim(state.approvals.map(a => a.outcome === 'pending' ? {...a, outcome: 'expired', decidedAt: event.time} : a))}
31
+ }
32
+ return state
33
+ }
34
+
@@ -0,0 +1,36 @@
1
+ import { z } from 'zod'
2
+ import { APPROVAL_KEY, emptyApprovals, foldApprovals, approvalDisplayItems } from './approvals-fold.js'
3
+
4
+ export function install(rt) {
5
+ const item = z.object({id:z.string(), toolName:z.string(), callId:z.string(), reason:z.string(), command:z.string(), permission:z.string(), askedAt:z.number(), outcome:z.string(), decidedAt:z.number().optional()})
6
+ const viewSchema = z.array(item)
7
+ rt.ctx.sessionProjections.register({key:APPROVAL_KEY, stateVersion:1,
8
+ stateSchema:z.object({calls:z.array(z.object({id:z.string(),name:z.string(),arguments:z.string()})),approvals:viewSchema}),
9
+ init:emptyApprovals, apply:foldApprovals, wire:{viewSchema,view:state=>state.approvals}})
10
+
11
+ const owners = group => {
12
+ const map = new Map()
13
+ for (const [roleId, entry] of Object.entries(group.sessions ?? {})) if (entry.id) map.set(entry.id,{roleId})
14
+ for (const run of rt.executionRuns?.() ?? []) if (run.groupId === group.id && run.sessionId) map.set(run.sessionId,{roleId:run.roleId,taskId:run.taskId,runId:run.id})
15
+ return map
16
+ }
17
+ // Read-only UI data. Approval is answered ONLY by the original Client interaction.
18
+ rt.groupApprovals = async groupId => {
19
+ const group = rt.getGroup(groupId)
20
+ if (!group || group.status !== 'active') throw new Error('群组不存在或不可用')
21
+ const items = [], sessions = [], errors = []
22
+ for (const [sessionId, owner] of owners(group)) {
23
+ try {
24
+ const projection = await rt.ctx.sessionController.projections({sessionId}, AbortSignal.timeout(5000))
25
+ const approvals = projection?.values?.[APPROVAL_KEY] ?? []
26
+ const run = owner.runId ? rt.executionRuns?.().find(r=>r.id===owner.runId) : null
27
+ const live = rt.ctx.get?.('agents')?.get(sessionId)
28
+ const pending = approvals.some(a=>a.outcome==='pending')
29
+ if (pending || live?.status === 'running' || (run && ['starting','running','approval-wait'].includes(run.status))) sessions.push(sessionId)
30
+ for (const approval of approvals) items.push({...approval, ...owner, sessionId, roleName:rt.getRole(owner.roleId)?.name ?? owner.roleId,
31
+ live:!!live && pending, taskTitle:owner.taskId ? rt.getTask(owner.taskId)?.title ?? owner.taskId : ''})
32
+ } catch(error) {errors.push({sessionId,error:String(error?.message??error)})}
33
+ }
34
+ return {items:approvalDisplayItems(items), sessions:[...new Set(sessions)], errors}
35
+ }
36
+ }