@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
package/lib/host/index.js CHANGED
@@ -5,7 +5,7 @@ import path from 'node:path'
5
5
  import { WATERPLUS_PERSONA } from '../shared/persona.js'
6
6
  import { createExpertStore, isExpertEnabled } from './expert-store.js'
7
7
  import { createExpertToolDefinitions } from './expert-tools.js'
8
- import { EXPERT_DIVISIONS, loadExpertCatalog, mergeCustomExperts, readExpertBody, resolveExpertAvatar } from './experts.js'
8
+ import { EXPERT_DIVISIONS, loadExpertCatalog, mergeCustomExperts, readExpertBody, readExpertDetails, resolveExpertAvatar, resolveExpertMemberAvatar } from './experts.js'
9
9
  import { bindLlmCredential, describeLlmCredential, ensureLlmCredential, formatBalanceText, isGatewayActive, readActiveProvider, refreshLlmQuota, restoreDeviceLlmCredential, switchDefaultProviderWithRetry } from './llm-gateway.js'
10
10
 
11
11
  const WATERPLUS_ICON = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', 'base64')
@@ -73,7 +73,7 @@ function savePersistedAuth(auth) {
73
73
  fs.writeFileSync(file, JSON.stringify(auth), { mode: 0o600 })
74
74
  try { fs.chmodSync(file, 0o600) } catch {}
75
75
  } catch (error) {
76
- console.warn('[WaterBuddy] failed to persist login:', error.message)
76
+ console.warn('[WaterPlusAI] failed to persist login:', error.message)
77
77
  }
78
78
  }
79
79
 
@@ -110,7 +110,7 @@ function normalizeUser(profile) {
110
110
  return user
111
111
  }
112
112
 
113
- const MANIFEST = JSON.stringify({ id: '/', name: 'WaterBuddy', short_name: 'WaterBuddy', start_url: '/', scope: '/', display: 'fullscreen', icons: [{ src: '/waterplus-mark.png', sizes: '84x84', type: 'image/png', purpose: 'any' }] })
113
+ const MANIFEST = JSON.stringify({ id: '/', name: 'WaterPlusAI', short_name: 'WaterPlusAI', start_url: '/', scope: '/', display: 'fullscreen', icons: [{ src: '/waterplus-mark.png', sizes: '84x84', type: 'image/png', purpose: 'any' }] })
114
114
 
115
115
  function json(res, status, body) {
116
116
  res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' })
@@ -191,7 +191,7 @@ async function finalizeLogin(token) {
191
191
  try {
192
192
  await withTimeout(Promise.resolve().then(() => llmLoginHook(token)), LLM_SWAP_TIMEOUT_MS, 'llm-bind')
193
193
  } catch (error) {
194
- console.warn('[WaterBuddy] llm bind after login failed:', error.message)
194
+ console.warn('[WaterPlusAI] llm bind after login failed:', error.message)
195
195
  }
196
196
  }
197
197
  return { token, user }
@@ -362,7 +362,7 @@ export async function bootstrapLlmCredential(ctx, config) {
362
362
  } catch (error) {
363
363
  // 会话过期或网关不可用时回落设备档,保证仍能对话。
364
364
  console.warn(
365
- '[WaterBuddy] startup bind failed; falling back to the device credential:',
365
+ '[WaterPlusAI] startup bind failed; falling back to the device credential:',
366
366
  error.message,
367
367
  )
368
368
  }
@@ -374,10 +374,10 @@ export async function bootstrapLlmCredential(ctx, config) {
374
374
  try {
375
375
  await switchDefaultProviderWithRetry(ctx)
376
376
  } catch (error) {
377
- console.warn('[WaterBuddy] default provider not switched:', error.message)
377
+ console.warn('[WaterPlusAI] default provider not switched:', error.message)
378
378
  }
379
379
  } catch (error) {
380
- console.warn('[WaterBuddy] LLM credential bootstrap failed:', error.message)
380
+ console.warn('[WaterPlusAI] LLM credential bootstrap failed:', error.message)
381
381
  llmStatus = { ...(llmStatus || {}), error: error.message }
382
382
  }
383
383
  return llmStatus || null
@@ -429,6 +429,9 @@ function expertPromptSection(catalog) {
429
429
  '',
430
430
  `名册共 ${catalog.bySlug.size} 位专家,分 ${catalog.divisions.length} 个方向:`,
431
431
  divisions,
432
+ catalog.teams?.length > 0
433
+ ? `专家团:${catalog.teams.map(team => `${team.slug}(${team.name},成员:${(team.members || []).map(member => member.name || member.slug).join('、')})`).join('、')}`
434
+ : '',
432
435
  '',
433
436
  '专家返回的是独立分析,仍需你结合上下文判断后再交付给用户。',
434
437
  ].join('\n')
@@ -463,8 +466,12 @@ function expertRoutes(snapshot, store) {
463
466
  name: expert.name,
464
467
  emoji: expert.emoji,
465
468
  description: expert.description,
466
- division: expert.division,
467
- divisionName: EXPERT_DIVISIONS[expert.division] || expert.division,
469
+ division: expert.kind === 'team' ? 'teams' : expert.division,
470
+ divisionName: expert.kind === 'team' ? '专家团' : EXPERT_DIVISIONS[expert.division] || expert.division,
471
+ kind: expert.kind || 'expert',
472
+ expertType: expert.expertType || 'expert',
473
+ members: expert.members || [],
474
+ skills: expert.skills || [],
468
475
  custom: expert.custom === true,
469
476
  enabled: isExpertEnabled(state, expert.slug),
470
477
  // 外链直接用;本地图片走我们的取图路由(按需读、带缓存,不塞进快照)。
@@ -475,6 +482,16 @@ function expertRoutes(snapshot, store) {
475
482
  : '',
476
483
  })
477
484
 
485
+ const divisionBriefs = catalog => {
486
+ const divisions = catalog.divisions.map(division => ({
487
+ slug: division.slug,
488
+ name: division.name,
489
+ count: division.experts.length,
490
+ }))
491
+ if (catalog.teams?.length > 0) divisions.push({ slug: 'teams', name: '专家团', count: catalog.teams.length })
492
+ return divisions
493
+ }
494
+
478
495
  /** 统一把异常收敛成 JSON:带错误码的校验失败返回 400,其余 502。 */
479
496
  const guard = handler => async (req, res) => {
480
497
  try {
@@ -505,16 +522,17 @@ function expertRoutes(snapshot, store) {
505
522
  const expert = catalog.bySlug.get(slug)
506
523
  if (!expert) return json(res, 404, { error: 'expert-not-found' })
507
524
  // 展示返回未消毒的原文:零宽空格不该出现在用户能复制走的文本里。
508
- return json(res, 200, { expert: brief(expert, state), body: await readExpertBody(expert) })
525
+ const details = await readExpertDetails(expert)
526
+ return json(res, 200, {
527
+ expert: { ...brief(expert, state), ...details },
528
+ body: await readExpertBody(expert),
529
+ })
509
530
  }
510
531
  const experts = [...catalog.bySlug.values()].map(expert => brief(expert, state))
511
532
  json(res, 200, {
512
533
  experts,
513
- divisions: catalog.divisions.map(division => ({
514
- slug: division.slug,
515
- name: division.name,
516
- count: division.experts.length,
517
- })),
534
+ teams: (catalog.teams || []).map(team => brief(team, state)),
535
+ divisions: divisionBriefs(catalog),
518
536
  enabledCount: experts.filter(expert => expert.enabled).length,
519
537
  revision: state.revision,
520
538
  // 版本号由宿主下发:客户端不该自己硬编码一份会漂移的副本。
@@ -527,10 +545,15 @@ function expertRoutes(snapshot, store) {
527
545
  path: '/api/waterbuddy/experts/avatar',
528
546
  handler: guard(async (req, res) => {
529
547
  if (req.method !== 'GET') return json(res, 405, { error: 'method-not-allowed' })
530
- const slug = new URL(req.url, 'http://127.0.0.1').searchParams.get('slug') || ''
548
+ const query = new URL(req.url, 'http://127.0.0.1').searchParams
549
+ const slug = query.get('slug') || ''
550
+ const memberSlug = query.get('member') || ''
531
551
  const { catalog } = await snapshot()
532
552
  const expert = catalog.bySlug.get(slug)
533
- const avatar = expert ? resolveExpertAvatar(expert) : undefined
553
+ const member = memberSlug && expert?.members?.find(item => item.slug === memberSlug)
554
+ const avatar = expert
555
+ ? (member ? resolveExpertMemberAvatar(expert, member) : resolveExpertAvatar(expert))
556
+ : undefined
534
557
  if (!avatar) return json(res, 404, { error: 'avatar-not-found' })
535
558
  if (avatar.kind === 'remote') return json(res, 400, { error: 'avatar-is-remote' })
536
559
  const extension = (avatar.path.match(/\.[a-z0-9]+$/i) || [''])[0].toLowerCase()
@@ -554,6 +577,8 @@ function expertRoutes(snapshot, store) {
554
577
  const experts = [...catalog.bySlug.values()].map(expert => brief(expert, state))
555
578
  json(res, 200, {
556
579
  experts,
580
+ teams: (catalog.teams || []).map(team => brief(team, state)),
581
+ divisions: divisionBriefs(catalog),
557
582
  enabledCount: experts.filter(expert => expert.enabled).length,
558
583
  revision: state.revision,
559
584
  })
@@ -570,6 +595,8 @@ function expertRoutes(snapshot, store) {
570
595
  const experts = [...catalog.bySlug.values()].map(expert => brief(expert, state))
571
596
  json(res, 200, {
572
597
  experts,
598
+ teams: (catalog.teams || []).map(team => brief(team, state)),
599
+ divisions: divisionBriefs(catalog),
573
600
  enabledCount: experts.filter(expert => expert.enabled).length,
574
601
  revision: state.revision,
575
602
  })
@@ -586,6 +613,8 @@ function expertRoutes(snapshot, store) {
586
613
  const experts = [...catalog.bySlug.values()].map(expert => brief(expert, state))
587
614
  json(res, 200, {
588
615
  experts,
616
+ teams: (catalog.teams || []).map(team => brief(team, state)),
617
+ divisions: divisionBriefs(catalog),
589
618
  enabledCount: experts.filter(expert => expert.enabled).length,
590
619
  revision: state.revision,
591
620
  })
@@ -598,7 +627,7 @@ function setupExpertTools(ctx, config, { getCatalog, getEnabled }) {
598
627
  const tools = readService(ctx, 'tools')
599
628
  const subagents = readService(ctx, 'subagents')
600
629
  if (!tools?.register || !subagents) {
601
- console.warn('[WaterBuddy] experts disabled: host exposes no tools/subagents service')
630
+ console.warn('[WaterPlusAI] experts disabled: host exposes no tools/subagents service')
602
631
  return []
603
632
  }
604
633
 
@@ -626,7 +655,7 @@ function setupExpertTools(ctx, config, { getCatalog, getEnabled }) {
626
655
  },
627
656
  }))
628
657
  } catch (error) {
629
- console.warn('[WaterBuddy] expert tools registration failed:', error.message)
658
+ console.warn('[WaterPlusAI] expert tools registration failed:', error.message)
630
659
  for (const dispose of disposers) {
631
660
  try { dispose() } catch { /* 已失败,回滚本身不再抛 */ }
632
661
  }
@@ -644,7 +673,7 @@ export function apply(ctx, config) {
644
673
  // 内置名册读一次(异步、失败只记日志);自定义专家与启用状态存在 settings 里,
645
674
  // 每次快照时合并,所以用户在设置页改完立刻生效,不需要重启。
646
675
  const builtinCatalog = loadExpertCatalog(config?.expertsDir || undefined)
647
- builtinCatalog.catch(error => console.warn('[WaterBuddy] expert catalog failed to load:', error.message))
676
+ builtinCatalog.catch(error => console.warn('[WaterPlusAI] expert catalog failed to load:', error.message))
648
677
  // 状态存在 $DSH_HOME/waterbuddy-experts.json(`expertsStateFile` 可覆盖,测试用临时文件)。
649
678
  // 名册加载完成后记下全部 slug,供 store 判断白名单是否来自上一代名册。
650
679
  let knownExpertSlugs
@@ -689,12 +718,12 @@ export function apply(ctx, config) {
689
718
  })
690
719
  } },
691
720
  { kind: 'exact', path: '/api/waterbuddy/llm/refresh', handler: async (req, res) => { if (req.method !== 'POST') return json(res, 405, { error: 'method-not-allowed' }); try { json(res, 200, { llm: await bootstrapLlmCredential(ctx, config) }) } catch (error) { json(res, 502, { error: error.message }) } } },
692
- { kind: 'exact', path: '/api/waterbuddy/logout', handler: async (_req, res) => { currentUser = undefined; for (const session of sessions.values()) delete session.token; clearPersistedAuth(); if (llmLogoutHook) { try { await withTimeout(Promise.resolve().then(() => llmLogoutHook()), LLM_SWAP_TIMEOUT_MS, 'llm-unbind') } catch (error) { console.warn('[WaterBuddy] llm restore after logout failed:', error.message) } } json(res, 200, { ok: true }) } },
721
+ { kind: 'exact', path: '/api/waterbuddy/logout', handler: async (_req, res) => { currentUser = undefined; for (const session of sessions.values()) delete session.token; clearPersistedAuth(); if (llmLogoutHook) { try { await withTimeout(Promise.resolve().then(() => llmLogoutHook()), LLM_SWAP_TIMEOUT_MS, 'llm-unbind') } catch (error) { console.warn('[WaterPlusAI] llm restore after logout failed:', error.message) } } json(res, 200, { ok: true }) } },
693
722
  { kind: 'prefix', path: '/api/waterbuddy/mcp', handler: async (req, res) => { try { await proxyMcp(req, res) } catch (error) { json(res, 502, { error: error.message }) } } },
694
723
  ...expertRoutes(expertSnapshot, expertStore),
695
724
  ]
696
725
  const disposers = routes.map(route => ctx.webServer.register(route))
697
- const disposeTitle = ctx.webServer.tapIndex(html => html.replace(/<title>[^<]*<\/title>/i, '<title>WaterBuddy</title>').replace(/<link[^>]+rel=["']icon["'][^>]*>/i, '<link rel="icon" type="image/png" href="/waterplus-mark.png" />'))
726
+ const disposeTitle = ctx.webServer.tapIndex(html => html.replace(/<title>[^<]*<\/title>/i, '<title>WaterPlusAI</title>').replace(/<link[^>]+rel=["']icon["'][^>]*>/i, '<link rel="icon" type="image/png" href="/waterplus-mark.png" />'))
698
727
  const disposePersona = ctx.systemPrompt.section({ name: 'waterplus:persona', order: 100, text: WATERPLUS_PERSONA })
699
728
  const expertDisposers = setupExpertTools(ctx, config, {
700
729
  getCatalog: async () => (await expertSnapshot()).catalog,
@@ -98,7 +98,7 @@ function writeDeviceState(state) {
98
98
  fs.writeFileSync(file, JSON.stringify(state), { mode: 0o600 })
99
99
  try { fs.chmodSync(file, 0o600) } catch {}
100
100
  } catch (error) {
101
- console.warn('[WaterBuddy] failed to persist device state:', error.message)
101
+ console.warn('[WaterPlusAI] failed to persist device state:', error.message)
102
102
  }
103
103
  }
104
104
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@waterplus-ai/waterbuddy",
3
- "version": "0.1.78",
4
- "description": "WaterBuddy branding and Web profile bundle for DeepSeek Harness",
3
+ "version": "0.2.5",
4
+ "description": "WaterPlusAI branding and Web profile bundle for DeepSeek Harness",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
7
7
  "packageManager": "pnpm@11.7.0",
@@ -37,7 +37,7 @@
37
37
  }
38
38
  },
39
39
  "scripts": {
40
- "test": "node --test tests/*.test.mjs",
40
+ "test": "node --test \"tests/**/*.test.mjs\"",
41
41
  "pack:check": "pnpm run build:client && pnpm pack --dry-run",
42
42
  "pack:release": "pnpm run build:client && pnpm pack --pack-destination releases",
43
43
  "release:cdn": "pnpm run build:client && node scripts/build-cdn-release.mjs",