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.
@@ -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
- return entries;
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
- console.log(`[pet] assets root: ${resolveAssetsRoot()} actions=${String(actionCatalog.length)}`);
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;
@@ -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 (d && d.error) setMsg('失败: ' + d.error)
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 } }, String(book.affection ?? 0)),
219
- 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) + ' 天)'),
220
- h('span', { style: muted }, '累计 ' + String(book.total ?? 0) + ' 次'))
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', { style: Object.assign({}, inputStyle, { width: 340, flex: '1 1 260px' }), value: String((iv.boundSessions && iv.boundSessions.electron) || ''), placeholder: '会话 id,留空 = 自动管理', onChange: function (e) {
321
- var v = e.target.value.trim()
322
- var next = Object.assign({}, iv.boundSessions || {})
323
- if (v !== '') next.electron = v
324
- else delete next.electron
325
- update({ innerVoice: { boundSessions: next } })
326
- } })),
327
- h(Hint, null, '宠物在自己专属的会话里醒来,用气泡和动作自主表达;那条「鲸鱼娘的内心戏」会话可在会话列表查看,也可以直接在那里和它聊天。醒来有两个来源:世界时钟是环境节拍,按上面的间隔和唤醒概率随机轻拍她,用户忙于会话或宠物表达未完成时不会打扰;自主唤醒是她用 pet_schedule_wake 给自己定的闹钟,到点带着当初的打算醒来,不受这个概率影响(上面的「自主唤醒遇用户忙」决定你正忙时她是否照醒,默认到点必醒)。会话模式切换只对新建会话生效:归档旧会话后,她会在下次对话时自动换代重建(旧会话记忆不迁移)。指定会话立即生效——下一次对话就用新绑定的会话;清空后自动回落到她之前的会话(那条若已被归档,会自动创建新的)。')),
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) } })