@waterplus-ai/waterbuddy 0.1.78 → 0.2.5

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.
Files changed (29) hide show
  1. package/README.md +13 -3
  2. package/assets/experts/teams/smart-water-delivery-team/.codebuddy-plugin/plugin.json +19 -0
  3. package/assets/experts/teams/smart-water-delivery-team/agents/lead.md +10 -0
  4. package/assets/experts/teams/software-development-team/.codebuddy-plugin/plugin.json +57 -0
  5. package/assets/experts/teams/software-development-team/agents/gstack-designer.md +209 -0
  6. package/assets/experts/teams/software-development-team/agents/gstack-investigator.md +208 -0
  7. package/assets/experts/teams/software-development-team/agents/gstack-lead.md +264 -0
  8. package/assets/experts/teams/software-development-team/agents/gstack-product-reviewer.md +516 -0
  9. package/assets/experts/teams/software-development-team/agents/gstack-qa-lead.md +187 -0
  10. package/assets/experts/teams/software-development-team/agents/gstack-security-officer.md +408 -0
  11. package/assets/experts/teams/software-development-team/avatars/gstack-designer.svg +1 -0
  12. package/assets/experts/teams/software-development-team/avatars/gstack-investigator.svg +1 -0
  13. package/assets/experts/teams/software-development-team/avatars/gstack-lead.svg +1 -0
  14. package/assets/experts/teams/software-development-team/avatars/gstack-product-reviewer.svg +1 -0
  15. package/assets/experts/teams/software-development-team/avatars/gstack-qa-lead.svg +1 -0
  16. package/assets/experts/teams/software-development-team/avatars/gstack-security-officer.svg +1 -0
  17. package/assets/experts/teams/software-development-team/avatars/team.svg +1 -0
  18. package/assets/experts/teams/software-development-team/skills/design-html/SKILL.md +35 -0
  19. package/assets/experts/teams/software-development-team/skills/qa/SKILL.md +44 -0
  20. package/assets/experts/teams/software-development-team/skills/review/SKILL.md +43 -0
  21. package/assets/experts/teams/water-operations-team/.codebuddy-plugin/plugin.json +19 -0
  22. package/assets/experts/teams/water-operations-team/agents/lead.md +9 -0
  23. package/lib/client/index.js +296 -120
  24. package/lib/host/expert-store.js +1 -1
  25. package/lib/host/expert-tools.js +156 -25
  26. package/lib/host/experts.js +511 -26
  27. package/lib/host/index.js +51 -22
  28. package/lib/host/llm-gateway.js +1 -1
  29. package/package.json +3 -3
@@ -138,7 +138,7 @@ export function createExpertStore(options = {}) {
138
138
  } catch (error) {
139
139
  if (error.code !== 'ENOENT') {
140
140
  // 坏文件不能让设置页打不开:退回默认值并记一条 warning,下次写入会覆盖它。
141
- console.warn('[WaterBuddy] expert config is unreadable:', error.message)
141
+ console.warn('[WaterPlusAI] expert config is unreadable:', error.message)
142
142
  }
143
143
  return normalizeExpertState(undefined)
144
144
  }
@@ -9,7 +9,7 @@
9
9
  * 召唤走 `ctx.subagents`:把 persona 注入子代理(provider 的 `persona` 能力),
10
10
  * 并用 `toolFilter` 禁掉这三个工具本身——否则专家会继续召唤专家,无限套娃。
11
11
  */
12
- import { EXPERT_DIVISIONS, readExpertPersona } from './experts.js'
12
+ import { EXPERT_DIVISIONS, readExpertMemberPersona, readExpertPersona } from './experts.js'
13
13
 
14
14
  /** 三个工具的名字。`summon*` 会互相禁用,列表见 {@link EXPERT_SUMMON_DENY}。 */
15
15
  export const EXPERT_TOOL_NAMES = ['list_experts', 'summon_expert', 'summon_experts']
@@ -152,6 +152,74 @@ function requireTask(task) {
152
152
  return text
153
153
  }
154
154
 
155
+ function normalizeTeamMemberId(value) {
156
+ return String(value || '')
157
+ .replaceAll('\\', '/')
158
+ .replace(/^\.\//, '')
159
+ .replace(/^agents\//, '')
160
+ .replace(/\.md$/i, '')
161
+ .split('/')
162
+ .pop()
163
+ .trim()
164
+ }
165
+
166
+ function localizedTeamValue(value) {
167
+ if (value && typeof value === 'object') return String(value.zh || value.en || Object.values(value)[0] || '').trim()
168
+ return String(value || '').trim()
169
+ }
170
+
171
+ function teamMemberEntries(expert) {
172
+ const entries = []
173
+ const seen = new Set()
174
+ const leadId = normalizeTeamMemberId(expert.leadAgent || expert.agentName)
175
+ const add = (id, source = 'member') => {
176
+ const normalized = normalizeTeamMemberId(id)
177
+ if (!normalized || seen.has(normalized)) return
178
+ const metadata = (expert.members || []).find(member => normalizeTeamMemberId(member.slug) === normalized)
179
+ const pluginMetadata = (Array.isArray(expert.plugin?.members) ? expert.plugin.members : [])
180
+ .find(member => normalizeTeamMemberId(member?.id || member?.slug) === normalized)
181
+ const referenced = (expert.memberExperts || []).find(member => normalizeTeamMemberId(member.slug) === normalized)
182
+ const role = metadata?.role === 'lead' || pluginMetadata?.role === 'lead' || normalized === leadId
183
+ ? 'lead'
184
+ : source === 'lead' ? 'lead' : 'member'
185
+ seen.add(normalized)
186
+ entries.push({
187
+ id: normalized,
188
+ name: metadata?.name || referenced?.name || (role === 'lead' ? expert.name : normalized),
189
+ focus: metadata?.profession
190
+ || metadata?.roleName
191
+ || localizedTeamValue(pluginMetadata?.profession || pluginMetadata?.roleName)
192
+ || referenced?.description
193
+ || '',
194
+ role,
195
+ })
196
+ }
197
+ for (const file of Array.isArray(expert.plugin?.agents) ? expert.plugin.agents : []) {
198
+ add(file, normalizeTeamMemberId(file) === leadId ? 'lead' : 'member')
199
+ }
200
+ for (const member of expert.members || []) add(member.slug, member.role === 'lead' ? 'lead' : 'member')
201
+ for (const member of expert.memberExperts || []) add(member.slug)
202
+ if (leadId && !seen.has(leadId)) add(leadId, 'lead')
203
+ if (entries.length === 0) add(leadId || expert.slug, 'lead')
204
+ return entries
205
+ }
206
+
207
+ function teamMemberTask(expert, member, task) {
208
+ const role = member.role === 'lead' ? '负责人' : '成员'
209
+ const focus = member.focus ? `专业重点:${member.focus}。` : ''
210
+ const instruction = member.role === 'lead'
211
+ ? '请作为总负责人独立给出判断、优先级和可执行的汇总方案。'
212
+ : '请只从自己的专业视角评审,给出具体发现、风险和建议,不要重复完整实现,也不要假设其他成员已经完成工作。'
213
+ return [
214
+ `你是“${expert.name}”的${role}「${member.name}」。`,
215
+ focus,
216
+ instruction,
217
+ '',
218
+ '用户原始任务:',
219
+ task,
220
+ ].filter(Boolean).join('\n')
221
+ }
222
+
155
223
  /**
156
224
  * 构造三个工具定义。
157
225
  *
@@ -175,9 +243,14 @@ export function createExpertToolDefinitions({ getCatalog, getEnabled, config, su
175
243
  }
176
244
 
177
245
  /** 只保留启用中的专家;一个分类被全部关掉时整条不显示,免得名册里冒出空分类。 */
178
- const visibleDivisions = catalog => catalog.divisions
179
- .map(division => ({ ...division, experts: division.experts.filter(expert => isEnabled(expert.slug)) }))
180
- .filter(division => division.experts.length > 0)
246
+ const visibleDivisions = catalog => {
247
+ const groups = catalog.divisions
248
+ .map(division => ({ ...division, experts: division.experts.filter(expert => isEnabled(expert.slug)) }))
249
+ .filter(division => division.experts.length > 0)
250
+ const teams = (catalog.teams || []).filter(team => isEnabled(team.slug))
251
+ if (teams.length > 0) groups.push({ slug: 'teams', name: '专家团', experts: teams })
252
+ return groups
253
+ }
181
254
 
182
255
  /** 名册总览:分区永远给(便宜),指定分区时才附专家列表。 */
183
256
  const divisionOverview = catalog => visibleDivisions(catalog).map(division => ({
@@ -193,28 +266,16 @@ export function createExpertToolDefinitions({ getCatalog, getEnabled, config, su
193
266
  description: codePointLength(expert.description) > EXPERT_DESCRIPTION_MAX_CHARS
194
267
  ? `${Array.from(expert.description).slice(0, EXPERT_DESCRIPTION_MAX_CHARS).join('')}…`
195
268
  : expert.description,
269
+ kind: expert.kind || 'expert',
270
+ expertType: expert.expertType || 'expert',
271
+ members: expert.members || [],
196
272
  })
197
273
 
198
- /** 召唤一个专家并等它跑完;失败按仓库口径抛出(注册表转成 isError)。 */
199
- async function summonOnce(expert, task, exec) {
200
- if (!exec?.agent) {
201
- throw new Error('EXPERT_REQUIRES_AGENT: 专家召唤需要所属的 agent 会话')
202
- }
203
- // 停用的专家必须显式拒绝:静默"召唤成功"会让人以为开关没生效。
204
- if (!isEnabled(expert.slug)) {
205
- throw new Error(`EXPERT_DISABLED: 专家 ${expert.slug} 已在设置里停用`)
206
- }
207
- const { providerName, provider } = resolveExpertProvider(subagents, config)
208
- const persona = await readExpertPersona(expert)
209
- if (!persona) {
210
- throw new Error(`EXPERT_PERSONA_EMPTY: ${expert.slug} 的正文为空`)
211
- }
274
+ async function runSubagent(providerName, request, exec) {
212
275
  const maxDepth = Number(config?.expertMaxDepth)
213
276
  const run = await subagents.start(providerName, {
214
- label: `expert:${expert.slug}`,
215
- prompt: [{ type: 'text', text: task }],
277
+ ...request,
216
278
  parent: exec.agent,
217
- persona,
218
279
  // 禁掉召唤工具:专家不该再召唤专家。
219
280
  toolFilter: { deny: EXPERT_SUMMON_DENY },
220
281
  ...(Number.isSafeInteger(maxDepth) && maxDepth >= 0 ? { maxDepth } : {}),
@@ -227,18 +288,88 @@ export function createExpertToolDefinitions({ getCatalog, getEnabled, config, su
227
288
  if (failure !== undefined) {
228
289
  throw new Error(`${failure}:${output || '(无输出)'}`)
229
290
  }
230
- return { expert: expert.slug, name: expert.name, provider: providerName, output }
291
+ return output
231
292
  } finally {
232
293
  // 无论成功失败都要释放子代理资源。
233
294
  await run.dispose()
234
295
  }
235
296
  }
236
297
 
298
+ /** 召唤一个专家并等它跑完;失败按仓库口径抛出(注册表转成 isError)。 */
299
+ async function summonOnce(expert, task, exec) {
300
+ if (!exec?.agent) {
301
+ throw new Error('EXPERT_REQUIRES_AGENT: 专家召唤需要所属的 agent 会话')
302
+ }
303
+ // 停用的专家必须显式拒绝:静默"召唤成功"会让人以为开关没生效。
304
+ if (!isEnabled(expert.slug)) {
305
+ throw new Error(`EXPERT_DISABLED: 专家 ${expert.slug} 已在设置里停用`)
306
+ }
307
+ const { providerName } = resolveExpertProvider(subagents, config)
308
+ if (expert.kind !== 'team') {
309
+ const persona = await readExpertPersona(expert)
310
+ if (!persona) {
311
+ throw new Error(`EXPERT_PERSONA_EMPTY: ${expert.slug} 的正文为空`)
312
+ }
313
+ const output = await runSubagent(providerName, {
314
+ label: `expert:${expert.slug}`,
315
+ prompt: [{ type: 'text', text: task }],
316
+ persona,
317
+ }, exec)
318
+ return { expert: expert.slug, name: expert.name, provider: providerName, output }
319
+ }
320
+
321
+ const members = teamMemberEntries(expert)
322
+ const invokeMember = async (member, persona) => {
323
+ const label = `expert:${expert.slug}:${member.id}`
324
+ try {
325
+ const memberPersona = persona || await readExpertMemberPersona(expert, member.id)
326
+ if (!memberPersona) throw new Error(`EXPERT_MEMBER_PERSONA_EMPTY: ${expert.slug}/${member.id}`)
327
+ const output = await runSubagent(providerName, {
328
+ label,
329
+ prompt: [{ type: 'text', text: teamMemberTask(expert, member, task) }],
330
+ persona: memberPersona,
331
+ }, exec)
332
+ return { ok: true, member, output }
333
+ } catch (error) {
334
+ return { ok: false, member, error: error.message }
335
+ }
336
+ }
337
+ const lead = members.find(member => member.role === 'lead') || members[0]
338
+ let leadResult
339
+ if (lead) {
340
+ try {
341
+ leadResult = invokeMember(lead, await readExpertPersona(expert))
342
+ } catch (error) {
343
+ leadResult = Promise.resolve({ ok: false, member: lead, error: error.message })
344
+ }
345
+ }
346
+ const preparedMembers = await Promise.all(members
347
+ .filter(member => member !== lead)
348
+ .map(async member => {
349
+ try {
350
+ return { member, persona: await readExpertMemberPersona(expert, member.id) }
351
+ } catch (error) {
352
+ return { member, error: error.message }
353
+ }
354
+ }))
355
+ const memberResults = await Promise.all(preparedMembers.map(item => item.error
356
+ ? { ok: false, member: item.member, error: item.error }
357
+ : invokeMember(item.member, item.persona)))
358
+ const results = leadResult ? [await leadResult, ...memberResults] : memberResults
359
+ const output = [
360
+ `已召集“${expert.name}”${results.length}位成员并行评审:`,
361
+ ...results.map(result => result.ok
362
+ ? `【${result.member.name}】\n${result.output || '(无输出)'}`
363
+ : `【${result.member.name}】\n失败:${result.error}`),
364
+ ].join('\n\n')
365
+ return { expert: expert.slug, name: expert.name, provider: providerName, output }
366
+ }
367
+
237
368
  return [
238
369
  {
239
370
  name: 'list_experts',
240
371
  description:
241
- '浏览专家名册。不传 division 时只返回分区与各分区专家数量;传了 division 才返回该分区的专家名单。',
372
+ '浏览专家名册。不传 division 时只返回分区与各分区专家数量;传了 division 才返回该分区的专家名单。专家团使用 division=teams。',
242
373
  // 注意:本插件是纯 JS、绕过了 `defineTool`,所以 parameters / output.schema 都必须写成
243
374
  // **原始 JSON Schema**(`required` 是对象级的字符串数组)。宿主里的内置工具用的是
244
375
  // 属性级 `required: true` 的「spec 方言」,那由 `defineTool` 转换——我们不经过它。
@@ -248,7 +379,7 @@ export function createExpertToolDefinitions({ getCatalog, getEnabled, config, su
248
379
  properties: {
249
380
  division: {
250
381
  type: 'string',
251
- description: `分区 slug,可选值:${Object.keys(EXPERT_DIVISIONS).join('、')}`,
382
+ description: `分区 slug,可选值:${[...Object.keys(EXPERT_DIVISIONS), 'teams'].join('、')}`,
252
383
  },
253
384
  },
254
385
  },
@@ -296,7 +427,7 @@ export function createExpertToolDefinitions({ getCatalog, getEnabled, config, su
296
427
  {
297
428
  name: 'summon_expert',
298
429
  description:
299
- '召唤一位专家分身执行任务,并等它返回结果。专家是独立的子代理,拥有该领域的系统提示词与完整的常备工具集。',
430
+ '召唤一位专家分身执行任务,并等它返回结果。专家团会自动并行召唤负责人和所有成员;普通专家只启动一个子代理。每个子代理拥有对应领域的系统提示词与完整的常备工具集。',
300
431
  parameters: {
301
432
  type: 'object',
302
433
  additionalProperties: false,