@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 +14 -49
- package/lib/client/index.js +128 -6
- package/lib/host/index.js +104 -59
- package/package.json +4 -3
- package/patches/cordis.patch.yml +1 -1
package/README.md
CHANGED
|
@@ -1,60 +1,25 @@
|
|
|
1
|
-
#
|
|
1
|
+
# WaterBuddy
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
7
|
+
## Features
|
|
6
8
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
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
|
-
|
|
17
|
+
Install the bundle into the DSH Web profile, then restart DSH:
|
|
16
18
|
|
|
17
19
|
```sh
|
|
18
|
-
dsh plugin --profile web add /
|
|
20
|
+
dsh plugin --profile web add @waterplus-ai/waterbuddy
|
|
19
21
|
dsh --profile web
|
|
20
22
|
```
|
|
21
23
|
|
|
22
|
-
|
|
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.
|
package/lib/client/index.js
CHANGED
|
@@ -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
|
|
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: '
|
|
16
|
+
alt: 'WaterBuddy',
|
|
17
17
|
})
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
function
|
|
20
|
+
function WaterBuddyName() {
|
|
21
21
|
return jsxs(Fragment, {
|
|
22
22
|
children: [
|
|
23
|
-
jsx('
|
|
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' },
|
|
60
|
-
yield ctx.slots.register({ name: 'sidebar.brand.name' },
|
|
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
|
-
|
|
2
|
-
|
|
1
|
+
import https from 'node:https'
|
|
3
2
|
import { WATERPLUS_PERSONA } from '../shared/persona.js'
|
|
4
3
|
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
27
|
-
kind: 'exact',
|
|
28
|
-
path: '/
|
|
29
|
-
handler: (
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
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.
|
|
4
|
-
"description": "
|
|
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
|
}
|
package/patches/cordis.patch.yml
CHANGED