@waterplus-ai/waterbuddy 0.1.9 → 0.1.12
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/lib/host/index.js +68 -4
- package/package.json +1 -1
package/lib/host/index.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import https from 'node:https'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
2
5
|
import { WATERPLUS_PERSONA } from '../shared/persona.js'
|
|
3
6
|
|
|
4
7
|
const WATERPLUS_ICON = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', 'base64')
|
|
@@ -6,8 +9,55 @@ const CLIENT_ID = '2CC7F925-07AE-4AB9-9F17-75B7378D5890'
|
|
|
6
9
|
const PLATFORM_URL = 'https://web.shuiwujia.com'
|
|
7
10
|
const AI_URL = 'https://ai.shuiwujia.com'
|
|
8
11
|
const sessions = new Map()
|
|
12
|
+
const authFile = path.join(process.env.DSH_HOME || path.join(os.homedir(), '.dsh'), 'waterbuddy-auth.json')
|
|
13
|
+
let persistedAuth
|
|
9
14
|
let currentUser
|
|
10
15
|
|
|
16
|
+
function readPersistedAuth() {
|
|
17
|
+
try {
|
|
18
|
+
const value = JSON.parse(fs.readFileSync(authFile, 'utf8'))
|
|
19
|
+
if (value?.token && value?.user) {
|
|
20
|
+
persistedAuth = value
|
|
21
|
+
currentUser = value.user
|
|
22
|
+
}
|
|
23
|
+
} catch {}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function savePersistedAuth(auth) {
|
|
27
|
+
try {
|
|
28
|
+
fs.mkdirSync(path.dirname(authFile), { recursive: true })
|
|
29
|
+
fs.writeFileSync(authFile, JSON.stringify(auth), { mode: 0o600 })
|
|
30
|
+
try { fs.chmodSync(authFile, 0o600) } catch {}
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.warn('[WaterBuddy] failed to persist login:', error.message)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function clearPersistedAuth() {
|
|
37
|
+
persistedAuth = undefined
|
|
38
|
+
try { fs.rmSync(authFile, { force: true }) } catch {}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isSupportedAvatarUrl(url) {
|
|
42
|
+
return (url.protocol === 'http:' || url.protocol === 'https:') && !url.username && !url.password
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function normalizeAvatar(value) {
|
|
46
|
+
if (typeof value !== 'string' || !value) return undefined
|
|
47
|
+
if (value.startsWith('data:')) return value
|
|
48
|
+
try {
|
|
49
|
+
const url = new URL(value, AI_URL)
|
|
50
|
+
if (!isSupportedAvatarUrl(url)) return undefined
|
|
51
|
+
return url.href
|
|
52
|
+
} catch { return undefined }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function normalizeUser(profile) {
|
|
56
|
+
const user = { ...(profile || {}), provider: '水务之窗' }
|
|
57
|
+
user.avatar = normalizeAvatar(user.avatar || user.avatarUrl || user.headImg || user.headImgUrl)
|
|
58
|
+
return user
|
|
59
|
+
}
|
|
60
|
+
|
|
11
61
|
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
62
|
|
|
13
63
|
function json(res, status, body) {
|
|
@@ -71,15 +121,17 @@ async function pollLogin(session) {
|
|
|
71
121
|
const token = validated.body.result
|
|
72
122
|
const profile = await requestJson(`${AI_URL}/swj-waterplusai-api/client/user/info`, { headers: { Authorization: `Bearer ${token}` } })
|
|
73
123
|
session.token = token
|
|
74
|
-
session.user =
|
|
124
|
+
session.user = normalizeUser(profile.body.result)
|
|
75
125
|
currentUser = session.user
|
|
126
|
+
persistedAuth = { token, user: currentUser }
|
|
127
|
+
savePersistedAuth(persistedAuth)
|
|
76
128
|
return { status: 'SUCCESS', user: currentUser }
|
|
77
129
|
}
|
|
78
130
|
|
|
79
131
|
async function proxyMcp(req, res) {
|
|
80
132
|
const headers = {}
|
|
81
133
|
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
|
|
134
|
+
const token = [...sessions.values()].find(session => session.token)?.token || persistedAuth?.token
|
|
83
135
|
if (token) headers.authorization = `Bearer ${token}`
|
|
84
136
|
const chunks = []
|
|
85
137
|
for await (const chunk of req) chunks.push(chunk)
|
|
@@ -91,9 +143,20 @@ async function proxyMcp(req, res) {
|
|
|
91
143
|
res.end()
|
|
92
144
|
}
|
|
93
145
|
|
|
146
|
+
async function proxyAvatar(req, res) {
|
|
147
|
+
const value = new URL(req.url, 'http://127.0.0.1').searchParams.get('url')
|
|
148
|
+
let target
|
|
149
|
+
try { target = new URL(value) } catch { return json(res, 400, { error: 'invalid-avatar-url' }) }
|
|
150
|
+
if (!isSupportedAvatarUrl(target)) return json(res, 400, { error: 'avatar-url-not-supported' })
|
|
151
|
+
res.writeHead(302, { location: target.href, 'cache-control': 'private, max-age=3600' })
|
|
152
|
+
res.end()
|
|
153
|
+
}
|
|
154
|
+
|
|
94
155
|
export const inject = ['webServer', 'systemPrompt']
|
|
156
|
+
export { normalizeAvatar }
|
|
95
157
|
|
|
96
158
|
export function apply(ctx) {
|
|
159
|
+
readPersistedAuth()
|
|
97
160
|
ctx.effect(() => {
|
|
98
161
|
const routes = [
|
|
99
162
|
{ 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) } },
|
|
@@ -101,12 +164,13 @@ export function apply(ctx) {
|
|
|
101
164
|
{ 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
165
|
{ 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
166
|
{ kind: 'exact', path: '/api/waterbuddy/user', handler: (_req, res) => json(res, 200, { user: currentUser || null }) },
|
|
104
|
-
{ kind: 'exact', path: '/api/waterbuddy/
|
|
167
|
+
{ kind: 'exact', path: '/api/waterbuddy/avatar', handler: async (req, res) => { try { await proxyAvatar(req, res) } catch (error) { json(res, 502, { error: error.message }) } } },
|
|
168
|
+
{ kind: 'exact', path: '/api/waterbuddy/logout', handler: (_req, res) => { currentUser = undefined; for (const session of sessions.values()) delete session.token; clearPersistedAuth(); json(res, 200, { ok: true }) } },
|
|
105
169
|
{ kind: 'prefix', path: '/api/waterbuddy/mcp', handler: async (req, res) => { try { await proxyMcp(req, res) } catch (error) { json(res, 502, { error: error.message }) } } },
|
|
106
170
|
]
|
|
107
171
|
const disposers = routes.map(route => ctx.webServer.register(route))
|
|
108
172
|
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
173
|
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 }
|
|
174
|
+
return () => { for (const dispose of disposers) dispose(); disposeTitle(); disposePersona(); sessions.clear(); currentUser = undefined; persistedAuth = undefined }
|
|
111
175
|
}, 'waterbuddy: identity and auth')
|
|
112
176
|
}
|