dsh-agentic-pet 0.4.9 → 0.4.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/carrier/lib/main.js +48 -2
- package/client/pet-settings.js +308 -14
- package/lib/{service-BZbhT92u.js → driver-CpRgkle-.js} +3243 -2003
- package/lib/index.js +433 -48
- package/lib/tools.js +1 -1
- package/lib/types/actions.d.ts +28 -0
- package/lib/types/binding.d.ts +99 -0
- package/lib/types/driver.d.ts +33 -2
- package/lib/types/electron-lock.d.ts +25 -0
- package/lib/types/electron-runtime-status.d.ts +20 -0
- package/lib/types/inner-voice.d.ts +18 -1
- package/lib/types/journal.d.ts +58 -0
- package/lib/types/presence.d.ts +27 -0
- package/lib/types/rhythm.d.ts +21 -0
- package/lib/types/service.d.ts +6 -0
- package/lib/types/settings-page.d.ts +2 -1
- package/package.json +3 -3
- package/presets/pet-inner-voice.patch.yml +2 -0
- package/presets/pet.patch.yml +1 -0
package/carrier/lib/main.js
CHANGED
|
@@ -45,6 +45,38 @@ const CHARACTER = 'whale';
|
|
|
45
45
|
* change. Failure degrades to an empty catalog — the pet then advertises
|
|
46
46
|
* only the universal `celebrate` alias, i.e. yesterday's behavior.
|
|
47
47
|
*/
|
|
48
|
+
async function readActionTags() {
|
|
49
|
+
const file = join(resolveAssetsRoot(), CHARACTER, 'actions.tags.json');
|
|
50
|
+
try {
|
|
51
|
+
const raw = JSON.parse(await readFile(file, 'utf8'));
|
|
52
|
+
const map = new Map();
|
|
53
|
+
for (const [id, value] of Object.entries(raw)) {
|
|
54
|
+
if (typeof value !== 'object' || value === null)
|
|
55
|
+
continue;
|
|
56
|
+
const record = value;
|
|
57
|
+
const tags = Array.isArray(record.tags)
|
|
58
|
+
? record.tags.filter(tag => typeof tag === 'string' && tag !== '')
|
|
59
|
+
: [];
|
|
60
|
+
const energy = record.energy === 'low' || record.energy === 'mid' || record.energy === 'high'
|
|
61
|
+
? record.energy
|
|
62
|
+
: undefined;
|
|
63
|
+
const durationHintMs = typeof record.durationHintMs === 'number' && Number.isFinite(record.durationHintMs)
|
|
64
|
+
? record.durationHintMs
|
|
65
|
+
: undefined;
|
|
66
|
+
const textInFrame = typeof record.textInFrame === 'boolean' ? record.textInFrame : undefined;
|
|
67
|
+
map.set(id, {
|
|
68
|
+
tags,
|
|
69
|
+
...(energy === undefined ? {} : { energy }),
|
|
70
|
+
...(durationHintMs === undefined ? {} : { durationHintMs }),
|
|
71
|
+
...(textInFrame === undefined ? {} : { textInFrame }),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return map;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return new Map();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
48
80
|
async function scanActionCatalog() {
|
|
49
81
|
const videosRoot = join(resolveAssetsRoot(), CHARACTER, 'videos');
|
|
50
82
|
const entries = [];
|
|
@@ -74,7 +106,8 @@ async function scanActionCatalog() {
|
|
|
74
106
|
if (category.isDirectory())
|
|
75
107
|
await collect(join(videosRoot, category.name), category.name);
|
|
76
108
|
}
|
|
77
|
-
|
|
109
|
+
const tags = await readActionTags();
|
|
110
|
+
return entries.map(entry => ({ ...entry, ...(tags.get(entry.id) ?? {}) }));
|
|
78
111
|
}
|
|
79
112
|
/**
|
|
80
113
|
* The window's one canonical size: creation and every drag setBounds pin it.
|
|
@@ -414,7 +447,9 @@ async function main() {
|
|
|
414
447
|
// The advertised menu: the universal celebrate alias first, then every
|
|
415
448
|
// clip id the media tree holds.
|
|
416
449
|
const actionIds = ['celebrate', ...actionCatalog.map(entry => entry.id)];
|
|
417
|
-
|
|
450
|
+
const actionTagVocabulary = [...new Set(actionCatalog.flatMap(entry => entry.tags ?? []))];
|
|
451
|
+
const tagged = actionCatalog.some(entry => (entry.tags?.length ?? 0) > 0);
|
|
452
|
+
console.log(`[pet] assets root: ${resolveAssetsRoot()} actions=${String(actionCatalog.length)} tags=${String(actionTagVocabulary.length)}`);
|
|
418
453
|
let handle;
|
|
419
454
|
const attachUrl = process.env.DSH_PET_URL ?? await readLocatorUrl();
|
|
420
455
|
let authenticated;
|
|
@@ -844,12 +879,23 @@ async function main() {
|
|
|
844
879
|
// v4: the endpoints/events word-lists retired — the version field
|
|
845
880
|
// carries endpoint existence, the event body's own `type` field
|
|
846
881
|
// carries event-type tolerance (§6 revision record).
|
|
882
|
+
// 2026-11-06: actionCatalog/actionTagVocabulary/tagged are additive —
|
|
883
|
+
// per §6, added fields do not bump the contract version.
|
|
847
884
|
send(200, {
|
|
848
885
|
contractVersion: 4,
|
|
849
886
|
states: ['idle', 'thinking', 'working', 'error'],
|
|
850
887
|
// The pet's own action menu, scanned from the media tree at boot —
|
|
851
888
|
// celebrate first (the universal alias), then every playable clip.
|
|
852
889
|
actions: actionIds,
|
|
890
|
+
actionCatalog: actionCatalog.map(entry => ({
|
|
891
|
+
id: entry.id,
|
|
892
|
+
tags: entry.tags ?? [],
|
|
893
|
+
...(entry.energy === undefined ? {} : { energy: entry.energy }),
|
|
894
|
+
...(entry.durationHintMs === undefined ? {} : { durationHintMs: entry.durationHintMs }),
|
|
895
|
+
...(entry.textInFrame === undefined ? {} : { textInFrame: entry.textInFrame }),
|
|
896
|
+
})),
|
|
897
|
+
actionTagVocabulary,
|
|
898
|
+
tagged,
|
|
853
899
|
interactions: ['approval', 'question'],
|
|
854
900
|
});
|
|
855
901
|
return;
|
package/client/pet-settings.js
CHANGED
|
@@ -16,6 +16,7 @@ window.__ModuleLoader__.load({
|
|
|
16
16
|
|
|
17
17
|
var READ = '/api/pet-settings/read'
|
|
18
18
|
var UPDATE = '/api/pet-settings/update'
|
|
19
|
+
var RUNTIME = '/api/pet-settings/electron-runtime'
|
|
19
20
|
|
|
20
21
|
// Theme-neutral: inherit foreground, translucent borders — readable on
|
|
21
22
|
// both the host's dark and light themes without a design system.
|
|
@@ -24,6 +25,10 @@ window.__ModuleLoader__.load({
|
|
|
24
25
|
var row = { display: 'flex', alignItems: 'center', gap: 10, margin: '6px 0', fontSize: 13, flexWrap: 'wrap' }
|
|
25
26
|
var muted = { color: 'rgba(127,127,127,.9)', fontSize: 12 }
|
|
26
27
|
var inputStyle = { width: 64, padding: '2px 6px', border: '1px solid rgba(127,127,127,.4)', borderRadius: 6, background: 'transparent', color: 'inherit', font: 'inherit' }
|
|
28
|
+
var eyeButton = { border: 0, background: 'transparent', color: 'inherit', cursor: 'pointer', padding: 2, display: 'inline-flex', alignItems: 'center', opacity: .65 }
|
|
29
|
+
var runtimeCard = { border: '1px solid rgba(124,108,240,.45)', borderRadius: 10, padding: '10px 14px 12px', margin: '10px 0', background: 'rgba(124,108,240,.06)' }
|
|
30
|
+
var runtimeTrack = { position: 'relative', height: 4, borderRadius: 2, background: 'rgba(127,127,127,.18)', overflow: 'hidden', marginTop: 8 }
|
|
31
|
+
var runtimeBar = { position: 'absolute', top: 0, left: '-35%', width: '35%', height: '100%', borderRadius: 2, background: 'rgba(124,108,240,.85)', animation: 'petRuntimeSlide 1.15s ease-in-out infinite' }
|
|
27
32
|
function btnStyle(variant) {
|
|
28
33
|
var base = { border: '1px solid rgba(127,127,127,.4)', borderRadius: 6, cursor: 'pointer', font: 'inherit', fontSize: 12, padding: '3px 12px', background: 'transparent', color: 'inherit' }
|
|
29
34
|
if (variant === 'primary') return Object.assign(base, { fontWeight: 700, borderColor: 'rgba(124,108,240,.6)' })
|
|
@@ -44,12 +49,43 @@ window.__ModuleLoader__.load({
|
|
|
44
49
|
},
|
|
45
50
|
}, props.value ? '已开启' : '已关闭')
|
|
46
51
|
}
|
|
52
|
+
function EyeIcon(props) {
|
|
53
|
+
return h('svg', {
|
|
54
|
+
width: 15, height: 15, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor',
|
|
55
|
+
strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true,
|
|
56
|
+
},
|
|
57
|
+
h('path', { d: 'M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z' }),
|
|
58
|
+
h('circle', { cx: 12, cy: 12, r: 3 }),
|
|
59
|
+
props.off ? h('path', { d: 'M3 3l18 18' }) : null)
|
|
60
|
+
}
|
|
47
61
|
function Section(props) {
|
|
48
62
|
return h('div', { style: card }, h('div', { style: cardTitle }, props.title), props.children)
|
|
49
63
|
}
|
|
50
64
|
function Hint(props) {
|
|
51
65
|
return h('div', { style: Object.assign({ margin: '6px 0 2px' }, muted) }, props.children)
|
|
52
66
|
}
|
|
67
|
+
// First-install visibility: the settings page is open while the runtime is
|
|
68
|
+
// still downloading, so the only honest progress is phase + elapsed time.
|
|
69
|
+
function RuntimeBanner(props) {
|
|
70
|
+
var r = props.runtime
|
|
71
|
+
if (!r || r.state === 'ready') return null
|
|
72
|
+
var active = r.state === 'downloading' || r.state === 'preparing'
|
|
73
|
+
var elapsed = r.startedAt ? Math.max(0, Math.floor((Date.now() - r.startedAt) / 1000)) : 0
|
|
74
|
+
var sourceLabel = r.source === 'npmmirror' ? '镜像源 npmmirror' : r.source === 'github' ? 'GitHub' : r.source === 'explicit' ? '自定义镜像' : ''
|
|
75
|
+
var text
|
|
76
|
+
if (r.state === 'busy') text = '另一个 DSH 窗口正在准备 Electron 运行时,完成后宠物会自动出现。'
|
|
77
|
+
else if (r.state === 'unavailable') text = '插件缺少 electron 安装器,无法自动下载。请重新安装 dsh-agentic-pet。'
|
|
78
|
+
else if (r.state === 'failed') text = '运行时安装失败:' + String(r.error || '未知错误').slice(0, 160) + ',后台会自动重试。'
|
|
79
|
+
else text = '首次安装正在准备 Electron 运行时(约 158 MB),完成后宠物会自动出现。' + (elapsed > 0 ? ' 已等待 ' + elapsed + ' 秒。' : '')
|
|
80
|
+
return h('div', { style: runtimeCard },
|
|
81
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' } },
|
|
82
|
+
h('span', { style: { fontWeight: 700, fontSize: 13 } }, 'Electron 运行时'),
|
|
83
|
+
r.attempt ? h('span', { style: muted }, '第 ' + r.attempt + ' 次尝试') : null,
|
|
84
|
+
sourceLabel ? h('span', { style: muted }, sourceLabel) : null),
|
|
85
|
+
h('div', { style: Object.assign({ margin: '4px 0 0' }, muted) }, text),
|
|
86
|
+
active ? h('div', { style: runtimeTrack }, h('div', { style: runtimeBar })) : null,
|
|
87
|
+
active ? h('style', null, '@keyframes petRuntimeSlide { 0% { left: -35% } 100% { left: 100% } }') : null)
|
|
88
|
+
}
|
|
53
89
|
|
|
54
90
|
function PetSettingsPage() {
|
|
55
91
|
var state = useState(null)
|
|
@@ -65,8 +101,33 @@ window.__ModuleLoader__.load({
|
|
|
65
101
|
var voiceState = useState(null)
|
|
66
102
|
var voice = voiceState[0]
|
|
67
103
|
var setVoice = voiceState[1]
|
|
104
|
+
var runtimeState = useState(null)
|
|
105
|
+
var runtime = runtimeState[0]
|
|
106
|
+
var setRuntime = runtimeState[1]
|
|
107
|
+
var bindingState = useState(null)
|
|
108
|
+
var bindingData = bindingState[0]
|
|
109
|
+
var setBindingData = bindingState[1]
|
|
110
|
+
var bindingBusyState = useState(false)
|
|
111
|
+
var bindingBusy = bindingBusyState[0]
|
|
112
|
+
var setBindingBusy = bindingBusyState[1]
|
|
113
|
+
var warnState = useState('')
|
|
114
|
+
var warnings = warnState[0]
|
|
115
|
+
var setWarnings = warnState[1]
|
|
116
|
+
var bindingMsgState = useState('')
|
|
117
|
+
var bindingMsg = bindingMsgState[0]
|
|
118
|
+
var setBindingMsg = bindingMsgState[1]
|
|
119
|
+
var bindingDraftState = useState(null)
|
|
120
|
+
var bindingDraft = bindingDraftState[0]
|
|
121
|
+
var setBindingDraft = bindingDraftState[1]
|
|
122
|
+
var bindingAttemptState = useState(null)
|
|
123
|
+
var bindingAttempt = bindingAttemptState[0]
|
|
124
|
+
var setBindingAttempt = bindingAttemptState[1]
|
|
68
125
|
var previewAudioRef = useRef(null)
|
|
69
126
|
var keyInputRef = useRef(null)
|
|
127
|
+
// 好感度默认盖住:密码式的星星遮罩,眼睛按钮按宠物单独揭示。
|
|
128
|
+
var affectionRevealState = useState({})
|
|
129
|
+
var affectionReveal = affectionRevealState[0]
|
|
130
|
+
var setAffectionReveal = affectionRevealState[1]
|
|
70
131
|
|
|
71
132
|
var refresh = useCallback(function () {
|
|
72
133
|
fetch(READ, { credentials: 'same-origin' })
|
|
@@ -75,8 +136,48 @@ window.__ModuleLoader__.load({
|
|
|
75
136
|
.catch(function (e) { setMsg('读取失败: ' + (e && e.message)) })
|
|
76
137
|
}, [])
|
|
77
138
|
useEffect(function () { refresh() }, [refresh])
|
|
139
|
+
// While the first install downloads Electron, poll the runtime status
|
|
140
|
+
// until it is ready. A ready/absent status ends the loop; a failed one
|
|
141
|
+
// keeps polling because the host retries on its own.
|
|
142
|
+
useEffect(function () {
|
|
143
|
+
var alive = true
|
|
144
|
+
var timer = null
|
|
145
|
+
var emptyTries = 0
|
|
146
|
+
function load() {
|
|
147
|
+
fetch(RUNTIME, { credentials: 'same-origin' })
|
|
148
|
+
.then(function (r) { return r.json() })
|
|
149
|
+
.then(function (d) {
|
|
150
|
+
if (!alive) return
|
|
151
|
+
var next = (d && d.runtime) || null
|
|
152
|
+
setRuntime(next)
|
|
153
|
+
if (next && (next.state === 'downloading' || next.state === 'preparing' || next.state === 'busy' || next.state === 'failed')) {
|
|
154
|
+
emptyTries = 0
|
|
155
|
+
timer = setTimeout(load, 2000)
|
|
156
|
+
} else if (!next && emptyTries < 5) {
|
|
157
|
+
// The first activation may still be writing its status when the
|
|
158
|
+
// page mounts; give it a few seconds instead of hiding forever.
|
|
159
|
+
emptyTries += 1
|
|
160
|
+
timer = setTimeout(load, 3000)
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
.catch(function () { if (alive) timer = setTimeout(load, 5000) })
|
|
164
|
+
}
|
|
165
|
+
load()
|
|
166
|
+
return function () { alive = false; if (timer) clearTimeout(timer) }
|
|
167
|
+
}, [])
|
|
168
|
+
|
|
169
|
+
var loadBinding = useCallback(function () {
|
|
170
|
+
fetch('/api/pet-settings/binding-status', { credentials: 'same-origin' })
|
|
171
|
+
.then(function (r) { return r.json() })
|
|
172
|
+
.then(function (d) { setBindingData((d && d.status) || null) })
|
|
173
|
+
.catch(function () { /* binding status is optional */ })
|
|
174
|
+
}, [])
|
|
175
|
+
useEffect(function () { loadBinding() }, [loadBinding])
|
|
78
176
|
|
|
79
177
|
var act = useCallback(function (path, body, doneMessage) {
|
|
178
|
+
// Binding edits own their message under the binding row; the global
|
|
179
|
+
// banner at the top stays for the rest of the settings page.
|
|
180
|
+
var isBinding = !!(body && body.patch && body.patch.innerVoice && body.patch.innerVoice.boundSessions)
|
|
80
181
|
setBusy(true)
|
|
81
182
|
setMsg('')
|
|
82
183
|
fetch(path, {
|
|
@@ -87,19 +188,38 @@ window.__ModuleLoader__.load({
|
|
|
87
188
|
})
|
|
88
189
|
.then(function (r) { return r.json() })
|
|
89
190
|
.then(function (d) {
|
|
90
|
-
if (
|
|
191
|
+
if (isBinding) {
|
|
192
|
+
if (d && d.error) setBindingMsg('失败: ' + d.error)
|
|
193
|
+
else {
|
|
194
|
+
setBindingMsg(doneMessage || '绑定已保存')
|
|
195
|
+
setBindingAttempt(null)
|
|
196
|
+
}
|
|
197
|
+
} else if (d && d.error) setMsg('失败: ' + d.error)
|
|
91
198
|
else setMsg(doneMessage || '完成')
|
|
199
|
+
setWarnings(d && d.warnings && d.warnings.length ? d.warnings.join(';') : '')
|
|
92
200
|
refresh()
|
|
201
|
+
loadBinding()
|
|
202
|
+
})
|
|
203
|
+
.catch(function (e) {
|
|
204
|
+
if (isBinding) setBindingMsg('请求失败: ' + (e && e.message))
|
|
205
|
+
else setMsg('请求失败: ' + (e && e.message))
|
|
93
206
|
})
|
|
94
|
-
.catch(function (e) { setMsg('请求失败: ' + (e && e.message)) })
|
|
95
207
|
.finally(function () { setBusy(false) })
|
|
96
|
-
}, [refresh])
|
|
208
|
+
}, [refresh, loadBinding])
|
|
97
209
|
|
|
98
210
|
var settings = (data && data.settings) || {}
|
|
99
211
|
var pro = settings.proactivity || {}
|
|
100
212
|
var iv = settings.innerVoice || {}
|
|
213
|
+
var rhythm = settings.rhythm || {}
|
|
214
|
+
var quiet = rhythm.quiet || {}
|
|
215
|
+
var quietOn = !!rhythm.quiet
|
|
216
|
+
var journal = settings.journal || {}
|
|
217
|
+
var journalOn = journal.enabled !== false
|
|
218
|
+
var presenceSummary = settings.presenceSummary || {}
|
|
219
|
+
var presenceOn = presenceSummary.enabled === true
|
|
101
220
|
var tts = settings.tts || {}
|
|
102
221
|
var debug = settings.debug || {}
|
|
222
|
+
var savedBinding = String((iv.boundSessions && iv.boundSessions.electron) || '')
|
|
103
223
|
var affection = (data && data.affection) || {}
|
|
104
224
|
var pets = (data && data.pets) || []
|
|
105
225
|
if (!(pets && pets.length) && data && data.pet) pets = [Object.assign({ id: 'electron' }, data.pet)]
|
|
@@ -134,6 +254,64 @@ window.__ModuleLoader__.load({
|
|
|
134
254
|
|
|
135
255
|
function update(patch, doneMessage) { act(UPDATE, { patch: patch }, doneMessage) }
|
|
136
256
|
|
|
257
|
+
// Binding is commit-on-blur/Enter, like the name field: a per-keystroke
|
|
258
|
+
// save would persist a partial session id before validation runs.
|
|
259
|
+
function commitBinding() {
|
|
260
|
+
var v = (bindingDraft !== null ? bindingDraft : savedBinding).trim()
|
|
261
|
+
if (v === savedBinding) {
|
|
262
|
+
setBindingDraft(null)
|
|
263
|
+
setBindingAttempt(null)
|
|
264
|
+
setBindingMsg('')
|
|
265
|
+
return
|
|
266
|
+
}
|
|
267
|
+
// A blur repeats on every focus loss; do not resend the exact value
|
|
268
|
+
// that already failed (the visible error would flash on each retry).
|
|
269
|
+
if (v === bindingAttempt) return
|
|
270
|
+
setBindingAttempt(v)
|
|
271
|
+
var next = Object.assign({}, iv.boundSessions || {})
|
|
272
|
+
if (v !== '') next.electron = v
|
|
273
|
+
else delete next.electron
|
|
274
|
+
update({ innerVoice: { boundSessions: next } }, '绑定已保存')
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ensure + attach + status, no visible prompt: the one-click way to
|
|
278
|
+
// finish the "binding is saved but not loaded" step.
|
|
279
|
+
function testBinding() {
|
|
280
|
+
if (bindingDraft !== null && bindingDraft.trim() !== savedBinding) {
|
|
281
|
+
setBindingMsg('请先保存绑定:点一下输入框外面或按回车')
|
|
282
|
+
return
|
|
283
|
+
}
|
|
284
|
+
setBindingBusy(true)
|
|
285
|
+
setMsg('')
|
|
286
|
+
setBindingMsg('')
|
|
287
|
+
fetch('/api/pet-settings/binding-test', {
|
|
288
|
+
method: 'POST', credentials: 'same-origin',
|
|
289
|
+
headers: { 'content-type': 'application/json' },
|
|
290
|
+
body: JSON.stringify({ petId: 'electron' }),
|
|
291
|
+
})
|
|
292
|
+
.then(function (r) { return r.json() })
|
|
293
|
+
.then(function (d) {
|
|
294
|
+
if (d && d.error) { setBindingMsg('失败: ' + d.error); return }
|
|
295
|
+
if (d && d.ok) {
|
|
296
|
+
var notes = []
|
|
297
|
+
if (d.binding && d.binding.presetWarning) notes.push('当前模式不是宠物模式,不会有气泡回应')
|
|
298
|
+
if (d.binding && d.binding.attach && d.binding.attach.errorCode === 'cwd-mismatch') {
|
|
299
|
+
notes.push('会话在另一个项目,不会归到 whale598 项目下')
|
|
300
|
+
}
|
|
301
|
+
setBindingMsg(notes.length ? '绑定可用;' + notes.join(';') : '绑定测试通过')
|
|
302
|
+
} else {
|
|
303
|
+
var b = d && d.binding
|
|
304
|
+
var reason = (b && (b.lastError || (b.attach && b.attach.errorCode))) || '未知原因'
|
|
305
|
+
setBindingMsg('绑定测试未通过:' + reason)
|
|
306
|
+
}
|
|
307
|
+
if (d && d.binding) {
|
|
308
|
+
setBindingData({ updatedAt: Date.now(), pets: { electron: d.binding }, failures: (bindingData && bindingData.failures) || [] })
|
|
309
|
+
}
|
|
310
|
+
})
|
|
311
|
+
.catch(function (e) { setBindingMsg('请求失败: ' + (e && e.message)) })
|
|
312
|
+
.finally(function () { setBindingBusy(false); loadBinding() })
|
|
313
|
+
}
|
|
314
|
+
|
|
137
315
|
// ── 语音朗读(BreezeBlue TTS)的取声/试听 ──
|
|
138
316
|
// 列表走宿主代理路由(浏览器直连 api.breeze.blue 会被 CORS 拦下,
|
|
139
317
|
// key 也不该进页面日志);试听直接播 preview_url,不经过本插件。
|
|
@@ -179,6 +357,7 @@ window.__ModuleLoader__.load({
|
|
|
179
357
|
h('span', { style: { flex: 1 } }),
|
|
180
358
|
h(SButton, { onClick: refresh, disabled: busy }, '刷新')),
|
|
181
359
|
msg ? h('div', { style: { fontSize: 12, margin: '0 0 6px', color: msg.indexOf('失败') >= 0 ? '#ff7369' : 'inherit' } }, msg) : null,
|
|
360
|
+
h(RuntimeBanner, { runtime: runtime }),
|
|
182
361
|
|
|
183
362
|
// ── 状态(每宠一行) ──
|
|
184
363
|
h(Section, { title: '宠物' },
|
|
@@ -213,11 +392,28 @@ window.__ModuleLoader__.load({
|
|
|
213
392
|
return ids.map(function (id) {
|
|
214
393
|
var book = (affection && affection[id]) || { affection: 0, total: 0 }
|
|
215
394
|
var name = petName(id)
|
|
395
|
+
var revealed = !!affectionReveal[id]
|
|
396
|
+
var bondDays = book.bond ? (book.bond.value ?? book.bond.days ?? 0) : 0
|
|
216
397
|
return h('div', { key: id, style: row },
|
|
217
398
|
h('span', { style: { fontWeight: 700 } }, name),
|
|
218
|
-
h('span', { style: { fontSize: 18, fontWeight: 700 }
|
|
219
|
-
|
|
220
|
-
h('span', { style: muted }, '
|
|
399
|
+
h('span', { style: revealed ? { fontSize: 18, fontWeight: 700 } : { fontSize: 16, letterSpacing: 4, color: 'rgba(127,127,127,.95)' } },
|
|
400
|
+
revealed ? String(book.affection ?? 0) : '★★★★★★'),
|
|
401
|
+
revealed ? h('span', { style: muted }, '/ 100 · ' + moodOf(book.affection ?? 0) + ' · 羁绊:' + bondTier(bondDays) + '(' + String(bondDays) + ' 天)') : null,
|
|
402
|
+
revealed ? h('span', { style: muted }, '累计 ' + String(book.total ?? 0) + ' 次') : null,
|
|
403
|
+
h('button', {
|
|
404
|
+
type: 'button',
|
|
405
|
+
title: revealed ? '隐藏好感度' : '显示好感度',
|
|
406
|
+
'aria-label': revealed ? '隐藏好感度' : '显示好感度',
|
|
407
|
+
'aria-pressed': revealed,
|
|
408
|
+
onClick: function () {
|
|
409
|
+
setAffectionReveal(function (prev) {
|
|
410
|
+
var next = Object.assign({}, prev)
|
|
411
|
+
next[id] = !prev[id]
|
|
412
|
+
return next
|
|
413
|
+
})
|
|
414
|
+
},
|
|
415
|
+
style: eyeButton,
|
|
416
|
+
}, h(EyeIcon, { off: revealed })))
|
|
221
417
|
})
|
|
222
418
|
})(),
|
|
223
419
|
h('div', { style: row }, h(SButton, { variant: 'danger', disabled: busy, onClick: function () {
|
|
@@ -317,15 +513,113 @@ window.__ModuleLoader__.load({
|
|
|
317
513
|
} }, '普通(含工作工具)'))
|
|
318
514
|
})()),
|
|
319
515
|
h('div', { style: row }, '指定会话(高级)',
|
|
320
|
-
h('input', {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
516
|
+
h('input', {
|
|
517
|
+
style: Object.assign({}, inputStyle, { width: 340, flex: '1 1 260px' }),
|
|
518
|
+
value: bindingDraft !== null ? bindingDraft : savedBinding,
|
|
519
|
+
placeholder: '会话 id,留空 = 自动管理',
|
|
520
|
+
onChange: function (e) { setBindingDraft(e.target.value) },
|
|
521
|
+
onBlur: commitBinding,
|
|
522
|
+
onKeyDown: function (e) { if (e.key === 'Enter') commitBinding() },
|
|
523
|
+
}),
|
|
524
|
+
h(SButton, {
|
|
525
|
+
onClick: testBinding,
|
|
526
|
+
disabled: bindingBusy || (bindingDraft !== null && bindingDraft.trim() !== savedBinding),
|
|
527
|
+
}, bindingBusy ? '测试中…' : '测试')),
|
|
528
|
+
(function () {
|
|
529
|
+
var petBinding = bindingData && bindingData.pets && bindingData.pets.electron
|
|
530
|
+
if (!petBinding) return null
|
|
531
|
+
var bad = !petBinding.usable
|
|
532
|
+
var color = bad ? '#ff7369' : (petBinding.presetWarning || !petBinding.armed ? '#f0b429' : '#3ddc84')
|
|
533
|
+
var parts = []
|
|
534
|
+
parts.push('配置会话 ' + (petBinding.configuredSessionId || '自动管理'))
|
|
535
|
+
parts.push('生效会话 ' + (petBinding.effectiveSessionId || '无'))
|
|
536
|
+
parts.push(petBinding.armed ? '已装载' : '未装载')
|
|
537
|
+
if (petBinding.preset) parts.push('当前模式 ' + petBinding.preset)
|
|
538
|
+
if (petBinding.cwd) parts.push(petBinding.cwd)
|
|
539
|
+
if (petBinding.attach) {
|
|
540
|
+
parts.push(petBinding.attach.ok
|
|
541
|
+
? 'attach ok'
|
|
542
|
+
: (petBinding.attach.errorCode === 'cwd-mismatch'
|
|
543
|
+
? '另一项目(未归组)'
|
|
544
|
+
: 'attach ' + petBinding.attach.errorCode))
|
|
545
|
+
}
|
|
546
|
+
if (petBinding.presetWarning) parts.push('不是宠物模式,没有气泡')
|
|
547
|
+
if (petBinding.fallback) parts.push('已回退到内部会话')
|
|
548
|
+
if (petBinding.staleOwners && petBinding.staleOwners.length) parts.push('旧 owner ' + petBinding.staleOwners.join(','))
|
|
549
|
+
return h('div', { style: { color: color, fontSize: 12, margin: '2px 0 4px', wordBreak: 'break-all' } }, parts.join(' · '))
|
|
550
|
+
})(),
|
|
551
|
+
warnings ? h('div', { style: { color: '#f0b429', fontSize: 12, margin: '2px 0 4px' } }, '提示:' + warnings) : null,
|
|
552
|
+
bindingMsg ? h('div', {
|
|
553
|
+
style: {
|
|
554
|
+
fontSize: 12,
|
|
555
|
+
margin: '2px 0 4px',
|
|
556
|
+
color: /失败|未通过/.test(bindingMsg)
|
|
557
|
+
? '#ff7369'
|
|
558
|
+
: (/通过|可用|已保存/.test(bindingMsg) ? '#3ddc84' : 'rgba(127,127,127,.9)'),
|
|
559
|
+
},
|
|
560
|
+
}, bindingMsg) : null,
|
|
561
|
+
h(Hint, null, '指定会话会校验当前模式:标准模式(没有 pet_say/pet_do)会被拒绝,请使用「精简(仅表达)」或「普通(含工作工具)」的宠物模式会话。会话在别的项目也没关系,唤醒和气泡照常,只是不会归到 whale598 项目下。保存后状态显示「已装载/未装载/失败」,点「测试」可立即验证;清空后回落到她之前的会话。宠物在自己专属的会话里醒来,用气泡和动作自主表达;那条「鲸鱼娘的内心戏」会话可在会话列表查看,也可以直接在那里和它聊天。醒来有两个来源:世界时钟是环境节拍,按上面的间隔和唤醒概率随机轻拍她,用户忙于会话或宠物表达未完成时不会打扰;自主唤醒是她用 pet_schedule_wake 给自己定的闹钟,到点带着当初的打算醒来,不受这个概率影响(上面的「自主唤醒遇用户忙」决定你正忙时她是否照醒,默认到点必醒)。会话模式切换只对新建会话生效:归档旧会话后,她会在下次对话时自动换代重建(旧会话记忆不迁移)。')),
|
|
328
562
|
|
|
563
|
+
h(Section, { title: '静默时段' },
|
|
564
|
+
h('div', { style: row }, '启用静默', h(Toggle, { value: quietOn, onChange: function () {
|
|
565
|
+
if (quietOn) update({ rhythm: { quiet: null } })
|
|
566
|
+
else update({ rhythm: { quiet: { from: '01:00', to: '09:00', allowTouchReply: true } } })
|
|
567
|
+
} })),
|
|
568
|
+
quietOn ? h('div', { style: row },
|
|
569
|
+
'从', h('input', {
|
|
570
|
+
type: 'time',
|
|
571
|
+
style: Object.assign({}, inputStyle, { width: 92 }),
|
|
572
|
+
value: quiet.from || '01:00',
|
|
573
|
+
onChange: function (e) {
|
|
574
|
+
update({ rhythm: { quiet: {
|
|
575
|
+
from: e.target.value,
|
|
576
|
+
to: quiet.to || '09:00',
|
|
577
|
+
allowTouchReply: quiet.allowTouchReply !== false,
|
|
578
|
+
} } })
|
|
579
|
+
},
|
|
580
|
+
}),
|
|
581
|
+
'到', h('input', {
|
|
582
|
+
type: 'time',
|
|
583
|
+
style: Object.assign({}, inputStyle, { width: 92 }),
|
|
584
|
+
value: quiet.to || '09:00',
|
|
585
|
+
onChange: function (e) {
|
|
586
|
+
update({ rhythm: { quiet: {
|
|
587
|
+
from: quiet.from || '01:00',
|
|
588
|
+
to: e.target.value,
|
|
589
|
+
allowTouchReply: quiet.allowTouchReply !== false,
|
|
590
|
+
} } })
|
|
591
|
+
},
|
|
592
|
+
})) : null,
|
|
593
|
+
quietOn ? h('div', { style: row }, '静默时仍回应触摸', h(Toggle, {
|
|
594
|
+
value: quiet.allowTouchReply !== false,
|
|
595
|
+
onChange: function () {
|
|
596
|
+
update({ rhythm: { quiet: {
|
|
597
|
+
from: quiet.from || '01:00',
|
|
598
|
+
to: quiet.to || '09:00',
|
|
599
|
+
allowTouchReply: quiet.allowTouchReply === false,
|
|
600
|
+
} } })
|
|
601
|
+
},
|
|
602
|
+
})) : null,
|
|
603
|
+
h(Hint, null, '静默只压世界时钟;自主唤醒照常,只会在 prompt 里提示“用户可能已睡”;启动问好不变。')),
|
|
604
|
+
h(Section, { title: '连续性本子' },
|
|
605
|
+
h('div', { style: row }, '启用', h(Toggle, { value: journalOn, onChange: function () {
|
|
606
|
+
update({ journal: { enabled: !journalOn } })
|
|
607
|
+
} })),
|
|
608
|
+
journalOn ? h('div', { style: row }, '每次唤醒注入最近',
|
|
609
|
+
h('input', { style: inputStyle, value: String(journal.injectRecent ?? 3), onChange: function (e) {
|
|
610
|
+
var v = parseInt(e.target.value, 10)
|
|
611
|
+
if (Number.isFinite(v)) update({ journal: { injectRecent: Math.min(20, Math.max(1, v)) } })
|
|
612
|
+
} }), '条') : null,
|
|
613
|
+
journalOn ? h('div', { style: row }, '允许无绑定会话追加', h(Toggle, {
|
|
614
|
+
value: journal.allowUnboundAppend === true,
|
|
615
|
+
onChange: function () { update({ journal: { allowUnboundAppend: journal.allowUnboundAppend !== true } }) },
|
|
616
|
+
})) : null,
|
|
617
|
+
h(Hint, null, '本子在 ~/.dsh/whale598/<petId>/journal.md:可读、可追加、不静默删改;手写本子已导入。默认只有宠物自己的会话可以追加。')),
|
|
618
|
+
h(Section, { title: '你不在的时候' },
|
|
619
|
+
h('div', { style: row }, '回来时给我一份摘要', h(Toggle, { value: presenceOn, onChange: function () {
|
|
620
|
+
update({ presenceSummary: { enabled: !presenceOn } })
|
|
621
|
+
} })),
|
|
622
|
+
h(Hint, null, '默认关闭;打开后,你回到桌面时她最多用三行说自己这段时间醒了几次、做了什么、有没有自排计划。')),
|
|
329
623
|
h(Section, { title: h('span', null, '自由活动 ', h('span', { style: { fontSize: 11, fontWeight: 400, color: 'rgba(127,127,127,.9)' } }, '逻辑驱动')) },
|
|
330
624
|
h('div', { style: row }, '自由活动(宠物自娱)', h(Toggle, { value: pro.idleAnimation !== false, onChange: function () {
|
|
331
625
|
update({ proactivity: { idleAnimation: !(pro.idleAnimation !== false) } })
|