@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.
Files changed (2) hide show
  1. package/lib/host/index.js +68 -4
  2. 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 = { ...(profile.body.result || {}), provider: '水务之窗' }
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/logout', handler: (_req, res) => { currentUser = undefined; for (const session of sessions.values()) delete session.token; json(res, 200, { ok: true }) } },
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@waterplus-ai/waterbuddy",
3
- "version": "0.1.9",
3
+ "version": "0.1.12",
4
4
  "description": "WaterBuddy branding and Web profile bundle for DeepSeek Harness",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",