@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.
- 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 +296 -120
- 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/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('[
|
|
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: '
|
|
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('[
|
|
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
|
-
'[
|
|
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('[
|
|
377
|
+
console.warn('[WaterPlusAI] default provider not switched:', error.message)
|
|
378
378
|
}
|
|
379
379
|
} catch (error) {
|
|
380
|
-
console.warn('[
|
|
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
|
-
|
|
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
|
-
|
|
514
|
-
|
|
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
|
|
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
|
|
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('[
|
|
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('[
|
|
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('[
|
|
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('[
|
|
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>
|
|
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,
|
package/lib/host/llm-gateway.js
CHANGED
|
@@ -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('[
|
|
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.
|
|
4
|
-
"description": "
|
|
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
|
|
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",
|