dsh-agentic-pet 0.3.2 → 0.4.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 +17 -2
- package/README.zh.md +17 -2
- package/carrier/dist-renderer/assets/{index-C61Q_dyJ.js → index-BMH7auTh.js} +1 -1
- package/carrier/dist-renderer/index.html +1 -1
- package/carrier/lib/main.js +48 -26
- package/carrier/lib/preload.js +3 -1
- package/carrier/lib/tts.js +212 -0
- package/client/pet-settings.js +145 -10
- package/lib/index.js +126 -27
- package/lib/{service-Vj-lLpIv.js → service-BjTawwUn.js} +574 -86
- package/lib/tools.js +2 -2
- package/lib/types/index.d.ts +2 -2
- package/lib/types/inner-voice.d.ts +14 -0
- package/lib/types/petdata.d.ts +59 -0
- package/lib/types/registry.d.ts +2 -1
- package/package.json +3 -3
- package/presets/pet-inner-voice.patch.yml +3 -3
- package/presets/pet.patch.yml +3 -3
package/client/pet-settings.js
CHANGED
|
@@ -12,6 +12,7 @@ window.__ModuleLoader__.load({
|
|
|
12
12
|
var useState = React.useState
|
|
13
13
|
var useEffect = React.useEffect
|
|
14
14
|
var useCallback = React.useCallback
|
|
15
|
+
var useRef = React.useRef
|
|
15
16
|
|
|
16
17
|
var READ = '/api/pet-settings/read'
|
|
17
18
|
var UPDATE = '/api/pet-settings/update'
|
|
@@ -60,6 +61,12 @@ window.__ModuleLoader__.load({
|
|
|
60
61
|
var busyState = useState(false)
|
|
61
62
|
var busy = busyState[0]
|
|
62
63
|
var setBusy = busyState[1]
|
|
64
|
+
// 语音朗读的声音选择器:null = 收起;展开后 {loading, error, items, search}。
|
|
65
|
+
var voiceState = useState(null)
|
|
66
|
+
var voice = voiceState[0]
|
|
67
|
+
var setVoice = voiceState[1]
|
|
68
|
+
var previewAudioRef = useRef(null)
|
|
69
|
+
var keyInputRef = useRef(null)
|
|
63
70
|
|
|
64
71
|
var refresh = useCallback(function () {
|
|
65
72
|
fetch(READ, { credentials: 'same-origin' })
|
|
@@ -91,14 +98,81 @@ window.__ModuleLoader__.load({
|
|
|
91
98
|
var settings = (data && data.settings) || {}
|
|
92
99
|
var pro = settings.proactivity || {}
|
|
93
100
|
var iv = settings.innerVoice || {}
|
|
101
|
+
var tts = settings.tts || {}
|
|
94
102
|
var debug = settings.debug || {}
|
|
95
103
|
var affection = (data && data.affection) || {}
|
|
96
104
|
var pets = (data && data.pets) || []
|
|
97
105
|
if (!(pets && pets.length) && data && data.pet) pets = [Object.assign({ id: 'electron' }, data.pet)]
|
|
98
106
|
var moodOf = function (a) { return a >= 80 ? '开心' : a >= 40 ? '满足' : '平静' }
|
|
107
|
+
// 显示名:用户自定义(settings.names)优先,缺省回落内置名;
|
|
108
|
+
// 宠物区改名后好感度区自动跟随(同一状态、同一渲染趟)。
|
|
109
|
+
var defaultName = function (id) { return id === 'electron' ? '鲸鱼娘' : id === 'pure' ? 'Pure 宠物' : id }
|
|
110
|
+
var BOND_TIERS = [[0, "陌生"], [5, "认识"], [15, "熟了"], [40, "亲近"], [90, "离不开"], [180, "只有你"]]
|
|
111
|
+
var bondTier = function (days) {
|
|
112
|
+
var name = "陌生"
|
|
113
|
+
BOND_TIERS.forEach(function (t) { if (days >= t[0]) name = t[1] })
|
|
114
|
+
return name
|
|
115
|
+
}
|
|
116
|
+
var petName = function (id) {
|
|
117
|
+
var custom = settings.names && settings.names[id]
|
|
118
|
+
return custom ? String(custom) : defaultName(id)
|
|
119
|
+
}
|
|
120
|
+
// 提交一次改名(失焦/回车调用):与当前值比较,无变化零开销;
|
|
121
|
+
// 空值删键(回落默认)。一次编辑 = 至多一次 update = 至多一条 [改名]。
|
|
122
|
+
var commitName = function (id, raw) {
|
|
123
|
+
var v = (raw || '').trim()
|
|
124
|
+
var next = Object.assign({}, (settings.names || {}))
|
|
125
|
+
var current = next[id]
|
|
126
|
+
if (v !== '' && v !== defaultName(id)) {
|
|
127
|
+
if (current === v) return
|
|
128
|
+
next[id] = v
|
|
129
|
+
} else {
|
|
130
|
+
if (current === undefined) return
|
|
131
|
+
delete next[id]
|
|
132
|
+
}
|
|
133
|
+
update({ names: next })
|
|
134
|
+
}
|
|
99
135
|
|
|
100
136
|
function update(patch, doneMessage) { act(UPDATE, { patch: patch }, doneMessage) }
|
|
101
137
|
|
|
138
|
+
// ── 语音朗读(BreezeBlue TTS)的取声/试听 ──
|
|
139
|
+
// 列表走宿主代理路由(浏览器直连 api.breeze.blue 会被 CORS 拦下,
|
|
140
|
+
// key 也不该进页面日志);试听直接播 preview_url,不经过本插件。
|
|
141
|
+
var loadVoices = function (search) {
|
|
142
|
+
setVoice({ loading: true, error: '', items: (voice && voice.items) || [], search: search || '' })
|
|
143
|
+
fetch('/api/pet-settings/tts/voices?search=' + encodeURIComponent(search || ''), { credentials: 'same-origin' })
|
|
144
|
+
.then(function (r) { return r.json() })
|
|
145
|
+
.then(function (d) {
|
|
146
|
+
if (d && d.error) setVoice({ loading: false, error: d.error, items: [], search: search || '' })
|
|
147
|
+
else setVoice({ loading: false, error: '', items: (d && d.voices) || [], search: search || '' })
|
|
148
|
+
})
|
|
149
|
+
.catch(function (e) {
|
|
150
|
+
setVoice({ loading: false, error: '请求失败: ' + ((e && e.message) || e), items: [], search: search || '' })
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
var playPreview = function (url) {
|
|
154
|
+
if (!url) return
|
|
155
|
+
try {
|
|
156
|
+
if (previewAudioRef.current) previewAudioRef.current.pause()
|
|
157
|
+
var audio = new Audio(url)
|
|
158
|
+
previewAudioRef.current = audio
|
|
159
|
+
audio.play().catch(function () { /* 试听失败不打断配置 */ })
|
|
160
|
+
} catch (e) { /* ignore */ }
|
|
161
|
+
}
|
|
162
|
+
// 取声音列表前先把 key 输入框里未提交的值落盘(输入框是失焦提交,
|
|
163
|
+
// “输完 key 立刻点选择声音”时 blur 的 POST 会和列表 GET 赛跑——
|
|
164
|
+
// 代理读到旧文件,401。这里等提交完成再查。)
|
|
165
|
+
var flushKeyThen = function (done) {
|
|
166
|
+
var el = keyInputRef.current
|
|
167
|
+
var current = el ? String(el.value || '').trim() : ''
|
|
168
|
+
if (current === '' || current === (tts.apiKey || '')) { done(); return }
|
|
169
|
+
fetch(UPDATE, {
|
|
170
|
+
method: 'POST', credentials: 'same-origin',
|
|
171
|
+
headers: { 'content-type': 'application/json' },
|
|
172
|
+
body: JSON.stringify({ patch: { tts: { apiKey: current } } }),
|
|
173
|
+
}).then(function () { done() }, function () { done() })
|
|
174
|
+
}
|
|
175
|
+
|
|
102
176
|
return h('div', { style: { width: '100%', maxWidth: 720 } },
|
|
103
177
|
h('div', { style: { display: 'flex', alignItems: 'center', gap: 10, margin: '2px 0 10px' } },
|
|
104
178
|
h('span', { style: { fontWeight: 700, fontSize: 15 } }, '桌宠'),
|
|
@@ -111,9 +185,13 @@ window.__ModuleLoader__.load({
|
|
|
111
185
|
h(Section, { title: '宠物' },
|
|
112
186
|
pets.length
|
|
113
187
|
? pets.map(function (p) {
|
|
114
|
-
var name = p.id === 'electron' ? 'TS 宠物' : p.id === 'pure' ? 'Pure 宠物' : String(p.id)
|
|
115
188
|
return h('div', { key: p.id, style: row },
|
|
116
|
-
|
|
189
|
+
// Uncontrolled + commit-on-blur/Enter: typing is LOCAL until the
|
|
190
|
+
// edit completes — a per-keystroke update would fire a [改名]
|
|
191
|
+
// turn per character.清空 + 失焦 = 恢复默认名。
|
|
192
|
+
h('input', { style: Object.assign({}, inputStyle, { width: 120, fontWeight: 700 }), defaultValue: petName(p.id), title: '改名后点击空白处或回车生效;清空恢复默认',
|
|
193
|
+
onBlur: function (e) { commitName(p.id, e.target.value) },
|
|
194
|
+
onKeyDown: function (e) { if (e.key === 'Enter') { commitName(p.id, e.target.value); e.target.blur() } } }),
|
|
117
195
|
p.online
|
|
118
196
|
? h('span', null, '在线',
|
|
119
197
|
h('span', { style: muted }, 'pid ' + String(p.pid ?? '?')),
|
|
@@ -121,7 +199,7 @@ window.__ModuleLoader__.load({
|
|
|
121
199
|
p.animation ? h('span', { style: muted }, String(p.animation)) : null)
|
|
122
200
|
: h('span', { style: muted }, '离线'))
|
|
123
201
|
})
|
|
124
|
-
: h('div', null, '没有在线的宠物', h(Hint, null, '启动 DeepSeek Harness
|
|
202
|
+
: h('div', null, '没有在线的宠物', h(Hint, null, '启动 DeepSeek Harness 后鲸鱼娘会自动上桌;Pure 宠物需自行启动。'))),
|
|
125
203
|
|
|
126
204
|
// ── 好感度(每宠独立账本) ──
|
|
127
205
|
h(Section, { title: '好感度' },
|
|
@@ -133,18 +211,20 @@ window.__ModuleLoader__.load({
|
|
|
133
211
|
if (!ids.length) return h('div', { style: muted }, '还没有互动记录')
|
|
134
212
|
return ids.map(function (id) {
|
|
135
213
|
var book = (affection && affection[id]) || { affection: 0, total: 0 }
|
|
136
|
-
var name = id
|
|
214
|
+
var name = petName(id)
|
|
137
215
|
return h('div', { key: id, style: row },
|
|
138
216
|
h('span', { style: { fontWeight: 700 } }, name),
|
|
139
217
|
h('span', { style: { fontSize: 18, fontWeight: 700 } }, String(book.affection ?? 0)),
|
|
140
|
-
h('span', { style: muted }, '/ 100 · ' + moodOf(book.affection ?? 0)),
|
|
218
|
+
h('span', { style: muted }, '/ 100 · ' + moodOf(book.affection ?? 0) + ' · 羁绊:' + bondTier(book.bond ? (book.bond.value ?? book.bond.days ?? 0) : 0) + '(' + String(book.bond ? (book.bond.value ?? book.bond.days ?? 0) : 0) + ' 天)'),
|
|
141
219
|
h('span', { style: muted }, '累计 ' + String(book.total ?? 0) + ' 次'))
|
|
142
220
|
})
|
|
143
221
|
})(),
|
|
144
222
|
h('div', { style: row }, h(SButton, { variant: 'danger', disabled: busy, onClick: function () {
|
|
145
223
|
if (window.confirm('清空所有好感度账本?此操作不可撤销。')) act('/api/pet-settings/reset-affection', {}, '账本已清空')
|
|
146
224
|
} }, '重置账本'),
|
|
147
|
-
h('span', { style: muted }, '
|
|
225
|
+
h('span', { style: muted }, '每 10 次触摸该宠物会自主庆祝一次'))),
|
|
226
|
+
|
|
227
|
+
|
|
148
228
|
|
|
149
229
|
// ── 内心戏(宠物自主说话)──
|
|
150
230
|
h(Section, { title: h('span', null, '自主表达 ', h('span', { style: { fontSize: 11, fontWeight: 400, color: 'rgba(127,127,127,.9)' } }, '模型驱动')) },
|
|
@@ -190,11 +270,11 @@ window.__ModuleLoader__.load({
|
|
|
190
270
|
marginLeft: side === 'right' ? -1 : 0 }
|
|
191
271
|
}
|
|
192
272
|
return h('div', { style: { display: 'inline-flex' } },
|
|
193
|
-
h('button', { style: seg(iv.preset
|
|
194
|
-
if (iv.preset !== '
|
|
273
|
+
h('button', { style: seg(iv.preset === 'whale598', 'left'), onClick: function () {
|
|
274
|
+
if (iv.preset !== 'whale598') update({ innerVoice: { preset: 'whale598' } })
|
|
195
275
|
} }, '精简(仅表达)'),
|
|
196
|
-
h('button', { style: seg(iv.preset === '
|
|
197
|
-
if (iv.preset !== '
|
|
276
|
+
h('button', { style: seg(iv.preset === 'whale598-standard', 'right'), onClick: function () {
|
|
277
|
+
if (iv.preset !== 'whale598-standard') update({ innerVoice: { preset: 'whale598-standard' } })
|
|
198
278
|
} }, '普通(含工作工具)'))
|
|
199
279
|
})()),
|
|
200
280
|
h('div', { style: row }, '指定会话(高级)',
|
|
@@ -220,6 +300,61 @@ window.__ModuleLoader__.load({
|
|
|
220
300
|
'分钟'),
|
|
221
301
|
h(Hint, null, '自由活动:宠物空闲时随机播放一段小动画(纯动画,无模型参与),改动即时推送。')),
|
|
222
302
|
|
|
303
|
+
// ── 语音朗读(BreezeBlue TTS)──
|
|
304
|
+
h(Section, { title: h('span', null, '语音朗读 ', h('span', { style: { fontSize: 11, fontWeight: 400, color: 'rgba(127,127,127,.9)' } }, 'BreezeBlue TTS')) },
|
|
305
|
+
h('div', { style: row }, '朗读开关', h(Toggle, { value: tts.enabled === true, onChange: function () {
|
|
306
|
+
update({ tts: { enabled: !(tts.enabled === true) } })
|
|
307
|
+
} }),
|
|
308
|
+
h('span', { style: muted }, '宠物展示说话气泡的同时读出来(语音就绪立即一起显示;最多等 5 秒,超时只展示气泡)')),
|
|
309
|
+
// key / 声音 / 语速音量均为 commit-on-blur:输入期间不落盘,
|
|
310
|
+
// 清空 = 删除配置(宠物回落为不朗读)。
|
|
311
|
+
h('div', { style: row },
|
|
312
|
+
'API Key',
|
|
313
|
+
h('input', { key: 'tts-key-' + String(tts.apiKey || ''), ref: function (el) { keyInputRef.current = el }, style: Object.assign({}, inputStyle, { width: 260 }), type: 'password', defaultValue: tts.apiKey || '', placeholder: 'brz_...(breezeblue.ai 申请)',
|
|
314
|
+
onBlur: function (e) { var v = e.target.value.trim(); if (v !== (tts.apiKey || '')) update({ tts: { apiKey: v } }) },
|
|
315
|
+
onKeyDown: function (e) { if (e.key === 'Enter') e.target.blur() } }),
|
|
316
|
+
h('a', { href: 'https://breezeblue.ai/app/developer/api-keys', target: '_blank', rel: 'noopener noreferrer',
|
|
317
|
+
style: Object.assign(btnStyle('primary'), { display: 'inline-block', textDecoration: 'none' }), title: '打开 BreezeBlue 开发者控制台的 API Keys 页面' }, '获取 Key')),
|
|
318
|
+
h('div', { style: row },
|
|
319
|
+
'声音',
|
|
320
|
+
h('input', { key: 'tts-voice-' + String(tts.voiceId || ''), style: Object.assign({}, inputStyle, { width: 210, fontFamily: 'monospace' }), defaultValue: tts.voiceId || '', placeholder: 'voice_id,点右侧选择',
|
|
321
|
+
onBlur: function (e) { var v = e.target.value.trim(); if (v !== (tts.voiceId || '')) update({ tts: { voiceId: v } }) },
|
|
322
|
+
onKeyDown: function (e) { if (e.key === 'Enter') e.target.blur() } }),
|
|
323
|
+
h(SButton, { disabled: busy, onClick: function () {
|
|
324
|
+
if (voice) { setVoice(null); return }
|
|
325
|
+
flushKeyThen(function () { loadVoices('') })
|
|
326
|
+
} }, voice ? '收起' : '选择声音')),
|
|
327
|
+
voice
|
|
328
|
+
? h('div', { style: { border: '1px solid rgba(127,127,127,.3)', borderRadius: 8, padding: '8px 10px', margin: '4px 0 6px' } },
|
|
329
|
+
h('div', { style: row },
|
|
330
|
+
h('input', { style: Object.assign({}, inputStyle, { width: 190 }), value: voice.search || '', placeholder: '搜索声音(名称/风格)',
|
|
331
|
+
onChange: function (e) { setVoice(Object.assign({}, voice, { search: e.target.value })) },
|
|
332
|
+
onKeyDown: function (e) { if (e.key === 'Enter') loadVoices(voice.search) } }),
|
|
333
|
+
h(SButton, { disabled: voice.loading, onClick: function () { flushKeyThen(function () { loadVoices(voice.search) }) } }, '搜索')),
|
|
334
|
+
voice.error ? h('div', { style: { color: '#ff7369', fontSize: 12, margin: '2px 0' } }, voice.error) : null,
|
|
335
|
+
h('div', { style: { maxHeight: 220, overflowY: 'auto' } },
|
|
336
|
+
(voice.items || []).map(function (v) {
|
|
337
|
+
return h('div', { key: v.voice_id, style: Object.assign({}, row, { borderBottom: '1px solid rgba(127,127,127,.15)' }) },
|
|
338
|
+
h('span', { style: { fontWeight: 600 } }, v.name || v.voice_id),
|
|
339
|
+
h('span', { style: muted }, [v.language_code, v.gender, (v.tone || []).join('/')].filter(Boolean).join(' · ')),
|
|
340
|
+
v.voice_id === tts.voiceId ? h('span', { style: { color: '#3ddc84', fontSize: 12, fontWeight: 700 } }, '当前') : null,
|
|
341
|
+
h('span', { style: { flex: 1 } }),
|
|
342
|
+
v.preview_url ? h(SButton, { onClick: function () { playPreview(v.preview_url) } }, '试听') : null,
|
|
343
|
+
h(SButton, { variant: 'primary', onClick: function () { update({ tts: { voiceId: v.voice_id } }, '已选用 ' + (v.name || v.voice_id)) } }, '选用'))
|
|
344
|
+
}),
|
|
345
|
+
(!voice.loading && !voice.error && (voice.items || []).length === 0) ? h('div', { style: muted }, '没有匹配的声音') : null))
|
|
346
|
+
: null,
|
|
347
|
+
h('div', { style: row },
|
|
348
|
+
'语速',
|
|
349
|
+
h('input', { key: 'tts-speed-' + String(tts.speed ?? 1), style: inputStyle, defaultValue: String(tts.speed ?? 1), title: '0.5 – 2.0,1 = 正常',
|
|
350
|
+
onBlur: function (e) { var v = parseFloat(e.target.value); if (Number.isFinite(v) && v !== (tts.speed ?? 1)) update({ tts: { speed: Math.min(2, Math.max(0.5, v)) } }) },
|
|
351
|
+
onKeyDown: function (e) { if (e.key === 'Enter') e.target.blur() } }),
|
|
352
|
+
'音量',
|
|
353
|
+
h('input', { key: 'tts-volume-' + String(tts.volume ?? 1), style: inputStyle, defaultValue: String(tts.volume ?? 1), title: '0.01 – 2.0,1 = 默认',
|
|
354
|
+
onBlur: function (e) { var v = parseFloat(e.target.value); if (Number.isFinite(v) && v !== (tts.volume ?? 1)) update({ tts: { volume: Math.min(2, Math.max(0.01, v)) } }) },
|
|
355
|
+
onKeyDown: function (e) { if (e.key === 'Enter') e.target.blur() } })),
|
|
356
|
+
h(Hint, null, '接入 BreezeBlue(breezeblue.ai)文字转语音:说话气泡显示时用所选声音朗读。key 只保存在本机 pet-settings.json;改动即时生效,无需重启宠物。气泡等语音的超时上限可在 pet-settings.json 的 tts.holdMs 调整(0–8000 毫秒,默认 5000;语音先就绪就立刻展示,超时则只展示气泡)。')),
|
|
357
|
+
|
|
223
358
|
// ── 调试 ──
|
|
224
359
|
h(Section, { title: '调试' },
|
|
225
360
|
h('div', { style: row }, 'CDP 调试端口 (9222)', h(Toggle, { value: debug.cdp === true, onChange: function () {
|
package/lib/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { i as installPetDriver, n as installPetService } from "./service-
|
|
1
|
+
import { c as globalFilePath, d as readJsonl, f as readPetName, h as writePetName, i as installPetDriver, l as readAffectionRaw, m as writeGlobalJson, n as installPetService, p as writeAffectionRaw, r as petHome, s as eventsFile, u as readGlobalJson } from "./service-BjTawwUn.js";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
5
|
import { existsSync, readFileSync } from "node:fs";
|
|
6
|
-
import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
6
|
+
import { mkdir, readFile, rm, unlink, writeFile } from "node:fs/promises";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import { dirname, extname, join, normalize, resolve, sep } from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
@@ -20,6 +20,7 @@ import { fileURLToPath } from "node:url";
|
|
|
20
20
|
* dispatch — the webserver throws on duplicate path registration.
|
|
21
21
|
* @module dsh-agentic-pet/settings-page
|
|
22
22
|
*/
|
|
23
|
+
/** The touch ledger the driver owns; reset writes the driver's empty shape. */
|
|
23
24
|
const DEFAULT_SETTINGS = {
|
|
24
25
|
proactivity: {
|
|
25
26
|
enabled: false,
|
|
@@ -30,16 +31,16 @@ const DEFAULT_SETTINGS = {
|
|
|
30
31
|
},
|
|
31
32
|
innerVoice: {
|
|
32
33
|
enabled: true,
|
|
33
|
-
preset: "
|
|
34
|
+
preset: "whale598",
|
|
34
35
|
wakeIntervalMinutes: 2,
|
|
35
36
|
speakProbability: .5,
|
|
36
37
|
touchReplyProbability: .3,
|
|
37
38
|
touchReplyCooldownSeconds: 30,
|
|
38
39
|
boundSessions: {}
|
|
39
40
|
},
|
|
41
|
+
tts: { enabled: false },
|
|
40
42
|
debug: { cdp: false }
|
|
41
43
|
};
|
|
42
|
-
/** The touch ledger the driver owns; reset writes the driver's empty shape. */
|
|
43
44
|
const EMPTY_LEDGER = {
|
|
44
45
|
affection: 0,
|
|
45
46
|
total: 0,
|
|
@@ -141,10 +142,30 @@ function mergeSettings(current, patch) {
|
|
|
141
142
|
const d = patchDebug;
|
|
142
143
|
if (typeof d.cdp === "boolean") debug.cdp = d.cdp;
|
|
143
144
|
}
|
|
145
|
+
const tts = { ...current.tts };
|
|
146
|
+
const patchTts = patch.tts;
|
|
147
|
+
if (typeof patchTts === "object" && patchTts !== null) {
|
|
148
|
+
const t = patchTts;
|
|
149
|
+
if (typeof t.enabled === "boolean") tts.enabled = t.enabled;
|
|
150
|
+
const setText = (key) => {
|
|
151
|
+
const value = t[key];
|
|
152
|
+
if (typeof value !== "string") return;
|
|
153
|
+
const trimmed = value.trim();
|
|
154
|
+
if (trimmed === "") Reflect.deleteProperty(tts, key);
|
|
155
|
+
else tts[key] = trimmed;
|
|
156
|
+
};
|
|
157
|
+
setText("apiKey");
|
|
158
|
+
setText("voiceId");
|
|
159
|
+
setText("modelId");
|
|
160
|
+
setText("languageCode");
|
|
161
|
+
if (typeof t.speed === "number" && Number.isFinite(t.speed)) tts.speed = Math.min(2, Math.max(.5, t.speed));
|
|
162
|
+
if (typeof t.volume === "number" && Number.isFinite(t.volume)) tts.volume = Math.min(2, Math.max(.01, t.volume));
|
|
163
|
+
}
|
|
144
164
|
return {
|
|
145
165
|
...current,
|
|
146
166
|
proactivity,
|
|
147
167
|
innerVoice,
|
|
168
|
+
tts,
|
|
148
169
|
debug
|
|
149
170
|
};
|
|
150
171
|
}
|
|
@@ -168,9 +189,7 @@ function openPathInFileManager(path) {
|
|
|
168
189
|
* @param home - the dsh home holding the settings/ledger/manifest files.
|
|
169
190
|
*/
|
|
170
191
|
function installSettingsPage(ctx, home, pet) {
|
|
171
|
-
const
|
|
172
|
-
const ledgerPath = join(home, "pet-affection.json");
|
|
173
|
-
const windowPath = join(home, "pet-window.json");
|
|
192
|
+
const windowPath = globalFilePath(home, "pet-window.json");
|
|
174
193
|
/** Summarize EVERY live pet (id, online flag, normalized /status). */
|
|
175
194
|
const petStatus = async () => {
|
|
176
195
|
return { pets: (await Promise.all((await pet.list()).map((entry) => pet.snapshot(entry)))).map(({ id, online, ...rest }) => ({
|
|
@@ -180,11 +199,7 @@ function installSettingsPage(ctx, home, pet) {
|
|
|
180
199
|
})) };
|
|
181
200
|
};
|
|
182
201
|
const readSettings = async () => {
|
|
183
|
-
|
|
184
|
-
return mergeSettings(JSON.parse(await readFile(settingsPath, "utf8")), {});
|
|
185
|
-
} catch {
|
|
186
|
-
return mergeSettings({}, {});
|
|
187
|
-
}
|
|
202
|
+
return mergeSettings(await readGlobalJson(home, "pet-settings.json"), {});
|
|
188
203
|
};
|
|
189
204
|
const routeByPath = /* @__PURE__ */ new Map();
|
|
190
205
|
const route = (method, path, handler) => {
|
|
@@ -220,22 +235,46 @@ function installSettingsPage(ctx, home, pet) {
|
|
|
220
235
|
route("GET", "/api/pet-settings/read", async (_req, res) => {
|
|
221
236
|
const [settings, windowPosition, status] = await Promise.all([
|
|
222
237
|
readSettings(),
|
|
223
|
-
|
|
238
|
+
readGlobalJson(home, "pet-window.json").then((raw) => Object.keys(raw).length > 0 ? raw : null),
|
|
224
239
|
petStatus()
|
|
225
240
|
]);
|
|
226
241
|
const affection = {};
|
|
227
|
-
const legacyBook = await readFile(ledgerPath, "utf8").then((raw) => JSON.parse(raw), () => null);
|
|
228
242
|
const bookIds = new Set(status.pets.map((pet) => String(pet.id)));
|
|
229
243
|
bookIds.add("electron");
|
|
230
244
|
for (const id of bookIds) {
|
|
231
|
-
const book = await
|
|
245
|
+
const book = await readAffectionRaw(home, id);
|
|
232
246
|
if (book !== null) affection[id] = book;
|
|
233
247
|
}
|
|
248
|
+
const names = {};
|
|
249
|
+
for (const id of bookIds) {
|
|
250
|
+
const label = await readPetName(home, id);
|
|
251
|
+
if (label !== void 0) names[id] = label;
|
|
252
|
+
}
|
|
253
|
+
const composed = {
|
|
254
|
+
...settings,
|
|
255
|
+
names
|
|
256
|
+
};
|
|
257
|
+
const touchTotals = {};
|
|
258
|
+
for (const id of bookIds) {
|
|
259
|
+
const events = await readJsonl(eventsFile(home, id), 600);
|
|
260
|
+
const totals = {
|
|
261
|
+
total: 0,
|
|
262
|
+
byPart: {}
|
|
263
|
+
};
|
|
264
|
+
for (const event of events) {
|
|
265
|
+
if (event.type !== "touch") continue;
|
|
266
|
+
const part = typeof event.part === "string" ? event.part : "miss";
|
|
267
|
+
totals.total += 1;
|
|
268
|
+
totals.byPart[part] = (totals.byPart[part] ?? 0) + 1;
|
|
269
|
+
}
|
|
270
|
+
if (totals.total > 0) touchTotals[id] = totals;
|
|
271
|
+
}
|
|
234
272
|
writeJson(res, 200, {
|
|
235
|
-
settings,
|
|
273
|
+
settings: composed,
|
|
236
274
|
affection,
|
|
237
275
|
windowPosition,
|
|
238
|
-
pets: status.pets
|
|
276
|
+
pets: status.pets,
|
|
277
|
+
touchTotals
|
|
239
278
|
});
|
|
240
279
|
});
|
|
241
280
|
route("POST", "/api/pet-settings/update", async (req, res) => {
|
|
@@ -245,8 +284,13 @@ function installSettingsPage(ctx, home, pet) {
|
|
|
245
284
|
writeJson(res, 400, { error: "body must be {patch:{...}}" });
|
|
246
285
|
return;
|
|
247
286
|
}
|
|
287
|
+
const patchNames = patch.names;
|
|
288
|
+
if (typeof patchNames === "object" && patchNames !== null) {
|
|
289
|
+
for (const [petId, label] of Object.entries(patchNames)) await writePetName(home, petId, typeof label === "string" && label.trim() !== "" ? label : null);
|
|
290
|
+
delete patch.names;
|
|
291
|
+
}
|
|
248
292
|
const next = mergeSettings(await readSettings(), patch);
|
|
249
|
-
await
|
|
293
|
+
await writeGlobalJson(home, "pet-settings.json", next);
|
|
250
294
|
ctx.emit("pet-settings/changed");
|
|
251
295
|
writeJson(res, 200, {
|
|
252
296
|
ok: true,
|
|
@@ -254,14 +298,14 @@ function installSettingsPage(ctx, home, pet) {
|
|
|
254
298
|
});
|
|
255
299
|
});
|
|
256
300
|
route("POST", "/api/pet-settings/reset-affection", async (_req, res) => {
|
|
257
|
-
await
|
|
301
|
+
await writeAffectionRaw(home, "electron", EMPTY_LEDGER);
|
|
258
302
|
writeJson(res, 200, {
|
|
259
303
|
ok: true,
|
|
260
304
|
affection: EMPTY_LEDGER
|
|
261
305
|
});
|
|
262
306
|
});
|
|
263
307
|
route("POST", "/api/pet-settings/reset-position", async (_req, res) => {
|
|
264
|
-
|
|
308
|
+
for (const p of [windowPath, join(home, "pet-window.json")]) await rm(p, { force: true });
|
|
265
309
|
writeJson(res, 200, { ok: true });
|
|
266
310
|
});
|
|
267
311
|
route("POST", "/api/pet-settings/open-logs", async (_req, res) => {
|
|
@@ -273,6 +317,59 @@ function installSettingsPage(ctx, home, pet) {
|
|
|
273
317
|
opened: target
|
|
274
318
|
});
|
|
275
319
|
});
|
|
320
|
+
route("GET", "/api/pet-settings/tts/voices", async (req, res) => {
|
|
321
|
+
const settings = await readSettings();
|
|
322
|
+
const apiKey = typeof settings.tts?.apiKey === "string" ? settings.tts.apiKey : "";
|
|
323
|
+
if (apiKey === "") {
|
|
324
|
+
writeJson(res, 400, { error: "尚未配置 BreezeBlue API key" });
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
328
|
+
const params = new URLSearchParams();
|
|
329
|
+
for (const key of [
|
|
330
|
+
"search",
|
|
331
|
+
"language_code",
|
|
332
|
+
"gender",
|
|
333
|
+
"accent"
|
|
334
|
+
]) {
|
|
335
|
+
const value = url.searchParams.get(key);
|
|
336
|
+
if (value !== null && value !== "") params.set(key, value);
|
|
337
|
+
}
|
|
338
|
+
const rawPageSize = url.searchParams.get("page_size");
|
|
339
|
+
const pageSize = rawPageSize !== null ? clampNumber(Number(rawPageSize), 1, 100) : void 0;
|
|
340
|
+
params.set("page_size", String(pageSize ?? 30));
|
|
341
|
+
try {
|
|
342
|
+
const response = await fetch(`https://api.breeze.blue/v1/voices?${params.toString()}`, {
|
|
343
|
+
headers: { "xi-api-key": apiKey },
|
|
344
|
+
signal: AbortSignal.timeout(1e4)
|
|
345
|
+
});
|
|
346
|
+
if (!response.ok) {
|
|
347
|
+
const detail = await response.text().catch(() => "");
|
|
348
|
+
writeJson(res, 502, { error: response.status === 401 ? "BreezeBlue 拒绝了这个 key(401):请到 breezeblue.ai 的 Developer Console 复制 brz_ 开头的 API key" : `BreezeBlue HTTP ${String(response.status)}${detail ? `: ${detail.slice(0, 200)}` : ""}` });
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const raw = await response.json();
|
|
352
|
+
const voices = (Array.isArray(raw.voices) ? raw.voices : []).map((entry) => {
|
|
353
|
+
const voice = entry;
|
|
354
|
+
const str = (key) => typeof voice[key] === "string" ? voice[key] : null;
|
|
355
|
+
return {
|
|
356
|
+
voice_id: str("voice_id") ?? "",
|
|
357
|
+
name: str("name") ?? "",
|
|
358
|
+
language_code: str("language_code"),
|
|
359
|
+
gender: str("gender"),
|
|
360
|
+
tone: Array.isArray(voice.tone) ? voice.tone.filter((item) => typeof item === "string").slice(0, 3) : [],
|
|
361
|
+
accent: str("accent"),
|
|
362
|
+
preview_url: str("preview_url")
|
|
363
|
+
};
|
|
364
|
+
}).filter((voice) => voice.voice_id !== "");
|
|
365
|
+
writeJson(res, 200, {
|
|
366
|
+
voices,
|
|
367
|
+
total: typeof raw.total === "number" ? raw.total : voices.length
|
|
368
|
+
});
|
|
369
|
+
} catch (error) {
|
|
370
|
+
writeJson(res, 502, { error: `声音列表请求失败: ${error instanceof Error ? error.message : String(error)}` });
|
|
371
|
+
}
|
|
372
|
+
});
|
|
276
373
|
}
|
|
277
374
|
//#endregion
|
|
278
375
|
//#region lib/types/index.js
|
|
@@ -311,7 +408,7 @@ const inject = [
|
|
|
311
408
|
"workspaceController"
|
|
312
409
|
];
|
|
313
410
|
/** The URL path prefix the pet renderer is served under. No trailing slash. */
|
|
314
|
-
const PET_PATH_PREFIX = "/
|
|
411
|
+
const PET_PATH_PREFIX = "/whale598";
|
|
315
412
|
/**
|
|
316
413
|
* Locator file naming this runtime's authenticated root for the pet carrier.
|
|
317
414
|
* The desktop shell keeps its authenticated URL in process memory only, so a
|
|
@@ -320,7 +417,7 @@ const PET_PATH_PREFIX = "/pet";
|
|
|
320
417
|
* publisher, then falls back to spawning its own profile.
|
|
321
418
|
*/
|
|
322
419
|
function petLocatorPath() {
|
|
323
|
-
return join(process.env.DSH_HOME ?? join(homedir(), ".dsh"), "pet-surface-url.json");
|
|
420
|
+
return join(process.env.DSH_HOME ?? join(homedir(), ".dsh"), "whale598", "pet-surface-url.json");
|
|
324
421
|
}
|
|
325
422
|
const HTML_MIME = "text/html; charset=utf-8";
|
|
326
423
|
const MIME = {
|
|
@@ -367,7 +464,7 @@ function bytesBody(bytes) {
|
|
|
367
464
|
/**
|
|
368
465
|
* Answer one GET or HEAD `/pet` request. The URL path is the full server
|
|
369
466
|
* pathname (`/pet` prefix included); it must be exactly the prefix or stay
|
|
370
|
-
* under it. The index entry (`/
|
|
467
|
+
* under it. The index entry (`/whale598`, `/whale598/`, `/whale598/index.html`) passes
|
|
371
468
|
* Connection's browser authentication; every other file is public with a weak
|
|
372
469
|
* ETag and `cache-control: no-cache`, answering a matching `If-None-Match`
|
|
373
470
|
* with 304. Traversal outside the dist root is 403, missing paths are 404, a
|
|
@@ -379,7 +476,8 @@ function bytesBody(bytes) {
|
|
|
379
476
|
*/
|
|
380
477
|
async function servePetStatic(request, distRoot, distIndex, authorizeIndex) {
|
|
381
478
|
const pathname = decodeURIComponent(new URL(request.url).pathname);
|
|
382
|
-
|
|
479
|
+
if (!(pathname === "/whale598" || pathname.startsWith(`/whale598/`))) return new Response(null, { status: 404 });
|
|
480
|
+
const target = resolve(normalize(join(distRoot, pathname === "/whale598" || pathname === `/whale598/` ? "/" : pathname.slice(9))));
|
|
383
481
|
if (target !== distRoot && !target.startsWith(distRoot + sep)) return new Response(null, { status: 403 });
|
|
384
482
|
let body;
|
|
385
483
|
let headers;
|
|
@@ -587,9 +685,10 @@ function apply(ctx) {
|
|
|
587
685
|
const settled = ctx.get("loader")?.await();
|
|
588
686
|
if (settled === void 0) settledReady();
|
|
589
687
|
else settled.then(settledReady).catch(() => {});
|
|
590
|
-
const
|
|
591
|
-
|
|
592
|
-
|
|
688
|
+
const home = petHome();
|
|
689
|
+
const pet = installPetService(ctx, home);
|
|
690
|
+
installPetDriver(ctx, home, pet);
|
|
691
|
+
installSettingsPage(ctx, home, pet);
|
|
593
692
|
ctx.effect(() => () => {
|
|
594
693
|
readFile(petLocatorPath(), "utf8").then((raw) => {
|
|
595
694
|
if (JSON.parse(raw).pid === process.pid) return unlink(petLocatorPath());
|