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.
@@ -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 === "my_industrial_nodes") result = await toolMyIndustrialNodes(agentId);
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":";;;;;;;;;;;;;;;;;AAiBA,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,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;;;;"}
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;;;;"}
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentworkshop-prod",
3
- "version": "0.5.6",
3
+ "version": "0.5.7",
4
4
  "type": "module",
5
5
  "private": true,
6
6
  "dependencies": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentworkshop",
3
- "version": "0.5.6",
3
+ "version": "0.5.7",
4
4
  "type": "module",
5
5
  "description": "AgentWorkShop 软件开发系统 - 基于 Nuxt 4 的配置驱动运行范式",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",
@@ -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 === 'my_industrial_nodes') result = await toolMyIndustrialNodes(agentId)
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 })
@@ -3474,7 +3474,7 @@
3474
3474
  "channels": [],
3475
3475
  "degraded": true
3476
3476
  },
3477
- "evaluatedAt": "2026-09-02T12:07:31.704Z",
3477
+ "evaluatedAt": "2026-09-02T12:19:06.694Z",
3478
3478
  "windowAgg": {
3479
3479
  "at": "2026-08-31T16:25:17.398Z",
3480
3480
  "fromMs": 1788193396897,
@@ -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":[]}