dsh-agent-office 4.5.0 → 4.6.1

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 CHANGED
@@ -26,14 +26,16 @@
26
26
  - 🖼️ **自定义猫咪图片**:DSH 设置 →「Office」上传图片即可替换所有 Agent 头像(本地 localStorage 保存,不上传服务器);
27
27
  - 🎚️ **面板不透明度**:DSH 设置 →「Office」可调 HUD 卡片透明度(50%–95%),面板实时跟随;
28
28
  - 😅 **状态表情**:正常工作猫猫大汗淋漓;工作超过 5 分钟进入红温,头顶乱线;待机改成两条小缝;
29
- - 🏢 **双办公室 + MCP 链路**:主楼右侧新增 Annex(3 个固定工位、讲台、小型健身房、备考隔间),虚线 MCP 链路带流动光点,两端蓝色 ✕ 表示暂未接通;
29
+ - 🏢 **双办公室 + MCP 链路**:主楼右侧 Annex 是教室布局:后方讲台,三席面朝讲台——通用工位、备考隔间、题库工位(浮动 OCR / 数据库 / 试卷全息图标);虚线 MCP 链路带流动光点,两端蓝色 ✕ 表示暂未接通;
30
30
 
31
31
  - 🏢 **3D 实时办公室**:Three.js 自绘的错层办公室——每层有工位、显示器、会议桌、白板、服务器机柜、玻璃护栏和吊灯;每个会话是一个简洁的立方体 Agent(按状态做呼吸/浮动脉冲,工作/等待/卡住/待机各有反馈);
32
32
  - 🖱️ **楼层聚焦**:左键点任意楼层(或左侧楼层列表)→ 只显示并看清这一层;右键 /「全局视角」按钮 / `Esc` 回到整栋楼的全局视角;
33
33
  - 📡 **真实数据**:状态、今日执行次数、本周/今日 **Token 用量** 全部来自本机会话日志,数字不编造;
34
34
  - 🖱️ **可交互**:拖动旋转、滚轮缩放、点击立方体或名字看详情、详情卡 ×/Esc 关闭;
35
35
  - 🧩 **原生插件**:bundle 插件,装完即用(宿主路由 + 浏览器半侧悬浮按钮),无外部依赖;
36
- - 🔒 **只读本机**:只读 `$DSH_HOME/sessions`,不联网、不上传,没有频道名/密钥要配。
36
+ - 🔒 **默认只读本机**:只读 `$DSH_HOME/sessions`;只有你在设置里主动开启「本地天气」后,才会把坐标发给 Open-Meteo 获取天气,位置本身只保存在本地。
37
+ - 🏗️ **搭建入场**:每次打开面板先播放 0→1 的建造过程,楼层、工位、猫猫和 MCP 链路逐步落位。
38
+ - 🌦️ **本地天气**:设置里允许定位后,按所在城市显示晴/阴云/雨/雪/风;不开定位则不启用天气效果。
37
39
 
38
40
  ## 安装
39
41
 
package/lib/client.js CHANGED
@@ -8,6 +8,7 @@
8
8
  */
9
9
  const CUSTOM_FACE_KEY = 'agent-office-custom-face'
10
10
  const GLASS_KEY = 'agent-office-ui-opacity'
11
+ const WEATHER_KEY = 'agent-office-weather-location'
11
12
  const CAT_BUTTON = 'data:image/png;base64,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'
12
13
 
13
14
  window.__ModuleLoader__.load({
@@ -68,6 +69,13 @@ window.__ModuleLoader__.load({
68
69
  const [message, setMessage] = useState('')
69
70
  const [busy, setBusy] = useState(false)
70
71
  const [glass, setGlass] = useState(readGlass())
72
+ const [weatherOn, setWeatherOn] = useState(() => {
73
+ try {
74
+ const value = JSON.parse(localStorage.getItem(WEATHER_KEY) ?? 'null')
75
+ return Number.isFinite(value?.lat) && Number.isFinite(value?.lon)
76
+ } catch { return false }
77
+ })
78
+ const [weatherBusy, setWeatherBusy] = useState(false)
71
79
 
72
80
  const choose = async event => {
73
81
  const file = event.target.files?.[0]
@@ -108,6 +116,43 @@ window.__ModuleLoader__.load({
108
116
  setGlass(80)
109
117
  }
110
118
 
119
+ const askWeather = () => {
120
+ if (!navigator.geolocation) {
121
+ setMessage('当前环境不支持定位,无法获取本地天气')
122
+ return
123
+ }
124
+ setWeatherBusy(true)
125
+ setMessage('请在系统弹窗里允许定位…')
126
+ navigator.geolocation.getCurrentPosition(
127
+ position => {
128
+ try {
129
+ localStorage.setItem(WEATHER_KEY, JSON.stringify({
130
+ lat: position.coords.latitude,
131
+ lon: position.coords.longitude,
132
+ at: Date.now(),
133
+ }))
134
+ setWeatherOn(true)
135
+ setMessage('已开启本地天气,面板会按所在城市显示天气效果')
136
+ } catch {
137
+ setMessage('位置保存失败')
138
+ }
139
+ setWeatherBusy(false)
140
+ },
141
+ error => {
142
+ setWeatherBusy(false)
143
+ setWeatherOn(false)
144
+ setMessage('没有拿到定位权限:' + String(error?.message ?? '未授权'))
145
+ },
146
+ { enableHighAccuracy: false, timeout: 12000, maximumAge: 10 * 60 * 1000 },
147
+ )
148
+ }
149
+
150
+ const closeWeather = () => {
151
+ try { localStorage.removeItem(WEATHER_KEY) } catch {}
152
+ setWeatherOn(false)
153
+ setMessage('已关闭本地天气')
154
+ }
155
+
111
156
  const titleStyle = { fontSize: 15, fontWeight: 600, margin: '0 0 6px' }
112
157
  const textStyle = { fontSize: 13, lineHeight: 1.6, margin: '0 0 10px', opacity: 0.72 }
113
158
  const buttonStyle = { height: 32, padding: '0 14px', borderRadius: 8, border: '1px solid rgba(127,127,127,.35)', background: 'transparent', color: 'inherit', cursor: 'pointer', fontSize: 13 }
@@ -151,6 +196,23 @@ window.__ModuleLoader__.load({
151
196
  ] }),
152
197
  ],
153
198
  }),
199
+ jsx('div', {
200
+ style: { marginTop: 14, padding: '14px 16px', border: '1px solid rgba(127,127,127,.28)', borderRadius: 12, background: 'rgba(127,127,127,.06)' },
201
+ children: [
202
+ jsx('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12 }, children: [
203
+ jsx('span', { style: { fontSize: 13, fontWeight: 600 }, children: '本地天气' }),
204
+ jsx('button', {
205
+ type: 'button',
206
+ className: 'ns-setting-btn',
207
+ style: { ...buttonStyle, height: 30, padding: '0 12px', flex: 'none' },
208
+ onClick: weatherOn ? closeWeather : askWeather,
209
+ disabled: weatherBusy,
210
+ children: weatherBusy ? '定位中…' : weatherOn ? '关闭天气' : '允许定位',
211
+ }),
212
+ ] }),
213
+ jsx('p', { style: { ...textStyle, margin: '8px 0 0', fontSize: 12 }, children: weatherOn ? '已允许定位。面板会获取所在城市天气,显示晴天、云、雨、雪或风。位置只保存在本地。' : '允许定位后,面板会获取所在城市天气,显示晴天、云、雨、雪或风;位置只保存在本地,不上传服务器。' }),
214
+ ],
215
+ }),
154
216
  message ? jsx('p', { style: { ...textStyle, marginTop: 10, marginBottom: 0 }, children: message }) : null,
155
217
  ],
156
218
  })
@@ -194,7 +256,7 @@ window.__ModuleLoader__.load({
194
256
  <section class="dsh-ns-overlay" hidden>
195
257
  <button type="button" class="dsh-ns-open">在新标签打开</button>
196
258
  <button type="button" class="dsh-ns-close" aria-label="关闭">×</button>
197
- <iframe title="Office 3D 面板"></iframe>
259
+ <iframe title="Office 3D 面板" allow="geolocation"></iframe>
198
260
  </section>
199
261
  `
200
262
  document.body.append(host)
@@ -223,7 +285,7 @@ window.__ModuleLoader__.load({
223
285
  if (event.key === 'Escape') close()
224
286
  }
225
287
  const open = () => {
226
- if (frame.getAttribute('src') !== '/agent-office/') frame.setAttribute('src', '/agent-office/')
288
+ frame.setAttribute('src', '/agent-office/?open=' + Date.now())
227
289
  overlay.hidden = false
228
290
  document.addEventListener('keydown', closedByEscape, true)
229
291
  }
package/lib/index.js CHANGED
@@ -8,7 +8,7 @@
8
8
  * 不依赖 ntfy、不依赖外部进程;面板与 DSH 同源,直接轮询本机 API。
9
9
  */
10
10
  import { readFile } from 'node:fs/promises'
11
- import { createNightShiftStore, fetchDeepSeekBalance } from './state.js'
11
+ import { createNightShiftStore, fetchDeepSeekBalance, fetchWeather } from './state.js'
12
12
 
13
13
  export const name = 'agent-office'
14
14
  export const inject = ['webServer']
@@ -71,6 +71,24 @@ export function apply(ctx, config) {
71
71
  },
72
72
  }), 'agent-office: api')
73
73
 
74
+ ctx.effect(() => ctx.webServer.register({
75
+ kind: 'exact',
76
+ path: '/agent-office/api/weather',
77
+ handler: async (req, res) => {
78
+ if (!hostTrusted(req)) return json(res, { ok: false, error: '不被信任的 Host' }, 403)
79
+ try {
80
+ const url = new URL(req.url ?? '/', 'http://localhost')
81
+ return json(res, await fetchWeather({
82
+ lat: url.searchParams.get('lat'),
83
+ lon: url.searchParams.get('lon'),
84
+ }))
85
+ } catch (error) {
86
+ ctx.logger.error(error instanceof Error ? error : new Error(String(error)))
87
+ return json(res, { ok: false, error: '天气暂时不可用' }, 500)
88
+ }
89
+ },
90
+ }), 'agent-office: weather')
91
+
74
92
  ctx.effect(() => ctx.webServer.register({
75
93
  kind: 'exact',
76
94
  path: '/agent-office/api/balance',