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,175 @@
1
+ // memory.js —— 记忆(P1:个人记忆 + 团队记忆,Markdown 存储,工具 + RPC 管理)。
2
+ // 格式(参考 AgentMemoryRepo 行格式,自研实现):
3
+ // 每条一行:`- 内容 [added: YYYY-MM-DD]`
4
+ // 存储:$home/memory/person-<id>/MEMORY.md 与 $home/memory/team/MEMORY.md
5
+ // 注入:群会话种子消息带记忆摘要;工具 dwb_remember/dwb_forget/dwb_recall 供人员随时读写。
6
+ import { mkdir, readFile, writeFile } from 'node:fs/promises'
7
+ import { defineTool } from '@deepseek-ai/dsh-tools'
8
+ import { join } from 'node:path'
9
+
10
+ const MAX_ENTRIES = 200 // 每个作用域条目上限
11
+ const MAX_LINE = 300 // 单条最多 300 字符
12
+
13
+ function today() {
14
+ const d = new Date()
15
+ const p = (n) => String(n).padStart(2, '0')
16
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`
17
+ }
18
+
19
+ export function install(rt) {
20
+ const { ctx } = rt
21
+
22
+ const memRoot = () => join(rt.home, 'memory')
23
+ const personPath = (roleId) => join(memRoot(), `person-${roleId}`, 'MEMORY.md')
24
+ const teamPath = () => join(memRoot(), 'team', 'MEMORY.md')
25
+
26
+ async function readScope(path) {
27
+ try {
28
+ const raw = await readFile(path, 'utf8')
29
+ return raw.split('\n')
30
+ .map(l => l.trim())
31
+ .filter(l => l.startsWith('- '))
32
+ .map(l => {
33
+ const m = l.match(/^- (.*) \[added: (\d{4}-\d{2}-\d{2})\]$/)
34
+ if (m) return { text: m[1], added: m[2] }
35
+ // 容错:无日期尾注的行
36
+ return { text: l.slice(2).replace(/ \[added: [^\]]*\]$/, ''), added: '' }
37
+ })
38
+ } catch {
39
+ return []
40
+ }
41
+ }
42
+
43
+ async function writeScope(path, entries) {
44
+ await mkdir(join(path, '..'), { recursive: true })
45
+ const body = entries.slice(0, MAX_ENTRIES)
46
+ .map(e => `- ${e.text} [added: ${e.added || today()}]`)
47
+ .join('\n')
48
+ await writeFile(path, body ? body + '\n' : '', 'utf8')
49
+ }
50
+
51
+ const scopePath = (scope, roleId) => (scope === 'team' ? teamPath() : personPath(roleId))
52
+
53
+ // ---- RPC 面(client 记忆面板) ----
54
+ rt.memoryList = async (roleId) => ({
55
+ own: roleId ? await readScope(personPath(roleId)) : [],
56
+ team: await readScope(teamPath()),
57
+ })
58
+ rt.memoryAdd = async (roleId, scope, text) => {
59
+ const clean = String(text ?? '').trim().slice(0, MAX_LINE)
60
+ if (!clean) throw new Error('记忆内容不能为空')
61
+ if (/api[_-]?key|password|token|secret|密钥|密码/i.test(clean)) {
62
+ throw new Error('记忆不允许保存密钥/密码类内容')
63
+ }
64
+ const path = scopePath(scope, roleId)
65
+ const entries = await readScope(path)
66
+ if (entries.length >= MAX_ENTRIES) throw new Error(`记忆已达上限(${MAX_ENTRIES} 条),请先清理`)
67
+ entries.push({ text: clean, added: today() })
68
+ await writeScope(path, entries)
69
+ return entries.length
70
+ }
71
+ rt.memoryRemove = async (roleId, scope, index) => {
72
+ const path = scopePath(scope, roleId)
73
+ const entries = await readScope(path)
74
+ if (index < 0 || index >= entries.length) throw new Error('条目不存在')
75
+ entries.splice(index, 1)
76
+ await writeScope(path, entries)
77
+ return entries.length
78
+ }
79
+
80
+ // 种子消息用的记忆摘要(各作用域最多取 12 条)。
81
+ rt.memoryDigest = async (roleId) => {
82
+ const [own, team] = await Promise.all([
83
+ readScope(personPath(roleId)),
84
+ readScope(teamPath()),
85
+ ])
86
+ const parts = []
87
+ if (own.length > 0) parts.push(`【你的记忆】\n${own.slice(-12).map(e => `- ${e.text}`).join('\n')}`)
88
+ if (team.length > 0) parts.push(`【团队记忆】\n${team.slice(-12).map(e => `- ${e.text}`).join('\n')}`)
89
+ return parts.join('\n')
90
+ }
91
+
92
+ // ---- 人员工具面 ----
93
+ const OUTPUT = {
94
+ schema: { type: 'string' },
95
+ render: (_args, value) => [{ type: 'text', text: String(value) }],
96
+ }
97
+
98
+ ctx.tools.register(defineTool({
99
+ name: 'dwb_remember',
100
+ description: '保存一条记忆(跨对话持久)。scope=me 存入你的个人记忆,scope=team 存入团队共享记忆(管理员/群管理员可用)。单条 ≤300 字符,禁止保存密钥/密码。',
101
+ parameters: {
102
+ text: { type: 'string', description: '要记住的内容(偏好/决策/纠错/事实)', required: true },
103
+ scope: { type: 'string', enum: ['me', 'team'], description: 'me=个人(默认),team=团队共享' },
104
+ roleId: { type: 'string', description: '你的角色 id(dwb_post_group_message 同款)' },
105
+ },
106
+ output: OUTPUT,
107
+ async execute(args) {
108
+ const me = args.roleId ?? currentRoleId.get()
109
+ if (!me) return '请提供你的 roleId'
110
+ if (args.scope === 'team') {
111
+ // 团队记忆:管理员或任意群管理员可写
112
+ const role = rt.getRole(me)
113
+ const isAdmin = (rt.listGroups() ?? []).some(g => (g.admins ?? []).includes(me))
114
+ if (!isAdmin && role?.type !== '研发负责人') {
115
+ return '团队记忆仅管理员可写;个人记忆请用 scope=me'
116
+ }
117
+ }
118
+ const n = await rt.memoryAdd(me, args.scope ?? 'me', args.text)
119
+ return `已记住(${args.scope === 'team' ? '团队' : '个人'},共 ${n} 条)`
120
+ },
121
+ }))
122
+
123
+ ctx.tools.register(defineTool({
124
+ name: 'dwb_forget',
125
+ description: '删除一条记忆:给出要删除内容的完整原文或前缀。',
126
+ parameters: {
127
+ text: { type: 'string', description: '要删除的记忆内容(原文或明显前缀)', required: true },
128
+ roleId: { type: 'string', description: '你的角色 id' },
129
+ },
130
+ output: OUTPUT,
131
+ async execute(args) {
132
+ const me = args.roleId ?? currentRoleId.get()
133
+ if (!me) return '请提供你的 roleId'
134
+ for (const scope of ['me', 'team']) {
135
+ const path = scopePath(scope, me)
136
+ const entries = await readScope(path)
137
+ const idx = entries.findIndex(e => e.text === args.text || e.text.startsWith(args.text))
138
+ if (idx >= 0) {
139
+ entries.splice(idx, 1)
140
+ await writeScope(path, entries)
141
+ return `已从${scope === 'team' ? '团队' : '个人'}记忆删除 1 条`
142
+ }
143
+ }
144
+ return '未找到匹配的记忆'
145
+ },
146
+ }))
147
+
148
+ ctx.tools.register(defineTool({
149
+ name: 'dwb_recall',
150
+ description: '搜索记忆(个人 + 团队)。query 为空时列出全部条目。',
151
+ parameters: {
152
+ query: { type: 'string', description: '关键词(空=列出全部)' },
153
+ roleId: { type: 'string', description: '你的角色 id' },
154
+ },
155
+ output: OUTPUT,
156
+ async execute(args) {
157
+ const me = args.roleId ?? currentRoleId.get()
158
+ if (!me) return '请提供你的 roleId'
159
+ const { own, team } = await rt.memoryList(me)
160
+ const q = String(args.query ?? '').trim().toLowerCase()
161
+ const hit = (list) => (q ? list.filter(e => e.text.toLowerCase().includes(q)) : list)
162
+ const fmt = (list) => list.map(e => `- ${e.text}${e.added ? `(${e.added})` : ''}`).join('\n') || '(无)'
163
+ return `【个人记忆】\n${fmt(hit(own))}\n\n【团队记忆】\n${fmt(hit(team))}`
164
+ },
165
+ }))
166
+
167
+ // 当前投递上下文中的人员 id(deliverToMember 投递时登记,工具执行时可读到)。
168
+ const currentRoleId = {
169
+ value: null,
170
+ get: () => currentRoleId.value,
171
+ set: (v) => { currentRoleId.value = v },
172
+ }
173
+ rt.setCurrentDeliveryRole = currentRoleId.set
174
+ }
175
+
@@ -0,0 +1,43 @@
1
+ // policy.js —— 工作台规范(V2:职责字段只存纯净内容,不注入模板)。
2
+ // 早期版本把「工作台铁律」模板注入 role.instructions,导致:
3
+ // a) 序号错乱(rules[3] 误覆盖第 3 条)、职责行重复;
4
+ // b) 编辑弹窗带着模板往返存储,越改越乱。
5
+ // 现改为:instructions 只存用户职责;历史损坏数据由 migrateRoleInstructions 清洗。
6
+ export const MESSAGE_POLICY = '群聊只发需要用户知道、决定或处理的信息:负责人统一启动一次,仅重要变化汇报进度,最终交付一次(结果、入口、验证情况及遗留问题);成员不逐个宣告开工、不发收到/确认、不重复转述。开发报结果与验证,测试报通过与否、覆盖范围及未解决风险,产品/架构只报影响范围或实现方向的关键结论。独立执行用 dwb_report_execution 留存完整摘要与 evidence,系统生成简短回执,不再另发同一成果或阻塞;普通执行中状态不刷群。仅需对方行动时 @协调;阻塞说明问题、影响及需要谁做什么,相同阻塞不重复播报。决策说明问题、推荐选项和主要影响,需求明确不二次确认。普通消息尽量 100 字、最多 150 字;技术细节和完整证据留在文档/任务记录,通过 dwb_task_execution 查询。'
7
+
8
+ export const IRON_RULES = [
9
+ '所有沟通只发生在群组中,用 @角色名 点名;',
10
+ '不存在任何一对一私聊通道;',
11
+ '收到群消息后,把结论/进展/疑问回复到群组,保持群内可见。',
12
+ ]
13
+
14
+ // 清洗历史注入的模板:去掉铁律 boilerplate,提取纯职责。
15
+ // 兼容损坏样本(缺第3条、职责行重复、序号 3/4 不一):
16
+ // 取所有「N. 你的角色职责:」行中第一个冒号后非空的内容作为纯职责。
17
+ export function sanitizeInstructions(text) {
18
+ const raw = String(text ?? '')
19
+ if (!raw.includes('【工作台铁律】')) return raw
20
+ const lines = raw.split('\n')
21
+ for (const line of lines) {
22
+ const m = line.trim().match(/^\d+\.\s*你的角色职责:(.*)$/)
23
+ if (m && m[1].trim()) return m[1].trim()
24
+ }
25
+ return ''
26
+ }
27
+
28
+ export function install(rt) {
29
+ // 一次性迁移:清洗所有角色的 instructions(幂等,重启安全)。
30
+ rt.migrateRoleInstructions = () => {
31
+ let changed = false
32
+ for (const role of rt.state.roles.items) {
33
+ const clean = sanitizeInstructions(role.instructions ?? '')
34
+ if (clean !== (role.instructions ?? '')) {
35
+ role.instructions = clean
36
+ role.updatedAt = Date.now()
37
+ changed = true
38
+ }
39
+ }
40
+ if (changed) rt.touch()
41
+ return changed
42
+ }
43
+ }
@@ -0,0 +1,117 @@
1
+ // Tasks retain the existing UI status vocabulary; execution details live in execution.runs.
2
+ export const PRIORITIES = ['高', '中', '低']
3
+ export const TASK_STATUSES = ['待办', '进行中', '已完成']
4
+ const PATCH_FIELDS = new Set(['title', 'description', 'assignees', 'priority', 'status', 'dependencies'])
5
+
6
+ export function install(rt) {
7
+ rt.validatePriority = (value = '中') => {
8
+ if (!PRIORITIES.includes(value)) throw new Error(`优先级必须是 ${PRIORITIES.join('/')}`)
9
+ return value
10
+ }
11
+ rt.validateTaskStatus = (value = '待办') => {
12
+ if (!TASK_STATUSES.includes(value)) throw new Error(`状态必须是 ${TASK_STATUSES.join('/')}`)
13
+ return value
14
+ }
15
+ const validateAssignees = (values, group) => {
16
+ if (!Array.isArray(values) || !values.length) throw new Error('任务至少指派 1 名工程师')
17
+ const ids = [...new Set(values.map(String))]
18
+ for (const id of ids) {
19
+ const role = rt.getRole(id)
20
+ if (!role) throw new Error(`指派角色 ${id} 不存在`)
21
+ if (role.status !== 'active') throw new Error(`指派角色「${role.name}」已停用`)
22
+ if (!(group.members ?? []).includes(id)) throw new Error(`指派角色「${role.name}」必须是群组成员`)
23
+ }
24
+ return ids
25
+ }
26
+ const validateDependencies = (values = [], group, ownId) => {
27
+ if (!Array.isArray(values)) throw new Error('dependencies 必须是任务 id 数组')
28
+ const ids = [...new Set(values.map(String))]
29
+ const visit = (id, seen = new Set()) => {
30
+ if (id === ownId) throw new Error('任务依赖不能形成循环')
31
+ if (seen.has(id)) return
32
+ seen.add(id)
33
+ const t = rt.getTask(id)
34
+ if (!t || t.groupId !== group.id) throw new Error(`依赖任务 ${id} 必须存在且属于同一群组`)
35
+ for (const next of t.dependencies ?? []) visit(next, seen)
36
+ }
37
+ ids.forEach(id => visit(id))
38
+ return ids
39
+ }
40
+ const ready = async () => { await rt.loadAll(); await rt.executionReady }
41
+ const groupFor = id => {
42
+ const group = rt.getGroup(id)
43
+ if (!group || group.status !== 'active') throw new Error('群组不存在、已归档或正在删除')
44
+ return group
45
+ }
46
+ const notify = async (task, text, fromRoleId) => {
47
+ try {
48
+ const post = rt.postSystemGroupMessage ?? rt.postGroupMessage
49
+ await post({ groupId: task.groupId, taskId: task.id, text, fromRoleId, noWake: true })
50
+ } catch (error) {
51
+ // A task already committed is not a failed create; caller retries must not duplicate it.
52
+ task.notificationError = String(error?.message ?? error)
53
+ rt.touch()
54
+ }
55
+ }
56
+ rt.createTask = async ({ title, description, groupId, assignees, priority, dependencies, fromRoleId, autoPost = true }) => {
57
+ await ready()
58
+ const cleanTitle = String(title ?? '').trim()
59
+ if (!cleanTitle) throw new Error('任务标题不能为空')
60
+ const group = groupFor(groupId)
61
+ fromRoleId = rt.resolveGroupActor?.(group, fromRoleId) ?? fromRoleId
62
+ if (group.pendingDecision) throw new Error('群内有未回复的决策,等待用户拍板后再创建任务')
63
+ const task = {
64
+ id: rt.nextId('task'), title: cleanTitle, description: String(description ?? ''), groupId: group.id,
65
+ assignees: validateAssignees(assignees, group), priority: rt.validatePriority(priority),
66
+ dependencies: validateDependencies(dependencies, group), schedulerVersion: 1, revision: 1, status: '待办',
67
+ createdAt: Date.now(), updatedAt: Date.now(), completedAt: null,
68
+ }
69
+ rt.addTask(task)
70
+ // Persist task + run intents before notification or model execution.
71
+ await rt.taskCreated?.(task)
72
+ await rt.saveAll?.()
73
+ if (autoPost) await notify(task, `【新任务 ${task.id}】${task.title}\n优先级:${task.priority}\n描述:${task.description || '(无)'}\n指派:${task.assignees.map(id => `@${rt.getRole(id)?.name ?? id}`).join(' ')}\n已进入系统执行队列。`, fromRoleId)
74
+ rt.kickExecution?.()
75
+ return task
76
+ }
77
+ rt.updateTask = async (id, patch, { fromRoleId, autoPost = true } = {}) => {
78
+ await ready()
79
+ const task = rt.getTask(id)
80
+ if (!task) throw new Error(`任务 ${id} 不存在`)
81
+ const group = groupFor(task.groupId)
82
+ fromRoleId = rt.resolveGroupActor?.(group, fromRoleId) ?? fromRoleId
83
+ for (const key of Object.keys(patch)) if (!PATCH_FIELDS.has(key)) throw new Error(`不允许修改任务字段 ${key}`)
84
+ const next = { ...patch }
85
+ if (next.title !== undefined) {
86
+ next.title = String(next.title).trim()
87
+ if (!next.title) throw new Error('任务标题不能为空')
88
+ }
89
+ if (next.description !== undefined) next.description = String(next.description)
90
+ if (next.assignees !== undefined) next.assignees = validateAssignees(next.assignees, group)
91
+ if (next.priority !== undefined) next.priority = rt.validatePriority(next.priority)
92
+ if (next.dependencies !== undefined) next.dependencies = validateDependencies(next.dependencies, group, task.id)
93
+ if (next.status !== undefined) {
94
+ next.status = rt.validateTaskStatus(next.status)
95
+ next.completedAt = next.status === '已完成' ? (task.completedAt ?? Date.now()) : null
96
+ }
97
+ await rt.validateExecutionTaskUpdate?.(task, next)
98
+ if (['title','description','assignees','dependencies'].some(key => next[key] !== undefined && JSON.stringify(next[key]) !== JSON.stringify(task[key]))) next.revision = (task.revision ?? 1) + 1
99
+ const updated = rt.updateTaskRecord(id, { ...next, updatedAt: Date.now() })
100
+ await rt.taskChanged?.(updated, task)
101
+ await rt.saveAll?.()
102
+ if (autoPost) await notify(updated, `【任务更新 ${updated.id}】${updated.title}\n状态:${updated.status}\n优先级:${updated.priority}`, fromRoleId)
103
+ rt.kickExecution?.()
104
+ return updated
105
+ }
106
+ rt.listTasks = groupId => {
107
+ const canonical = groupId ? (rt.getGroup(groupId)?.id ?? groupId) : null
108
+ return rt.state.tasks.items.filter(t => !canonical || t.groupId === canonical)
109
+ }
110
+ rt.updateTaskRecord = (id, patch) => {
111
+ const index = rt.state.tasks.items.findIndex(t => t.id === id)
112
+ if (index < 0) return undefined
113
+ rt.state.tasks.items[index] = { ...rt.state.tasks.items[index], ...patch }
114
+ rt.touch()
115
+ return rt.state.tasks.items[index]
116
+ }
117
+ }