@waterplus-ai/waterbuddy 0.2.10 → 0.2.37
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/assets/experts/digital/it-assessment.jpg +0 -0
- package/assets/experts/digital/smart-water-assistant.jpg +0 -0
- package/assets/experts/digital/smart-water-case-expert.jpg +0 -0
- package/assets/experts/digital/water-industry-brain.jpg +0 -0
- package/assets/experts/drainage/drainage-sewage-ops.jpg +0 -0
- package/assets/experts/drainage/flood-control-pumping.jpg +0 -0
- package/assets/experts/general/article-writing.jpg +0 -0
- package/assets/experts/general/award-application.jpg +0 -0
- package/assets/experts/general/benchmarking.jpg +0 -0
- package/assets/experts/general/data-analysis.jpg +0 -0
- package/assets/experts/general/evaluation-assistant.jpg +0 -0
- package/assets/experts/general/event-planning.jpg +0 -0
- package/assets/experts/general/excel-formula.jpg +0 -0
- package/assets/experts/general/it-coding.jpg +0 -0
- package/assets/experts/general/meeting-minutes.jpg +0 -0
- package/assets/experts/general/summarization.jpg +0 -0
- package/assets/experts/general/video-script.jpg +0 -0
- package/assets/experts/general/work-report.jpg +0 -0
- package/assets/experts/management/admin-party-affairs.jpg +0 -0
- package/assets/experts/management/group-management-assistant.jpg +0 -0
- package/assets/experts/management/hr-performance.jpg +0 -0
- package/assets/experts/management/procurement-contract-risk.jpg +0 -0
- package/assets/experts/operations/customer-service.jpg +0 -0
- package/assets/experts/operations/metering-revenue.jpg +0 -0
- package/assets/experts/policy/smart-water-standards.jpg +0 -0
- package/assets/experts/policy/water-pricing.jpg +0 -0
- package/assets/experts/quality/plant-water-quality.jpg +0 -0
- package/assets/experts/supply/construction-safety.jpg +0 -0
- package/assets/experts/supply/energy-equipment-ops.jpg +0 -0
- package/assets/experts/supply/inspection-repair.jpg +0 -0
- package/assets/experts/supply/network-asset-leakage.jpg +0 -0
- package/assets/experts/supply/water-source-dispatch.jpg +0 -0
- package/assets/experts/teams/report-consulting-team/.codebuddy-plugin/plugin.json +42 -0
- package/assets/experts/teams/report-consulting-team/agents/lead.md +82 -0
- package/assets/experts/teams/report-consulting-team/avatars/team.svg +21 -0
- package/assets/experts/teams/report-consulting-team/references/report-templates.json +48 -0
- package/assets/experts/teams/report-consulting-team/skills/coherence-review/SKILL.md +11 -0
- package/assets/experts/teams/report-consulting-team/skills/office-deliverables/SKILL.md +15 -0
- package/assets/experts/teams/report-consulting-team/skills/report-planning/SKILL.md +13 -0
- package/assets/experts/teams/report-consulting-team/skills/report-writing/SKILL.md +13 -0
- package/assets/experts/teams/smart-water-delivery-team/.codebuddy-plugin/plugin.json +1 -0
- package/assets/experts/teams/smart-water-delivery-team/avatars/team.jpg +0 -0
- package/assets/experts/teams/software-development-team/.codebuddy-plugin/plugin.json +1 -1
- package/assets/experts/teams/software-development-team/avatars/team.jpg +0 -0
- package/assets/experts/teams/water-operations-team/.codebuddy-plugin/plugin.json +1 -0
- package/assets/experts/teams/water-operations-team/avatars/team.jpg +0 -0
- package/assets/recommendations/admin-party-affairs.jpg +0 -0
- package/assets/recommendations/article-writing.jpg +0 -0
- package/assets/recommendations/award-application.jpg +0 -0
- package/assets/recommendations/benchmarking.jpg +0 -0
- package/assets/recommendations/construction-safety.jpg +0 -0
- package/assets/recommendations/customer-service.jpg +0 -0
- package/assets/recommendations/data-analysis.jpg +0 -0
- package/assets/recommendations/digital.jpg +0 -0
- package/assets/recommendations/drainage-sewage-ops.jpg +0 -0
- package/assets/recommendations/drainage.jpg +0 -0
- package/assets/recommendations/energy-equipment-ops.jpg +0 -0
- package/assets/recommendations/evaluation-assistant.jpg +0 -0
- package/assets/recommendations/event-planning.jpg +0 -0
- package/assets/recommendations/excel-formula.jpg +0 -0
- package/assets/recommendations/flood-control-pumping.jpg +0 -0
- package/assets/recommendations/group-management-assistant.jpg +0 -0
- package/assets/recommendations/hr-performance.jpg +0 -0
- package/assets/recommendations/inspection-repair.jpg +0 -0
- package/assets/recommendations/it-assessment.jpg +0 -0
- package/assets/recommendations/it-coding.jpg +0 -0
- package/assets/recommendations/management.jpg +0 -0
- package/assets/recommendations/meeting-minutes.jpg +0 -0
- package/assets/recommendations/metering-revenue.jpg +0 -0
- package/assets/recommendations/network-asset-leakage.jpg +0 -0
- package/assets/recommendations/operations.jpg +0 -0
- package/assets/recommendations/plant-water-quality.jpg +0 -0
- package/assets/recommendations/policy.jpg +0 -0
- package/assets/recommendations/procurement-contract-risk.jpg +0 -0
- package/assets/recommendations/quality.jpg +0 -0
- package/assets/recommendations/report-assessment.svg +9 -0
- package/assets/recommendations/report-plan.svg +10 -0
- package/assets/recommendations/report-review.svg +11 -0
- package/assets/recommendations/smart-water-assistant.jpg +0 -0
- package/assets/recommendations/smart-water-case-expert.jpg +0 -0
- package/assets/recommendations/smart-water-delivery-team.jpg +0 -0
- package/assets/recommendations/smart-water-standards.jpg +0 -0
- package/assets/recommendations/software-development-team.jpg +0 -0
- package/assets/recommendations/summarization.jpg +0 -0
- package/assets/recommendations/supply.jpg +0 -0
- package/assets/recommendations/video-script.jpg +0 -0
- package/assets/recommendations/water-industry-brain.jpg +0 -0
- package/assets/recommendations/water-operations-team.jpg +0 -0
- package/assets/recommendations/water-pricing.jpg +0 -0
- package/assets/recommendations/water-source-dispatch.jpg +0 -0
- package/assets/recommendations/work-report.jpg +0 -0
- package/lib/client/index.js +799 -9
- package/lib/host/index.js +27 -0
- package/package.json +3 -3
package/lib/client/index.js
CHANGED
|
@@ -414,12 +414,179 @@ window.__ModuleLoader__.load({
|
|
|
414
414
|
] })
|
|
415
415
|
}
|
|
416
416
|
|
|
417
|
-
|
|
417
|
+
// —— 设置菜单里的账号条目:把官方账号菜单(settings.launcher)里的登录/退出登录取代成
|
|
418
|
+
// 我们的。菜单与条目都是官方 primitives(`button[role=menuitem]`),所以克隆官方条目继承
|
|
419
|
+
// 其样式(不手写动态类名哈希),只改文案与图标;克隆项不在官方 React fiber 里,交互只能走
|
|
420
|
+
// 原生捕获监听——与壳层 dsh-tauri-pet 处理同款菜单的手法一致。
|
|
421
|
+
const MENU_ITEM_SELECTOR = 'button[role="menuitem"]'
|
|
422
|
+
const MENU_LABEL_SELECTOR = '[class*="itemLabel"]'
|
|
423
|
+
const MENU_ICON_SELECTOR = '[class*="itemIcon"]'
|
|
424
|
+
const MENU_ANCHOR_LABELS = ['设置', 'Settings']
|
|
425
|
+
const MENU_ACCOUNT_LABELS = ['登录', 'Sign in', '退出登录', 'Sign out']
|
|
426
|
+
/** 官方菜单里的「意见反馈」(id=contact,locales.ts: contactUs)——按要求隐藏 */
|
|
427
|
+
const MENU_CONTACT_LABELS = ['意见反馈', 'Feedback']
|
|
428
|
+
const MENU_ITEM_ATTR = 'data-waterbuddy-menu-item'
|
|
429
|
+
const MENU_PATCH_ATTR = 'data-waterbuddy-menu-patched'
|
|
430
|
+
const MENU_ICON_SVG = '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8Z"/><path d="M4 20c0-3 3.6-5 8-5s8 2 8 5v1H4v-1Z"/></svg>'
|
|
431
|
+
|
|
432
|
+
/** 账号状态唯一真值:`UserAction` 每次拿到会话就更新它,菜单条目文案只读它。 */
|
|
433
|
+
const waterbuddyAccount = { signedIn: false, name: '', avatar: '', balance: '', hosted: false }
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
function waterbuddyMenuLabel() {
|
|
437
|
+
return waterbuddyAccount.signedIn ? '退出登录' : '登录 水务之窗'
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** 隐藏官方账号条目并插入我们的条目;文案变了就地更新,不重建节点。 */
|
|
441
|
+
function patchWaterbuddyMenu() {
|
|
442
|
+
if (typeof document === 'undefined' || typeof document.querySelectorAll !== 'function') return
|
|
443
|
+
for (const anchor of document.querySelectorAll(MENU_ITEM_SELECTOR)) {
|
|
444
|
+
const menu = typeof anchor.closest === 'function' ? anchor.closest('[role="menu"]') : null
|
|
445
|
+
if (!menu || !MENU_ANCHOR_LABELS.includes((anchor.textContent || '').trim())) continue
|
|
446
|
+
for (const item of menu.querySelectorAll(MENU_ITEM_SELECTOR)) {
|
|
447
|
+
if (item.hasAttribute(MENU_ITEM_ATTR)) continue
|
|
448
|
+
const itemText = (item.textContent || '').trim()
|
|
449
|
+
const hidden = MENU_ACCOUNT_LABELS.includes(itemText) || MENU_CONTACT_LABELS.includes(itemText)
|
|
450
|
+
if (!hidden) continue
|
|
451
|
+
if (item.style.display !== 'none') item.style.display = 'none'
|
|
452
|
+
}
|
|
453
|
+
const label = waterbuddyMenuLabel()
|
|
454
|
+
const existing = menu.querySelector(`[${MENU_ITEM_ATTR}]`)
|
|
455
|
+
if (existing) {
|
|
456
|
+
const node = existing.querySelector(MENU_LABEL_SELECTOR)
|
|
457
|
+
if (node && node.textContent !== label) node.textContent = label
|
|
458
|
+
continue
|
|
459
|
+
}
|
|
460
|
+
if (menu.hasAttribute(MENU_PATCH_ATTR)) continue
|
|
461
|
+
const clone = anchor.cloneNode(true)
|
|
462
|
+
const labelNode = typeof clone.querySelector === 'function' ? clone.querySelector(MENU_LABEL_SELECTOR) : null
|
|
463
|
+
// 非 primitives 结构(没有 itemLabel):中止插入,不追加半成品条目
|
|
464
|
+
if (!labelNode) continue
|
|
465
|
+
menu.setAttribute(MENU_PATCH_ATTR, '1')
|
|
466
|
+
clone.setAttribute(MENU_ITEM_ATTR, '1')
|
|
467
|
+
labelNode.textContent = label
|
|
468
|
+
const icon = clone.querySelector(MENU_ICON_SELECTOR)
|
|
469
|
+
if (icon) icon.innerHTML = MENU_ICON_SVG
|
|
470
|
+
// 登录项固定放菜单**最后一条**(不管官方/其它插件贡献了多少条目)
|
|
471
|
+
const items = menu.querySelectorAll(MENU_ITEM_SELECTOR)
|
|
472
|
+
const last = items.length > 0 ? items[items.length - 1] : anchor
|
|
473
|
+
last.after(clone)
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* 设置入口(官方 AccountMenu 的触发按钮)上的账号文案也归我们,否则侧栏底部会同时挂着
|
|
479
|
+
* 我们的卡片和官方的「尚未登录」。
|
|
480
|
+
*
|
|
481
|
+
* 认这个按钮用 `data-collapsed`:官方编译后的结构是
|
|
482
|
+
* `button[data-collapsed][aria-haspopup="menu"] > span.avatar + (wide && span.label)`,
|
|
483
|
+
* 而壳层自有的触发器没有该属性。label 的类名来自 CSS modules(形如 `yHnPSG_label`),
|
|
484
|
+
* 所以按 `[class*="label"]` 取;文案不再做状态匹配,无论官方显示什么一律换成我们的身份。
|
|
485
|
+
*/
|
|
486
|
+
function patchWaterbuddyTrigger() {
|
|
487
|
+
if (typeof document === 'undefined' || typeof document.querySelectorAll !== 'function') return
|
|
488
|
+
const entries = document.querySelectorAll('button[data-collapsed] [class*="label"]')
|
|
489
|
+
// 官方入口缺席(浏览器态 / 更老内核)时保留我们自己的卡片,否则底下就没有登录入口了
|
|
490
|
+
if (entries.length === 0) return
|
|
491
|
+
const label = waterbuddyAccount.name || '登录 水务之窗'
|
|
492
|
+
for (const node of entries) {
|
|
493
|
+
if (node.textContent !== label) node.textContent = label
|
|
494
|
+
}
|
|
495
|
+
// 官方入口既然承载了我们的登录,侧栏那条就只"不渲染卡片",**不能注销注册**——
|
|
496
|
+
// 登录面板挂在 UserAction 里,注销会让菜单里的登录点了没反应(实测踩过)。
|
|
497
|
+
if (!waterbuddyAccount.hosted) {
|
|
498
|
+
waterbuddyAccount.hosted = true
|
|
499
|
+
if (typeof Event === 'function') document.dispatchEvent(new Event('waterbuddy:account-hosted'))
|
|
500
|
+
}
|
|
501
|
+
// 官方入口在宿主那一行里是靠右的:给它一个 margin-right:auto 推回左侧(官方 CSS 本身是
|
|
502
|
+
// width:100% + flex-start,位置由外壳容器决定)
|
|
503
|
+
for (const node of entries) {
|
|
504
|
+
const anchor = node.parentElement
|
|
505
|
+
if (anchor && anchor.style.marginRight !== 'auto') anchor.style.marginRight = 'auto'
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// 额度靠右:在官方那一行(button 是 flex)里追加我们的额度元素,margin-left:auto 推到行尾。
|
|
509
|
+
// 图标内联 SVG(本插件是零依赖包,拿不到图标库;与菜单条目用同一做法),随 currentColor 变色。
|
|
510
|
+
// 没有额度(未登录 / 取不到)就移除,绝不显示一个不是用户在花的数字。
|
|
511
|
+
const POINTS_ICON = '<svg viewBox="0 0 1024 1024" width="12" height="12" fill="currentColor" aria-hidden="true" style="flex:none"><path d="M781.312 497.7152a34.133333 34.133333 0 0 1 46.916267-11.349333C897.809067 528.810667 938.666667 585.437867 938.666667 648.533333c0 137.966933-193.501867 238.933333-426.666667 238.933334S85.333333 786.500267 85.333333 648.533333c0-62.72 40.3968-119.074133 109.2608-161.450666a34.133333 34.133333 0 1 1 35.771734 58.146133C179.848533 576.3072 153.6 612.932267 153.6 648.533333c0 88.234667 157.986133 170.666667 358.4 170.666667s358.4-82.432 358.4-170.666667c0-35.84-26.589867-72.704-77.738667-103.901866a34.133333 34.133333 0 0 1-11.349333-46.916267z" fill="currentColor"/><path d="M512 136.533333C278.8352 136.533333 85.333333 237.499733 85.333333 375.466667s193.501867 238.933333 426.666667 238.933333 426.666667-100.9664 426.666667-238.933333S745.1648 136.533333 512 136.533333z m0 68.266667c200.413867 0 358.4 82.432 358.4 170.666667s-157.986133 170.666667-358.4 170.666666S153.6 463.701333 153.6 375.466667c0-87.210667 154.282667-168.721067 351.351467-170.632534L512 204.8z" fill="currentColor"/><path d="M458.24 477.764267l-6.3488-0.256a510.0032 510.0032 0 0 1-27.5968-2.423467c-26.248533-2.952533-52.565333-7.748267-77.431467-14.848-57.719467-16.503467-100.266667-42.871467-119.3472-84.4288l-1.962666-4.5056 63.214933-25.8048c8.123733 19.933867 35.259733 37.205333 76.8512 49.083733 20.855467 5.973333 43.5712 10.103467 66.338133 12.680534 6.007467 0.682667 11.6736 1.194667 16.913067 1.604266l9.147733 0.580267 3.6864 0.170667a34.133333 34.133333 0 1 1-1.809066 68.232533l-1.467734-0.068267h-0.187733z" fill="#00B386"/></svg>'
|
|
512
|
+
for (const node of entries) {
|
|
513
|
+
const row = node.parentElement
|
|
514
|
+
if (!row) continue
|
|
515
|
+
const text = waterbuddyAccount.balance || ''
|
|
516
|
+
const existing = row.querySelector('[data-waterbuddy-balance]')
|
|
517
|
+
if (!text) {
|
|
518
|
+
if (existing) existing.remove()
|
|
519
|
+
continue
|
|
520
|
+
}
|
|
521
|
+
const target = existing || (() => {
|
|
522
|
+
const created = document.createElement('span')
|
|
523
|
+
created.setAttribute('data-waterbuddy-balance', '1')
|
|
524
|
+
created.setAttribute('style', 'margin-left:auto;display:inline-flex;align-items:center;gap:4px;font-size:12px;opacity:.7;white-space:nowrap')
|
|
525
|
+
row.appendChild(created)
|
|
526
|
+
return created
|
|
527
|
+
})()
|
|
528
|
+
if (target.getAttribute('data-waterbuddy-balance-text') !== text) {
|
|
529
|
+
target.setAttribute('data-waterbuddy-balance-text', text)
|
|
530
|
+
while (target.firstChild) target.removeChild(target.firstChild)
|
|
531
|
+
target.innerHTML = POINTS_ICON
|
|
532
|
+
target.appendChild(document.createTextNode(text))
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// 头像:只有登录用户带真实头像(/api/waterbuddy/avatar?...)时才覆盖官方头像,
|
|
537
|
+
// 用背景图而不是替换 DOM —— 官方那个人形图标留在下面,退出登录后清掉覆盖即可恢复。
|
|
538
|
+
const officialAvatar = document.querySelector('button[data-collapsed] [class*="avatar"]')
|
|
539
|
+
if (officialAvatar) {
|
|
540
|
+
const url = waterbuddyAccount.avatar
|
|
541
|
+
if (url) {
|
|
542
|
+
if (officialAvatar.getAttribute('data-waterbuddy-avatar') !== url) {
|
|
543
|
+
officialAvatar.setAttribute('data-waterbuddy-avatar', url)
|
|
544
|
+
officialAvatar.style.backgroundImage = `url("${url}")`
|
|
545
|
+
officialAvatar.style.backgroundSize = 'cover'
|
|
546
|
+
officialAvatar.style.backgroundPosition = 'center'
|
|
547
|
+
}
|
|
548
|
+
// 官方的人形图标是子节点、绘制在背景之上,盖了头像必须把它藏起来(visibility 保留布局)
|
|
549
|
+
for (const child of officialAvatar.children) {
|
|
550
|
+
if (child.style.visibility !== 'hidden') child.style.visibility = 'hidden'
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
else if (officialAvatar.getAttribute('data-waterbuddy-avatar')) {
|
|
554
|
+
officialAvatar.removeAttribute('data-waterbuddy-avatar')
|
|
555
|
+
officialAvatar.style.backgroundImage = ''
|
|
556
|
+
officialAvatar.style.backgroundSize = ''
|
|
557
|
+
officialAvatar.style.backgroundPosition = ''
|
|
558
|
+
for (const child of officialAvatar.children) {
|
|
559
|
+
if (child.style.visibility === 'hidden') child.style.visibility = ''
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function startWaterbuddyMenu() {
|
|
566
|
+
if (typeof document === 'undefined' || typeof document.addEventListener !== 'function') return undefined
|
|
567
|
+
const onClick = event => {
|
|
568
|
+
const origin = event.target
|
|
569
|
+
if (!origin || typeof origin.closest !== 'function' || !origin.closest(`[${MENU_ITEM_ATTR}]`)) return
|
|
570
|
+
event.preventDefault()
|
|
571
|
+
event.stopImmediatePropagation()
|
|
572
|
+
// 复用 UserAction 里那套登录面板 / 登出,事件驱动,不写第二份逻辑
|
|
573
|
+
const name = waterbuddyAccount.signedIn ? 'waterbuddy:menu-logout' : 'waterbuddy:menu-login'
|
|
574
|
+
if (typeof Event === 'function') document.dispatchEvent(new Event(name))
|
|
575
|
+
// 克隆条目不会触发官方 onSelect(菜单不自关),派发一次外部 pointerdown 收起菜单
|
|
576
|
+
const PointerEventCtor = typeof PointerEvent === 'function' ? PointerEvent : (typeof MouseEvent === 'function' ? MouseEvent : null)
|
|
577
|
+
if (PointerEventCtor) document.dispatchEvent(new PointerEventCtor('pointerdown', { bubbles: true, cancelable: true }))
|
|
578
|
+
}
|
|
579
|
+
document.addEventListener('click', onClick, true)
|
|
580
|
+
return () => document.removeEventListener('click', onClick, true)
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function UserAction({ wide, inputActions }) {
|
|
418
584
|
const { useEffect, useRef, useState } = require('react')
|
|
419
585
|
const [open, setOpen] = useState(false)
|
|
420
586
|
const [user, setUser] = useState(null)
|
|
421
587
|
const [llm, setLlm] = useState(null)
|
|
422
588
|
const [session, setSession] = useState({ qrCode: '', status: '', error: '', loading: false })
|
|
589
|
+
const [hosted, setHosted] = useState(false)
|
|
423
590
|
|
|
424
591
|
useEffect(() => {
|
|
425
592
|
let cancelled = false
|
|
@@ -510,6 +677,9 @@ window.__ModuleLoader__.load({
|
|
|
510
677
|
return () => { stopped = true; if (timer) clearTimeout(timer) }
|
|
511
678
|
}, [open, session.sessionId, user])
|
|
512
679
|
|
|
680
|
+
// portal 渲染的推荐卡片拿不到 slot props,这里把写草稿的能力暴露给同一页面
|
|
681
|
+
// portal 渲染的推荐卡片拿不到 slot props,这里把「写草稿」的能力暴露给同一页面
|
|
682
|
+
if (typeof window !== 'undefined' && inputActions) window.__WB_INPUT_ACTIONS__ = inputActions
|
|
513
683
|
const openPanel = () => { setOpen(true); if (!user && !session.sessionId) startLogin() }
|
|
514
684
|
// 额度耗尽是**主动**提示:模型调用由 Harness 自己发起,插件看不到那次 403
|
|
515
685
|
// (harness 还会把它归类成「API 密钥无效」,文案完全误导),只能靠额度轮询
|
|
@@ -531,6 +701,36 @@ window.__ModuleLoader__.load({
|
|
|
531
701
|
if (llm.exhausted !== true) exhaustedNotified.current = false
|
|
532
702
|
}, [llm, user])
|
|
533
703
|
const logout = async () => { await fetch('/api/waterbuddy/logout', { method: 'POST' }); setUser(null); setOpen(false); setSession({ qrCode: '', status: '', error: '', loading: false }) }
|
|
704
|
+
|
|
705
|
+
// 设置菜单里的账号条目走事件驱动同一个面板 / 登出,避免第二套登录逻辑
|
|
706
|
+
useEffect(() => {
|
|
707
|
+
if (typeof document === 'undefined' || typeof document.addEventListener !== 'function') return undefined
|
|
708
|
+
const onLogin = () => openPanel()
|
|
709
|
+
const onLogout = () => { void logout() }
|
|
710
|
+
document.addEventListener('waterbuddy:menu-login', onLogin)
|
|
711
|
+
document.addEventListener('waterbuddy:menu-logout', onLogout)
|
|
712
|
+
return () => {
|
|
713
|
+
document.removeEventListener('waterbuddy:menu-login', onLogin)
|
|
714
|
+
document.removeEventListener('waterbuddy:menu-logout', onLogout)
|
|
715
|
+
}
|
|
716
|
+
}, [user, session.sessionId])
|
|
717
|
+
// 账号状态变化后就地刷新菜单条目文案(登录 / 退出登录)
|
|
718
|
+
// 官方入口接管后只隐藏卡片本身:组件必须继续挂载,否则菜单里的登录/登出没有宿主
|
|
719
|
+
useEffect(() => {
|
|
720
|
+
if (typeof document === 'undefined' || typeof document.addEventListener !== 'function') return undefined
|
|
721
|
+
const sync = () => setHosted(waterbuddyAccount.hosted)
|
|
722
|
+
document.addEventListener('waterbuddy:account-hosted', sync)
|
|
723
|
+
sync()
|
|
724
|
+
return () => document.removeEventListener('waterbuddy:account-hosted', sync)
|
|
725
|
+
}, [])
|
|
726
|
+
useEffect(() => {
|
|
727
|
+
waterbuddyAccount.signedIn = Boolean(user)
|
|
728
|
+
waterbuddyAccount.name = user ? userName(user) : ''
|
|
729
|
+
waterbuddyAccount.balance = llm && llm.balanceText ? llm.balanceText : ''
|
|
730
|
+
waterbuddyAccount.avatar = (user && user.avatar) || ''
|
|
731
|
+
patchWaterbuddyMenu()
|
|
732
|
+
patchWaterbuddyTrigger()
|
|
733
|
+
}, [user, llm])
|
|
534
734
|
const name = userName(user)
|
|
535
735
|
const buttonStyle = { borderRadius: 10, padding: '9px 18px', fontSize: 13, fontWeight: 600, cursor: 'pointer' }
|
|
536
736
|
// 额度以美元记账、按积分向用户展示;折算与格式化都在 Host 侧完成
|
|
@@ -589,9 +789,10 @@ window.__ModuleLoader__.load({
|
|
|
589
789
|
|
|
590
790
|
return jsxs(Fragment, {
|
|
591
791
|
children: [
|
|
592
|
-
jsx('button', {
|
|
792
|
+
hosted ? null : jsx('button', {
|
|
593
793
|
type: 'button',
|
|
594
794
|
onClick: openPanel,
|
|
795
|
+
'data-waterbuddy-account-card': '1',
|
|
595
796
|
title: user ? name : '登录 WaterPlusAI',
|
|
596
797
|
'data-waterbuddy-account': '',
|
|
597
798
|
// 注意:**不要**给它 `flex: 0 0 auto`。底栏被我们摆成一行时,宿主那层的
|
|
@@ -634,13 +835,16 @@ window.__ModuleLoader__.load({
|
|
|
634
835
|
const edge = typeof size === 'number' && size > 0 ? size : 24
|
|
635
836
|
// 与侧边栏左上角同一个 mark(模块级 data URI)。之前这里写的是
|
|
636
837
|
// `/waterplus-mark.png`,该路径在 hero 里取不到,渲染成了黑方块。
|
|
637
|
-
return jsx('img', {
|
|
838
|
+
return jsxs(Fragment, { children: [jsx('img', {
|
|
638
839
|
src: mark,
|
|
639
840
|
alt: 'WaterPlusAI',
|
|
640
841
|
width: edge,
|
|
641
842
|
height: edge,
|
|
642
843
|
style: { width: edge, height: edge, objectFit: 'contain', borderRadius: '22%' },
|
|
643
|
-
})
|
|
844
|
+
}) ,
|
|
845
|
+
// 推荐任务区跟随 hero 品牌槽位渲染(官方 hero 空态专属,进行中的对话不会出现)
|
|
846
|
+
jsx(RecommendationDock, {})
|
|
847
|
+
] })
|
|
644
848
|
}
|
|
645
849
|
|
|
646
850
|
// ── locales.js ──────────────────────────────────────────────────────────
|
|
@@ -758,6 +962,62 @@ window.__ModuleLoader__.load({
|
|
|
758
962
|
}
|
|
759
963
|
}
|
|
760
964
|
|
|
965
|
+
/**
|
|
966
|
+
* 在 chip 之后补一段**调用方给的**正文(推荐卡片用)。手法与 {@link appendExpertTaskExample}
|
|
967
|
+
* 完全一致:`insertText` 存在于输入 facade 但不在公开契约里,取不到就安静跳过。
|
|
968
|
+
*/
|
|
969
|
+
function appendExpertTaskText(target, text) {
|
|
970
|
+
if (typeof text !== 'string' || text.trim() === '' || typeof target.insertText !== 'function') return false
|
|
971
|
+
try {
|
|
972
|
+
const snapshot = target.state.getSnapshot()
|
|
973
|
+
const at = String(snapshot.draft || '').length
|
|
974
|
+
return target.insertText(' ' + text, { start: at, end: at, draftRev: snapshot.draftRev }) === true
|
|
975
|
+
} catch {
|
|
976
|
+
return false
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
/**
|
|
981
|
+
* 兜底:宿主没暴露 `insertText`(公开契约里没有)时,用**公开的 setDraft** 把正文接在引用之后。
|
|
982
|
+
* 插完 chip 后草稿文本里已经有引用占位符(U+FFFC),在它之后补正文即保持「chip 在前、正文在后」。
|
|
983
|
+
*/
|
|
984
|
+
function appendTaskViaDraft(target, text) {
|
|
985
|
+
if (typeof text !== 'string' || text.trim() === '') return false
|
|
986
|
+
try {
|
|
987
|
+
if (typeof target.setDraft !== 'function') return false
|
|
988
|
+
const snapshot = target.state.getSnapshot()
|
|
989
|
+
target.setDraft(String(snapshot.draft || '') + ' ' + text)
|
|
990
|
+
return true
|
|
991
|
+
} catch {
|
|
992
|
+
return false
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
/**
|
|
997
|
+
* 把输入框光标移到正文末尾。
|
|
998
|
+
*
|
|
999
|
+
* 为什么要在 DOM 层做:公开契约(SessionInput)只有 setDraft / insertReference / focus /
|
|
1000
|
+
* submit / notify / state,**没有光标或选区接口**;能精确定位的内部 insertText(text, {start,end})
|
|
1001
|
+
* 在本内核上未暴露(实测:插完引用光标停在 chip 之后)。这里在编辑器(contenteditable)上折叠
|
|
1002
|
+
* 选区到末尾,延后一拍执行,等宿主自己的更新先落完。
|
|
1003
|
+
*/
|
|
1004
|
+
function moveExpertCaretToEnd() {
|
|
1005
|
+
if (typeof document === 'undefined' || typeof window === 'undefined') return false
|
|
1006
|
+
try {
|
|
1007
|
+
const editable = document.querySelector('[contenteditable="true"]')
|
|
1008
|
+
const selection = window.getSelection()
|
|
1009
|
+
if (!editable || !selection || typeof document.createRange !== 'function') return false
|
|
1010
|
+
const range = document.createRange()
|
|
1011
|
+
range.selectNodeContents(editable)
|
|
1012
|
+
range.collapse(false)
|
|
1013
|
+
selection.removeAllRanges()
|
|
1014
|
+
selection.addRange(range)
|
|
1015
|
+
return true
|
|
1016
|
+
} catch {
|
|
1017
|
+
return false
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
761
1021
|
/** 名册读取(`@` 菜单与工具栏共用)。每次现读,用户一改开关立刻生效。 */
|
|
762
1022
|
async function loadExpertRoster() {
|
|
763
1023
|
const response = await fetch('/api/waterbuddy/experts')
|
|
@@ -862,6 +1122,16 @@ window.__ModuleLoader__.load({
|
|
|
862
1122
|
const MENU_WIDTH = 320
|
|
863
1123
|
|
|
864
1124
|
function ExpertInputEntry(props) {
|
|
1125
|
+
// 推荐任务区(portal 渲染在 React 会话上下文之外)需要同一套写草稿能力:
|
|
1126
|
+
// 只有 session 作用域的条目才拿得到 inputActions,因此在这里发布。
|
|
1127
|
+
if (typeof window !== 'undefined' && props && props.inputActions) window.__WB_INPUT_ACTIONS__ = props.inputActions
|
|
1128
|
+
// 推荐卡片在 portal 里渲染,拿不到 slot 注入,所以把「插引用 chip」的能力也发布出来 ——
|
|
1129
|
+
// 与「召唤专家」按钮走同一条路径(buildExpertReference + insertExpert),保证形态一致。
|
|
1130
|
+
if (typeof window !== 'undefined' && props && typeof props.summonExpertWithTask === 'function') {
|
|
1131
|
+
// 插 chip + 补任务正文,两步都做(只插 chip 会缺正文,用户会看到"做一半丢一半")
|
|
1132
|
+
window.__WB_SUMMON_TASK__ = (expert, task) => props.summonExpertWithTask(buildExpertReference(expert), expert, task) === true
|
|
1133
|
+
}
|
|
1134
|
+
|
|
865
1135
|
if (props && typeof props.insertExpert === 'function') return ExpertInputButton(props)
|
|
866
1136
|
if (props && props.inputActions && typeof props.inputActions.setDraft === 'function') {
|
|
867
1137
|
return ExpertInputButton({ ...props, insertExpert: undefined })
|
|
@@ -1557,6 +1827,511 @@ window.__ModuleLoader__.load({
|
|
|
1557
1827
|
|
|
1558
1828
|
// `inputTriggers` 必须**声明**才能拿到:`ctx.get` 同样受 inject 边界限制(实测未声明时
|
|
1559
1829
|
// 拿到 undefined,@ 菜单静默不注册)。参考实现同样把它列在 inject 里。
|
|
1830
|
+
|
|
1831
|
+
/**
|
|
1832
|
+
* 新会话页推荐任务的数据源:**只推荐我们真实可用的东西** —— 专家、专家团,以及专家自带的
|
|
1833
|
+
* skills(都来自 Host 的 /api/waterbuddy/experts 快照)。接口不可用时回落到水务行业种子任务,
|
|
1834
|
+
* 保证这一块永远不为空。
|
|
1835
|
+
*/
|
|
1836
|
+
|
|
1837
|
+
/**
|
|
1838
|
+
* 推荐卡片是「任务模板」,不是专家名片:标题描述用户要完成的工作,点击后补充可交付的输出结构。
|
|
1839
|
+
* 专家名仍作为协作对象显示在副标题里,避免标题和实际产出脱节。
|
|
1840
|
+
*/
|
|
1841
|
+
const RECOMMENDATION_TEMPLATES = {
|
|
1842
|
+
'smart-water-assistant': {
|
|
1843
|
+
title: '制定智慧水务建设路线图',
|
|
1844
|
+
subtitle: '从现状评估到分期落地的可执行方案',
|
|
1845
|
+
task: '请制定智慧水务建设路线图。先列出需要补充的现状信息,再输出目标架构、一期最小闭环、分期项目、投资与风险假设、里程碑和验收指标;无法确认的内容标注为待核实。',
|
|
1846
|
+
tag: '建设规划',
|
|
1847
|
+
},
|
|
1848
|
+
'smart-water-case-expert': {
|
|
1849
|
+
title: '拆解一个可迁移的智慧水务案例',
|
|
1850
|
+
subtitle: '从问题、投入、成效到适用条件逐项对照',
|
|
1851
|
+
task: '请拆解一个与当前水务企业相近的智慧水务案例,按“问题—方案—实施—投入—成效—依赖条件—可迁移做法—风险”输出,并区分已核实事实、宣传口径和待验证信息。',
|
|
1852
|
+
tag: '案例研究',
|
|
1853
|
+
},
|
|
1854
|
+
'water-industry-brain': {
|
|
1855
|
+
title: '研判水务行业趋势与机会',
|
|
1856
|
+
subtitle: '统一指标口径,给出证据与决策影响',
|
|
1857
|
+
task: '请围绕指定地区和细分领域研判水务行业趋势。先明确数据口径和时间范围,再输出关键趋势、证据来源、影响判断、机会与风险,以及仍需补充的数据;不要用单一案例推导行业结论。',
|
|
1858
|
+
tag: '行业洞察',
|
|
1859
|
+
},
|
|
1860
|
+
'it-assessment': {
|
|
1861
|
+
title: '完成水务企业 IT 现状评估',
|
|
1862
|
+
subtitle: '盘点系统、数据、治理与组织能力差距',
|
|
1863
|
+
task: '请设计一份水务企业 IT 现状评估方案,输出访谈提纲、资料清单、评估维度、成熟度判定口径、差距台账和分阶段改进路线;每项判断都注明证据要求。',
|
|
1864
|
+
tag: '现状评估',
|
|
1865
|
+
},
|
|
1866
|
+
'water-source-dispatch': {
|
|
1867
|
+
title: '编制供水调度与水量平衡方案',
|
|
1868
|
+
subtitle: '明确约束、监测指标、切换阈值和备用方案',
|
|
1869
|
+
task: '请编制供水调度方案。先核对水源、产能、库容、压力、需水预测和设备约束,再给推荐方案、备选方案、监测清单、触发阈值、执行反馈和撤销条件。',
|
|
1870
|
+
tag: '供水调度',
|
|
1871
|
+
},
|
|
1872
|
+
'network-asset-leakage': {
|
|
1873
|
+
title: '制定管网漏损治理计划',
|
|
1874
|
+
subtitle: '从水量平衡到 DMA、压力和资产更新闭环',
|
|
1875
|
+
task: '请制定管网漏损治理计划。先建立供水量与售水量的水量平衡,再区分计量误差、数据处理、合法未计量、非法用水和真实漏损,最后给出验证动作、优先级、指标和复盘周期。',
|
|
1876
|
+
tag: '漏损治理',
|
|
1877
|
+
},
|
|
1878
|
+
'inspection-repair': {
|
|
1879
|
+
title: '组织一次爆管抢修与保供',
|
|
1880
|
+
subtitle: '覆盖前 30 分钟、恢复条件和用户通知',
|
|
1881
|
+
task: '请针对一起供水管网爆管事件编制处置清单。先给前 30 分钟行动,再给隔离、抢修、检测、复水、用户通知和复盘安排,并明确责任岗位、启动条件、完成时限和现场待核实项。',
|
|
1882
|
+
tag: '应急抢修',
|
|
1883
|
+
},
|
|
1884
|
+
'plant-water-quality': {
|
|
1885
|
+
title: '排查出厂水质异常',
|
|
1886
|
+
subtitle: '按原水—工艺—出厂—管网链路定位风险',
|
|
1887
|
+
task: '请排查一次出厂水质异常。按“风险等级—数据有效性—原因排序—补测与现场核查—立即控制—恢复验证—长期预防”输出,注明采样点、时间、标准依据和不确定性。',
|
|
1888
|
+
tag: '水质诊断',
|
|
1889
|
+
},
|
|
1890
|
+
'drainage-sewage-ops': {
|
|
1891
|
+
title: '诊断排水与污水运营异常',
|
|
1892
|
+
subtitle: '把现象、指标、验证步骤和处置建议串成闭环',
|
|
1893
|
+
task: '请诊断一项排水或污水运营异常,按“现象—关键指标—可能原因—验证步骤—临时措施—恢复条件—长期改进”输出;区分日常养护、小修和改造事项。',
|
|
1894
|
+
tag: '排水运营',
|
|
1895
|
+
},
|
|
1896
|
+
'flood-control-pumping': {
|
|
1897
|
+
title: '制定防汛排涝泵站响应方案',
|
|
1898
|
+
subtitle: '结合雨情、水情、设施能力和队伍资源分级调度',
|
|
1899
|
+
task: '请制定防汛排涝响应方案。先判断当前风险等级,再给未来一至三小时的滚动行动清单,按区域、设施、队伍、物资、交通和信息报送分工,并写明升级与解除条件。',
|
|
1900
|
+
tag: '防汛调度',
|
|
1901
|
+
},
|
|
1902
|
+
'energy-equipment-ops': {
|
|
1903
|
+
title: '分析水务设备能耗异常',
|
|
1904
|
+
subtitle: '定位泵组、风机、加药和运行组合的节能机会',
|
|
1905
|
+
task: '请分析一项水务设备能耗异常,输出现象、可能原因、验证数据、立即措施、后续措施和节能项目基线;写清公式、边界、单位、停机窗口和回退条件。',
|
|
1906
|
+
tag: '设备节能',
|
|
1907
|
+
},
|
|
1908
|
+
'construction-safety': {
|
|
1909
|
+
title: '建立水务工程安全检查清单',
|
|
1910
|
+
subtitle: '把风险、责任、整改时限和验收证据落到现场',
|
|
1911
|
+
task: '请为一项水务工程建立安全与质量检查清单,按风险事件、触发条件、可能后果、控制措施、责任人、整改时限和验收证据组织,注明必须现场核验的事项。',
|
|
1912
|
+
tag: '工程安全',
|
|
1913
|
+
},
|
|
1914
|
+
'smart-water-standards': {
|
|
1915
|
+
title: '做一次智慧水务标准合规对照',
|
|
1916
|
+
subtitle: '把条款要求映射到设计、采购、实施和验收证据',
|
|
1917
|
+
task: '请对指定智慧水务项目做标准合规对照,输出适用标准清单、条款要求、当前做法、符合性、证据、差距和整改建议;无法确认现行性的文件标注待核验。',
|
|
1918
|
+
tag: '标准合规',
|
|
1919
|
+
},
|
|
1920
|
+
'water-pricing': {
|
|
1921
|
+
title: '论证一次水价调整方案',
|
|
1922
|
+
subtitle: '明确成本口径、用户影响、程序要求和敏感性',
|
|
1923
|
+
task: '请论证一次水价调整方案,先列政策与数据基础,再比较方案、测算成本和用户影响,最后给出程序要求、风险沟通和实施评估;所有假设标明基准年、单位和来源。',
|
|
1924
|
+
tag: '水价论证',
|
|
1925
|
+
},
|
|
1926
|
+
'group-management-assistant': {
|
|
1927
|
+
title: '设计水务集团年度经营考核',
|
|
1928
|
+
subtitle: '统一指标口径、数据来源、权重和改进闭环',
|
|
1929
|
+
task: '请设计水务集团年度经营考核方案,输出指标体系、定义与口径、数据来源、权重、考核周期、评分规则、异常处理和改进闭环,并指出需要管理层确认的事项。',
|
|
1930
|
+
tag: '经营考核',
|
|
1931
|
+
},
|
|
1932
|
+
'procurement-contract-risk': {
|
|
1933
|
+
title: '审查水务采购与合同风险',
|
|
1934
|
+
subtitle: '按条款、流程、证据和责任界面形成风险清单',
|
|
1935
|
+
task: '请审查一份水务采购或合同方案,输出风险清单、条款依据、风险等级、可能后果、修改建议、流程节点和需补充的证据;不要替代法务作最终结论。',
|
|
1936
|
+
tag: '合同风控',
|
|
1937
|
+
},
|
|
1938
|
+
'hr-performance': {
|
|
1939
|
+
title: '设计水务企业绩效改进方案',
|
|
1940
|
+
subtitle: '把岗位目标、指标口径、反馈和复盘机制连起来',
|
|
1941
|
+
task: '请设计一套水务企业绩效改进方案,先明确组织目标和岗位边界,再给指标、数据来源、评分规则、反馈节奏、申诉机制和复盘安排,区分待确认的制度前提。',
|
|
1942
|
+
tag: '绩效管理',
|
|
1943
|
+
},
|
|
1944
|
+
'admin-party-affairs': {
|
|
1945
|
+
title: '起草一份规范的行政与党群材料',
|
|
1946
|
+
subtitle: '先梳理事实和结构,再输出可直接修改的初稿',
|
|
1947
|
+
task: '请根据我提供的素材起草行政或党群材料,先列事实缺口和结构,再给出正文初稿、标题备选、需核验的名称日期和可替换段落;不要补造事实。',
|
|
1948
|
+
tag: '材料起草',
|
|
1949
|
+
},
|
|
1950
|
+
'customer-service': {
|
|
1951
|
+
title: '优化供水客服工单处置流程',
|
|
1952
|
+
subtitle: '覆盖分级、时限、回访、升级和服务口径',
|
|
1953
|
+
task: '请优化一套供水客服工单流程,输出问题分类、分级标准、响应与解决时限、责任岗位、升级条件、回访口径和服务质量指标,并标出需要结合现行制度核对的内容。',
|
|
1954
|
+
tag: '客服运营',
|
|
1955
|
+
},
|
|
1956
|
+
'metering-revenue': {
|
|
1957
|
+
title: '排查抄收营收与对账差异',
|
|
1958
|
+
subtitle: '沿户、表、量、价、费、款逐环节定位问题',
|
|
1959
|
+
task: '请排查一项抄收营收或财务对账差异,按客户/账户、异常类型、判断依据、风险、核查动作和处理状态输出,并写清统计时点、单位和未纳入因素。',
|
|
1960
|
+
tag: '营收对账',
|
|
1961
|
+
},
|
|
1962
|
+
'benchmarking': {
|
|
1963
|
+
title: '建立水务企业对标分析框架',
|
|
1964
|
+
subtitle: '先统一口径,再解释差距来源和改进目标',
|
|
1965
|
+
task: '请建立水务企业对标分析框架,输出标杆选择条件、指标定义、数据口径、可比性限制、差距归因、优先改进目标和验证周期,不把单一案例当作行业平均。',
|
|
1966
|
+
tag: '对标分析',
|
|
1967
|
+
},
|
|
1968
|
+
'data-analysis': {
|
|
1969
|
+
title: '把水务业务数据转成决策结论',
|
|
1970
|
+
subtitle: '从清洗、口径、分析到可视化和行动建议',
|
|
1971
|
+
task: '请为一份水务业务数据设计分析方案,输出数据质量检查、指标定义、分析方法、可视化结构、异常解释、结论边界和下一步行动;明确哪些结论需要补充数据验证。',
|
|
1972
|
+
tag: '数据分析',
|
|
1973
|
+
},
|
|
1974
|
+
'evaluation-assistant': {
|
|
1975
|
+
title: '设计项目或成果评估框架',
|
|
1976
|
+
subtitle: '让评分标准、证据、权重和结论可复核',
|
|
1977
|
+
task: '请设计一个水务项目或成果评估框架,输出评价目标、维度、指标、评分规则、权重、证据要求、打分表和结论模板,并区分事实、判断和待验证事项。',
|
|
1978
|
+
tag: '项目评估',
|
|
1979
|
+
},
|
|
1980
|
+
'meeting-minutes': {
|
|
1981
|
+
title: '把会议记录整理成可执行清单',
|
|
1982
|
+
subtitle: '提炼决议、责任人、时限、依赖和验收标准',
|
|
1983
|
+
task: '请把以下会议记录整理成结构化纪要,先给会议结论,再列任务、责任人、完成时限、前置依赖、验收标准和待确认事项;不要把讨论意见误写成已确认决议。',
|
|
1984
|
+
tag: '会议纪要',
|
|
1985
|
+
},
|
|
1986
|
+
'work-report': {
|
|
1987
|
+
title: '把工作进展写成管理层汇报',
|
|
1988
|
+
subtitle: '突出结果、证据、问题、风险和下一步动作',
|
|
1989
|
+
task: '请把以下工作素材整理成管理层汇报,输出标题、结论摘要、已完成成果、数据证据、问题与风险、需要协调的事项和下一步计划;缺少数据的地方标注待补。',
|
|
1990
|
+
tag: '工作汇报',
|
|
1991
|
+
},
|
|
1992
|
+
'article-writing': {
|
|
1993
|
+
title: '写一篇有证据的水务行业文章',
|
|
1994
|
+
subtitle: '先给标题与结构,再输出可核验正文',
|
|
1995
|
+
task: '请围绕给定主题写一篇水务行业文章,先给标题备选和文章结构,再给正文;核对数字、时间、机构和政策名称,不确定的信息标注待核实。',
|
|
1996
|
+
tag: '文章创作',
|
|
1997
|
+
},
|
|
1998
|
+
'video-script': {
|
|
1999
|
+
title: '把水务主题改成短视频脚本',
|
|
2000
|
+
subtitle: '按镜头、画面、口播和节奏输出可拍版本',
|
|
2001
|
+
task: '请把以下水务主题改写成短视频脚本,输出目标受众、时长、分镜、画面、口播、字幕、转场和结尾行动引导;不要虚构案例数据。',
|
|
2002
|
+
tag: '视频脚本',
|
|
2003
|
+
},
|
|
2004
|
+
'award-application': {
|
|
2005
|
+
title: '提炼水务项目奖项申报亮点',
|
|
2006
|
+
subtitle: '把问题、做法、成效和佐证材料一一对应',
|
|
2007
|
+
task: '请根据真实素材整理奖项申报框架,输出匹配度、材料缺口、核心亮点、问题—做法—创新—成效—证据—推广条件对照表,以及佐证材料清单;没有证据的亮点列为待补。',
|
|
2008
|
+
tag: '奖项申报',
|
|
2009
|
+
},
|
|
2010
|
+
'event-planning': {
|
|
2011
|
+
title: '策划一次水务主题活动',
|
|
2012
|
+
subtitle: '从目标、流程、人员、预算到风险预案完整展开',
|
|
2013
|
+
task: '请策划一次水务主题活动,输出目标与受众、流程、时间表、人员分工、物料、预算框架、宣传内容、现场风险和复盘指标;标出需要主办方确认的信息。',
|
|
2014
|
+
tag: '活动策划',
|
|
2015
|
+
},
|
|
2016
|
+
'summarization': {
|
|
2017
|
+
title: '把长材料压缩成决策摘要',
|
|
2018
|
+
subtitle: '保留结论、证据、争议和待办,不丢关键限定条件',
|
|
2019
|
+
task: '请把以下长材料整理成决策摘要,输出三条以内结论、关键证据、争议与不确定性、对业务的影响和待办事项;保留原文中的限定条件,不补写材料没有的事实。',
|
|
2020
|
+
tag: '总结摘要',
|
|
2021
|
+
},
|
|
2022
|
+
'excel-formula': {
|
|
2023
|
+
title: '生成可复核的 Excel 处理方案',
|
|
2024
|
+
subtitle: '先确认表结构,再给公式、示例和异常处理',
|
|
2025
|
+
task: '请根据表头和示例数据生成 Excel 处理方案,先确认字段含义,再给公式、适用范围、示例结果、空值与异常处理和验证方法;不要假设未提供的列。',
|
|
2026
|
+
tag: '表格自动化',
|
|
2027
|
+
},
|
|
2028
|
+
'it-coding': {
|
|
2029
|
+
title: '定位报错并给出可验证的修复',
|
|
2030
|
+
subtitle: '先解释根因,再给最小改动、验证命令和回滚方式',
|
|
2031
|
+
task: '请分析以下代码或日志,先判断最可能的根因和证据,再给最小改动方案、完整修改片段、验证命令、预期结果和回滚方式;不确定的环境信息列出来。',
|
|
2032
|
+
tag: '开发调试',
|
|
2033
|
+
},
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
const RECOMMENDATION_TASKS = {
|
|
2037
|
+
supply: RECOMMENDATION_TEMPLATES['water-source-dispatch'],
|
|
2038
|
+
drainage: RECOMMENDATION_TEMPLATES['drainage-sewage-ops'],
|
|
2039
|
+
quality: RECOMMENDATION_TEMPLATES['plant-water-quality'],
|
|
2040
|
+
digital: RECOMMENDATION_TEMPLATES['smart-water-assistant'],
|
|
2041
|
+
policy: RECOMMENDATION_TEMPLATES['smart-water-standards'],
|
|
2042
|
+
operations: RECOMMENDATION_TEMPLATES['customer-service'],
|
|
2043
|
+
management: RECOMMENDATION_TEMPLATES['group-management-assistant'],
|
|
2044
|
+
general: RECOMMENDATION_TEMPLATES['meeting-minutes'],
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
const RECOMMENDATION_DIVISION_IMAGES = {
|
|
2048
|
+
supply: 'water-source-dispatch',
|
|
2049
|
+
drainage: 'drainage-sewage-ops',
|
|
2050
|
+
quality: 'plant-water-quality',
|
|
2051
|
+
digital: 'smart-water-assistant',
|
|
2052
|
+
policy: 'smart-water-standards',
|
|
2053
|
+
operations: 'customer-service',
|
|
2054
|
+
management: 'group-management-assistant',
|
|
2055
|
+
general: 'meeting-minutes',
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
const RECOMMENDATION_TEAM_IMAGES = new Set([
|
|
2059
|
+
'smart-water-delivery-team',
|
|
2060
|
+
'software-development-team',
|
|
2061
|
+
'water-operations-team',
|
|
2062
|
+
])
|
|
2063
|
+
|
|
2064
|
+
const REPORT_TEAM_SLUG = 'report-consulting-team'
|
|
2065
|
+
|
|
2066
|
+
const REPORT_RECOMMENDATION_TEMPLATES = [
|
|
2067
|
+
{
|
|
2068
|
+
image: 'report-review',
|
|
2069
|
+
title: '复盘十四五数字化建设',
|
|
2070
|
+
subtitle: '从建设成效、问题根因到十五五行动建议',
|
|
2071
|
+
task: '请按《十四五数字化回顾》固定模板开展报告。先建立资料台账、事实表、投资和指标口径,再按“数字化建设总体情况与综合评价、六大体系建设成效评估、分子公司/业务板块建设情况评估、存在问题与原因分析、经验启示与典型案例、面向十五五的工作思路与行动建议”连续撰写。每章承接前章,缺失数字标注待补,最后同步给出执行摘要、图表清单、Word结构、PPT大纲和Excel数据表建议。',
|
|
2072
|
+
tag: '十四五回顾',
|
|
2073
|
+
},
|
|
2074
|
+
{
|
|
2075
|
+
image: 'report-assessment',
|
|
2076
|
+
title: '形成数字化现状与需求报告',
|
|
2077
|
+
subtitle: '按七大章节梳理业务、系统、数据和治理诊断',
|
|
2078
|
+
task: '请按《数字化现状总结与需求分析报告》固定模板开展报告。严格沿用七章结构:项目概述、集团战略理解、业务分析与诊断、数字化现状评估、数字化现状调研总结、数字化建设对标分析、数字化转型建设需求及主要问题总结。先整理资料依据和缺口,再按章节连续写作,统一术语与数据口径,区分事实、判断和待确认项,最后输出需求优先级、执行摘要及Word/PPT/Excel交付设计。',
|
|
2079
|
+
tag: '现状诊断',
|
|
2080
|
+
},
|
|
2081
|
+
{
|
|
2082
|
+
image: 'report-plan',
|
|
2083
|
+
title: '规划数字化建设总体蓝图',
|
|
2084
|
+
subtitle: '把战略、架构、治理和标准串成可落地计划',
|
|
2085
|
+
task: '请按《数字化建设计划》固定模板开展总体规划报告。严格沿用八章结构:总体战略与总体蓝图设计、应用架构设计、数据资源架构设计、基础设施架构设计、信息安全架构设计、IT治理架构设计、标准体系架构设计、总体规划报告编制与交付要求。围绕“战略目标—总体蓝图—应用体系—数据底座—基础设施—安全防护—治理标准—规划交付”连续展开,不虚构投资、系统数量或成熟度,最后同步给出蓝图图表、项目卡片、PPT汇报页和Excel项目台账字段。',
|
|
2086
|
+
tag: '建设规划',
|
|
2087
|
+
},
|
|
2088
|
+
]
|
|
2089
|
+
|
|
2090
|
+
function recommendationImage(name, version = '') {
|
|
2091
|
+
const query = new URLSearchParams({ name })
|
|
2092
|
+
if (version) query.set('v', version)
|
|
2093
|
+
return `/api/waterbuddy/recommendations/image?${query}`
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
/** 兜底卡片(接口不可用时用,同样是真实任务,不是占位文案)。 */
|
|
2097
|
+
const RECOMMENDATION_SEEDS = [
|
|
2098
|
+
{ title: '拆解智慧水务标杆案例', subtitle: '对比建设要点、成效证据和适用条件', prompt: '请拆解一个典型智慧水务案例,按“问题—方案—实施—成效—依赖条件—可迁移做法—风险”输出,并区分已核实事实与待验证信息。', tag: '案例研究', image: recommendationImage('smart-water-case-expert') },
|
|
2099
|
+
{ title: '制定管网漏损治理路径', subtitle: '从水量平衡、分区计量到持续复盘', prompt: '请制定水务企业管网漏损治理路径,先建立水量平衡,再给分区计量、压力管理、主动检漏、表务治理和效果验证的分阶段计划。', tag: '漏损治理', image: recommendationImage('network-asset-leakage') },
|
|
2100
|
+
{ title: '规划智慧水厂改造方案', subtitle: '从现状盘点到分阶段实施与验收', prompt: '请规划智慧水厂改造方案,先列现状与数据缺口,再输出目标架构、一期闭环、实施阶段、投资假设、风险和验收指标。', tag: '水厂建设', image: recommendationImage('smart-water-assistant') },
|
|
2101
|
+
{ title: '编制防汛排涝联动预案', subtitle: '明确预警、调度、队伍和信息报送', prompt: '请编制城市防汛排涝联动预案,输出风险分级、启动条件、设施与队伍调度、交通和信息报送分工,以及升级、解除和复盘条件。', tag: '防汛调度', image: recommendationImage('flood-control-pumping') },
|
|
2102
|
+
]
|
|
2103
|
+
|
|
2104
|
+
let recommendationItems = null
|
|
2105
|
+
|
|
2106
|
+
function recommendationPickFour(list, cursor) {
|
|
2107
|
+
const size = Math.min(4, list.length)
|
|
2108
|
+
return Array.from({ length: size }, (_, index) => list[(cursor + index) % list.length])
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
/** 由名册快照生成卡片:专家团在前,其次是各专家 —— 任务文本按分区区分,不再所有卡片一个句式。 */
|
|
2112
|
+
function recommendationFromRoster(roster) {
|
|
2113
|
+
const items = []
|
|
2114
|
+
for (const team of roster.teams || []) {
|
|
2115
|
+
if (team.slug === REPORT_TEAM_SLUG) {
|
|
2116
|
+
for (const template of REPORT_RECOMMENDATION_TEMPLATES) {
|
|
2117
|
+
items.push({
|
|
2118
|
+
title: template.title,
|
|
2119
|
+
subtitle: template.subtitle,
|
|
2120
|
+
task: template.task,
|
|
2121
|
+
prompt: '@' + team.name + ' ' + template.task,
|
|
2122
|
+
tag: template.tag,
|
|
2123
|
+
expert: team,
|
|
2124
|
+
avatar: team.avatar || '',
|
|
2125
|
+
emoji: team.emoji || '📊',
|
|
2126
|
+
image: recommendationImage(template.image, roster.version),
|
|
2127
|
+
})
|
|
2128
|
+
}
|
|
2129
|
+
continue
|
|
2130
|
+
}
|
|
2131
|
+
const members = (team.members || []).map(member => member.name || member.slug).filter(Boolean)
|
|
2132
|
+
const focus = (team.description || '').trim()
|
|
2133
|
+
items.push({
|
|
2134
|
+
title: '协同评估一个智慧水务项目',
|
|
2135
|
+
subtitle: members.length > 0 ? `${members.length} 位专家协同:${members.slice(0, 3).join('、')}` : (focus || '专家团'),
|
|
2136
|
+
task: '请协同评估一个智慧水务项目,分别输出需求、方案、投资、实施风险和待核实信息,最后汇总推荐路径与下一步行动。',
|
|
2137
|
+
prompt: `@${team.name} 请协同评估一个智慧水务项目,分别输出需求、方案、投资、实施风险和待核实信息,最后汇总推荐路径与下一步行动。`,
|
|
2138
|
+
tag: '专家团',
|
|
2139
|
+
expert: team,
|
|
2140
|
+
avatar: team.avatar || '',
|
|
2141
|
+
emoji: team.emoji || '团',
|
|
2142
|
+
image: recommendationImage(RECOMMENDATION_TEAM_IMAGES.has(team.slug) ? team.slug : 'smart-water-assistant', roster.version),
|
|
2143
|
+
})
|
|
2144
|
+
}
|
|
2145
|
+
for (const expert of roster.experts || []) {
|
|
2146
|
+
// 名册的 experts 里**已经包含专家团**(expertType/kind = team),而上面 teams 那一轮
|
|
2147
|
+
// 已经推过它们 —— 不过滤就会一份数据两张卡,"换一批"时重复项反复出现(实测 bug)。
|
|
2148
|
+
if (expert.expertType === 'team' || expert.kind === 'team') continue
|
|
2149
|
+
if (expert.enabled === false) continue
|
|
2150
|
+
const template = RECOMMENDATION_TEMPLATES[expert.slug] || RECOMMENDATION_TASKS[expert.division] || RECOMMENDATION_TASKS.general
|
|
2151
|
+
items.push({
|
|
2152
|
+
title: template.title,
|
|
2153
|
+
subtitle: `${expert.name} · ${template.subtitle}`,
|
|
2154
|
+
task: template.task,
|
|
2155
|
+
prompt: `@${expert.name} ${template.task}`,
|
|
2156
|
+
tag: template.tag || expert.divisionName || '专家',
|
|
2157
|
+
expert,
|
|
2158
|
+
avatar: expert.avatar || '',
|
|
2159
|
+
image: recommendationImage(Object.hasOwn(RECOMMENDATION_TEMPLATES, expert.slug) ? expert.slug : (RECOMMENDATION_DIVISION_IMAGES[expert.division] || 'meeting-minutes'), roster.version),
|
|
2160
|
+
emoji: expert.emoji || (expert.name || '').slice(0, 1),
|
|
2161
|
+
})
|
|
2162
|
+
}
|
|
2163
|
+
return items
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
async function recommendationLoad() {
|
|
2167
|
+
if (recommendationItems) return recommendationItems
|
|
2168
|
+
try {
|
|
2169
|
+
const response = await fetch('/api/waterbuddy/experts', { headers: { accept: 'application/json' } })
|
|
2170
|
+
const roster = await response.json()
|
|
2171
|
+
const items = recommendationFromRoster(roster)
|
|
2172
|
+
if (items.length > 0) {
|
|
2173
|
+
recommendationItems = items
|
|
2174
|
+
return items
|
|
2175
|
+
}
|
|
2176
|
+
} catch (error) {
|
|
2177
|
+
console.warn('[WaterBuddy] recommendations: roster unavailable, using seeds', error)
|
|
2178
|
+
}
|
|
2179
|
+
recommendationItems = RECOMMENDATION_SEEDS
|
|
2180
|
+
return recommendationItems
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
/**
|
|
2184
|
+
* 新会话页的推荐任务区。
|
|
2185
|
+
*
|
|
2186
|
+
* 位置(方案 B):官方空态页的输入框**下方没有槽位** —— conversation.composer.dock 要求
|
|
2187
|
+
* sessionId 且 variant='composer'(hero 态都不成立),conversation.hero.agentPreset 已被官方
|
|
2188
|
+
* ui-agent-preset 占用。所以本组件挂在 hero 品牌槽位上(保证只在空态挂载),但内容用 React
|
|
2189
|
+
* portal 渲染到 composer 栈**之后**的容器里 —— 原地不留空壳,标题行不会被挤,官方重渲染时由
|
|
2190
|
+
* MutationObserver 重新挂回。
|
|
2191
|
+
*/
|
|
2192
|
+
function RecommendationDock() {
|
|
2193
|
+
const { useEffect, useState } = require('react')
|
|
2194
|
+
// 插件的客户端模块表不一定提供 react-dom;取不到就整体不渲染(绝不让插件加载失败)
|
|
2195
|
+
let createPortal = null
|
|
2196
|
+
try {
|
|
2197
|
+
const reactDom = require('react-dom')
|
|
2198
|
+
createPortal = reactDom && reactDom.createPortal
|
|
2199
|
+
} catch (error) {
|
|
2200
|
+
console.warn('[WaterBuddy] recommendations: react-dom unavailable', error)
|
|
2201
|
+
}
|
|
2202
|
+
const [items, setItems] = useState(null)
|
|
2203
|
+
const [cursor, setCursor] = useState(0)
|
|
2204
|
+
const [target, setTarget] = useState(null)
|
|
2205
|
+
const [isOwner] = useState(() => {
|
|
2206
|
+
if (typeof window === 'undefined') return true
|
|
2207
|
+
if (window.__WB_RECOMMENDATIONS_OWNER__) return false
|
|
2208
|
+
window.__WB_RECOMMENDATIONS_OWNER__ = true
|
|
2209
|
+
return true
|
|
2210
|
+
})
|
|
2211
|
+
|
|
2212
|
+
useEffect(() => () => {
|
|
2213
|
+
if (isOwner && typeof window !== 'undefined' && window.__WB_RECOMMENDATIONS_OWNER__) {
|
|
2214
|
+
delete window.__WB_RECOMMENDATIONS_OWNER__
|
|
2215
|
+
}
|
|
2216
|
+
}, [isOwner])
|
|
2217
|
+
|
|
2218
|
+
useEffect(() => {
|
|
2219
|
+
if (!isOwner) return undefined
|
|
2220
|
+
let cancelled = false
|
|
2221
|
+
recommendationLoad().then(list => { if (!cancelled) setItems(list) }).catch(() => {})
|
|
2222
|
+
return () => { cancelled = true }
|
|
2223
|
+
}, [isOwner])
|
|
2224
|
+
|
|
2225
|
+
useEffect(() => {
|
|
2226
|
+
if (!isOwner) return undefined
|
|
2227
|
+
if (typeof document === 'undefined' || typeof MutationObserver !== 'function') return undefined
|
|
2228
|
+
const ATTR = 'data-waterbuddy-recommendations-host'
|
|
2229
|
+
const ensure = () => {
|
|
2230
|
+
const stack = document.querySelector('[class*="composerHero"]') || document.querySelector('[class*="composerStack"]')
|
|
2231
|
+
const parent = stack && stack.parentElement
|
|
2232
|
+
if (!parent) return
|
|
2233
|
+
let host = document.querySelector('[' + ATTR + ']')
|
|
2234
|
+
if (!host) {
|
|
2235
|
+
host = document.createElement('div')
|
|
2236
|
+
host.setAttribute(ATTR, '1')
|
|
2237
|
+
}
|
|
2238
|
+
document.querySelectorAll('[' + ATTR + ']').forEach(candidate => {
|
|
2239
|
+
if (candidate !== host) candidate.remove()
|
|
2240
|
+
})
|
|
2241
|
+
if (host.parentElement !== parent || host.previousElementSibling !== stack) stack.after(host)
|
|
2242
|
+
setTarget(current => (current === host ? current : host))
|
|
2243
|
+
}
|
|
2244
|
+
ensure()
|
|
2245
|
+
const observer = new MutationObserver(ensure)
|
|
2246
|
+
observer.observe(document.body, { childList: true, subtree: true })
|
|
2247
|
+
return () => observer.disconnect()
|
|
2248
|
+
}, [isOwner])
|
|
2249
|
+
|
|
2250
|
+
if (!isOwner || !createPortal || !target || !items || items.length === 0) return null
|
|
2251
|
+
const visible = recommendationPickFour(items, cursor)
|
|
2252
|
+
|
|
2253
|
+
const pick = item => {
|
|
2254
|
+
// 先走宿主引用:插真 chip(与手动 @ 选择专家团完全同形),插不进去才退回纯文本。
|
|
2255
|
+
if (typeof window !== 'undefined' && typeof window.__WB_SUMMON_TASK__ === 'function' && item.expert) {
|
|
2256
|
+
try {
|
|
2257
|
+
// 插 chip + 补正文都要成功才算完成(只插 chip 等于丢正文)
|
|
2258
|
+
if (window.__WB_SUMMON_TASK__(item.expert, item.task || '') === true) return
|
|
2259
|
+
} catch (error) {
|
|
2260
|
+
console.warn('[WaterBuddy] recommendations: summon with task failed', error)
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
const actions = typeof window !== 'undefined' ? window.__WB_INPUT_ACTIONS__ : null
|
|
2264
|
+
if (actions && typeof actions.setDraft === 'function') actions.setDraft(item.prompt)
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
// 注意:jsx/jsxs 的第三个参数是 key,不是 children —— children 必须放进 props.children,
|
|
2268
|
+
// 否则真实运行时什么都不渲染(卡片会只剩空框,本插件早期已踩过同一个坑)。
|
|
2269
|
+
// 卡片(对齐参考图):**图就是卡片** —— 圆角图片直接铺满,标题放在图下面、无白底卡片框。
|
|
2270
|
+
// key 必须**全局唯一**:同一分区/同一模板的卡片标题会重复(3 个专家团标题完全相同),
|
|
2271
|
+
// 用 title+tag 当 key 会让 React 复用节点、旧卡片不被移除 —— 表现就是"换一批越点越多"(实测)。
|
|
2272
|
+
const card = (item, index) => jsxs('button', {
|
|
2273
|
+
type: 'button',
|
|
2274
|
+
key: `${index}:${(item.expert && (item.expert.slug || item.expert.name)) || item.title}`,
|
|
2275
|
+
onClick: () => pick(item),
|
|
2276
|
+
'aria-label': `${item.title}:${item.subtitle || ''}`,
|
|
2277
|
+
title: item.subtitle || item.title,
|
|
2278
|
+
style: {
|
|
2279
|
+
flex: '1 1 0', minWidth: 0, textAlign: 'left', cursor: 'pointer', padding: 0,
|
|
2280
|
+
border: 0, background: 'transparent', display: 'flex', flexDirection: 'column', gap: 8,
|
|
2281
|
+
},
|
|
2282
|
+
children: [
|
|
2283
|
+
jsx('span', {
|
|
2284
|
+
style: {
|
|
2285
|
+
height: 136, flex: 'none', borderRadius: 14, overflow: 'hidden',
|
|
2286
|
+
background: 'linear-gradient(135deg,#eaf3ff,#f7fbff)',
|
|
2287
|
+
backgroundImage: item.image ? `url("${item.image}")` : undefined,
|
|
2288
|
+
backgroundSize: 'cover', backgroundPosition: 'center',
|
|
2289
|
+
display: item.image ? 'block' : 'flex', alignItems: 'center', justifyContent: 'center',
|
|
2290
|
+
boxShadow: '0 1px 2px rgba(15,23,42,.05)',
|
|
2291
|
+
},
|
|
2292
|
+
children: item.image
|
|
2293
|
+
? null
|
|
2294
|
+
: (item.avatar
|
|
2295
|
+
? jsx('img', { src: item.avatar, alt: item.title, style: { width: 52, height: 52, borderRadius: 14, objectFit: 'cover', background: '#fff' } })
|
|
2296
|
+
: jsx('span', { style: { width: 52, height: 52, borderRadius: 14, background: '#fff', color: '#2563eb', fontSize: 22, display: 'flex', alignItems: 'center', justifyContent: 'center' }, children: item.emoji || '水' })),
|
|
2297
|
+
}),
|
|
2298
|
+
jsx('span', {
|
|
2299
|
+
style: { fontSize: 13, fontWeight: 650, color: '#162033', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
|
|
2300
|
+
children: item.title,
|
|
2301
|
+
}),
|
|
2302
|
+
jsx('span', {
|
|
2303
|
+
style: { minHeight: 32, fontSize: 11.5, lineHeight: 1.45, color: '#718096', display: '-webkit-box', WebkitBoxOrient: 'vertical', WebkitLineClamp: 2, overflow: 'hidden' },
|
|
2304
|
+
children: item.subtitle || item.tag || '',
|
|
2305
|
+
}),
|
|
2306
|
+
],
|
|
2307
|
+
})
|
|
2308
|
+
|
|
2309
|
+
return createPortal(
|
|
2310
|
+
// 宽度对齐输入框:复用宿主自己的两个 CSS 变量(会话根节点上定义,逐级继承)。
|
|
2311
|
+
// 外层宽度 = 卡片上限 + 两侧 clearance,内边距 = clearance —— 与官方 hero 容器的算法完全一致,
|
|
2312
|
+
// 这样推荐区的内容左右边缘与输入卡片严格对齐,且随宿主响应式规则一起变。
|
|
2313
|
+
jsxs('div', {
|
|
2314
|
+
style: {
|
|
2315
|
+
width: 'min(calc(var(--dsh-composer-card-max-width, 952px) + 2 * var(--dsh-composer-side-clearance, 16px)), 100%)',
|
|
2316
|
+
margin: '18px auto 0',
|
|
2317
|
+
padding: '0 var(--dsh-composer-side-clearance, 16px)',
|
|
2318
|
+
boxSizing: 'border-box',
|
|
2319
|
+
},
|
|
2320
|
+
children: [
|
|
2321
|
+
jsxs('div', { style: { display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }, children: [
|
|
2322
|
+
jsx('span', { style: { fontSize: 13, fontWeight: 600, color: '#162033' }, children: '不知道从哪开始?试试这些水务工作模板' }),
|
|
2323
|
+
jsx('button', {
|
|
2324
|
+
type: 'button',
|
|
2325
|
+
onClick: () => setCursor(current => current + 4),
|
|
2326
|
+
style: { marginLeft: 'auto', border: '1px solid #e3ebf7', background: '#fff', color: '#2563eb', fontSize: 12.5, cursor: 'pointer', padding: '4px 12px', borderRadius: 999 },
|
|
2327
|
+
children: '换一批',
|
|
2328
|
+
}),
|
|
2329
|
+
] }),
|
|
2330
|
+
jsx('div', { style: { display: 'flex', gap: 10 }, children: visible.map((item, index) => card(item, index)) }),
|
|
2331
|
+
] }),
|
|
2332
|
+
target,
|
|
2333
|
+
)
|
|
2334
|
+
}
|
|
1560
2335
|
const inject = ['slots', 'inputTriggers']
|
|
1561
2336
|
function apply(ctx) {
|
|
1562
2337
|
ctx.effect(() => {
|
|
@@ -1592,7 +2367,6 @@ window.__ModuleLoader__.load({
|
|
|
1592
2367
|
// b. 必须允许 wrap —— 宿主默认 nowrap,放不下时会**裁掉**最后一个子元素(「设置」消失);
|
|
1593
2368
|
// c. 我们自己那部分必须能收缩且足够窄 —— 行是"放得下才成立",宽度只由我们的卡片贡献。
|
|
1594
2369
|
// 三个条件都满足时:放得下 → 一行;放不下 → 优雅换成两行,且绝不丢元素。
|
|
1595
|
-
'[class*="footArea"]{display:flex!important;flex-direction:row!important;flex-wrap:wrap!important;align-items:center!important;gap:8px}',
|
|
1596
2370
|
|
|
1597
2371
|
// **关键的一半,前几轮都漏了**:宿主 `SidebarRoot.module.css` 里
|
|
1598
2372
|
//
|
|
@@ -1604,8 +2378,6 @@ window.__ModuleLoader__.load({
|
|
|
1604
2378
|
// 账号卡那一侧**吃掉剩余空间**(用户区占满整行),设置区保持自身宽度 →
|
|
1605
2379
|
// 被自然推到最右。两者都是局部类名,不碰祖先;设置区 flex 为 0 0 auto,
|
|
1606
2380
|
// 所以卡片再胖也不会挤到它。
|
|
1607
|
-
'[class*="footerActions"]{width:auto!important;flex:1 1 auto!important;min-width:0!important}',
|
|
1608
|
-
'[class*="settingsArea"]{width:auto!important;flex:0 0 auto!important;min-width:0!important}',
|
|
1609
2381
|
|
|
1610
2382
|
// 以下是最初的写法记录(保留以免重复踩坑):
|
|
1611
2383
|
//
|
|
@@ -1636,7 +2408,7 @@ window.__ModuleLoader__.load({
|
|
|
1636
2408
|
yield ctx.slots.register({ name: 'sidebar.brand.mark', priority: -10 }, WaterBuddyMark)
|
|
1637
2409
|
yield ctx.slots.register({ name: 'sidebar.brand.name', priority: -10 }, WaterBuddyName)
|
|
1638
2410
|
}))
|
|
1639
|
-
ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({ name: 'sidebar.footer.action', id: 'waterbuddy-user', order: 90, label: '登录
|
|
2411
|
+
ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({ name: 'sidebar.footer.action', id: 'waterbuddy-user', order: 90, label: '登录 水务之窗' }, UserAction))
|
|
1640
2412
|
ctx.slots.inject('settings.onboarding', () => ctx.slots.register({ name: 'settings.onboarding', id: 'welcome-notice', priority: -1000, order: -100 }, SkipWelcomeNotice))
|
|
1641
2413
|
// 专家名册页:在设置左侧列出一项「专家」。`label` 用纯字符串——本插件的界面文案
|
|
1642
2414
|
// 都是硬编码中文,没有走宿主词典(见 AGENTS.md 的产物面边界)。
|
|
@@ -1690,11 +2462,13 @@ window.__ModuleLoader__.load({
|
|
|
1690
2462
|
}
|
|
1691
2463
|
}
|
|
1692
2464
|
swap(document.body)
|
|
1693
|
-
const observer = new MutationObserver(() => swap(document.body))
|
|
2465
|
+
const observer = new MutationObserver(() => { swap(document.body); patchWaterbuddyMenu(); patchWaterbuddyTrigger() })
|
|
1694
2466
|
observer.observe(document.body, { childList: true, subtree: true })
|
|
1695
2467
|
return () => observer.disconnect()
|
|
1696
2468
|
}, 'waterbuddy: hero headline')
|
|
1697
2469
|
|
|
2470
|
+
ctx.effect(() => startWaterbuddyMenu(), 'waterbuddy: settings account entry')
|
|
2471
|
+
|
|
1698
2472
|
// 会话输入框左侧的专家入口。`inject` 是宿主支持的 slot owner 注入:框架按会话调用它,
|
|
1699
2473
|
// 把返回值并进组件 props——我们的实现靠它拿到当前会话的输入机,才能写原生引用 chip。
|
|
1700
2474
|
ctx.slots.inject('conversation.input.left', () => ctx.slots.register({
|
|
@@ -1714,6 +2488,22 @@ window.__ModuleLoader__.load({
|
|
|
1714
2488
|
if (expertDraftBodyIsEmpty(target)) appendExpertTaskExample(target, expert)
|
|
1715
2489
|
return true
|
|
1716
2490
|
},
|
|
2491
|
+
/** 推荐卡片用:插 chip 之后补上卡片那句任务正文(插引用 + 补正文,两步都不省)。 */
|
|
2492
|
+
summonExpertWithTask: (reference, expert, task) => {
|
|
2493
|
+
const target = resolveExpertInputTarget(ctx, sessionId)
|
|
2494
|
+
if (target === undefined) return false
|
|
2495
|
+
// 顺序很关键:setDraft 会清掉草稿里已有的引用节点(实测:只剩 @名字 纯文本,
|
|
2496
|
+
// 药丸消失),所以没有 insertText 的内核必须**先写正文、再插引用**,
|
|
2497
|
+
// 这样 chip 与正文两半都在(chip 落在正文之后)。
|
|
2498
|
+
if (typeof task === 'string' && task.trim() !== '' && typeof target.setDraft === 'function'
|
|
2499
|
+
&& !appendExpertTaskText(target, task)) {
|
|
2500
|
+
target.setDraft(task)
|
|
2501
|
+
}
|
|
2502
|
+
const inserted = insertExpertReference(target, reference)
|
|
2503
|
+
// 光标移到末尾:宿主没有光标接口,只能延后一拍在编辑器上折叠选区
|
|
2504
|
+
if (inserted) setTimeout(moveExpertCaretToEnd, 0)
|
|
2505
|
+
return inserted
|
|
2506
|
+
},
|
|
1717
2507
|
}),
|
|
1718
2508
|
}, ExpertInputEntry))
|
|
1719
2509
|
|