agentworkshop 0.5.6 → 0.5.7
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/.AgentWorkShop/prompts/host-tools.json +31 -0
- package/.output/.AgentWorkShop/prompts/host-tools.json +31 -0
- package/.output/nitro.json +1 -1
- package/.output/public/_nuxt/builds/latest.json +1 -1
- package/.output/public/_nuxt/builds/meta/02278199-eeea-4f96-882a-94c5408c6cb6.json +1 -0
- package/.output/server/chunks/_/nitro.mjs +778 -623
- package/.output/server/chunks/routes/api/workshop/agent-tools/invoke.post.mjs +5 -2
- package/.output/server/chunks/routes/api/workshop/agent-tools/invoke.post.mjs.map +1 -1
- package/.output/server/package.json +1 -1
- package/package.json +1 -1
- package/scripts/_dbg-cross-team-visibility-e2e.mjs +129 -0
- package/server/api/workshop/agent-tools/invoke.post.ts +8 -1
- package/server/data/dcw-rollback.json +1 -1
- package/server/services/workshop/agents/agent-interface.ts +18 -0
- package/server/services/workshop/agents/omp-agent.ts +27 -0
- package/server/services/workshop/db/memory.repo.ts +18 -0
- package/server/services/workshop/runtime/manager.ts +122 -0
- package/.output/public/_nuxt/builds/meta/d50c6ec2-801c-4ba2-97d8-a4db401b702c.json +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as defineApiHandler, o as readBody, n as createError, X as toolMyIndustrialNodes, Y as toolDcwControl, Z as toolDaqQuery, _ as toolDcwJudge, $ as toolDcwRollback, a0 as toolDcwJournal } from '../../../../_/nitro.mjs';
|
|
1
|
+
import { c as defineApiHandler, o as readBody, n as createError, g as getWorkshopManager, X as toolMyIndustrialNodes, Y as toolDcwControl, Z as toolDaqQuery, _ as toolDcwJudge, $ as toolDcwRollback, a0 as toolDcwJournal } from '../../../../_/nitro.mjs';
|
|
2
2
|
import { resolveUser } from '../caller.mjs';
|
|
3
3
|
import 'node:crypto';
|
|
4
4
|
import 'node:http';
|
|
@@ -15,6 +15,7 @@ import 'node:net';
|
|
|
15
15
|
import 'node:util';
|
|
16
16
|
import 'node:module';
|
|
17
17
|
|
|
18
|
+
const COLLAB_TOOLS = /* @__PURE__ */ new Set(["list_other_teams", "search_other_teams_memory", "send_cross_channel_message"]);
|
|
18
19
|
const invoke_post = defineApiHandler(async (event) => {
|
|
19
20
|
var _a, _b, _c, _d;
|
|
20
21
|
resolveUser(event);
|
|
@@ -24,7 +25,9 @@ const invoke_post = defineApiHandler(async (event) => {
|
|
|
24
25
|
const args = (_d = body.args) != null ? _d : {};
|
|
25
26
|
if (!agentId) throw createError({ statusCode: 400, statusMessage: "agentId required" });
|
|
26
27
|
let result;
|
|
27
|
-
if (tool
|
|
28
|
+
if (COLLAB_TOOLS.has(tool)) {
|
|
29
|
+
result = getWorkshopManager().invokeAgentWorkspaceTool(agentId, tool, args);
|
|
30
|
+
} else if (tool === "my_industrial_nodes") result = await toolMyIndustrialNodes(agentId);
|
|
28
31
|
else if (tool === "dcw_control") result = await toolDcwControl(agentId, args);
|
|
29
32
|
else if (tool === "daq_query") result = await toolDaqQuery(agentId, args);
|
|
30
33
|
else if (tool === "dcw_judge") result = await toolDcwJudge(agentId, args);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"invoke.post.mjs","sources":["../../../../../../../server/api/workshop/agent-tools/invoke.post.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"invoke.post.mjs","sources":["../../../../../../../server/api/workshop/agent-tools/invoke.post.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAmBA,MAAA,+BAAA,IAAA,GAAA,CAAA,CAAA,kBAAA,EAAA,2BAAA,EAAA,4BAAA,CAAA,CAAA;AAEA,oBAAA,gBAAA,CAAA,OAAA,KAAA,KAAA;;AACA,EAAA,WAAA,CAAA,KAAA,CAAA;AACA,EAAA,MAAA,QAAA,EAAA,GAAA,MAAA,QAAA,CAAA,KAAA,CAAA,KAAA,YAAA,EAAA;AACA,EAAA,MAAA,OAAA,GAAA,MAAA,CAAA,CAAA,EAAA,GAAA,IAAA,CAAA,OAAA,KAAA,YAAA,EAAA,CAAA;AACA,EAAA,MAAA,IAAA,GAAA,MAAA,CAAA,CAAA,EAAA,GAAA,IAAA,CAAA,IAAA,KAAA,YAAA,EAAA,CAAA;AACA,EAAA,MAAA,IAAA,GAAA,CAAA,EAAA,GAAA,IAAA,CAAA,IAAA,KAAA,IAAA,GAAA,EAAA,GAAA,EAAA;AACA,EAAA,IAAA,CAAA,SAAA,MAAA,WAAA,CAAA,EAAA,UAAA,EAAA,GAAA,EAAA,aAAA,EAAA,kBAAA,EAAA,CAAA;AACA,EAAA,IAAA,MAAA;AACA,EAAA,IAAA,YAAA,CAAA,GAAA,CAAA,IAAA,CAAA,EAAA;AACA,IAAA,MAAA,GAAA,kBAAA,EAAA,CAAA,wBAAA,CAAA,OAAA,EAAA,MAAA,IAAA,CAAA;AAAA,EACA,WACA,IAAA,KAAA,qBAAA,EAAA,MAAA,GAAA,MAAA,sBAAA,OAAA,CAAA;AAAA,OAAA,IACA,SAAA,aAAA,EAAA,MAAA,GAAA,MAAA,cAAA,CAAA,SAAA,IAAA,CAAA;AAAA,OAAA,IACA,SAAA,WAAA,EAAA,MAAA,GAAA,MAAA,YAAA,CAAA,SAAA,IAAA,CAAA;AAAA,OAAA,IACA,SAAA,WAAA,EAAA,MAAA,GAAA,MAAA,YAAA,CAAA,SAAA,IAAA,CAAA;AAAA,OAAA,IACA,SAAA,cAAA,EAAA,MAAA,GAAA,MAAA,eAAA,CAAA,SAAA,IAAA,CAAA;AAAA,OAAA,IACA,SAAA,aAAA,EAAA,MAAA,GAAA,MAAA,cAAA,CAAA,SAAA,IAAA,CAAA;AAAA,OACA,MAAA,YAAA,EAAA,UAAA,EAAA,KAAA,aAAA,EAAA,CAAA,cAAA,EAAA,IAAA,CAAA,CAAA,EAAA,CAAA;AACA,EAAA,OAAA,EAAA,MAAA,EAAA;AACA,CAAA,CAAA;;;;"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 跨团队可见性端到端验证:leader 看其他团队任务场景(list_other_teams)、
|
|
3
|
+
* 全员查其他团队共享记忆(search_other_teams_memory)、权限反证(worker 查概览被拒)。
|
|
4
|
+
* 前置:运行中的环境(默认 3000)。运行: node scripts/_dbg-cross-team-visibility-e2e.mjs
|
|
5
|
+
*/
|
|
6
|
+
const ROOT = process.env.AW_BASE ?? 'http://127.0.0.1:3000'
|
|
7
|
+
let TOKEN = ''
|
|
8
|
+
const H = () => ({ authorization: `Bearer ${TOKEN}`, 'content-type': 'application/json' })
|
|
9
|
+
const sleep = (ms) => new Promise(r => setTimeout(r, ms))
|
|
10
|
+
let failures = 0
|
|
11
|
+
const check = (name, ok, detail = '') => {
|
|
12
|
+
console.log(` ${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ` — ${detail}` : ''}`)
|
|
13
|
+
if (!ok) failures++
|
|
14
|
+
}
|
|
15
|
+
const fail = (m) => { console.error('FATAL:', m); process.exit(1) }
|
|
16
|
+
|
|
17
|
+
async function api(method, path, body) {
|
|
18
|
+
const res = await fetch(ROOT + path, { method, headers: H(), body: body !== undefined ? JSON.stringify(body) : undefined })
|
|
19
|
+
const json = await res.json().catch(() => ({}))
|
|
20
|
+
return { status: res.status, ...json }
|
|
21
|
+
}
|
|
22
|
+
const jpost = (u, b) => api('POST', u, b ?? {})
|
|
23
|
+
const jget = (u) => api('GET', u)
|
|
24
|
+
const invoke = async (agentId, tool, args = {}) => {
|
|
25
|
+
const r = await jpost('/api/workshop/agent-tools/invoke', { agentId, tool, args })
|
|
26
|
+
const result = r.data?.result
|
|
27
|
+
return { status: r.status, text: String(result?.text ?? JSON.stringify(r).slice(0, 200)), isError: result?.isError === true }
|
|
28
|
+
}
|
|
29
|
+
const cleanup = []
|
|
30
|
+
const tag = Math.random().toString(36).slice(2, 8)
|
|
31
|
+
|
|
32
|
+
async function deployTeam(name, withWorker) {
|
|
33
|
+
const chRes = (await jpost('/api/workshop/channels', { name, description: '跨团队可见性验证' })).data
|
|
34
|
+
cleanup.push(['channel', chRes.channelId])
|
|
35
|
+
const team = (await jpost('/api/workshop/teams', { name: name + '-组', description: '跨团队可见性验证' })).data
|
|
36
|
+
cleanup.push(['team', team.id])
|
|
37
|
+
await jpost(`/api/workshop/teams/${team.id}/members`, { agentId: 'tpl-default-lead', role: 'lead' })
|
|
38
|
+
let worker = null
|
|
39
|
+
if (withWorker) {
|
|
40
|
+
const tpl = (await jpost('/api/workshop/agents', { name: name + '-worker', harness: 'mock', config: {} })).data
|
|
41
|
+
cleanup.push(['agent-tpl', tpl.id])
|
|
42
|
+
await jpost(`/api/workshop/teams/${team.id}/members`, { agentId: tpl.id, role: 'worker' })
|
|
43
|
+
}
|
|
44
|
+
await jpost(`/api/workshop/teams/${team.id}/deploy`, { channelId: chRes.channelId })
|
|
45
|
+
const agents = (await jget(`/api/workshop/channels/${chRes.channelId}/agents`)).data
|
|
46
|
+
const lead = agents.find(a => a.role === 'lead')
|
|
47
|
+
worker = agents.find(a => a.role === 'worker') ?? null
|
|
48
|
+
if (lead) cleanup.push(['channel-agent', `${chRes.channelId}:${lead.id}`])
|
|
49
|
+
if (worker) cleanup.push(['channel-agent', `${chRes.channelId}:${worker.id}`])
|
|
50
|
+
return { channelId: chRes.channelId, lead, worker }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function main() {
|
|
54
|
+
console.log('━━━ 0. 注册与双团队部署 ━━━')
|
|
55
|
+
const reg = await jpost('/api/workshop/users/register', { name: 'xvis-' + tag })
|
|
56
|
+
TOKEN = reg.data?.token
|
|
57
|
+
if (!TOKEN) fail('注册失败: ' + JSON.stringify(reg).slice(0, 120))
|
|
58
|
+
check('注册并取得 token', !!TOKEN)
|
|
59
|
+
const east = await deployTeam('东线团队-' + tag, false)
|
|
60
|
+
const west = await deployTeam('西线团队-' + tag, true)
|
|
61
|
+
check('东线(无 worker)+ 西线(含 worker)就绪', !!east.lead?.id && !!west.lead?.id && !!west.worker?.id,
|
|
62
|
+
`eastLead=${String(east.lead?.id ?? '').slice(0, 8)} westLead=${String(west.lead?.id ?? '').slice(0, 8)}`)
|
|
63
|
+
|
|
64
|
+
// ===== 1. 东线沉淀共享记忆(他人将检索的知识)=====
|
|
65
|
+
console.log('\n━━━ 1. 东线共享记忆沉淀 ━━━')
|
|
66
|
+
const mem = await jpost(`/api/workshop/channels/${east.channelId}/memories`, {
|
|
67
|
+
title: '淬火工艺结论-' + tag,
|
|
68
|
+
content: `东线团队完成淬火工序验证:淬火温度 182℃ 保温 30 分钟后硬度达标 HRC58,温度超过 190℃ 会出现过回火软化。结论可用于西线同类工件工艺制定(${tag})。`,
|
|
69
|
+
importance: 0.9,
|
|
70
|
+
})
|
|
71
|
+
check('东线共享记忆写入', mem.status === 200, `status=${mem.status}`)
|
|
72
|
+
|
|
73
|
+
// ===== 2. 东线创建进行中场景任务 =====
|
|
74
|
+
console.log('\n━━━ 2. 东线进行中任务 ━━━')
|
|
75
|
+
const task = await jpost(`/api/workshop/channels/${east.channelId}/tasks`, {
|
|
76
|
+
title: '东线批次复检-' + tag,
|
|
77
|
+
parts: [{ text: '对当前批次做复检并记录结论' }],
|
|
78
|
+
})
|
|
79
|
+
const taskId = task.data?.task?.id ?? task.data?.id
|
|
80
|
+
cleanup.push(['task', taskId])
|
|
81
|
+
check('东线任务创建(进行中)', !!taskId, `id=${String(taskId ?? '').slice(0, 8)}`)
|
|
82
|
+
|
|
83
|
+
// ===== 3. 西线 lead:list_other_teams 看到东线的任务场景 =====
|
|
84
|
+
console.log('\n━━━ 3. Leader 跨团队观察面 ━━━')
|
|
85
|
+
const ov = await invoke(west.lead.id, 'list_other_teams')
|
|
86
|
+
const ovText = String(ov.text ?? '')
|
|
87
|
+
check('lead 查看其他团队概览', ov.status === 200 && ovText.includes('东线团队-' + tag), `status=${ov.status}`)
|
|
88
|
+
check('概览包含东线进行中任务标题', ovText.includes('东线批次复检-' + tag))
|
|
89
|
+
check('概览包含东线近期完成记录', ovText.includes('近期完成'))
|
|
90
|
+
|
|
91
|
+
// ===== 4. 西线 worker:search_other_teams_memory 查到东线共享知识 =====
|
|
92
|
+
console.log('\n━━━ 4. Worker 跨团队记忆检索 ━━━')
|
|
93
|
+
const hit = await invoke(west.worker.id, 'search_other_teams_memory', { query: '淬火 温度 硬度', limit: 5 })
|
|
94
|
+
const hitText = String(hit.text ?? '')
|
|
95
|
+
check('worker 检索到东线共享记忆', hit.status === 200 && hitText.includes('淬火工艺结论-' + tag),
|
|
96
|
+
`status=${hit.status} text=${hitText.slice(0, 60)}`)
|
|
97
|
+
check('检索结果带团队归属', hitText.includes('东线团队-' + tag))
|
|
98
|
+
|
|
99
|
+
// ===== 5. 权限反证:worker 查任务概览被拒(仅 lead)=====
|
|
100
|
+
const denied = await invoke(west.worker.id, 'list_other_teams')
|
|
101
|
+
check('worker 查任务概览被拒(仅 lead)', denied.isError === true || denied.status === 403 || String(denied.text ?? '').includes('Leader'),
|
|
102
|
+
`status=${denied.status} text=${String(denied.text ?? '').slice(0, 60)}`)
|
|
103
|
+
|
|
104
|
+
// ===== 6. 工作流语义:知识已存在 → lead 直接复用,无需发信 =====
|
|
105
|
+
const reuse = hitText.includes('182') && hitText.includes('HRC58')
|
|
106
|
+
check('跨团队知识可复用(检索内容含结论数值)', reuse)
|
|
107
|
+
console.log(' (工作流:命中知识 → 直接复用;未命中但有相关任务 → lead 用 send_cross_channel_message 发信请求协作)')
|
|
108
|
+
|
|
109
|
+
// ===== 清理 =====
|
|
110
|
+
console.log('\n━━━ 清理 ━━━')
|
|
111
|
+
for (const [kind, id] of [...cleanup].reverse()) {
|
|
112
|
+
try {
|
|
113
|
+
if (kind === 'channel') await api('DELETE', `/api/workshop/channels/${id}`)
|
|
114
|
+
else if (kind === 'team') await api('DELETE', `/api/workshop/teams/${id}`)
|
|
115
|
+
else if (kind === 'agent-tpl') await api('DELETE', `/api/workshop/agents/${id}`)
|
|
116
|
+
else if (kind === 'channel-agent') {
|
|
117
|
+
const [cid, aid] = String(id).split(':')
|
|
118
|
+
if (cid && aid) await api('DELETE', `/api/workshop/channels/${cid}/agents/${aid}`)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch { /* 清理失败不阻断 */ }
|
|
122
|
+
}
|
|
123
|
+
console.log('清理完成')
|
|
124
|
+
|
|
125
|
+
console.log(failures === 0 ? '\nE2E ALL PASS' : `\nE2E ${failures} FAILED`)
|
|
126
|
+
if (failures > 0) process.exit(1)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
main().catch((e) => { console.error(e); process.exit(1) })
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { readBody } from 'h3'
|
|
7
7
|
import { resolveUser } from '@/server/api/workshop/caller'
|
|
8
8
|
import { defineApiHandler } from '@/server/utils/response'
|
|
9
|
+
import { getWorkshopManager } from '@/server/plugins/workshop'
|
|
9
10
|
import {
|
|
10
11
|
toolDaqQuery,
|
|
11
12
|
toolDcwControl,
|
|
@@ -15,6 +16,9 @@ import {
|
|
|
15
16
|
toolMyIndustrialNodes,
|
|
16
17
|
} from '@/server/services/workshop/agents/industrial-tools'
|
|
17
18
|
|
|
19
|
+
/** 协作工具族:经 manager 工具桥落到 agent workspace(跨团队可见性/跨 Channel 通信) */
|
|
20
|
+
const COLLAB_TOOLS = new Set(['list_other_teams', 'search_other_teams_memory', 'send_cross_channel_message'])
|
|
21
|
+
|
|
18
22
|
export default defineApiHandler(async (event) => {
|
|
19
23
|
resolveUser(event)
|
|
20
24
|
const body = await readBody<{ agentId?: string, tool?: string, args?: Record<string, unknown> }>(event) ?? {}
|
|
@@ -23,7 +27,10 @@ export default defineApiHandler(async (event) => {
|
|
|
23
27
|
const args = body.args ?? {}
|
|
24
28
|
if (!agentId) throw createError({ statusCode: 400, statusMessage: 'agentId required' })
|
|
25
29
|
let result: { text: string, isError?: boolean }
|
|
26
|
-
if (tool
|
|
30
|
+
if (COLLAB_TOOLS.has(tool)) {
|
|
31
|
+
result = getWorkshopManager().invokeAgentWorkspaceTool(agentId, tool, args)
|
|
32
|
+
}
|
|
33
|
+
else if (tool === 'my_industrial_nodes') result = await toolMyIndustrialNodes(agentId)
|
|
27
34
|
else if (tool === 'dcw_control') result = await toolDcwControl(agentId, args as { node_id?: string, value?: number, hypothesis?: string, task_id?: string })
|
|
28
35
|
else if (tool === 'daq_query') result = await toolDaqQuery(agentId, args as Parameters<typeof toolDaqQuery>[1])
|
|
29
36
|
else if (tool === 'dcw_judge') result = await toolDcwJudge(agentId, args as { record_id?: string, verdict?: string, reason?: string })
|
|
@@ -69,6 +69,24 @@ export interface AgentWorkspace {
|
|
|
69
69
|
toChannelName: string
|
|
70
70
|
toLeadAgentId: string
|
|
71
71
|
}>
|
|
72
|
+
/** (仅 lead)其他团队概览:同主其余 channel 的场景任务(进行中/近期完成)——跨团队协同的观察面 */
|
|
73
|
+
listOtherTeams(): Promise<Array<{
|
|
74
|
+
channelId: string
|
|
75
|
+
name: string
|
|
76
|
+
description: string
|
|
77
|
+
leadName: string | null
|
|
78
|
+
activeTasks: Array<{ id: string, title: string, state: string }>
|
|
79
|
+
recentCompleted: Array<{ title: string }>
|
|
80
|
+
}>>
|
|
81
|
+
/** (全员)跨团队共享记忆检索:其他 channel 的 __team__ 公共域,只读,带团队归属 */
|
|
82
|
+
searchOtherTeamsMemory(input: { query: string, limit?: number }): Promise<Array<{
|
|
83
|
+
channelId: string
|
|
84
|
+
channelName: string
|
|
85
|
+
title: string
|
|
86
|
+
content: string
|
|
87
|
+
importance: number
|
|
88
|
+
createdAt: string
|
|
89
|
+
}>>
|
|
72
90
|
/**
|
|
73
91
|
* 拒绝指派给自己的任务(能力/范畴不匹配):任务置 FAILED(调度器改派他人),
|
|
74
92
|
* 并向派发方(任务创建者,缺省 channel lead)回执拒绝原因。
|
|
@@ -1578,6 +1578,33 @@ export class OmpRpcAgentImpl implements AgentInterface {
|
|
|
1578
1578
|
}
|
|
1579
1579
|
}
|
|
1580
1580
|
|
|
1581
|
+
case 'list_other_teams': {
|
|
1582
|
+
// 跨团队观察面(仅 lead):其他团队的场景任务概览
|
|
1583
|
+
const teams = await ws.listOtherTeams()
|
|
1584
|
+
if (teams.length === 0) return { text: '当前没有其他团队(或均未启用)。' }
|
|
1585
|
+
const text = teams.map((t) => {
|
|
1586
|
+
const active = t.activeTasks.length > 0
|
|
1587
|
+
? t.activeTasks.map(x => `「${x.title}」(${x.state})`).join('、')
|
|
1588
|
+
: '无进行中任务'
|
|
1589
|
+
const done = t.recentCompleted.length > 0
|
|
1590
|
+
? t.recentCompleted.map(x => `「${x.title}」`).join('、')
|
|
1591
|
+
: '无'
|
|
1592
|
+
return `- ${t.name}${t.description ? `(${t.description})` : ''} · lead=${t.leadName ?? '?'}\n 进行中: ${active}\n 近期完成: ${done}\n channel_id: ${t.channelId}`
|
|
1593
|
+
}).join('\n')
|
|
1594
|
+
return { text: `其他团队概览:\n${text}\n(需要协作时用 send_cross_channel_message 向对应团队 Leader 发信;查具体知识用 search_other_teams_memory)` }
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
case 'search_other_teams_memory': {
|
|
1598
|
+
// 跨团队共享记忆(全员只读):别的团队沉淀的结论/经验,先查再决定是否发信
|
|
1599
|
+
const query = (req.arguments.query as string | undefined) ?? ''
|
|
1600
|
+
if (!query) return { text: '缺少 query', isError: true }
|
|
1601
|
+
const rows = await ws.searchOtherTeamsMemory({ query, limit: Number(req.arguments.limit ?? 5) })
|
|
1602
|
+
if (rows.length === 0) return { text: `其他团队的共享记忆中没有命中「${query}」的内容。` }
|
|
1603
|
+
const text = rows.map(r =>
|
|
1604
|
+
`- [${r.channelName}] 「${r.title}」(${r.createdAt.slice(0, 10)}): ${r.content}`).join('\n')
|
|
1605
|
+
return { text: `其他团队共享记忆命中 ${rows.length} 条:\n${text}` }
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1581
1608
|
case 'refuse_task': {
|
|
1582
1609
|
const taskId = req.arguments.task_id as string
|
|
1583
1610
|
const reason = req.arguments.reason as string
|
|
@@ -133,6 +133,24 @@ export function createMemoryRepo(db: DatabaseSync) {
|
|
|
133
133
|
return searchStmt.all(matchQuery, agentId, TEAM_AGENT_ID, limit) as unknown as Array<MemoryRow & { bm25: number }>
|
|
134
134
|
},
|
|
135
135
|
|
|
136
|
+
/**
|
|
137
|
+
* 跨团队共享域检索(hardening 后的协作扩展;只读):仅 __team__ 行,
|
|
138
|
+
* 限定目标 channel 集合(调用方保证同主/启用),按 bm25 排序。
|
|
139
|
+
* matchQuery 为 OR 连接的切分词(与 search 同规约)。
|
|
140
|
+
*/
|
|
141
|
+
searchTeamSharedAcross(matchQuery: string, channelIds: string[], limit: number): Array<MemoryRow & { bm25: number }> {
|
|
142
|
+
if (channelIds.length === 0) return []
|
|
143
|
+
const placeholders = channelIds.map(() => '?').join(', ')
|
|
144
|
+
const stmt = db.prepare(
|
|
145
|
+
`SELECT ${COLS}, f.rank AS bm25
|
|
146
|
+
FROM agent_memories_fts f
|
|
147
|
+
JOIN agent_memories m ON m.rowid = f.memory_rowid
|
|
148
|
+
WHERE agent_memories_fts MATCH ? AND m.agent_id = ? AND m.channel_id IN (${placeholders})
|
|
149
|
+
ORDER BY f.rank LIMIT ?`,
|
|
150
|
+
)
|
|
151
|
+
return stmt.all(matchQuery, TEAM_AGENT_ID, ...channelIds, limit) as unknown as Array<MemoryRow & { bm25: number }>
|
|
152
|
+
},
|
|
153
|
+
|
|
136
154
|
listRecent(agentId: string, limit: number): MemoryRow[] {
|
|
137
155
|
return listRecentStmt.all(agentId, limit) as unknown as MemoryRow[]
|
|
138
156
|
},
|
|
@@ -978,6 +978,8 @@ export class AgentChannelManager {
|
|
|
978
978
|
reassignTask: (taskId, toAgentId) => this.reassignTask(channelId, agent.id, taskId, toAgentId),
|
|
979
979
|
sendMessage: input => this.sendA2A(channelId, agent.id, input),
|
|
980
980
|
sendCrossChannelMessage: input => this.sendCrossChannelMessage(channelId, agent.id, input),
|
|
981
|
+
listOtherTeams: () => this.listOtherTeamsOverview(channelId, agent.id),
|
|
982
|
+
searchOtherTeamsMemory: input => this.recallOtherTeamsMemory(channelId, agent.id, input),
|
|
981
983
|
refuseTask: (taskId, reason) => this.refuseTask(channelId, agent.id, taskId, reason),
|
|
982
984
|
pollMailbox: limit => this.pollMailbox(channelId, agent.id, limit),
|
|
983
985
|
waitMailbox: (limit, waitMs) => this.waitMailbox(channelId, agent.id, limit ?? 10, waitMs ?? 0),
|
|
@@ -2538,6 +2540,126 @@ export class AgentChannelManager {
|
|
|
2538
2540
|
}
|
|
2539
2541
|
}
|
|
2540
2542
|
|
|
2543
|
+
/**
|
|
2544
|
+
* 跨团队可见性(仅 lead):其他 channel 的场景任务概览(同主、启用)。
|
|
2545
|
+
* 协同工作流的第一步——先看别的团队在做什么/做过什么,再决定是否发信请求协作。
|
|
2546
|
+
*/
|
|
2547
|
+
listOtherTeamsOverview(channelId: string, agentId: string): Array<{
|
|
2548
|
+
channelId: string, name: string, description: string, leadName: string | null,
|
|
2549
|
+
activeTasks: Array<{ id: string, title: string, state: string }>,
|
|
2550
|
+
recentCompleted: Array<{ title: string }>,
|
|
2551
|
+
}> {
|
|
2552
|
+
this.requireMember(channelId, agentId)
|
|
2553
|
+
const sender = this.deps.repos.channelAgents.findByChannelAgent(channelId, agentId)
|
|
2554
|
+
if (!sender || sender.role !== 'lead') {
|
|
2555
|
+
throw new AppError(403, 'ROLE_FORBIDDEN', `团队任务概览仅限 Leader(${sender?.name ?? agentId} 是 ${sender?.role ?? '非成员'})`)
|
|
2556
|
+
}
|
|
2557
|
+
const terminal = new Set(['COMPLETED', 'FAILED', 'CANCELED'])
|
|
2558
|
+
return this.otherSameOwnerChannels(channelId).map((c) => {
|
|
2559
|
+
const tasks = this.deps.repos.tasks.listByChannel(c.id)
|
|
2560
|
+
const leadRow = c.leadAgentId ? this.deps.repos.channelAgents.findById(c.leadAgentId) : undefined
|
|
2561
|
+
return {
|
|
2562
|
+
channelId: c.id,
|
|
2563
|
+
name: c.name,
|
|
2564
|
+
description: c.description ?? '',
|
|
2565
|
+
leadName: leadRow?.name ?? null,
|
|
2566
|
+
activeTasks: tasks
|
|
2567
|
+
.filter(t => !terminal.has(t.state))
|
|
2568
|
+
.map(t => ({ id: t.id, title: t.title, state: t.state })),
|
|
2569
|
+
recentCompleted: tasks
|
|
2570
|
+
.filter(t => t.state === 'COMPLETED')
|
|
2571
|
+
.slice(0, 3)
|
|
2572
|
+
.map(t => ({ title: t.title })),
|
|
2573
|
+
}
|
|
2574
|
+
})
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
/**
|
|
2578
|
+
* 跨团队共享记忆检索(lead/worker 皆可,只读):其他 channel 的 __team__ 公共域。
|
|
2579
|
+
* 全员可见——worker 由此知道别的团队做过什么;发现相关经验/结论可直接复用,
|
|
2580
|
+
* 无需跨 channel 发信;查无所得而有协作必要时,再由 lead 发起跨 channel 通信。
|
|
2581
|
+
*/
|
|
2582
|
+
recallOtherTeamsMemory(channelId: string, agentId: string, input: { query: string, limit?: number }): Array<{
|
|
2583
|
+
channelId: string, channelName: string, title: string, content: string, importance: number, createdAt: string,
|
|
2584
|
+
}> {
|
|
2585
|
+
this.requireMember(channelId, agentId)
|
|
2586
|
+
const q = input.query.trim()
|
|
2587
|
+
if (!q) return []
|
|
2588
|
+
const others = this.otherSameOwnerChannels(channelId)
|
|
2589
|
+
if (others.length === 0) return []
|
|
2590
|
+
const matchQuery = segmentCJK(q).split(/\s+/).filter(Boolean).join(' OR ')
|
|
2591
|
+
const rows = this.deps.repos.memories.searchTeamSharedAcross(
|
|
2592
|
+
matchQuery,
|
|
2593
|
+
others.map(c => c.id),
|
|
2594
|
+
Math.min(Math.max(input.limit ?? 5, 1), 20),
|
|
2595
|
+
)
|
|
2596
|
+
return rows.map(r => ({
|
|
2597
|
+
channelId: r.channelId,
|
|
2598
|
+
channelName: others.find(c => c.id === r.channelId)?.name ?? r.channelId,
|
|
2599
|
+
title: r.title,
|
|
2600
|
+
content: unsegmentCJK(r.content).slice(0, 400),
|
|
2601
|
+
importance: r.importance,
|
|
2602
|
+
createdAt: r.createdAt,
|
|
2603
|
+
}))
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
/** 同主且启用的其他 channel 列表(跨团队可见性的作用域) */
|
|
2607
|
+
private otherSameOwnerChannels(channelId: string): ChannelRow[] {
|
|
2608
|
+
const self = this.deps.repos.channels.findById(channelId)
|
|
2609
|
+
return this.deps.repos.channels.list().filter(c =>
|
|
2610
|
+
c.id !== channelId
|
|
2611
|
+
&& c.enabled === 1
|
|
2612
|
+
&& (self?.ownerUserId == null || c.ownerUserId == null || c.ownerUserId === self.ownerUserId))
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2615
|
+
/**
|
|
2616
|
+
* 工具桥(协作工具族):按 agentId 定位 channel 并调用其 workspace 方法。
|
|
2617
|
+
* 与工业工具桥同一服务层,供 REST 直调(测试/运维/控制台)。
|
|
2618
|
+
*/
|
|
2619
|
+
invokeAgentWorkspaceTool(agentId: string, tool: string, args: Record<string, unknown> = {}): { text: string, isError?: boolean } {
|
|
2620
|
+
const row = this.deps.repos.channelAgents.findById(agentId)
|
|
2621
|
+
if (!row || row.enabled !== 1) throw new AppError(404, 'NOT_FOUND', `agent 不存在或已停用: ${agentId}`)
|
|
2622
|
+
const ws = this.ensureAgentRuntime(row.channelId, agentId)?.workspace
|
|
2623
|
+
if (!ws) throw new AppError(400, 'AGENT_NOT_READY', `agent 运行时未装配: ${agentId}`)
|
|
2624
|
+
switch (tool) {
|
|
2625
|
+
case 'list_other_teams': {
|
|
2626
|
+
const teams = ws.listOtherTeams()
|
|
2627
|
+
if (teams.length === 0) return { text: '当前没有其他团队(或均未启用)。' }
|
|
2628
|
+
const text = teams.map((t) => {
|
|
2629
|
+
const active = t.activeTasks.length > 0
|
|
2630
|
+
? t.activeTasks.map(x => `「${x.title}」(${x.state})`).join('、')
|
|
2631
|
+
: '无进行中任务'
|
|
2632
|
+
const done = t.recentCompleted.length > 0
|
|
2633
|
+
? t.recentCompleted.map(x => `「${x.title}」`).join('、')
|
|
2634
|
+
: '无'
|
|
2635
|
+
return `- ${t.name}${t.description ? `(${t.description})` : ''} · lead=${t.leadName ?? '?'}\n 进行中: ${active}\n 近期完成: ${done}\n channel_id: ${t.channelId}`
|
|
2636
|
+
}).join('\n')
|
|
2637
|
+
return { text: `其他团队概览:\n${text}\n(需要协作时用 send_cross_channel_message 向对应团队 Leader 发信;查具体知识用 search_other_teams_memory)` }
|
|
2638
|
+
}
|
|
2639
|
+
case 'search_other_teams_memory': {
|
|
2640
|
+
const query = String(args.query ?? '')
|
|
2641
|
+
if (!query) return { text: '缺少 query', isError: true }
|
|
2642
|
+
const rows = ws.searchOtherTeamsMemory({ query, limit: Number(args.limit ?? 5) })
|
|
2643
|
+
if (rows.length === 0) return { text: `其他团队的共享记忆中没有命中「${query}」的内容。` }
|
|
2644
|
+
const text = rows.map(r =>
|
|
2645
|
+
`- [${r.channelName}] 「${r.title}」(${r.createdAt.slice(0, 10)}): ${r.content}`).join('\n')
|
|
2646
|
+
return { text: `其他团队共享记忆命中 ${rows.length} 条:\n${text}` }
|
|
2647
|
+
}
|
|
2648
|
+
case 'send_cross_channel_message': {
|
|
2649
|
+
if (!args.to_channel_id || !args.message) return { text: '缺少 to_channel_id 或 message', isError: true }
|
|
2650
|
+
const r = ws.sendCrossChannelMessage({
|
|
2651
|
+
toChannelId: String(args.to_channel_id),
|
|
2652
|
+
parts: [{ text: String(args.message) }],
|
|
2653
|
+
requireReply: args.require_reply === true,
|
|
2654
|
+
inReplyTo: typeof args.in_reply_to === 'string' ? args.in_reply_to : undefined,
|
|
2655
|
+
})
|
|
2656
|
+
return { text: `跨 Channel 消息已送达「${r.toChannelName}」的 Leader(channel=${r.toChannelId}, lead=${r.toLeadAgentId.slice(0, 8)}),消息 id=${r.messageId.slice(0, 8)}。` }
|
|
2657
|
+
}
|
|
2658
|
+
default:
|
|
2659
|
+
throw new AppError(400, 'BAD_REQUEST', `工具桥不支持该协作工具: ${tool}`)
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2541
2663
|
/** 阻塞长轮询(poll_messages):到信即时唤醒,250ms 兜底重查 */
|
|
2542
2664
|
async waitMailbox(channelId: string, callerAgentId: string, limit: number, waitMs: number): Promise<A2AMessage[]> {
|
|
2543
2665
|
this.requireMember(channelId, callerAgentId)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"id":"d50c6ec2-801c-4ba2-97d8-a4db401b702c","timestamp":1788350746387,"prerendered":[]}
|