@waterplus-ai/waterbuddy 0.1.79 → 0.2.6
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/README.md +13 -3
- package/assets/experts/teams/smart-water-delivery-team/.codebuddy-plugin/plugin.json +19 -0
- package/assets/experts/teams/smart-water-delivery-team/agents/lead.md +10 -0
- package/assets/experts/teams/software-development-team/.codebuddy-plugin/plugin.json +57 -0
- package/assets/experts/teams/software-development-team/agents/gstack-designer.md +209 -0
- package/assets/experts/teams/software-development-team/agents/gstack-investigator.md +208 -0
- package/assets/experts/teams/software-development-team/agents/gstack-lead.md +264 -0
- package/assets/experts/teams/software-development-team/agents/gstack-product-reviewer.md +516 -0
- package/assets/experts/teams/software-development-team/agents/gstack-qa-lead.md +187 -0
- package/assets/experts/teams/software-development-team/agents/gstack-security-officer.md +408 -0
- package/assets/experts/teams/software-development-team/avatars/gstack-designer.svg +1 -0
- package/assets/experts/teams/software-development-team/avatars/gstack-investigator.svg +1 -0
- package/assets/experts/teams/software-development-team/avatars/gstack-lead.svg +1 -0
- package/assets/experts/teams/software-development-team/avatars/gstack-product-reviewer.svg +1 -0
- package/assets/experts/teams/software-development-team/avatars/gstack-qa-lead.svg +1 -0
- package/assets/experts/teams/software-development-team/avatars/gstack-security-officer.svg +1 -0
- package/assets/experts/teams/software-development-team/avatars/team.svg +1 -0
- package/assets/experts/teams/software-development-team/skills/design-html/SKILL.md +35 -0
- package/assets/experts/teams/software-development-team/skills/qa/SKILL.md +44 -0
- package/assets/experts/teams/software-development-team/skills/review/SKILL.md +43 -0
- package/assets/experts/teams/water-operations-team/.codebuddy-plugin/plugin.json +19 -0
- package/assets/experts/teams/water-operations-team/agents/lead.md +9 -0
- package/lib/client/index.js +288 -118
- package/lib/host/expert-store.js +1 -1
- package/lib/host/expert-tools.js +156 -25
- package/lib/host/experts.js +511 -26
- package/lib/host/index.js +51 -22
- package/lib/host/llm-gateway.js +1 -1
- package/package.json +3 -3
package/lib/host/expert-store.js
CHANGED
|
@@ -138,7 +138,7 @@ export function createExpertStore(options = {}) {
|
|
|
138
138
|
} catch (error) {
|
|
139
139
|
if (error.code !== 'ENOENT') {
|
|
140
140
|
// 坏文件不能让设置页打不开:退回默认值并记一条 warning,下次写入会覆盖它。
|
|
141
|
-
console.warn('[
|
|
141
|
+
console.warn('[WaterPlusAI] expert config is unreadable:', error.message)
|
|
142
142
|
}
|
|
143
143
|
return normalizeExpertState(undefined)
|
|
144
144
|
}
|
package/lib/host/expert-tools.js
CHANGED
|
@@ -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 =>
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|