@waterplus-ai/waterbuddy 0.1.0 → 0.1.9

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 CHANGED
@@ -1,60 +1,25 @@
1
- # WaterPlus AI WaterBuddy Bundle
1
+ # WaterBuddy
2
2
 
3
- This package turns an official DeepSeek Harness Web profile into the WaterPlus AI internal deployment used by company staff.
3
+ WaterBuddy is a WaterPlus integration bundle for DeepSeek Harness. It adds a branded Web
4
+ profile for internal water-industry work while keeping the official Harness base and Web bundles
5
+ unchanged.
4
6
 
5
- It provides:
7
+ ## Features
6
8
 
7
- - The approved WaterPlus AI sidebar mark and wordmark with `Powered by DSH` attribution.
8
- - The WaterPlus AI internal assistant persona “小加”, focused on water-industry work.
9
- - The public Xiaojia MCP service at `https://ai.shuiwujia.com/mcp`, exposed as model tools.
10
- - `WaterBuddy` browser title, favicon, and PWA manifest through the Web server extension points.
11
- - A single installable `dsh.bundle` layer; the official base and Web bundles remain untouched.
9
+ - WaterBuddy branding, favicon, and page title.
10
+ - Water-industry assistant persona for internal users.
11
+ - In-page WaterBuddy SSO QR-code login.
12
+ - User profile and avatar display after login.
13
+ - Xiaojia MCP access with the current SSO token attached by a local proxy.
12
14
 
13
15
  ## Install
14
16
 
15
- From a clean DSH installation, run:
17
+ Install the bundle into the DSH Web profile, then restart DSH:
16
18
 
17
19
  ```sh
18
- dsh plugin --profile web add /absolute/path/to/waterbuddy-0.1.0.tgz
20
+ dsh plugin --profile web add @waterplus-ai/waterbuddy
19
21
  dsh --profile web
20
22
  ```
21
23
 
22
- For a published private registry package, replace the path with `@waterplus-ai/waterbuddy@0.1.0` and configure the registry credentials in the profile.
23
-
24
- The package is a profile layer, not a desktop application replacement. It changes the Web profile after installation; a Tauri or other desktop shell still needs its own native branding resources.
25
-
26
- ## Extension points
27
-
28
- The bundle disables only `ui-brand-official`, inserts its own browser row into the same sidebar slots, and adds one `@deepseek-ai/dsh-mcp-client` row for Xiaojia. It does not modify `dsh-base`, `dsh-web-app`, the agent loop, or the official package source. The Host half uses `WebServer.register` for the brand icon and PWA manifest, `WebServer.tapIndex` for the initial document title, and `ctx.systemPrompt.section()` for the WaterPlus role description. The browser half retains the WaterBuddy suffix when session titles change.
29
-
30
- The MCP endpoint is currently public, so the bundle sends no credentials. When company SSO is ready, add an access-token header through the profile's MCP configuration rather than baking a token into this package; the SSO client can refresh the token and restart or update the MCP connection as required.
31
-
32
- ## Directory layout
33
-
34
- ```text
35
- waterplus-dsh-enterprise/
36
- ├── lib/
37
- │ ├── host/ # Host entrypoint: Web server and system prompt
38
- │ ├── client/ # Browser ModuleLoader bundle and UI slots
39
- │ └── shared/ # Shared role text and future cross-half contracts
40
- ├── patches/ # Cordis bundle patch declarations
41
- ├── tests/ # Host/client regression tests
42
- ├── releases/ # Generated .tgz files; one file per published version
43
- ├── docs/ # Design notes and WaterBuddy feature specifications
44
- ├── package.json # Package metadata and build commands
45
- ├── pnpm-lock.yaml # Reproducible package-manager metadata
46
- └── README.md
47
- ```
48
-
49
- Build and test from this directory:
50
-
51
- ```sh
52
- pnpm test
53
- pnpm run pack:check
54
- pnpm run pack:release
55
- ```
56
-
57
- New WaterBuddy capabilities should be added under the matching `lib/host/`, `lib/client/`, or
58
- `lib/shared/` area, with corresponding tests under `tests/` and design decisions recorded under
59
- `docs/`. Keep generated archives in `releases/` and do not install an old archive after a newer
60
- version is created.
24
+ This package is a Web profile layer. It does not replace the desktop shell or add native desktop
25
+ branding resources.
@@ -7,20 +7,30 @@ window.__ModuleLoader__.load({
7
7
  const mark = 'data:image/png;base64,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'
8
8
  const wordmark = 'data:image/png;base64,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'
9
9
 
10
- function WaterPlusMark({ size, className }) {
10
+ function WaterBuddyMark({ size, className }) {
11
11
  return jsx('img', {
12
12
  src: mark,
13
13
  width: size,
14
14
  height: size,
15
15
  className,
16
- alt: 'WaterPlus AI',
16
+ alt: 'WaterBuddy',
17
17
  })
18
18
  }
19
19
 
20
- function WaterPlusName() {
20
+ function WaterBuddyName() {
21
21
  return jsxs(Fragment, {
22
22
  children: [
23
- jsx('img', { src: wordmark, width: 99, height: 24, alt: 'WaterPlus AI' }),
23
+ jsx('strong', {
24
+ style: {
25
+ color: '#1677d2',
26
+ fontSize: 18,
27
+ fontWeight: 700,
28
+ letterSpacing: '-.02em',
29
+ lineHeight: '24px',
30
+ whiteSpace: 'nowrap',
31
+ },
32
+ children: 'WaterBuddy',
33
+ }),
24
34
  jsx('small', {
25
35
  style: {
26
36
  color: 'var(--dsw-text-tertiary, #7b8494)',
@@ -35,6 +45,104 @@ window.__ModuleLoader__.load({
35
45
  })
36
46
  }
37
47
 
48
+ const SSO = {
49
+ title: '水务之窗',
50
+ clientId: '2CC7F925-07AE-4AB9-9F17-75B7378D5890',
51
+ image: 'https://ai.shuiwujia.com/swj-waterplusai-api/upload/0bf0f985-7b4b-4aff-86a4-cfe7bf57b939.png',
52
+ }
53
+
54
+ function userName(user) {
55
+ return user?.name || user?.nickname || user?.username || 'WaterBuddy 用户'
56
+ }
57
+
58
+ function UserAvatar({ user, size = 34 }) {
59
+ const name = userName(user)
60
+ const style = { width: size, height: size, borderRadius: '50%', objectFit: 'cover', flex: '0 0 auto' }
61
+ return user?.avatar
62
+ ? jsx('img', { src: user.avatar, alt: `${name}头像`, style })
63
+ : jsx('span', { style: { ...style, display: 'grid', placeItems: 'center', background: 'linear-gradient(135deg, #1d9bf0, #2563eb)', color: '#fff', fontSize: Math.max(14, size * 0.42), fontWeight: 700 }, children: user ? name.slice(0, 1) : '🔐' })
64
+ }
65
+
66
+ function UserAction({ wide }) {
67
+ const { useEffect, useState } = require('react')
68
+ const [open, setOpen] = useState(false)
69
+ const [loading, setLoading] = useState(false)
70
+ const [qr, setQr] = useState(null)
71
+ const [status, setStatus] = useState('')
72
+ const [user, setUser] = useState(null)
73
+ const [error, setError] = useState('')
74
+
75
+ useEffect(() => {
76
+ let cancelled = false
77
+ fetch('/api/waterbuddy/user').then(response => response.json()).then(body => {
78
+ if (!cancelled) setUser(body.user || null)
79
+ }).catch(() => {})
80
+ return () => { cancelled = true }
81
+ }, [])
82
+
83
+ useEffect(() => {
84
+ if (!open || !qr?.sessionId) return undefined
85
+ let stopped = false
86
+ let timer
87
+ const poll = async () => {
88
+ try {
89
+ const response = await fetch(`/api/waterbuddy/auth/status?sessionId=${encodeURIComponent(qr.sessionId)}`)
90
+ const body = await response.json()
91
+ if (stopped) return
92
+ setStatus(body.status || '')
93
+ if (body.status === 'SUCCESS') {
94
+ setUser(body.user || null)
95
+ setLoading(false)
96
+ return
97
+ }
98
+ if (body.status === 'EXPIRED') {
99
+ setError('二维码已过期,请重新获取')
100
+ setLoading(false)
101
+ return
102
+ }
103
+ } catch (_) {
104
+ if (!stopped) setError('查询扫码状态失败,请重试')
105
+ }
106
+ if (!stopped) timer = setTimeout(poll, 2000)
107
+ }
108
+ poll()
109
+ return () => { stopped = true; if (timer) clearTimeout(timer) }
110
+ }, [open, qr])
111
+
112
+ const startLogin = async () => {
113
+ setLoading(true); setError(''); setStatus('二维码加载中'); setQr(null)
114
+ try {
115
+ const response = await fetch('/api/waterbuddy/auth/qr', { method: 'POST' })
116
+ const body = await response.json()
117
+ if (!response.ok) throw new Error(body.error || '二维码获取失败')
118
+ setQr(body); setStatus('等待扫码')
119
+ } catch (failure) {
120
+ setError(failure.message); setLoading(false)
121
+ }
122
+ }
123
+
124
+ const openPanel = () => { setOpen(true); if (!user) startLogin() }
125
+ const logout = async () => { await fetch('/api/waterbuddy/logout', { method: 'POST' }); setUser(null); setOpen(false) }
126
+ const statusText = { WAITING: '等待扫码', DEALING: '已扫码,请在手机上确认', SUCCESS: '登录成功', EXPIRED: '二维码已过期' }[status] || status
127
+ const name = userName(user)
128
+ const account = user?.loginId || (user?.id ? `${user.id.slice(0, 8)}…` : '水务之窗账号')
129
+ const isAdmin = user?.isAdmin === 1 || user?.isAdmin === true
130
+ const buttonStyle = { borderRadius: 10, padding: '9px 18px', fontSize: 13, fontWeight: 600, cursor: 'pointer' }
131
+
132
+ return jsxs(Fragment, {
133
+ children: [
134
+ jsx('button', {
135
+ type: 'button',
136
+ onClick: openPanel,
137
+ title: user ? name : '登录 WaterBuddy',
138
+ style: { display: 'flex', alignItems: 'center', gap: 8, width: wide ? 'auto' : 40, maxWidth: wide ? 150 : 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' },
139
+ 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: { color: '#8a94a6', fontSize: 10.5, lineHeight: '14px' }, children: user ? '水务之窗' : '扫码登录' })] }) : null],
140
+ }),
141
+ 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 ? jsxs(Fragment, { children: [jsx('div', { style: { position: 'relative', padding: '28px 28px 44px', textAlign: 'center', color: '#fff', background: 'linear-gradient(135deg, #0f6fd6 0%, #1d9bf0 58%, #38bdf8 100%)' }, children: [jsx('button', { type: 'button', onClick: () => setOpen(false), 'aria-label': '关闭', style: { position: 'absolute', 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: '×' }), jsx('div', { style: { fontSize: 12, opacity: .82, letterSpacing: 1 }, children: 'WATERBUDDY' }), jsx('div', { style: { marginTop: 8, fontSize: 15, fontWeight: 600 }, children: 'WaterBuddy' }), 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 }) })] }), jsxs('div', { style: { padding: '48px 28px 26px', textAlign: 'center' }, children: [jsx('h3', { style: { margin: 0, color: '#162033', fontSize: 22 }, children: name }), jsx('p', { style: { margin: '7px 0 18px', color: '#7b8799', fontSize: 13 }, children: user.organization || user.department || '水务之窗' }), jsxs('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10, textAlign: 'left' }, children: [jsxs('div', { style: { padding: '12px 14px', borderRadius: 12, background: '#f5f8fc' }, children: [jsx('small', { style: { color: '#8a94a6', fontSize: 11 }, children: '账号' }), jsx('div', { style: { marginTop: 4, color: '#334155', fontSize: 13, fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, children: account })] }), jsxs('div', { style: { padding: '12px 14px', borderRadius: 12, background: '#f5f8fc' }, children: [jsx('small', { style: { color: '#8a94a6', fontSize: 11 }, children: '身份' }), jsx('div', { style: { marginTop: 4, color: '#334155', fontSize: 13, fontWeight: 600 }, children: isAdmin ? '管理员' : '内部成员' })] })] }), jsx('button', { type: 'button', onClick: logout, style: { ...buttonStyle, width: '100%', marginTop: 18, border: '1px solid #d9e1ec', background: '#fff', color: '#475569' }, children: '退出登录' })] })] }) : jsxs('div', { style: { padding: 28, textAlign: 'center' }, children: [jsx('button', { type: 'button', onClick: () => setOpen(false), 'aria-label': '关闭', style: { display: 'block', marginLeft: 'auto', width: 30, height: 30, border: 0, borderRadius: '50%', background: '#f1f5f9', color: '#64748b', fontSize: 20, lineHeight: 1, cursor: 'pointer' }, children: '×' }), jsx('img', { src: SSO.image, alt: SSO.title, style: { display: 'block', width: 52, height: 52, objectFit: 'contain', margin: '0 auto 12px' } }), jsx('h3', { style: { margin: '14px 0 6px', color: '#162033', fontSize: 21 }, children: 'WaterBuddy' }), jsx('p', { style: { margin: 0, color: '#7b8799', fontSize: 13 }, children: `使用${SSO.title}扫码,安全登录 WaterBuddy` }), qr?.qrCode ? jsx('img', { src: qr.qrCode, alt: '水务之窗登录二维码', style: { display: 'block', width: 220, height: 220, boxSizing: 'border-box', objectFit: 'contain', margin: '20px auto 12px', padding: 10, border: '1px solid #e5ebf3', borderRadius: 16, background: '#fff' } }) : jsx('div', { style: { width: 220, height: 220, boxSizing: 'border-box', margin: '20px auto 12px', display: 'grid', placeItems: 'center', border: '1px solid #e5ebf3', borderRadius: 16, background: '#f8fafc', color: '#7b8799', fontSize: 13 }, children: loading ? '二维码加载中…' : '二维码暂不可用' }), jsx('div', { style: { minHeight: 24, color: error ? '#dc2626' : status === 'SUCCESS' ? '#16a34a' : '#64748b', fontSize: 13 }, children: error || statusText || '请扫码登录' }), jsxs('div', { style: { display: 'flex', gap: 10, justifyContent: 'center', marginTop: 18 }, children: [jsx('button', { type: 'button', onClick: startLogin, disabled: loading, style: { ...buttonStyle, border: 0, background: '#1677d2', color: '#fff', opacity: loading ? .65 : 1 }, children: '刷新二维码' }), jsx('button', { type: 'button', onClick: () => setOpen(false), style: { ...buttonStyle, border: '1px solid #d9e1ec', background: '#fff', color: '#475569' }, children: '关闭' })] })] }) }) }) : null,
142
+ ],
143
+ })
144
+ }
145
+
38
146
  const inject = ['slots']
39
147
  function apply(ctx) {
40
148
  ctx.effect(() => {
@@ -54,11 +162,25 @@ window.__ModuleLoader__.load({
54
162
  observer.observe(document.head, { childList: true, subtree: true, characterData: true })
55
163
  return () => { observer.disconnect() }
56
164
  }, 'waterbuddy: document title')
165
+ ctx.effect(() => {
166
+ if (typeof document.createElement !== 'function' || typeof document.head?.append !== 'function') return undefined
167
+ const style = document.createElement('style')
168
+ style.dataset.waterbuddy = 'sidebar-footer-layout'
169
+ style.textContent = [
170
+ '.dshp-root.dshp-wide .dshp-footArea{flex-direction:row;align-items:center;gap:8px}',
171
+ '.dshp-root.dshp-wide .dshp-settingsArea{order:1;flex:0 0 auto;width:auto;min-width:0}',
172
+ '.dshp-root.dshp-wide .dshp-footerActions{order:2;flex:1 1 auto;width:auto;min-width:0;justify-content:flex-end}',
173
+ '.dshp-root.dshp-wide .dshp-footerActions>div{display:flex;justify-content:flex-end;min-width:0}',
174
+ ].join('')
175
+ document.head.append(style)
176
+ return () => { style.remove() }
177
+ }, 'waterbuddy: sidebar footer layout')
57
178
  ctx.slots.inject('sidebar.brand.mark', () =>
58
179
  ctx.slots.inject('sidebar.brand.name', function* () {
59
- yield ctx.slots.register({ name: 'sidebar.brand.mark' }, WaterPlusMark)
60
- yield ctx.slots.register({ name: 'sidebar.brand.name' }, WaterPlusName)
180
+ yield ctx.slots.register({ name: 'sidebar.brand.mark', priority: -10 }, WaterBuddyMark)
181
+ yield ctx.slots.register({ name: 'sidebar.brand.name', priority: -10 }, WaterBuddyName)
61
182
  }))
183
+ ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({ name: 'sidebar.footer.action', id: 'waterbuddy-user', order: 90, label: '登录 WaterBuddy' }, UserAction))
62
184
  }
63
185
 
64
186
  exports.inject = inject
package/lib/host/index.js CHANGED
@@ -1,67 +1,112 @@
1
- const WATERPLUS_ICON = Buffer.from('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', 'base64')
2
-
1
+ import https from 'node:https'
3
2
  import { WATERPLUS_PERSONA } from '../shared/persona.js'
4
3
 
5
- const MANIFEST = JSON.stringify({
6
- id: '/',
7
- name: 'WaterBuddy',
8
- short_name: 'WaterBuddy',
9
- start_url: '/',
10
- scope: '/',
11
- display: 'fullscreen',
12
- icons: [{ src: '/waterplus-mark.png', sizes: '84x84', type: 'image/png', purpose: 'any' }],
13
- })
4
+ const WATERPLUS_ICON = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', 'base64')
5
+ const CLIENT_ID = '2CC7F925-07AE-4AB9-9F17-75B7378D5890'
6
+ const PLATFORM_URL = 'https://web.shuiwujia.com'
7
+ const AI_URL = 'https://ai.shuiwujia.com'
8
+ const sessions = new Map()
9
+ let currentUser
10
+
11
+ const MANIFEST = JSON.stringify({ id: '/', name: 'WaterBuddy', short_name: 'WaterBuddy', start_url: '/', scope: '/', display: 'fullscreen', icons: [{ src: '/waterplus-mark.png', sizes: '84x84', type: 'image/png', purpose: 'any' }] })
12
+
13
+ function json(res, status, body) {
14
+ res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' })
15
+ res.end(JSON.stringify(body))
16
+ }
17
+
18
+ function queryUrl(path, params) {
19
+ const url = new URL(path, PLATFORM_URL)
20
+ for (const [key, value] of Object.entries(params)) url.searchParams.set(key, value)
21
+ return url.href
22
+ }
23
+
24
+ function cookieHeader(response) {
25
+ if (typeof response.headers.getSetCookie !== 'function') return ''
26
+ return response.headers.getSetCookie().map(value => value.split(';', 1)[0]).join('; ')
27
+ }
28
+
29
+ async function requestJson(url, options = {}) {
30
+ const parsed = new URL(url)
31
+ const headers = { plaform: 'PC', ...(options.headers || {}) }
32
+ const body = options.body
33
+ return await new Promise((resolve, reject) => {
34
+ const request = https.request(parsed, { method: options.method || 'GET', headers, insecureHTTPParser: true }, response => {
35
+ const chunks = []
36
+ response.on('data', chunk => chunks.push(chunk))
37
+ response.on('end', () => {
38
+ const text = Buffer.concat(chunks).toString('utf8')
39
+ let payload
40
+ try { payload = text ? JSON.parse(text) : {} } catch { payload = { message: text } }
41
+ if ((response.statusCode || 500) >= 400 || (payload.code !== undefined && payload.code !== 200 && !(payload.code >= 20000 && payload.code < 30000))) return reject(new Error(payload.message || `upstream request failed: ${response.statusCode}`))
42
+ resolve({ body: payload, cookie: (response.headers['set-cookie'] || []).map(value => value.split(';', 1)[0]).join('; ') })
43
+ })
44
+ })
45
+ request.on('error', reject)
46
+ if (body) request.write(body)
47
+ request.end()
48
+ })
49
+ }
50
+
51
+ async function createLogin() {
52
+ const login = await requestJson(queryUrl('/swj-platform-api/waterplusUser/login', { orgId: CLIENT_ID }))
53
+ const scene = login.body.result?.scene
54
+ if (!scene) throw new Error('登录场景创建失败')
55
+ const qr = await requestJson(`${PLATFORM_URL}/swj-platform-api/platformWxappUser/getQRCode`, { method: 'POST', headers: { 'content-type': 'application/json', ...(login.cookie ? { cookie: login.cookie } : {}) }, body: JSON.stringify({ page: 'main/home/index', scene, check_path: true, env_version: 'release' }) })
56
+ const sessionId = crypto.randomUUID()
57
+ sessions.set(sessionId, { scene, cookie: [login.cookie, qr.cookie].filter(Boolean).join('; '), createdAt: Date.now() })
58
+ const encoded = String(qr.body.result)
59
+ return { sessionId, qrCode: `data:${encoded.startsWith('/9j/') ? 'image/jpeg' : 'image/png'};base64,${encoded}`, status: 'WAITING' }
60
+ }
61
+
62
+ async function pollLogin(session) {
63
+ if (Date.now() - session.createdAt > 5 * 60_000) return { status: 'EXPIRED' }
64
+ const statusResponse = await requestJson(queryUrl('/swj-platform-api/platformWxappUser/findRegisterStatus', { scene: session.scene }), { headers: session.cookie ? { cookie: session.cookie } : {} })
65
+ const status = statusResponse.body.result || 'WAITING'
66
+ session.cookie = [session.cookie, statusResponse.cookie].filter(Boolean).join('; ')
67
+ if (status !== 'SUCCESS') return { status }
68
+ if (session.user) return { status: 'SUCCESS', user: session.user }
69
+ const auth = await requestJson(queryUrl('/swj-platform-api/platformUser/authAI', { clientId: CLIENT_ID }), { headers: session.cookie ? { cookie: session.cookie } : {} })
70
+ const validated = await requestJson(`${AI_URL}/swj-waterplusai-api/client/user/sso/validate?authInfo=${encodeURIComponent(String(auth.body.result).replace(/ /g, '+'))}&clientId=${CLIENT_ID}`)
71
+ const token = validated.body.result
72
+ const profile = await requestJson(`${AI_URL}/swj-waterplusai-api/client/user/info`, { headers: { Authorization: `Bearer ${token}` } })
73
+ session.token = token
74
+ session.user = { ...(profile.body.result || {}), provider: '水务之窗' }
75
+ currentUser = session.user
76
+ return { status: 'SUCCESS', user: currentUser }
77
+ }
78
+
79
+ async function proxyMcp(req, res) {
80
+ const headers = {}
81
+ for (const name of ['accept', 'content-type', 'mcp-session-id', 'last-event-id']) if (req.headers[name]) headers[name] = req.headers[name]
82
+ const token = [...sessions.values()].find(session => session.token)?.token
83
+ if (token) headers.authorization = `Bearer ${token}`
84
+ const chunks = []
85
+ for await (const chunk of req) chunks.push(chunk)
86
+ const upstream = await fetch(`${AI_URL}/mcp`, { method: req.method, headers, body: chunks.length ? Buffer.concat(chunks) : undefined })
87
+ const responseHeaders = {}
88
+ for (const name of ['content-type', 'mcp-session-id', 'cache-control']) if (upstream.headers.get(name)) responseHeaders[name] = upstream.headers.get(name)
89
+ res.writeHead(upstream.status, responseHeaders)
90
+ if (upstream.body) for await (const chunk of upstream.body) res.write(chunk)
91
+ res.end()
92
+ }
14
93
 
15
- /** Required host service: the HTTP server that serves the Web profile. */
16
94
  export const inject = ['webServer', 'systemPrompt']
17
95
 
18
- /**
19
- * Install the WaterBuddy Web identity without replacing dsh-web-app. The
20
- * named routes own only the brand icon and manifest; all other assets
21
- * continue through the official static frontend fallback.
22
- * @param ctx - Host context carrying the Web server service.
23
- */
24
96
  export function apply(ctx) {
25
97
  ctx.effect(() => {
26
- const disposeFavicon = ctx.webServer.register({
27
- kind: 'exact',
28
- path: '/waterplus-mark.png',
29
- handler: (_req, res) => {
30
- res.writeHead(200, {
31
- 'content-type': 'image/png',
32
- 'cache-control': 'public, max-age=3600',
33
- })
34
- res.end(WATERPLUS_ICON)
35
- },
36
- })
37
- const disposeManifest = ctx.webServer.register({
38
- kind: 'exact',
39
- path: '/manifest.webmanifest',
40
- handler: (_req, res) => {
41
- res.writeHead(200, {
42
- 'content-type': 'application/manifest+json; charset=utf-8',
43
- 'cache-control': 'public, max-age=3600',
44
- })
45
- res.end(MANIFEST)
46
- },
47
- })
48
- const disposeTitle = ctx.webServer.tapIndex((html) => {
49
- const titled = html.replace(/<title>[^<]*<\/title>/i, '<title>WaterBuddy</title>')
50
- return titled.replace(
51
- /<link[^>]+rel=["']icon["'][^>]*>/i,
52
- '<link rel="icon" type="image/png" href="/waterplus-mark.png" />',
53
- )
54
- })
55
- const disposePersona = ctx.systemPrompt.section({
56
- name: 'waterplus:persona',
57
- order: 100,
58
- text: WATERPLUS_PERSONA,
59
- })
60
- return () => {
61
- disposePersona()
62
- disposeTitle()
63
- disposeManifest()
64
- disposeFavicon()
65
- }
66
- }, 'waterbuddy: Web identity')
98
+ const routes = [
99
+ { kind: 'exact', path: '/waterplus-mark.png', handler: (_req, res) => { res.writeHead(200, { 'content-type': 'image/png', 'cache-control': 'public, max-age=3600' }); res.end(WATERPLUS_ICON) } },
100
+ { kind: 'exact', path: '/manifest.webmanifest', handler: (_req, res) => { res.writeHead(200, { 'content-type': 'application/manifest+json; charset=utf-8', 'cache-control': 'public, max-age=3600' }); res.end(MANIFEST) } },
101
+ { kind: 'exact', path: '/api/waterbuddy/auth/qr', handler: async (req, res) => { if (req.method !== 'POST') return json(res, 405, { error: 'method-not-allowed' }); try { json(res, 200, await createLogin()) } catch (error) { json(res, 502, { error: error.message }) } } },
102
+ { kind: 'exact', path: '/api/waterbuddy/auth/status', handler: async (req, res) => { if (req.method !== 'GET') return json(res, 405, { error: 'method-not-allowed' }); const sessionId = new URL(req.url, 'http://127.0.0.1').searchParams.get('sessionId'); const session = sessionId && sessions.get(sessionId); if (!session) return json(res, 404, { error: 'session-not-found' }); try { json(res, 200, await pollLogin(session)) } catch (error) { json(res, 502, { error: error.message }) } } },
103
+ { kind: 'exact', path: '/api/waterbuddy/user', handler: (_req, res) => json(res, 200, { user: currentUser || null }) },
104
+ { kind: 'exact', path: '/api/waterbuddy/logout', handler: (_req, res) => { currentUser = undefined; for (const session of sessions.values()) delete session.token; json(res, 200, { ok: true }) } },
105
+ { kind: 'prefix', path: '/api/waterbuddy/mcp', handler: async (req, res) => { try { await proxyMcp(req, res) } catch (error) { json(res, 502, { error: error.message }) } } },
106
+ ]
107
+ const disposers = routes.map(route => ctx.webServer.register(route))
108
+ const disposeTitle = ctx.webServer.tapIndex(html => html.replace(/<title>[^<]*<\/title>/i, '<title>WaterBuddy</title>').replace(/<link[^>]+rel=["']icon["'][^>]*>/i, '<link rel="icon" type="image/png" href="/waterplus-mark.png" />'))
109
+ const disposePersona = ctx.systemPrompt.section({ name: 'waterplus:persona', order: 100, text: WATERPLUS_PERSONA })
110
+ return () => { for (const dispose of disposers) dispose(); disposeTitle(); disposePersona(); sessions.clear(); currentUser = undefined }
111
+ }, 'waterbuddy: identity and auth')
67
112
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@waterplus-ai/waterbuddy",
3
- "version": "0.1.0",
4
- "description": "WaterPlus AI WaterBuddy branding and Web profile bundle for DeepSeek Harness",
3
+ "version": "0.1.9",
4
+ "description": "WaterBuddy branding and Web profile bundle for DeepSeek Harness",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
7
7
  "main": "lib/host/index.js",
@@ -33,6 +33,7 @@
33
33
  "scripts": {
34
34
  "test": "node --test tests/*.test.mjs",
35
35
  "pack:check": "pnpm pack --dry-run",
36
- "pack:release": "pnpm pack --pack-destination releases"
36
+ "pack:release": "pnpm pack --pack-destination releases",
37
+ "release:cdn": "node scripts/build-cdn-release.mjs"
37
38
  }
38
39
  }
@@ -12,4 +12,4 @@
12
12
  config:
13
13
  serverName: Xiaojia
14
14
  transport: streamable-http
15
- url: https://ai.shuiwujia.com/mcp
15
+ url: http://127.0.0.1:3080/api/waterbuddy/mcp