@waterplus-ai/waterbuddy 0.1.9 → 0.1.10
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 +66 -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,51 @@ 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 normalizeAvatar(value) {
|
|
42
|
+
if (typeof value !== 'string' || !value) return undefined
|
|
43
|
+
if (value.startsWith('data:')) return value
|
|
44
|
+
try {
|
|
45
|
+
const url = new URL(value, AI_URL)
|
|
46
|
+
if (url.protocol !== 'https:' || url.host !== new URL(AI_URL).host) return undefined
|
|
47
|
+
return `/api/waterbuddy/avatar?url=${encodeURIComponent(url.href)}`
|
|
48
|
+
} catch { return undefined }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function normalizeUser(profile) {
|
|
52
|
+
const user = { ...(profile || {}), provider: '水务之窗' }
|
|
53
|
+
user.avatar = normalizeAvatar(user.avatar || user.avatarUrl || user.headImg || user.headImgUrl)
|
|
54
|
+
return user
|
|
55
|
+
}
|
|
56
|
+
|
|
11
57
|
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
58
|
|
|
13
59
|
function json(res, status, body) {
|
|
@@ -71,15 +117,17 @@ async function pollLogin(session) {
|
|
|
71
117
|
const token = validated.body.result
|
|
72
118
|
const profile = await requestJson(`${AI_URL}/swj-waterplusai-api/client/user/info`, { headers: { Authorization: `Bearer ${token}` } })
|
|
73
119
|
session.token = token
|
|
74
|
-
session.user =
|
|
120
|
+
session.user = normalizeUser(profile.body.result)
|
|
75
121
|
currentUser = session.user
|
|
122
|
+
persistedAuth = { token, user: currentUser }
|
|
123
|
+
savePersistedAuth(persistedAuth)
|
|
76
124
|
return { status: 'SUCCESS', user: currentUser }
|
|
77
125
|
}
|
|
78
126
|
|
|
79
127
|
async function proxyMcp(req, res) {
|
|
80
128
|
const headers = {}
|
|
81
129
|
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
|
|
130
|
+
const token = [...sessions.values()].find(session => session.token)?.token || persistedAuth?.token
|
|
83
131
|
if (token) headers.authorization = `Bearer ${token}`
|
|
84
132
|
const chunks = []
|
|
85
133
|
for await (const chunk of req) chunks.push(chunk)
|
|
@@ -91,9 +139,22 @@ async function proxyMcp(req, res) {
|
|
|
91
139
|
res.end()
|
|
92
140
|
}
|
|
93
141
|
|
|
142
|
+
async function proxyAvatar(req, res) {
|
|
143
|
+
const value = new URL(req.url, 'http://127.0.0.1').searchParams.get('url')
|
|
144
|
+
let target
|
|
145
|
+
try { target = new URL(value) } catch { return json(res, 400, { error: 'invalid-avatar-url' }) }
|
|
146
|
+
if (target.protocol !== 'https:' || target.host !== new URL(AI_URL).host) return json(res, 403, { error: 'avatar-host-not-allowed' })
|
|
147
|
+
const response = await fetch(target)
|
|
148
|
+
if (!response.ok || !response.body) return json(res, response.status || 502, { error: 'avatar-fetch-failed' })
|
|
149
|
+
res.writeHead(response.status, { 'content-type': response.headers.get('content-type') || 'image/jpeg', 'cache-control': 'private, max-age=3600' })
|
|
150
|
+
for await (const chunk of response.body) res.write(chunk)
|
|
151
|
+
res.end()
|
|
152
|
+
}
|
|
153
|
+
|
|
94
154
|
export const inject = ['webServer', 'systemPrompt']
|
|
95
155
|
|
|
96
156
|
export function apply(ctx) {
|
|
157
|
+
readPersistedAuth()
|
|
97
158
|
ctx.effect(() => {
|
|
98
159
|
const routes = [
|
|
99
160
|
{ 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 +162,13 @@ export function apply(ctx) {
|
|
|
101
162
|
{ 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
163
|
{ 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
164
|
{ kind: 'exact', path: '/api/waterbuddy/user', handler: (_req, res) => json(res, 200, { user: currentUser || null }) },
|
|
104
|
-
{ kind: 'exact', path: '/api/waterbuddy/
|
|
165
|
+
{ kind: 'exact', path: '/api/waterbuddy/avatar', handler: async (req, res) => { try { await proxyAvatar(req, res) } catch (error) { json(res, 502, { error: error.message }) } } },
|
|
166
|
+
{ 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
167
|
{ kind: 'prefix', path: '/api/waterbuddy/mcp', handler: async (req, res) => { try { await proxyMcp(req, res) } catch (error) { json(res, 502, { error: error.message }) } } },
|
|
106
168
|
]
|
|
107
169
|
const disposers = routes.map(route => ctx.webServer.register(route))
|
|
108
170
|
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
171
|
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 }
|
|
172
|
+
return () => { for (const dispose of disposers) dispose(); disposeTitle(); disposePersona(); sessions.clear(); currentUser = undefined; persistedAuth = undefined }
|
|
111
173
|
}, 'waterbuddy: identity and auth')
|
|
112
174
|
}
|