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