@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.
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 +288 -118
  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
@@ -13,7 +13,7 @@ window.__ModuleLoader__.load({
13
13
  width: size,
14
14
  height: size,
15
15
  className,
16
- alt: 'WaterBuddy',
16
+ alt: 'WaterPlusAI',
17
17
  })
18
18
  }
19
19
 
@@ -30,7 +30,7 @@ window.__ModuleLoader__.load({
30
30
  lineHeight: '24px',
31
31
  whiteSpace: 'nowrap',
32
32
  },
33
- children: 'WaterBuddy',
33
+ children: 'WaterPlusAI',
34
34
  }),
35
35
  ],
36
36
  })
@@ -43,7 +43,7 @@ window.__ModuleLoader__.load({
43
43
  }
44
44
 
45
45
  function userName(user) {
46
- return user?.name || user?.nickname || user?.username || 'WaterBuddy 用户'
46
+ return user?.name || user?.nickname || user?.username || 'WaterPlusAI 用户'
47
47
  }
48
48
 
49
49
  function UserAvatar({ user, size = 34 }) {
@@ -157,7 +157,7 @@ window.__ModuleLoader__.load({
157
157
  const statusText = { WAITING: '等待扫码', DEALING: '已扫码,请在手机上确认', SUCCESS: '登录成功', EXPIRED: '二维码已过期' }[status] || status
158
158
  const tone = error ? '#dc2626' : status === 'SUCCESS' ? '#16a34a' : '#64748b'
159
159
  return jsxs('div', { style: { display: 'flex', flexDirection: 'column', alignItems: 'center', textAlign: 'center' }, children: [
160
- jsx('div', { style: { fontSize: 13.5, color: '#7b8799' }, children: `使用${SSO.title}扫码,安全登录 WaterBuddy` }),
160
+ jsx('div', { style: { fontSize: 13.5, color: '#7b8799' }, children: `使用${SSO.title}扫码,安全登录 WaterPlusAI` }),
161
161
  qrCode
162
162
  ? jsx('img', { src: qrCode, alt: `${SSO.title}登录二维码`, style: { display: 'block', width: 206, height: 206, boxSizing: 'border-box', objectFit: 'contain', margin: '14px auto 10px', padding: 10, border: '1px solid #e5ebf3', borderRadius: 16, background: '#fff' } })
163
163
  : jsx('div', { style: { width: 206, height: 206, boxSizing: 'border-box', margin: '14px auto 10px', display: 'grid', placeItems: 'center', border: '1px solid #e5ebf3', borderRadius: 16, background: '#f8fafc', color: '#7b8799', fontSize: 13 }, children: loading ? '二维码加载中…' : '二维码暂不可用' }),
@@ -432,7 +432,33 @@ window.__ModuleLoader__.load({
432
432
  }
433
433
  load()
434
434
  const timer = setInterval(load, 15000)
435
- return () => { cancelled = true; clearInterval(timer) }
435
+
436
+ // 切换模型/provider 后,`managesQuota`(决定积分界面显不显示)会变,但等下一次
437
+ // 轮询最长要 15 秒,用户看到的就是"切了模型状态没跟上"。这里在任何点击/窗口重新
438
+ // 聚焦后补拉一次——本地接口、代价极低,去抖 300ms 避免连点刷屏。
439
+ let debounce
440
+ const refreshSoon = () => {
441
+ clearTimeout(debounce)
442
+ debounce = setTimeout(load, 300)
443
+ }
444
+ // 两个全局都要探测:只查 document 会漏掉没有 addEventListener 的 window,
445
+ // 调用时抛错会让整个 effect 中断、后续的 load() 都不再发生(实测表现为额度不显示)。
446
+ const canListen = typeof document !== 'undefined' && typeof document.addEventListener === 'function'
447
+ && typeof window !== 'undefined' && typeof window.addEventListener === 'function'
448
+ if (canListen) {
449
+ document.addEventListener('click', refreshSoon, true)
450
+ window.addEventListener('focus', refreshSoon)
451
+ }
452
+
453
+ return () => {
454
+ cancelled = true
455
+ clearInterval(timer)
456
+ clearTimeout(debounce)
457
+ if (canListen) {
458
+ document.removeEventListener('click', refreshSoon, true)
459
+ window.removeEventListener('focus', refreshSoon)
460
+ }
461
+ }
436
462
  }, [user])
437
463
 
438
464
  const startLogin = async () => {
@@ -504,11 +530,14 @@ window.__ModuleLoader__.load({
504
530
  const balanceText = typeof llm?.balanceText === 'string' && llm.balanceText
505
531
  ? llm.balanceText
506
532
  : '--'
507
- // 用户把默认模型换成别人的提供方时,消耗的不是我们的额度,一律不展示额度。
508
- const managesQuota = llm?.managesQuota === true
509
- const exhausted = managesQuota && llm?.exhausted === true
533
+ // 额度**一律展示**(产品要求"一直显示"):不再因为默认模型被换成别家就把额度藏起来。
534
+ // 只要状态接口带回余额文案就显示;`managesQuota` 仍由 Host 上报,但只用于限流提示,
535
+ // 不再决定这块界面显不显示。
536
+ // 有余额文案或有余额数值都算"有额度"——只认 balanceText 会漏掉只带 balance 的响应。
537
+ const hasQuota = Boolean(llm) && (typeof llm.balanceText === 'string' && llm.balanceText.length > 0 || typeof llm.balance === 'number')
538
+ const exhausted = llm?.exhausted === true
510
539
  // 未登录时展示试用额度,登录后展示账号额度;耗尽一律优先提示
511
- const subtitle = !managesQuota
540
+ const subtitle = !hasQuota
512
541
  ? (user ? '水务之窗' : '扫码或账号登录')
513
542
  : exhausted
514
543
  ? '额度已用完'
@@ -523,8 +552,8 @@ window.__ModuleLoader__.load({
523
552
  // 下方,而它们在 DOM 里排在按钮之后(后绘制者在上),会抢走按钮中下部的点击
524
553
  // ——表现为「上半能点、下半点不中」。定位元素给了 z-index 才稳定盖住它们。
525
554
  jsx('button', { type: 'button', onClick: () => setOpen(false), 'aria-label': '关闭', style: { position: 'absolute', zIndex: 1, top: 14, right: 16, width: 30, height: 30, border: 0, borderRadius: '50%', background: 'rgba(255,255,255,.18)', color: '#fff', fontSize: 20, lineHeight: 1, cursor: 'pointer' }, children: '×' }),
526
- jsx('div', { style: { fontSize: 12, opacity: .82, letterSpacing: 1 }, children: 'WATERBUDDY' }),
527
- jsx('div', { style: { marginTop: 8, fontSize: 15, fontWeight: 600 }, children: 'WaterBuddy' }),
555
+ jsx('div', { style: { fontSize: 12, opacity: .82, letterSpacing: 1 }, children: 'WaterPlusAI' }),
556
+ jsx('div', { style: { marginTop: 8, fontSize: 15, fontWeight: 600 }, children: 'WaterPlusAI' }),
528
557
  jsx('div', { style: { position: 'absolute', left: '50%', bottom: -35, transform: 'translateX(-50%)', padding: 5, borderRadius: '50%', background: '#fff', boxShadow: '0 6px 18px rgba(15,23,42,.18)' }, children: jsx(UserAvatar, { user, size: 62 }) }),
529
558
  ] }),
530
559
  jsxs('div', { style: { padding: '48px 28px 26px', textAlign: 'center' }, children: [
@@ -539,7 +568,7 @@ window.__ModuleLoader__.load({
539
568
  jsx('small', { style: { color: '#8a94a6', fontSize: 11 }, children: '身份' }),
540
569
  jsx('div', { style: { marginTop: 4, color: '#334155', fontSize: 13, fontWeight: 600 }, children: isAdmin ? '管理员' : '内部成员' }),
541
570
  ] }),
542
- managesQuota ? jsxs('div', { style: { padding: '12px 14px', borderRadius: 12, background: exhausted ? '#fff7ed' : '#f5f8fc' }, children: [
571
+ hasQuota ? jsxs('div', { style: { padding: '12px 14px', borderRadius: 12, background: exhausted ? '#fff7ed' : '#f5f8fc' }, children: [
543
572
  jsx('small', { style: { color: '#8a94a6', fontSize: 11 }, children: '剩余额度' }),
544
573
  jsx('div', { style: { marginTop: 4, color: exhausted ? '#b45309' : '#334155', fontSize: 13, fontWeight: 600 }, children: balanceText }),
545
574
  ] }) : null,
@@ -554,9 +583,16 @@ window.__ModuleLoader__.load({
554
583
  jsx('button', {
555
584
  type: 'button',
556
585
  onClick: openPanel,
557
- title: user ? name : '登录 WaterBuddy',
586
+ title: user ? name : '登录 WaterPlusAI',
558
587
  'data-waterbuddy-account': '',
559
- style: { display: 'flex', alignItems: 'center', gap: 8, flex: '0 0 auto', width: wide ? 'auto' : 40, maxWidth: wide ? 116 : 40, minHeight: 38, padding: wide ? '5px 9px 5px 6px' : 3, border: user ? '1px solid #e1eafa' : '1px solid transparent', borderRadius: 12, background: user ? '#f5f8ff' : 'transparent', color: 'var(--dsw-text-secondary, #586174)', cursor: 'pointer', textAlign: 'left', transition: 'background .15s ease, border-color .15s ease' },
588
+ // 注意:**不要**给它 `flex: 0 0 auto`。底栏被我们摆成一行时,宿主那层的
589
+ // `flex-wrap` 是默认的 nowrap,卡片一旦不可收缩,行就放不下、最后一个子元素
590
+ // (「设置」)被直接裁掉——实测就是这个原因。默认可收缩 + minWidth:0 才安全。
591
+ // 卡片宽度必须小:底栏能不能排成一行,取决于"卡片 + 设置区"是否放得下,
592
+ // 而这两者里只有我们能收。副标题省略号截断,保住名字。
593
+ // 底栏已成一行后这里可以放宽:行宽 = 卡片 + 设置区,两者都是 auto 且有 wrap 兜底,
594
+ // 放宽只是让「水务之窗」这类副标题显示完整;真放不下也只会换行,不会裁掉东西。
595
+ style: { display: 'flex', alignItems: 'center', gap: 6, minWidth: 0, width: wide ? 'auto' : 40, maxWidth: wide ? 124 : 40, minHeight: 38, padding: wide ? '5px 9px 5px 6px' : 3, border: user ? '1px solid #e1eafa' : '1px solid transparent', borderRadius: 12, background: user ? '#f5f8ff' : 'transparent', color: 'var(--dsw-text-secondary, #586174)', cursor: 'pointer', textAlign: 'left', transition: 'background .15s ease, border-color .15s ease' },
560
596
  children: [user ? jsx(UserAvatar, { user, size: 28 }) : jsx('img', { src: SSO.image, alt: SSO.title, style: { width: 28, height: 28, borderRadius: 8, objectFit: 'contain', padding: 2, boxSizing: 'border-box', flex: '0 0 auto', background: '#fff', border: '1px solid #dce9fb' } }), wide ? jsxs('span', { style: { minWidth: 0, display: 'grid', gap: 1 }, children: [jsx('strong', { style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontSize: 12.5, lineHeight: '16px', color: 'var(--dsw-text-primary, #1f2937)' }, children: user ? name : SSO.title }), jsx('small', { style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', color: exhausted ? '#b45309' : '#8a94a6', fontSize: 10.5, lineHeight: '14px' }, children: subtitle })] }) : null],
561
597
  }),
562
598
  open ? jsx('div', { style: { position: 'fixed', inset: 0, zIndex: 1000, background: 'rgba(15, 23, 42, .42)', backdropFilter: 'blur(5px)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16 }, onClick: event => { if (event.target === event.currentTarget) setOpen(false) }, children: jsx('div', { style: { width: 420, maxWidth: 'calc(100vw - 32px)', borderRadius: 22, background: '#fff', overflow: 'hidden', boxShadow: '0 24px 80px rgba(15,23,42,.28)' }, children: user ? userPanel() : jsxs('div', { children: [
@@ -564,7 +600,7 @@ window.__ModuleLoader__.load({
564
600
  // 与已登录面板同一个坑:标题/说明是整行宽的块级元素,必须靠 z-index 让按钮稳定在上。
565
601
  jsx('button', { type: 'button', onClick: () => setOpen(false), 'aria-label': '关闭', style: { position: 'absolute', zIndex: 1, top: 14, right: 16, width: 30, height: 30, border: 0, borderRadius: '50%', background: '#f1f5f9', color: '#64748b', fontSize: 20, lineHeight: 1, cursor: 'pointer' }, children: '×' }),
566
602
  jsx('img', { src: SSO.image, alt: SSO.title, style: { display: 'block', width: 52, height: 52, objectFit: 'contain', margin: '0 auto' } }),
567
- jsx('h3', { style: { margin: '10px 0 4px', color: '#162033', fontSize: 21 }, children: 'WaterBuddy' }),
603
+ jsx('h3', { style: { margin: '10px 0 4px', color: '#162033', fontSize: 21 }, children: 'WaterPlusAI' }),
568
604
  jsx('p', { style: { margin: 0, color: '#7b8799', fontSize: 12.5 }, children: '登录后同步水务之窗账号与知识库权限' }),
569
605
  exhausted ? jsx('div', { style: { margin: '14px 26px 0', padding: '10px 12px', borderRadius: 10, background: '#fff7ed', border: '1px solid #fed7aa', color: '#b45309', fontSize: 12.5, lineHeight: 1.5 }, children: '试用额度已用完,登录后即可继续使用(额度升为账号档)' }) : null,
570
606
  ] }),
@@ -627,7 +663,7 @@ window.__ModuleLoader__.load({
627
663
  // `/waterplus-mark.png`,该路径在 hero 里取不到,渲染成了黑方块。
628
664
  return jsx('img', {
629
665
  src: mark,
630
- alt: 'WaterBuddy',
666
+ alt: 'WaterPlusAI',
631
667
  width: edge,
632
668
  height: edge,
633
669
  style: { width: edge, height: edge, objectFit: 'contain', borderRadius: '22%' },
@@ -754,7 +790,12 @@ window.__ModuleLoader__.load({
754
790
  const response = await fetch('/api/waterbuddy/experts')
755
791
  const payload = await response.json()
756
792
  if (!response.ok) throw new Error(payload.error || 'load-failed')
757
- return { experts: payload.experts || [], divisions: payload.divisions || [] }
793
+ const teams = payload.teams || (payload.experts || []).filter(expert => expert.kind === 'team' || expert.expertType === 'team')
794
+ const divisions = [...(payload.divisions || [])]
795
+ if (teams.length > 0 && !divisions.some(division => division.slug === 'teams')) {
796
+ divisions.push({ slug: 'teams', name: '专家团', count: teams.length })
797
+ }
798
+ return { experts: payload.experts || [], teams, divisions }
758
799
  }
759
800
 
760
801
  /** `@` 菜单的最近一次启用快照:`lexicon` / `codec.clipboardText` 是同步接口,只能读缓存。 */
@@ -1107,25 +1148,135 @@ window.__ModuleLoader__.load({
1107
1148
  * 直接函数调用会把它的 hook 并进父组件的 hook 序列,弹窗一开一关就改变 hook 数量,
1108
1149
  * React 会抛 "Rendered more hooks than during the previous render"。
1109
1150
  */
1151
+ function formatExpertSize(bytes) {
1152
+ const size = Number(bytes) || 0
1153
+ if (size >= 1024 * 1024) return (size / 1024 / 1024).toFixed(1) + ' MB'
1154
+ if (size >= 1024) return (size / 1024).toFixed(1) + ' KB'
1155
+ return size + ' B'
1156
+ }
1157
+
1158
+ function detailRow(label, value) {
1159
+ return value
1160
+ ? jsxs('div', { style: { display: 'flex', gap: 12, padding: '3px 0', alignItems: 'baseline' }, children: [
1161
+ jsx('span', { style: { color: '#8a94a6', minWidth: 42, flexShrink: 0 }, children: label }),
1162
+ jsx('span', { style: { color: '#596579', wordBreak: 'break-all' }, children: value }),
1163
+ ] })
1164
+ : null
1165
+ }
1166
+
1167
+ function memberAvatar(expert, member) {
1168
+ const raw = String(member?.avatar || '').trim()
1169
+ if (!raw) return null
1170
+ const src = /^https?:\/\//i.test(raw)
1171
+ ? raw
1172
+ : '/api/waterbuddy/experts/avatar?slug=' + encodeURIComponent(expert.slug) + '&member=' + encodeURIComponent(member.slug || '')
1173
+ return jsx('img', { src, alt: '', style: { width: 28, height: 28, borderRadius: '50%', objectFit: 'cover', flexShrink: 0 } })
1174
+ }
1175
+
1110
1176
  function ExpertPromptDialog({ dialog, onClose }) {
1177
+ const expert = dialog.expert || {}
1178
+ const members = expert.members || []
1179
+ const agents = expert.agentFiles || []
1180
+ const skills = expert.skillMeta || []
1181
+ const knowledgeBases = expert.knowledgeBases || []
1182
+ const quickPrompts = expert.quickPrompts || []
1183
+ const tags = (expert.tags || []).map(tag => typeof tag === 'string' ? tag : tag.zh || tag.en).filter(Boolean)
1184
+ const avatar = expert.avatar
1185
+ ? jsx('img', { src: expert.avatar, alt: '', style: { width: 48, height: 48, borderRadius: '50%', objectFit: 'cover', flexShrink: 0 } })
1186
+ : jsx('span', { style: { width: 48, height: 48, borderRadius: '50%', display: 'grid', placeItems: 'center', background: '#eef4ff', fontSize: 27, flexShrink: 0 }, children: expert.emoji || '🧠' })
1187
+ const copySummary = () => {
1188
+ const lines = [
1189
+ expert.name,
1190
+ expert.profession ? `职业:${expert.profession}` : '',
1191
+ expert.description,
1192
+ expert.version ? `版本:${expert.version}` : '',
1193
+ expert.defaultInitPrompt ? `默认开场:${expert.defaultInitPrompt}` : '',
1194
+ members.length > 0 ? `团队成员:${members.map(member => `${member.name || member.slug}(${member.role === 'lead' ? '负责人' : '成员'})`).join('、')}` : '',
1195
+ skills.length > 0 ? `关联技能:${skills.map(skill => skill.skillName).join('、')}` : '',
1196
+ knowledgeBases.length > 0 ? `关联知识库:${knowledgeBases.map(item => item.name || item.id || item).join('、')}` : '',
1197
+ ...quickPrompts.map(item => `- ${item}`),
1198
+ ].filter(Boolean)
1199
+ void writeClipboard(lines.join('\n')).catch(() => {})
1200
+ }
1111
1201
  return jsx('div', { style: EXPERT_STYLE.overlay, onClick: event => { if (event.target === event.currentTarget) onClose() }, children:
1112
- jsxs('div', { style: EXPERT_STYLE.modal, children: [
1202
+ jsxs('div', { style: { ...EXPERT_STYLE.modal, width: 'min(860px, 100%)', maxHeight: '86vh' }, children: [
1113
1203
  jsxs('div', { style: EXPERT_STYLE.modalHead, children: [
1114
- jsx('strong', { style: { color: '#0f172a', fontSize: 14 }, children: dialog.expert.name }),
1115
- jsx('span', { style: { color: '#94a3b8', fontSize: 11.5 }, children: dialog.expert.slug }),
1204
+ avatar,
1205
+ jsxs('div', { style: { minWidth: 0, display: 'grid', gap: 3 }, children: [
1206
+ jsx('strong', { style: { color: '#0f172a', fontSize: 16 }, children: expert.name }),
1207
+ jsx('span', { style: { color: '#7b8799', fontSize: 12 }, children: expert.profession || expert.description || expert.slug }),
1208
+ ] }),
1116
1209
  jsx('button', { type: 'button', onClick: onClose, style: { ...EXPERT_STYLE.ghost, marginLeft: 'auto' }, children: '关闭' }),
1117
1210
  ] }),
1118
- jsx('div', { style: EXPERT_STYLE.modalBody, children:
1119
- dialog.loading
1120
- ? jsx('span', { style: { color: '#7b8799', fontSize: 12.5 }, children: '正在加载提示词…' })
1121
- : jsx('pre', { style: EXPERT_STYLE.pre, children: dialog.body }) }),
1211
+ jsx('div', { style: EXPERT_STYLE.modalBody, children: dialog.loading
1212
+ ? jsx('span', { style: { color: '#7b8799', fontSize: 12.5 }, children: '正在加载专家详情…' })
1213
+ : jsxs('div', { style: { display: 'grid', gap: 16 }, children: [
1214
+ jsx('div', { style: { color: '#596579', fontSize: 13, lineHeight: 1.7 }, children: expert.description }),
1215
+ jsxs('div', { style: { borderBottom: '1px solid #eef2f8', paddingBottom: 10 }, children: [
1216
+ detailRow('来源', expert.sourceLabel ? `${expert.sourceLabel}${expert.source ? `(${expert.source})` : ''}` : ''),
1217
+ detailRow('版本', expert.version),
1218
+ detailRow('文件', expert.fileCount !== undefined ? `${expert.fileCount} / ${formatExpertSize(expert.totalSize)}` : ''),
1219
+ detailRow('目录', expert.dir),
1220
+ expert.categoryId ? detailRow('分类', expert.categoryId) : null,
1221
+ ] }),
1222
+ expert.readOnly ? jsx('div', { style: { color: '#8a94a6', fontSize: 12 }, children: '只读来源(可在 NTD 中管理,或安装到用户库)' }) : null,
1223
+ expert.defaultInitPrompt ? jsxs('div', { style: { display: 'grid', gap: 6 }, children: [
1224
+ jsx('strong', { style: { color: '#64748b', fontSize: 12 }, children: '默认开场' }),
1225
+ jsx('div', { style: { color: '#596579', fontSize: 12.5, lineHeight: 1.6 }, children: expert.defaultInitPrompt }),
1226
+ ] }) : null,
1227
+ quickPrompts.length > 0 ? jsxs('div', { style: { display: 'grid', gap: 6 }, children: [
1228
+ jsx('strong', { style: { color: '#64748b', fontSize: 12 }, children: '快捷指令' }),
1229
+ ...quickPrompts.slice(0, 5).map((prompt, index) => jsx('div', { key: index, style: { color: '#596579', fontSize: 12.5, lineHeight: 1.6 }, children: '• ' + prompt })),
1230
+ ] }) : null,
1231
+ tags.length > 0 ? jsxs('div', { style: { display: 'flex', gap: 6, flexWrap: 'wrap' }, children: [
1232
+ jsx('strong', { style: { color: '#64748b', fontSize: 12, marginRight: 4 }, children: '标签' }),
1233
+ ...tags.map(tag => jsx('span', { key: tag, style: { padding: '2px 7px', borderRadius: 8, background: '#f1f5f9', color: '#64748b', fontSize: 11.5 }, children: tag })),
1234
+ ] }) : null,
1235
+ members.length > 0 ? jsxs('div', { style: { display: 'grid', gap: 8 }, children: [
1236
+ jsx('strong', { style: { color: '#64748b', fontSize: 12 }, children: '团队成员' }),
1237
+ jsx('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(210px, 1fr))', gap: 8 }, children: members.map(member =>
1238
+ jsxs('div', { key: member.slug || member.name, style: { padding: '9px 10px', border: '1px solid #edf1f7', borderRadius: 9, background: '#fbfcfe', display: 'flex', alignItems: 'center', gap: 8 }, children: [
1239
+ memberAvatar(expert, member) || jsx('span', { style: { width: 28, height: 28, borderRadius: '50%', display: 'grid', placeItems: 'center', background: '#eef4ff', fontSize: 15, flexShrink: 0 }, children: member.role === 'lead' ? '★' : '👤' }),
1240
+ jsxs('div', { style: { minWidth: 0 }, children: [
1241
+ jsx('div', { style: { color: '#334155', fontSize: 13 }, children: member.name || member.slug }),
1242
+ jsx('div', { style: { color: '#8a94a6', fontSize: 11.5, marginTop: 3 }, children: `${member.profession || ''}${member.profession ? ' · ' : ''}${member.role === 'lead' ? '负责人' : '成员'}` }),
1243
+ ] }),
1244
+ ] }),
1245
+ ) }),
1246
+ ] }) : null,
1247
+ skills.length > 0 ? jsxs('div', { style: { display: 'grid', gap: 7 }, children: [
1248
+ jsx('strong', { style: { color: '#64748b', fontSize: 12 }, children: '关联技能' }),
1249
+ ...skills.map(skill => jsxs('div', { key: skill.skillName, style: { padding: '8px 0', borderBottom: '1px solid #f1f5f9' }, children: [
1250
+ jsx('div', { style: { color: '#334155', fontSize: 13 }, children: `${skill.emoji ? skill.emoji + ' ' : ''}${skill.skillName}` }),
1251
+ jsx('div', { style: { color: '#8a94a6', fontSize: 11.5, marginTop: 3 }, children: skill.descriptionZh || skill.descriptionEn || skill.description || '暂无说明' }),
1252
+ ] })),
1253
+ ] }) : null,
1254
+ knowledgeBases.length > 0 ? jsxs('div', { style: { display: 'grid', gap: 7 }, children: [
1255
+ jsx('strong', { style: { color: '#64748b', fontSize: 12 }, children: '关联知识库' }),
1256
+ ...knowledgeBases.map((knowledgeBase, index) => jsxs('div', { key: knowledgeBase.id || knowledgeBase.name || index, style: { padding: '8px 0', borderBottom: '1px solid #f1f5f9' }, children: [
1257
+ jsx('div', { style: { color: '#334155', fontSize: 13 }, children: knowledgeBase.name || knowledgeBase.id || String(knowledgeBase) }),
1258
+ knowledgeBase.id ? jsx('div', { style: { color: '#8a94a6', fontSize: 11.5, marginTop: 3, wordBreak: 'break-all' }, children: knowledgeBase.id }) : null,
1259
+ ] })),
1260
+ ] }) : null,
1261
+ agents.length > 0 ? jsxs('div', { style: { display: 'grid', gap: 7 }, children: [
1262
+ jsx('strong', { style: { color: '#64748b', fontSize: 12 }, children: '角色定义(AGENT MD)' }),
1263
+ ...agents.map(agent => jsxs('div', { key: agent.relPath, style: { padding: '8px 0', borderBottom: '1px solid #f1f5f9' }, children: [
1264
+ jsx('div', { style: { color: '#334155', fontSize: 13 }, children: `${agent.emoji ? agent.emoji + ' ' : ''}${agent.name}${agent.relPath === expert.leadAgentFile || agent.role === 'lead' ? ' ★' : ''}` }),
1265
+ jsx('div', { style: { color: '#8a94a6', fontSize: 11.5, marginTop: 3 }, children: agent.description || '暂无角色摘要' }),
1266
+ ] })),
1267
+ ] }) : null,
1268
+ jsx('details', { children: [
1269
+ jsx('summary', { style: { cursor: 'pointer', color: '#64748b', fontSize: 12 }, children: 'plugin.json' }),
1270
+ jsx('pre', { style: { ...EXPERT_STYLE.pre, marginTop: 8, padding: 10, borderRadius: 9, background: '#f8fafc', maxHeight: 260, overflow: 'auto' }, children: expert.pluginJson ? JSON.stringify(expert.pluginJson, null, 2) : '暂无 plugin.json' }),
1271
+ ] }),
1272
+ jsx('details', { children: [
1273
+ jsx('summary', { style: { cursor: 'pointer', color: '#64748b', fontSize: 12 }, children: '查看角色定义全文' }),
1274
+ jsx('pre', { style: { ...EXPERT_STYLE.pre, marginTop: 8, padding: 10, borderRadius: 9, background: '#f8fafc', maxHeight: 360, overflow: 'auto' }, children: dialog.body || '暂无角色定义' }),
1275
+ ] }),
1276
+ ] }) }),
1122
1277
  jsxs('div', { style: EXPERT_STYLE.modalFoot, children: [
1123
- jsx('button', {
1124
- type: 'button',
1125
- onClick: () => { void writeClipboard(dialog.body || '').catch(() => onClose()) },
1126
- style: EXPERT_STYLE.ghost,
1127
- children: '复制提示词',
1128
- }),
1278
+ jsx('button', { type: 'button', onClick: copySummary, style: EXPERT_STYLE.ghost, children: '分享' }),
1279
+ jsx('button', { type: 'button', onClick: () => { void writeClipboard(dialog.body || '').catch(() => {}) }, style: EXPERT_STYLE.ghost, children: '复制提示词' }),
1129
1280
  ] }),
1130
1281
  ] }) })
1131
1282
  }
@@ -1192,17 +1343,6 @@ window.__ModuleLoader__.load({
1192
1343
 
1193
1344
  // ── roster.js ────────────────────────────────────────────────────
1194
1345
 
1195
- /** 正文缓存:`slug → persona 正文`。跨渲染存活,供"预取后同步写剪贴板"用。 */
1196
- const promptCache = new Map()
1197
-
1198
- /** 取一位专家的 persona 正文(展示用原文)。 */
1199
- async function fetchExpertBody(slug) {
1200
- const response = await fetch('/api/waterbuddy/experts?slug=' + encodeURIComponent(slug))
1201
- const payload = await response.json()
1202
- if (!response.ok) throw new Error(payload.error || 'load-failed')
1203
- return payload.body || ''
1204
- }
1205
-
1206
1346
  /** 设置里的「专家」页:浏览、启用停用、以及管理自定义专家。
1207
1347
  *
1208
1348
  * 三件事刻意分开:
@@ -1286,52 +1426,26 @@ window.__ModuleLoader__.load({
1286
1426
  const response = await fetch('/api/waterbuddy/experts?slug=' + encodeURIComponent(expert.slug))
1287
1427
  const payload = await response.json()
1288
1428
  if (!response.ok) throw new Error(payload.error || 'load-failed')
1289
- setDialog({ mode: 'view', expert, body: payload.body || '', loading: false })
1429
+ setDialog({ mode: 'view', expert: payload.expert || expert, body: payload.body || '', loading: false })
1290
1430
  } catch (err) {
1291
1431
  setDialog(null)
1292
1432
  setError(err.message)
1293
1433
  }
1294
1434
  }
1295
1435
 
1296
- /**
1297
- * 复制提示词。
1298
- *
1299
- * 关键在时序:WebKit(Tauri 用的就是它)要求剪贴板写入发生在**用户手势的同步调用栈**里,
1300
- * 先 `await fetch` 再写就会被拒(`NotAllowedError`),连 execCommand 兜底也一起失效——
1301
- * 这正是"复制提示词不起效果"的原因。所以:悬停/聚焦时先把正文预取进缓存,点击时
1302
- * 缓存命中就是同步发起写入,手势不丢;没命中才走异步路径(并在失败时提示再点一次)。
1303
- */
1304
- const copyPrompt = async expert => {
1305
- const cached = promptCache.get(expert.slug)
1306
- if (cached !== undefined) {
1307
- try {
1308
- await writeClipboard(cached)
1309
- setError('')
1310
- } catch (err) {
1311
- setError('复制失败:' + err.message)
1312
- }
1313
- return
1314
- }
1315
- try {
1316
- const body = await fetchExpertBody(expert.slug)
1317
- promptCache.set(expert.slug, body)
1318
- await writeClipboard(body)
1319
- setError('')
1320
- } catch (err) {
1321
- setError('复制失败:' + err.message + '(正文已缓存,再点一次即可)')
1322
- }
1323
- }
1324
-
1325
- /** 悬停/聚焦时预取正文,让点击那一刻能同步写入剪贴板。 */
1326
- const prefetchPrompt = expert => {
1327
- if (promptCache.has(expert.slug)) return
1328
- void fetchExpertBody(expert.slug)
1329
- .then(body => { promptCache.set(expert.slug, body) })
1330
- .catch(() => {})
1331
- }
1436
+ // 卡片上的「复制提示词」按钮已移除:卡片要先按 slug 取正文,而 WebKit(Tauri 用的就是它)
1437
+ // 要求剪贴板写入发生在用户手势的同步调用栈里,异步路径下写入常被拒(`NotAllowedError`)。
1438
+ // 复制入口保留在「查看提示词」弹窗内——正文已经拿到手,点击即同步写入。
1332
1439
 
1333
1440
  const experts = snapshot ? snapshot.experts || [] : []
1334
- const divisions = snapshot ? snapshot.divisions || [] : []
1441
+ const teams = snapshot
1442
+ ? snapshot.teams || experts.filter(expert => expert.kind === 'team' || expert.expertType === 'team')
1443
+ : []
1444
+ const divisions = snapshot ? [...(snapshot.divisions || [])] : []
1445
+ if (teams.length > 0 && !divisions.some(item => item.slug === 'teams')) {
1446
+ divisions.push({ slug: 'teams', name: '专家团', count: teams.length })
1447
+ }
1448
+ const editableDivisions = divisions.filter(item => item.slug !== 'teams')
1335
1449
 
1336
1450
  /** 打开新建/编辑弹窗:初始化表单草稿。 */
1337
1451
  const openEditor = (mode, expert) => {
@@ -1339,7 +1453,7 @@ window.__ModuleLoader__.load({
1339
1453
  slug: expert.slug || '',
1340
1454
  name: expert.name || '',
1341
1455
  description: expert.description || '',
1342
- division: expert.division || (divisions[0] ? divisions[0].slug : 'supply'),
1456
+ division: expert.division || (editableDivisions[0] ? editableDivisions[0].slug : 'supply'),
1343
1457
  emoji: expert.emoji || '🧠',
1344
1458
  avatar: expert.avatar || '',
1345
1459
  prompt: expert.prompt || '',
@@ -1349,8 +1463,9 @@ window.__ModuleLoader__.load({
1349
1463
  const onField = (key, value) => setForm(current => ({ ...current, [key]: value }))
1350
1464
  const needle = query.trim().toLowerCase()
1351
1465
  const shown = experts.filter(expert => {
1352
- if (tab === 'custom' && !expert.custom) return false
1353
- if (tab === 'builtin' && expert.custom) return false
1466
+ if (tab === 'team' && expert.kind !== 'team' && expert.expertType !== 'team') return false
1467
+ if (tab === 'custom' && (!expert.custom || expert.kind === 'team')) return false
1468
+ if (tab === 'builtin' && (expert.custom || expert.kind === 'team')) return false
1354
1469
  if (division !== 'all' && expert.division !== division) return false
1355
1470
  if (status === 'enabled' && !expert.enabled) return false
1356
1471
  if (status === 'disabled' && expert.enabled) return false
@@ -1369,7 +1484,6 @@ window.__ModuleLoader__.load({
1369
1484
 
1370
1485
  const header = jsxs('div', { style: { display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap', marginBottom: 4 }, children: [
1371
1486
  jsx('h2', { style: { margin: 0, color: '#0f172a', fontSize: 19, fontWeight: 600 }, children: '专家' }),
1372
- jsx('span', { style: { padding: '2px 8px', borderRadius: 6, background: '#f1f5f9', color: '#64748b', fontSize: 11.5 }, children: 'v' + (snapshot && snapshot.version ? snapshot.version : '') }),
1373
1487
  jsx('span', { style: { color: '#8a94a6', fontSize: 12.5 }, children: experts.length + ' 位专家 · 已启用 ' + (snapshot ? snapshot.enabledCount : 0) }),
1374
1488
  jsxs('span', { style: { marginLeft: 'auto', display: 'flex', gap: 8 }, children: [
1375
1489
  jsx('button', {
@@ -1396,17 +1510,36 @@ window.__ModuleLoader__.load({
1396
1510
 
1397
1511
  const card = expert => jsxs('div', { style: EXPERT_STYLE.card, children: [
1398
1512
  jsxs('div', { style: { display: 'flex', alignItems: 'flex-start', gap: 10 }, children: [
1399
- expert.avatar
1400
- ? jsx('img', { src: expert.avatar, alt: '', style: { width: 32, height: 32, borderRadius: '50%', objectFit: 'cover', flexShrink: 0 } })
1401
- : jsx('span', { style: { fontSize: 22, lineHeight: '24px' }, children: expert.emoji || '🧠' }),
1402
- jsxs('div', { style: { minWidth: 0, display: 'grid', gap: 3, flex: 1 }, children: [
1403
- jsxs('div', { style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }, children: [
1404
- jsx('strong', { style: { color: '#0f172a', fontSize: 13.5 }, children: expert.name }),
1405
- jsx('span', { style: { padding: '1px 7px', borderRadius: 6, background: expert.custom ? '#fef3c7' : '#eef2f8', color: expert.custom ? '#92400e' : '#64748b', fontSize: 11 }, children: expert.custom ? '自定义' : '内置' }),
1406
- jsx('span', { style: { color: '#94a3b8', fontSize: 11.5 }, children: expert.divisionName }),
1407
- ] }),
1408
- jsx('span', { style: { color: '#7b8799', fontSize: 12, lineHeight: 1.6 }, children: expert.description }),
1409
- ] }),
1513
+ // 点专家本体看提示词:卡片上不再放按钮,可点区域(头像 + 文字)与右侧开关是**兄弟节点**,
1514
+ // 所以掰开关不会顺带弹出提示词,也不需要 stopPropagation。
1515
+ jsxs('div', {
1516
+ role: 'button',
1517
+ tabIndex: 0,
1518
+ title: '查看提示词',
1519
+ onClick: () => { void openPrompt(expert) },
1520
+ onKeyDown: event => {
1521
+ if (event.key !== 'Enter' && event.key !== ' ') return
1522
+ event.preventDefault()
1523
+ void openPrompt(expert)
1524
+ },
1525
+ style: { display: 'flex', alignItems: 'flex-start', gap: 10, flex: 1, minWidth: 0, cursor: 'pointer' },
1526
+ children: [
1527
+ expert.avatar
1528
+ ? jsx('img', { src: expert.avatar, alt: '', style: { width: 32, height: 32, borderRadius: '50%', objectFit: 'cover', flexShrink: 0 } })
1529
+ : jsx('span', { style: { fontSize: 22, lineHeight: '24px' }, children: expert.emoji || '🧠' }),
1530
+ jsxs('div', { style: { minWidth: 0, display: 'grid', gap: 3, flex: 1 }, children: [
1531
+ jsxs('div', { style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }, children: [
1532
+ jsx('strong', { style: { color: '#0f172a', fontSize: 13.5 }, children: expert.name }),
1533
+ jsx('span', { style: { padding: '1px 7px', borderRadius: 6, background: expert.kind === 'team' ? '#ede9fe' : expert.custom ? '#fef3c7' : '#eef2f8', color: expert.kind === 'team' ? '#6d28d9' : expert.custom ? '#92400e' : '#64748b', fontSize: 11 }, children: expert.kind === 'team' ? '专家团' : expert.custom ? '自定义' : '内置' }),
1534
+ jsx('span', { style: { color: '#94a3b8', fontSize: 11.5 }, children: expert.divisionName }),
1535
+ ] }),
1536
+ jsx('span', { style: { color: '#7b8799', fontSize: 12, lineHeight: 1.6 }, children: expert.description }),
1537
+ expert.kind === 'team' && expert.members?.length > 0
1538
+ ? jsx('span', { style: { color: '#8a94a6', fontSize: 11.5, lineHeight: 1.5 }, children: '成员:' + expert.members.map(member => member.name || member.slug).join('、') })
1539
+ : null,
1540
+ ] }),
1541
+ ],
1542
+ }),
1410
1543
  jsx('button', {
1411
1544
  type: 'button',
1412
1545
  'aria-label': (expert.enabled ? '停用 ' : '启用 ') + expert.name,
@@ -1417,21 +1550,11 @@ window.__ModuleLoader__.load({
1417
1550
  children: jsx('span', { style: { position: 'absolute', top: 3, left: expert.enabled ? 21 : 3, width: 18, height: 18, borderRadius: '50%', background: '#fff', transition: 'left .15s' } }),
1418
1551
  }),
1419
1552
  ] }),
1420
- jsxs('div', { style: { display: 'flex', gap: 8, borderTop: '1px solid #eef2f8', paddingTop: 10, marginTop: 'auto' }, children: [
1421
- jsx('button', { type: 'button', onClick: () => { void openPrompt(expert) }, style: EXPERT_STYLE.ghost, children: '查看提示词' }),
1422
- jsx('button', {
1423
- type: 'button',
1424
- onClick: () => { void copyPrompt(expert) },
1425
- // 悬停/聚焦就预取:点击时缓存已就绪,剪贴板写入能落在手势的同步栈里。
1426
- onMouseEnter: () => prefetchPrompt(expert),
1427
- onFocus: () => prefetchPrompt(expert),
1428
- style: EXPERT_STYLE.ghost,
1429
- children: '复制提示词',
1430
- }),
1431
- expert.custom
1432
- ? jsx('button', { type: 'button', onClick: () => openEditor('edit', expert), style: { ...EXPERT_STYLE.ghost, marginLeft: 'auto' }, children: '编辑' })
1433
- : null,
1434
- ] }),
1553
+ expert.custom
1554
+ ? jsxs('div', { style: { display: 'flex', gap: 8, borderTop: '1px solid #eef2f8', paddingTop: 10, marginTop: 'auto' }, children: [
1555
+ jsx('button', { type: 'button', onClick: () => openEditor('edit', expert), style: { ...EXPERT_STYLE.ghost, marginLeft: 'auto' }, children: '编辑' }),
1556
+ ] })
1557
+ : null,
1435
1558
  ] })
1436
1559
 
1437
1560
  return jsxs(Fragment, { children: [
@@ -1439,6 +1562,7 @@ window.__ModuleLoader__.load({
1439
1562
  jsxs('div', { style: { display: 'flex', gap: 8, marginTop: 14 }, children: [
1440
1563
  jsx('button', { type: 'button', onClick: () => setTab('all'), style: EXPERT_STYLE.tab(tab === 'all'), children: '全部' }),
1441
1564
  jsx('button', { type: 'button', onClick: () => setTab('builtin'), style: EXPERT_STYLE.tab(tab === 'builtin'), children: '内置' }),
1565
+ jsx('button', { type: 'button', onClick: () => setTab('team'), style: EXPERT_STYLE.tab(tab === 'team'), children: '专家团' }),
1442
1566
  jsx('button', { type: 'button', onClick: () => setTab('custom'), style: EXPERT_STYLE.tab(tab === 'custom'), children: '自定义' }),
1443
1567
  ] }),
1444
1568
  filters,
@@ -1446,7 +1570,7 @@ window.__ModuleLoader__.load({
1446
1570
  shown.length === 0
1447
1571
  ? jsx('div', { style: { padding: '28px 0', color: '#94a3b8', fontSize: 13, textAlign: 'center' }, children: '没有匹配的专家' })
1448
1572
  : jsx('div', { style: { display: 'grid', gap: 12, gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))' }, children: shown.map(expert => jsx('div', { key: expert.slug, children: card(expert)})) }),
1449
- dialog ? ExpertDialog({ dialog, divisions, busy, form, onField, setDialog, mutate }) : null,
1573
+ dialog ? ExpertDialog({ dialog, divisions: editableDivisions, busy, form, onField, setDialog, mutate }) : null,
1450
1574
  ] })
1451
1575
  }
1452
1576
 
@@ -1484,12 +1608,12 @@ window.__ModuleLoader__.load({
1484
1608
  ctx.effect(() => {
1485
1609
  const normalizeTitle = () => {
1486
1610
  if (document.title === 'DeepSeek Harness' || document.title === 'DSH Local Build') {
1487
- document.title = 'WaterBuddy'
1611
+ document.title = 'WaterPlusAI'
1488
1612
  return
1489
1613
  }
1490
1614
  const title = document.title.replace(
1491
1615
  / — (?:DeepSeek Harness|DSH Local Build)$/,
1492
- ' — WaterBuddy',
1616
+ ' — WaterPlusAI',
1493
1617
  )
1494
1618
  if (title !== document.title) document.title = title
1495
1619
  }
@@ -1509,9 +1633,37 @@ window.__ModuleLoader__.load({
1509
1633
  // 都是**每次构建都变的哈希**(`nArs4W_panel…`,实测已无任何 foot 类),补丁因此静默
1510
1634
  // 失效、底栏又折行。改成用**我们自己的标记**定位:账号卡(`data-waterbuddy-account`)
1511
1635
  // 的父级就是底栏区,隔一层也是——`:has()` 在 Tauri 的 WebKit 上有。
1512
- '*:has(> [data-waterbuddy-account]){flex-direction:row!important;align-items:center!important;flex-wrap:nowrap!important;gap:8px}',
1513
- '*:has(> * > [data-waterbuddy-account]){flex-direction:row!important;align-items:center!important;flex-wrap:nowrap!important;gap:8px}',
1514
- '*:has(> [data-waterbuddy-account])>*{min-width:0}',
1636
+ // 底栏排成一行。**必须同时满足两个条件,否则一定出问题**:
1637
+ // a. 容器要 row —— 宿主 `.footArea` 原生是 column,不改就永远是上下两行;
1638
+ // b. 必须允许 wrap —— 宿主默认 nowrap,放不下时会**裁掉**最后一个子元素(「设置」消失);
1639
+ // c. 我们自己那部分必须能收缩且足够窄 —— 行是"放得下才成立",宽度只由我们的卡片贡献。
1640
+ // 三个条件都满足时:放得下 → 一行;放不下 → 优雅换成两行,且绝不丢元素。
1641
+ '[class*="footArea"]{display:flex!important;flex-direction:row!important;flex-wrap:wrap!important;align-items:center!important;gap:8px}',
1642
+
1643
+ // **关键的一半,前几轮都漏了**:宿主 `SidebarRoot.module.css` 里
1644
+ //
1645
+ // .settingsArea, .footerActions { flex: none; min-width: 0; width: 100% }
1646
+ //
1647
+ // 两个子元素都是**整宽**(按列布局设计的)。父级改成 row 后它们仍各要 100%,
1648
+ // 于是永远并排不了——所以只压窄我们自己的卡片完全无效,卡片外面那层
1649
+ // `footerActions` 依旧是 100%。两个都要改成按内容宽度、且允许收缩。
1650
+ // 账号卡那一侧**吃掉剩余空间**(用户区占满整行),设置区保持自身宽度 →
1651
+ // 被自然推到最右。两者都是局部类名,不碰祖先;设置区 flex 为 0 0 auto,
1652
+ // 所以卡片再胖也不会挤到它。
1653
+ '[class*="footerActions"]{width:auto!important;flex:1 1 auto!important;min-width:0!important}',
1654
+ '[class*="settingsArea"]{width:auto!important;flex:0 0 auto!important;min-width:0!important}',
1655
+
1656
+ // 以下是最初的写法记录(保留以免重复踩坑):
1657
+ //
1658
+ // 试过并已全部撤回的路子,每条都伤了真实界面:
1659
+ // 1. `:has()` 按层数掰祖先 → 把侧边栏根容器掰成横排,整条侧边栏塌掉;
1660
+ // 2. `[class*="footArea"]` + nowrap → 放不下时裁掉「设置」,设置直接消失;
1661
+ // 3. 撤回卡片 `flex:0 0 auto` → 设置回来了,但仍两行;
1662
+ // 4. 加 `[class*="footerActions"]` 宽度 → 行依旧没成,且疑似牵连侧边栏其它条目。
1663
+ //
1664
+ // 结论:底栏的容器结构、子元素宽度基准都由宿主决定,插件侧改不动它——第五次尝试
1665
+ // 仍在用户机器上失败,说明我用的观测(本机会话)与桌面端真实 DOM 不一致,
1666
+ // 再试只是继续拿用户的界面做实验。改底栏请改宿主的 ui-sidebar。
1515
1667
  ].join('')
1516
1668
  document.head.append(style)
1517
1669
  return () => { style.remove() }
@@ -1521,7 +1673,7 @@ window.__ModuleLoader__.load({
1521
1673
  yield ctx.slots.register({ name: 'sidebar.brand.mark', priority: -10 }, WaterBuddyMark)
1522
1674
  yield ctx.slots.register({ name: 'sidebar.brand.name', priority: -10 }, WaterBuddyName)
1523
1675
  }))
1524
- ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({ name: 'sidebar.footer.action', id: 'waterbuddy-user', order: 90, label: '登录 WaterBuddy' }, UserAction))
1676
+ ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({ name: 'sidebar.footer.action', id: 'waterbuddy-user', order: 90, label: '登录 WaterPlusAI' }, UserAction))
1525
1677
  ctx.slots.inject('settings.onboarding', () => ctx.slots.register({ name: 'settings.onboarding', id: 'welcome-notice', priority: -1000, order: -100 }, SkipWelcomeNotice))
1526
1678
  // 专家名册页:在设置左侧列出一项「专家」。`label` 用纯字符串——本插件的界面文案
1527
1679
  // 都是硬编码中文,没有走宿主词典(见 AGENTS.md 的产物面边界)。
@@ -1531,6 +1683,24 @@ window.__ModuleLoader__.load({
1531
1683
  order: 95,
1532
1684
  label: '专家',
1533
1685
  }, ExpertRoster))
1686
+ // 静音 dsh-better-sidebar 的一条固定噪音。
1687
+ //
1688
+ // 它的插槽拦截器会为 harness 自己的一个 list 插槽打印 error:
1689
+ // [dsh-better-sidebar] interception error: list slot "conversation.chat.turnTail" requires options.id
1690
+ // 这个插槽属于 harness 的 @deepseek-ai/dsh-client-ui-deliverables,既不是我们注册的,
1691
+ // 也无法从插件侧修好(不能改宿主的注册),而桌面端会把 console.error 渲染成底部红条,
1692
+ // 非常显眼。这里按**精确前缀**过滤掉它——只消这一条,其它错误照常上报。
1693
+ ctx.effect(() => {
1694
+ if (typeof console === 'undefined' || typeof console.error !== 'function') return undefined
1695
+ const original = console.error
1696
+ const NOISE = /^\[dsh-better-sidebar\] interception error: list slot "conversation\.chat\.turnTail" requires options\.id/
1697
+ console.error = function (...args) {
1698
+ if (typeof args[0] === 'string' && NOISE.test(args[0])) return
1699
+ return original.apply(this, args)
1700
+ }
1701
+ return () => { console.error = original }
1702
+ }, 'waterplus: silence better-sidebar slot noise')
1703
+
1534
1704
  // 新会话 hero:标记换成我们的(宿主默认是鸟形图标)。
1535
1705
  ctx.slots.inject('conversation.hero.brand.mark', () =>
1536
1706
  ctx.slots.register({ name: 'conversation.hero.brand.mark', priority: -10 }, WaterBuddyHeroMark))
@@ -1589,7 +1759,7 @@ window.__ModuleLoader__.load({
1589
1759
  ctx.effect(() => {
1590
1760
  const inputTriggers = typeof ctx.get === 'function' ? ctx.get('inputTriggers') : undefined
1591
1761
  if (!inputTriggers || typeof inputTriggers.registerSource !== 'function') {
1592
- console.warn('[WaterBuddy] expert @ menu disabled: host exposes no inputTriggers service')
1762
+ console.warn('[WaterPlusAI] expert @ menu disabled: host exposes no inputTriggers service')
1593
1763
  return undefined
1594
1764
  }
1595
1765
  let disposers = []
@@ -1602,7 +1772,7 @@ window.__ModuleLoader__.load({
1602
1772
  expertNames = new Set(experts.map(expert => expert.name))
1603
1773
  disposers = divisions.map((division, index) => inputTriggers.registerSource(createExpertTriggerSource(ctx, division, index)))
1604
1774
  })
1605
- .catch(error => console.warn('[WaterBuddy] expert @ menu disabled:', error.message))
1775
+ .catch(error => console.warn('[WaterPlusAI] expert @ menu disabled:', error.message))
1606
1776
 
1607
1777
  // 历史消息里的 `@专家名` 会被宿主的**消息渲染器**渲染成「文件提及」
1608
1778
  // (`<button data-ref-chip="file" title="@专家名">`),点它就按文件路径打开预览,