@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/client/index.js
CHANGED
|
@@ -13,7 +13,7 @@ window.__ModuleLoader__.load({
|
|
|
13
13
|
width: size,
|
|
14
14
|
height: size,
|
|
15
15
|
className,
|
|
16
|
-
alt: '
|
|
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: '
|
|
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 || '
|
|
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}扫码,安全登录
|
|
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
|
-
|
|
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
|
-
|
|
509
|
-
|
|
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 = !
|
|
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: '
|
|
527
|
-
jsx('div', { style: { marginTop: 8, fontSize: 15, fontWeight: 600 }, children: '
|
|
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
|
-
|
|
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,16 +583,24 @@ window.__ModuleLoader__.load({
|
|
|
554
583
|
jsx('button', {
|
|
555
584
|
type: 'button',
|
|
556
585
|
onClick: openPanel,
|
|
557
|
-
title: user ? name : '登录
|
|
558
|
-
|
|
559
|
-
|
|
586
|
+
title: user ? name : '登录 WaterPlusAI',
|
|
587
|
+
'data-waterbuddy-account': '',
|
|
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' },
|
|
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],
|
|
560
597
|
}),
|
|
561
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: [
|
|
562
599
|
jsxs('div', { style: { position: 'relative', padding: '22px 26px 0', textAlign: 'center' }, children: [
|
|
563
600
|
// 与已登录面板同一个坑:标题/说明是整行宽的块级元素,必须靠 z-index 让按钮稳定在上。
|
|
564
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: '×' }),
|
|
565
602
|
jsx('img', { src: SSO.image, alt: SSO.title, style: { display: 'block', width: 52, height: 52, objectFit: 'contain', margin: '0 auto' } }),
|
|
566
|
-
jsx('h3', { style: { margin: '10px 0 4px', color: '#162033', fontSize: 21 }, children: '
|
|
603
|
+
jsx('h3', { style: { margin: '10px 0 4px', color: '#162033', fontSize: 21 }, children: 'WaterPlusAI' }),
|
|
567
604
|
jsx('p', { style: { margin: 0, color: '#7b8799', fontSize: 12.5 }, children: '登录后同步水务之窗账号与知识库权限' }),
|
|
568
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,
|
|
569
606
|
] }),
|
|
@@ -626,7 +663,7 @@ window.__ModuleLoader__.load({
|
|
|
626
663
|
// `/waterplus-mark.png`,该路径在 hero 里取不到,渲染成了黑方块。
|
|
627
664
|
return jsx('img', {
|
|
628
665
|
src: mark,
|
|
629
|
-
alt: '
|
|
666
|
+
alt: 'WaterPlusAI',
|
|
630
667
|
width: edge,
|
|
631
668
|
height: edge,
|
|
632
669
|
style: { width: edge, height: edge, objectFit: 'contain', borderRadius: '22%' },
|
|
@@ -753,7 +790,12 @@ window.__ModuleLoader__.load({
|
|
|
753
790
|
const response = await fetch('/api/waterbuddy/experts')
|
|
754
791
|
const payload = await response.json()
|
|
755
792
|
if (!response.ok) throw new Error(payload.error || 'load-failed')
|
|
756
|
-
|
|
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 }
|
|
757
799
|
}
|
|
758
800
|
|
|
759
801
|
/** `@` 菜单的最近一次启用快照:`lexicon` / `codec.clipboardText` 是同步接口,只能读缓存。 */
|
|
@@ -1106,25 +1148,135 @@ window.__ModuleLoader__.load({
|
|
|
1106
1148
|
* 直接函数调用会把它的 hook 并进父组件的 hook 序列,弹窗一开一关就改变 hook 数量,
|
|
1107
1149
|
* React 会抛 "Rendered more hooks than during the previous render"。
|
|
1108
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
|
+
|
|
1109
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
|
+
}
|
|
1110
1201
|
return jsx('div', { style: EXPERT_STYLE.overlay, onClick: event => { if (event.target === event.currentTarget) onClose() }, children:
|
|
1111
|
-
jsxs('div', { style: EXPERT_STYLE.modal, children: [
|
|
1202
|
+
jsxs('div', { style: { ...EXPERT_STYLE.modal, width: 'min(860px, 100%)', maxHeight: '86vh' }, children: [
|
|
1112
1203
|
jsxs('div', { style: EXPERT_STYLE.modalHead, children: [
|
|
1113
|
-
|
|
1114
|
-
|
|
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
|
+
] }),
|
|
1115
1209
|
jsx('button', { type: 'button', onClick: onClose, style: { ...EXPERT_STYLE.ghost, marginLeft: 'auto' }, children: '关闭' }),
|
|
1116
1210
|
] }),
|
|
1117
|
-
jsx('div', { style: EXPERT_STYLE.modalBody, children:
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
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
|
+
] }) }),
|
|
1121
1277
|
jsxs('div', { style: EXPERT_STYLE.modalFoot, children: [
|
|
1122
|
-
jsx('button', {
|
|
1123
|
-
|
|
1124
|
-
onClick: () => { void writeClipboard(dialog.body || '').catch(() => onClose()) },
|
|
1125
|
-
style: EXPERT_STYLE.ghost,
|
|
1126
|
-
children: '复制提示词',
|
|
1127
|
-
}),
|
|
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: '复制提示词' }),
|
|
1128
1280
|
] }),
|
|
1129
1281
|
] }) })
|
|
1130
1282
|
}
|
|
@@ -1191,17 +1343,6 @@ window.__ModuleLoader__.load({
|
|
|
1191
1343
|
|
|
1192
1344
|
// ── roster.js ────────────────────────────────────────────────────
|
|
1193
1345
|
|
|
1194
|
-
/** 正文缓存:`slug → persona 正文`。跨渲染存活,供"预取后同步写剪贴板"用。 */
|
|
1195
|
-
const promptCache = new Map()
|
|
1196
|
-
|
|
1197
|
-
/** 取一位专家的 persona 正文(展示用原文)。 */
|
|
1198
|
-
async function fetchExpertBody(slug) {
|
|
1199
|
-
const response = await fetch('/api/waterbuddy/experts?slug=' + encodeURIComponent(slug))
|
|
1200
|
-
const payload = await response.json()
|
|
1201
|
-
if (!response.ok) throw new Error(payload.error || 'load-failed')
|
|
1202
|
-
return payload.body || ''
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
1346
|
/** 设置里的「专家」页:浏览、启用停用、以及管理自定义专家。
|
|
1206
1347
|
*
|
|
1207
1348
|
* 三件事刻意分开:
|
|
@@ -1285,52 +1426,26 @@ window.__ModuleLoader__.load({
|
|
|
1285
1426
|
const response = await fetch('/api/waterbuddy/experts?slug=' + encodeURIComponent(expert.slug))
|
|
1286
1427
|
const payload = await response.json()
|
|
1287
1428
|
if (!response.ok) throw new Error(payload.error || 'load-failed')
|
|
1288
|
-
setDialog({ mode: 'view', expert, body: payload.body || '', loading: false })
|
|
1429
|
+
setDialog({ mode: 'view', expert: payload.expert || expert, body: payload.body || '', loading: false })
|
|
1289
1430
|
} catch (err) {
|
|
1290
1431
|
setDialog(null)
|
|
1291
1432
|
setError(err.message)
|
|
1292
1433
|
}
|
|
1293
1434
|
}
|
|
1294
1435
|
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
* 关键在时序:WebKit(Tauri 用的就是它)要求剪贴板写入发生在**用户手势的同步调用栈**里,
|
|
1299
|
-
* 先 `await fetch` 再写就会被拒(`NotAllowedError`),连 execCommand 兜底也一起失效——
|
|
1300
|
-
* 这正是"复制提示词不起效果"的原因。所以:悬停/聚焦时先把正文预取进缓存,点击时
|
|
1301
|
-
* 缓存命中就是同步发起写入,手势不丢;没命中才走异步路径(并在失败时提示再点一次)。
|
|
1302
|
-
*/
|
|
1303
|
-
const copyPrompt = async expert => {
|
|
1304
|
-
const cached = promptCache.get(expert.slug)
|
|
1305
|
-
if (cached !== undefined) {
|
|
1306
|
-
try {
|
|
1307
|
-
await writeClipboard(cached)
|
|
1308
|
-
setError('')
|
|
1309
|
-
} catch (err) {
|
|
1310
|
-
setError('复制失败:' + err.message)
|
|
1311
|
-
}
|
|
1312
|
-
return
|
|
1313
|
-
}
|
|
1314
|
-
try {
|
|
1315
|
-
const body = await fetchExpertBody(expert.slug)
|
|
1316
|
-
promptCache.set(expert.slug, body)
|
|
1317
|
-
await writeClipboard(body)
|
|
1318
|
-
setError('')
|
|
1319
|
-
} catch (err) {
|
|
1320
|
-
setError('复制失败:' + err.message + '(正文已缓存,再点一次即可)')
|
|
1321
|
-
}
|
|
1322
|
-
}
|
|
1323
|
-
|
|
1324
|
-
/** 悬停/聚焦时预取正文,让点击那一刻能同步写入剪贴板。 */
|
|
1325
|
-
const prefetchPrompt = expert => {
|
|
1326
|
-
if (promptCache.has(expert.slug)) return
|
|
1327
|
-
void fetchExpertBody(expert.slug)
|
|
1328
|
-
.then(body => { promptCache.set(expert.slug, body) })
|
|
1329
|
-
.catch(() => {})
|
|
1330
|
-
}
|
|
1436
|
+
// 卡片上的「复制提示词」按钮已移除:卡片要先按 slug 取正文,而 WebKit(Tauri 用的就是它)
|
|
1437
|
+
// 要求剪贴板写入发生在用户手势的同步调用栈里,异步路径下写入常被拒(`NotAllowedError`)。
|
|
1438
|
+
// 复制入口保留在「查看提示词」弹窗内——正文已经拿到手,点击即同步写入。
|
|
1331
1439
|
|
|
1332
1440
|
const experts = snapshot ? snapshot.experts || [] : []
|
|
1333
|
-
const
|
|
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')
|
|
1334
1449
|
|
|
1335
1450
|
/** 打开新建/编辑弹窗:初始化表单草稿。 */
|
|
1336
1451
|
const openEditor = (mode, expert) => {
|
|
@@ -1338,7 +1453,7 @@ window.__ModuleLoader__.load({
|
|
|
1338
1453
|
slug: expert.slug || '',
|
|
1339
1454
|
name: expert.name || '',
|
|
1340
1455
|
description: expert.description || '',
|
|
1341
|
-
division: expert.division || (
|
|
1456
|
+
division: expert.division || (editableDivisions[0] ? editableDivisions[0].slug : 'supply'),
|
|
1342
1457
|
emoji: expert.emoji || '🧠',
|
|
1343
1458
|
avatar: expert.avatar || '',
|
|
1344
1459
|
prompt: expert.prompt || '',
|
|
@@ -1348,8 +1463,9 @@ window.__ModuleLoader__.load({
|
|
|
1348
1463
|
const onField = (key, value) => setForm(current => ({ ...current, [key]: value }))
|
|
1349
1464
|
const needle = query.trim().toLowerCase()
|
|
1350
1465
|
const shown = experts.filter(expert => {
|
|
1351
|
-
if (tab === '
|
|
1352
|
-
if (tab === '
|
|
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
|
|
1353
1469
|
if (division !== 'all' && expert.division !== division) return false
|
|
1354
1470
|
if (status === 'enabled' && !expert.enabled) return false
|
|
1355
1471
|
if (status === 'disabled' && expert.enabled) return false
|
|
@@ -1368,7 +1484,6 @@ window.__ModuleLoader__.load({
|
|
|
1368
1484
|
|
|
1369
1485
|
const header = jsxs('div', { style: { display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap', marginBottom: 4 }, children: [
|
|
1370
1486
|
jsx('h2', { style: { margin: 0, color: '#0f172a', fontSize: 19, fontWeight: 600 }, children: '专家' }),
|
|
1371
|
-
jsx('span', { style: { padding: '2px 8px', borderRadius: 6, background: '#f1f5f9', color: '#64748b', fontSize: 11.5 }, children: 'v' + (snapshot && snapshot.version ? snapshot.version : '') }),
|
|
1372
1487
|
jsx('span', { style: { color: '#8a94a6', fontSize: 12.5 }, children: experts.length + ' 位专家 · 已启用 ' + (snapshot ? snapshot.enabledCount : 0) }),
|
|
1373
1488
|
jsxs('span', { style: { marginLeft: 'auto', display: 'flex', gap: 8 }, children: [
|
|
1374
1489
|
jsx('button', {
|
|
@@ -1395,17 +1510,36 @@ window.__ModuleLoader__.load({
|
|
|
1395
1510
|
|
|
1396
1511
|
const card = expert => jsxs('div', { style: EXPERT_STYLE.card, children: [
|
|
1397
1512
|
jsxs('div', { style: { display: 'flex', alignItems: 'flex-start', gap: 10 }, children: [
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
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
|
+
}),
|
|
1409
1543
|
jsx('button', {
|
|
1410
1544
|
type: 'button',
|
|
1411
1545
|
'aria-label': (expert.enabled ? '停用 ' : '启用 ') + expert.name,
|
|
@@ -1416,21 +1550,11 @@ window.__ModuleLoader__.load({
|
|
|
1416
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' } }),
|
|
1417
1551
|
}),
|
|
1418
1552
|
] }),
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
// 悬停/聚焦就预取:点击时缓存已就绪,剪贴板写入能落在手势的同步栈里。
|
|
1425
|
-
onMouseEnter: () => prefetchPrompt(expert),
|
|
1426
|
-
onFocus: () => prefetchPrompt(expert),
|
|
1427
|
-
style: EXPERT_STYLE.ghost,
|
|
1428
|
-
children: '复制提示词',
|
|
1429
|
-
}),
|
|
1430
|
-
expert.custom
|
|
1431
|
-
? jsx('button', { type: 'button', onClick: () => openEditor('edit', expert), style: { ...EXPERT_STYLE.ghost, marginLeft: 'auto' }, children: '编辑' })
|
|
1432
|
-
: null,
|
|
1433
|
-
] }),
|
|
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,
|
|
1434
1558
|
] })
|
|
1435
1559
|
|
|
1436
1560
|
return jsxs(Fragment, { children: [
|
|
@@ -1438,6 +1562,7 @@ window.__ModuleLoader__.load({
|
|
|
1438
1562
|
jsxs('div', { style: { display: 'flex', gap: 8, marginTop: 14 }, children: [
|
|
1439
1563
|
jsx('button', { type: 'button', onClick: () => setTab('all'), style: EXPERT_STYLE.tab(tab === 'all'), children: '全部' }),
|
|
1440
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: '专家团' }),
|
|
1441
1566
|
jsx('button', { type: 'button', onClick: () => setTab('custom'), style: EXPERT_STYLE.tab(tab === 'custom'), children: '自定义' }),
|
|
1442
1567
|
] }),
|
|
1443
1568
|
filters,
|
|
@@ -1445,7 +1570,7 @@ window.__ModuleLoader__.load({
|
|
|
1445
1570
|
shown.length === 0
|
|
1446
1571
|
? jsx('div', { style: { padding: '28px 0', color: '#94a3b8', fontSize: 13, textAlign: 'center' }, children: '没有匹配的专家' })
|
|
1447
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)})) }),
|
|
1448
|
-
dialog ? ExpertDialog({ dialog, divisions, busy, form, onField, setDialog, mutate }) : null,
|
|
1573
|
+
dialog ? ExpertDialog({ dialog, divisions: editableDivisions, busy, form, onField, setDialog, mutate }) : null,
|
|
1449
1574
|
] })
|
|
1450
1575
|
}
|
|
1451
1576
|
|
|
@@ -1483,12 +1608,12 @@ window.__ModuleLoader__.load({
|
|
|
1483
1608
|
ctx.effect(() => {
|
|
1484
1609
|
const normalizeTitle = () => {
|
|
1485
1610
|
if (document.title === 'DeepSeek Harness' || document.title === 'DSH Local Build') {
|
|
1486
|
-
document.title = '
|
|
1611
|
+
document.title = 'WaterPlusAI'
|
|
1487
1612
|
return
|
|
1488
1613
|
}
|
|
1489
1614
|
const title = document.title.replace(
|
|
1490
1615
|
/ — (?:DeepSeek Harness|DSH Local Build)$/,
|
|
1491
|
-
' —
|
|
1616
|
+
' — WaterPlusAI',
|
|
1492
1617
|
)
|
|
1493
1618
|
if (title !== document.title) document.title = title
|
|
1494
1619
|
}
|
|
@@ -1502,10 +1627,43 @@ window.__ModuleLoader__.load({
|
|
|
1502
1627
|
const style = document.createElement('style')
|
|
1503
1628
|
style.dataset.waterbuddy = 'sidebar-footer-layout'
|
|
1504
1629
|
style.textContent = [
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1630
|
+
// 底栏排成一行(账号卡在左、设置与桌宠在右)。
|
|
1631
|
+
//
|
|
1632
|
+
// 旧写法按 `.dshp-panel__foot-area` 这类类名选,而宿主与 dsh-better-sidebar 的类名
|
|
1633
|
+
// 都是**每次构建都变的哈希**(`nArs4W_panel…`,实测已无任何 foot 类),补丁因此静默
|
|
1634
|
+
// 失效、底栏又折行。改成用**我们自己的标记**定位:账号卡(`data-waterbuddy-account`)
|
|
1635
|
+
// 的父级就是底栏区,隔一层也是——`:has()` 在 Tauri 的 WebKit 上有。
|
|
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。
|
|
1509
1667
|
].join('')
|
|
1510
1668
|
document.head.append(style)
|
|
1511
1669
|
return () => { style.remove() }
|
|
@@ -1515,7 +1673,7 @@ window.__ModuleLoader__.load({
|
|
|
1515
1673
|
yield ctx.slots.register({ name: 'sidebar.brand.mark', priority: -10 }, WaterBuddyMark)
|
|
1516
1674
|
yield ctx.slots.register({ name: 'sidebar.brand.name', priority: -10 }, WaterBuddyName)
|
|
1517
1675
|
}))
|
|
1518
|
-
ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({ name: 'sidebar.footer.action', id: 'waterbuddy-user', order: 90, label: '登录
|
|
1676
|
+
ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({ name: 'sidebar.footer.action', id: 'waterbuddy-user', order: 90, label: '登录 WaterPlusAI' }, UserAction))
|
|
1519
1677
|
ctx.slots.inject('settings.onboarding', () => ctx.slots.register({ name: 'settings.onboarding', id: 'welcome-notice', priority: -1000, order: -100 }, SkipWelcomeNotice))
|
|
1520
1678
|
// 专家名册页:在设置左侧列出一项「专家」。`label` 用纯字符串——本插件的界面文案
|
|
1521
1679
|
// 都是硬编码中文,没有走宿主词典(见 AGENTS.md 的产物面边界)。
|
|
@@ -1525,6 +1683,24 @@ window.__ModuleLoader__.load({
|
|
|
1525
1683
|
order: 95,
|
|
1526
1684
|
label: '专家',
|
|
1527
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
|
+
|
|
1528
1704
|
// 新会话 hero:标记换成我们的(宿主默认是鸟形图标)。
|
|
1529
1705
|
ctx.slots.inject('conversation.hero.brand.mark', () =>
|
|
1530
1706
|
ctx.slots.register({ name: 'conversation.hero.brand.mark', priority: -10 }, WaterBuddyHeroMark))
|
|
@@ -1583,7 +1759,7 @@ window.__ModuleLoader__.load({
|
|
|
1583
1759
|
ctx.effect(() => {
|
|
1584
1760
|
const inputTriggers = typeof ctx.get === 'function' ? ctx.get('inputTriggers') : undefined
|
|
1585
1761
|
if (!inputTriggers || typeof inputTriggers.registerSource !== 'function') {
|
|
1586
|
-
console.warn('[
|
|
1762
|
+
console.warn('[WaterPlusAI] expert @ menu disabled: host exposes no inputTriggers service')
|
|
1587
1763
|
return undefined
|
|
1588
1764
|
}
|
|
1589
1765
|
let disposers = []
|
|
@@ -1596,7 +1772,7 @@ window.__ModuleLoader__.load({
|
|
|
1596
1772
|
expertNames = new Set(experts.map(expert => expert.name))
|
|
1597
1773
|
disposers = divisions.map((division, index) => inputTriggers.registerSource(createExpertTriggerSource(ctx, division, index)))
|
|
1598
1774
|
})
|
|
1599
|
-
.catch(error => console.warn('[
|
|
1775
|
+
.catch(error => console.warn('[WaterPlusAI] expert @ menu disabled:', error.message))
|
|
1600
1776
|
|
|
1601
1777
|
// 历史消息里的 `@专家名` 会被宿主的**消息渲染器**渲染成「文件提及」
|
|
1602
1778
|
// (`<button data-ref-chip="file" title="@专家名">`),点它就按文件路径打开预览,
|