dsh-whale-live 0.1.0 → 0.2.0

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
@@ -74,6 +74,21 @@ The model acts **through** these tools; the plugin never writes to DSH long-term
74
74
  - **Bilibili**: uses the unofficial `ajax/msg` endpoint; it can change or rate-limit, and some rooms return empty. Read-only — the plugin never sends danmaku.
75
75
  - **VTube Studio**: optional. Without a mouth driver directory the plugin degrades to plain audio playback.
76
76
 
77
+ ## Compatibility range (explicit)
78
+
79
+ | Axis | Declared | Notes |
80
+ | --- | --- | --- |
81
+ | OS | `win32` only | `os: ["win32"]`; MCI playback and the app launch helpers are Windows APIs |
82
+ | DSH | `>=0.2.1-rc.2 <0.4.0` | Verified on `0.2.2-rc.1` (desktop profile) |
83
+ | Node | `>=20` | Uses global `fetch`, ESM top-level await |
84
+ | Python | 3.10+ with `edge-tts` | Required only for speech; omit `pythonPath` to run silent |
85
+ | Bilibili | cookie JSON | Uses the unofficial `ajax/msg` endpoint; read-only; may break or rate-limit |
86
+ | VTube Studio | optional | Set `vtsPath` or leave it empty to launch through Steam; `vtubeDir` enables mouth sync |
87
+ | OBS / 直播姬 | optional | Launch helpers only; the plugin never starts the stream |
88
+
89
+ Anything outside these ranges is unsupported. DSH slot/service APIs are the main
90
+ breaking risk across minor releases.
91
+
77
92
  ## Privacy
78
93
 
79
94
  - No telemetry; no accounts; nothing is uploaded.
package/client.js CHANGED
@@ -23,6 +23,12 @@ window.__ModuleLoader__.load({
23
23
  tabConsciousness: '意识',
24
24
  tabDiary: '观察日记',
25
25
  tabCandidates: '入忆候选',
26
+ tabPrepare: '准备',
27
+ prepareHint: '开播前先把设备架好:拉起 VTube Studio / OBS / 直播间页面,只准备,不推流;自检会逐项报告缺什么。',
28
+ stageBtn: '架好设备',
29
+ diagnoseBtn: '自检',
30
+ prepareSummary: '{ok}/{total} 通过 · {failed} 项必须修 · {warned} 项可选',
31
+ stageDone: '设备已拉起(没有自动推流)。',
26
32
  stateLive: '直播中',
27
33
  stateIdle: '未开播',
28
34
  stat: '说话 {says} · 弹幕 {danmaku} · 记 {notes}',
@@ -65,6 +71,12 @@ window.__ModuleLoader__.load({
65
71
  tabConsciousness: 'Consciousness',
66
72
  tabDiary: 'Field diary',
67
73
  tabCandidates: 'Memory inbox',
74
+ tabPrepare: 'Prepare',
75
+ prepareHint: 'Set the gear up before going live: VTube Studio, OBS and the room page. Preparation only — nothing starts streaming.',
76
+ stageBtn: 'Set up',
77
+ diagnoseBtn: 'Self-check',
78
+ prepareSummary: '{ok}/{total} passed · {failed} required · {warned} optional',
79
+ stageDone: 'Gear is up (streaming was not started).',
68
80
  stateLive: 'Live',
69
81
  stateIdle: 'Off air',
70
82
  stat: '{says} spoken · {danmaku} danmaku · {notes} notes',
@@ -196,6 +208,8 @@ window.__ModuleLoader__.load({
196
208
  const [draft, setDraft] = useState('');
197
209
  const [doc, setDoc] = useState({ kind: '', text: '', loading: false, error: '' });
198
210
  const [docTick, setDocTick] = useState(0);
211
+ const [diag, setDiag] = useState(null);
212
+ const [diagLoading, setDiagLoading] = useState(false);
199
213
  const alive = useRef(true);
200
214
 
201
215
  useEffect(() => {
@@ -258,6 +272,22 @@ window.__ModuleLoader__.load({
258
272
  }
259
273
  }, [flash, t]);
260
274
 
275
+ const loadDiag = useCallback(async () => {
276
+ setDiagLoading(true);
277
+ try {
278
+ const data = await getJson('/diagnose');
279
+ if (alive.current) setDiag(data);
280
+ } catch (cause) {
281
+ flash(`${t('actionFailed')}:${cause instanceof Error ? cause.message : String(cause)}`);
282
+ } finally {
283
+ if (alive.current) setDiagLoading(false);
284
+ }
285
+ }, [flash, t]);
286
+
287
+ useEffect(() => {
288
+ if (tab === 'prepare') loadDiag();
289
+ }, [tab, loadDiag]);
290
+
261
291
  const onSay = useCallback(async () => {
262
292
  const text = draft.trim();
263
293
  if (!text) return;
@@ -278,6 +308,7 @@ window.__ModuleLoader__.load({
278
308
 
279
309
  const tabs = [
280
310
  ['live', t('tabLive')],
311
+ ['prepare', t('tabPrepare')],
281
312
  ['consciousness', t('tabConsciousness')],
282
313
  ['diary', t('tabDiary')],
283
314
  ['candidates', t('tabCandidates')],
@@ -397,6 +428,56 @@ window.__ModuleLoader__.load({
397
428
  : h('p', { key: 'none', className: 'dshwl-muted' }, t('noEvents')),
398
429
  ]);
399
430
  }
431
+ } else if (tab === 'prepare') {
432
+ body = h('div', {}, [
433
+ h('p', { key: 'hint', className: 'dshwl-hint' }, t('prepareHint')),
434
+ h('div', { key: 'actions', className: 'dshwl-actions' }, [
435
+ h('button', {
436
+ key: 'stage',
437
+ type: 'button',
438
+ className: 'dshwl-btn primary',
439
+ disabled: busy !== '',
440
+ onClick: async () => {
441
+ const result = await run('stage', 'stage');
442
+ if (result) {
443
+ flash(t('stageDone'));
444
+ loadDiag();
445
+ }
446
+ },
447
+ }, t('stageBtn')),
448
+ h('button', {
449
+ key: 'diag',
450
+ type: 'button',
451
+ className: 'dshwl-btn',
452
+ disabled: diagLoading,
453
+ onClick: loadDiag,
454
+ }, diagLoading ? '…' : t('diagnoseBtn')),
455
+ session ? null : h('button', {
456
+ key: 'live',
457
+ type: 'button',
458
+ className: 'dshwl-btn',
459
+ disabled: busy !== '',
460
+ onClick: () => run('session', 'open'),
461
+ }, t('open')),
462
+ ]),
463
+ diag
464
+ ? h('p', { key: 'sum', className: 'dshwl-muted' }, fill(t('prepareSummary'), diag.summary))
465
+ : (diagLoading ? h('p', { key: 'load', className: 'dshwl-muted' }, t('loading')) : null),
466
+ diag
467
+ ? h('div', { key: 'list', className: 'dshwl-checks' }, diag.checks.map((item) => h('div', {
468
+ key: item.id,
469
+ className: 'dshwl-check',
470
+ }, [
471
+ h('span', {
472
+ key: 'mark',
473
+ className: `dshwl-check-mark ${item.ok ? 'ok' : item.optional ? 'warn' : 'fail'}`,
474
+ }, item.ok ? '✓' : item.optional ? '!' : '✗'),
475
+ h('span', { key: 'label', className: 'dshwl-check-label' }, item.label),
476
+ h('span', { key: 'detail', className: 'dshwl-check-detail' }, item.detail),
477
+ !item.ok && item.hint ? h('span', { key: 'hint', className: 'dshwl-check-hint' }, item.hint) : null,
478
+ ])))
479
+ : null,
480
+ ]);
400
481
  } else if (tab === 'candidates') {
401
482
  body = h('div', {}, [
402
483
  h('p', { key: 'note', className: 'dshwl-hint' }, t('candNote')),
@@ -517,6 +598,15 @@ window.__ModuleLoader__.load({
517
598
  .dshwl-toolbar{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:8px}
518
599
  .dshwl-muted{color:var(--dsw-alias-label-secondary,#6b7280);font-size:11px;margin:4px 0}
519
600
  .dshwl-error{border:1px solid var(--dsw-alias-state-error-primary,#d33);color:var(--dsw-alias-state-error-primary,#d33);border-radius:8px;padding:7px 10px;margin:10px 0;font-size:12px;word-break:break-word}
601
+ .dshwl-checks{display:flex;flex-direction:column;gap:6px;margin-top:12px}
602
+ .dshwl-check{display:grid;grid-template-columns:24px 120px 1fr;gap:8px;align-items:baseline;padding:7px 10px;border:1px solid var(--dsw-alias-border-l1,#e5e7eb);border-radius:9px;background:var(--dsw-alias-bg-layer-1,#fff);font-size:12px}
603
+ .dshwl-check-mark{font-weight:700;text-align:center}
604
+ .dshwl-check-mark.ok{color:var(--dsw-alias-state-success-primary,#15803d)}
605
+ .dshwl-check-mark.warn{color:var(--dsw-alias-state-warn-primary,#b45309)}
606
+ .dshwl-check-mark.fail{color:var(--dsw-alias-state-error-primary,#d33)}
607
+ .dshwl-check-label{font-weight:600}
608
+ .dshwl-check-detail{color:var(--dsw-alias-label-secondary,#6b7280);word-break:break-word}
609
+ .dshwl-check-hint{grid-column:2 / 4;color:var(--dsw-alias-state-warn-primary,#b45309);font-size:11px}
520
610
  .dshwl-foot{margin:22px 0 0;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l1,#e5e7eb);color:var(--dsw-alias-label-secondary,#6b7280);font-size:11px;line-height:1.6}
521
611
  .dshwl-notice{position:sticky;bottom:0;margin-top:12px;border:1px solid var(--dsw-alias-border-l2,#d1d5db);background:var(--dsw-alias-bg-overlay,#fff);border-radius:8px;padding:7px 10px;font-size:12px;color:var(--dsw-alias-label-primary,#1f2329);box-shadow:0 2px 10px rgba(0,0,0,.06);word-break:break-word}
522
612
  `;
package/cordis.patch.yml CHANGED
@@ -1,12 +1,11 @@
1
1
  # dsh-whale-live bundle patch.
2
2
  #
3
- # One Host row: live sessions, the two persona documents (consciousness / diary),
4
- # Bilibili danmaku, edge-tts speech and the memory-candidate inbox. The Client
5
- # half adds the left-sidebar entry and the panel.
3
+ # One Host row: live sessions, the persona documents, Bilibili danmaku,
4
+ # edge-tts speech, the memory inbox, and the pre-live "set up the gear" helper.
6
5
  #
7
- # Defaults below are placeholders. Fill `roomId`, `credentialFile` and
8
- # `pythonPath` before going live. An empty `dataDir` uses
9
- # `~/.dsh/whale-live`; `vtubeDir` is optional (VTube Studio mouth sync).
6
+ # Defaults are placeholders. Fill `roomId`, `credentialFile`, `pythonPath`
7
+ # before going live. Empty paths disable that integration (VTS / OBS / 直播姬).
8
+ # Compatibility: Windows only, DSH >=0.2.1-rc.2 <0.4.0 (tested 0.2.2-rc.1).
10
9
  - insert:
11
10
  - id: whale-live
12
11
  name: 'dsh-whale-live'
@@ -17,7 +16,12 @@
17
16
  credentialFile: ''
18
17
  pythonPath: ''
19
18
  vtubeDir: ''
19
+ obsPath: ''
20
+ livehimePath: ''
21
+ vtsPath: ''
22
+ stageWaitSec: 90
23
+ openRoomPage: true
20
24
  voice: zh-CN-XiaoyiNeural
21
25
  rate: '+8%'
22
26
  pitch: '+4Hz'
23
- listenWaitSec: 45
27
+ listenWaitSec: 45
package/index.js CHANGED
@@ -15,6 +15,8 @@ import { createStore } from './lib/store.js';
15
15
  import { createDanmaku } from './lib/bili.js';
16
16
  import { createVoice } from './lib/voice.js';
17
17
  import { createAvatar } from './lib/avatar.js';
18
+ import { createStage } from './lib/stage.js';
19
+ import { diagnose } from './lib/diagnostics.js';
18
20
  import { createTools } from './lib/tools.js';
19
21
  import { registerHttp } from './lib/http.js';
20
22
 
@@ -31,6 +33,11 @@ export async function apply(ctx, config = {}) {
31
33
  vtubeDir: '',
32
34
  vtsPort: 8001,
33
35
  vtsSteamApp: '1325860',
36
+ obsPath: '',
37
+ livehimePath: '',
38
+ vtsPath: '',
39
+ stageWaitSec: 90,
40
+ openRoomPage: true,
34
41
  voice: 'zh-CN-XiaoyiNeural',
35
42
  rate: '+8%',
36
43
  pitch: '+4Hz',
@@ -46,14 +53,15 @@ export async function apply(ctx, config = {}) {
46
53
  store.ensure();
47
54
  const danmaku = createDanmaku(settings, ctx.logger);
48
55
  const avatar = createAvatar(settings, ctx.logger);
56
+ const stage = createStage(settings, avatar, ctx.logger);
49
57
  const voice = createVoice(settings, store, ctx.logger, avatar);
50
- const tools = createTools({ ctx, store, danmaku, voice, avatar, config: settings });
58
+ const tools = createTools({ ctx, store, danmaku, voice, avatar, stage, diagnose, config: settings });
51
59
 
52
60
  ctx.effect(() => {
53
61
  const disposers = [];
54
62
  try {
55
63
  for (const tool of tools) disposers.push(ctx.tools.register(tool));
56
- const disposeHttp = registerHttp(ctx, { store, danmaku, voice, avatar, config: settings, logger: ctx.logger });
64
+ const disposeHttp = registerHttp(ctx, { store, danmaku, voice, avatar, stage, diagnose, config: settings, logger: ctx.logger });
57
65
  if (disposeHttp) disposers.push(disposeHttp);
58
66
  disposers.push(() => voice.dispose());
59
67
  } catch (error) {
@@ -0,0 +1,98 @@
1
+ /**
2
+ * 装好之后的自检:不装任何东西,只如实报告「少了什么、去哪补」。
3
+ * 软件(OBS / VTube Studio / 直播姬)需要人自己装;Python 依赖给 pip 提示。
4
+ */
5
+ import { execFile } from 'node:child_process';
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+
9
+ function run(file, args, timeoutMs = 12000) {
10
+ return new Promise((resolve) => {
11
+ execFile(file, args, { timeout: timeoutMs, windowsHide: true }, (error, stdout, stderr) => {
12
+ resolve({ ok: !error, out: String(stdout ?? '').trim(), err: String(stderr ?? '').trim(), error: error ? error.message : null });
13
+ });
14
+ });
15
+ }
16
+
17
+ function check(id, label, ok, detail, hint = '', optional = false) {
18
+ return { id, label, ok: Boolean(ok), detail: String(detail ?? ''), hint: String(hint ?? ''), optional };
19
+ }
20
+
21
+ export async function diagnose({ store, danmaku, avatar, stage, config }) {
22
+ const checks = [];
23
+
24
+ try {
25
+ fs.accessSync(store.paths.dataDir, fs.constants.W_OK);
26
+ checks.push(check('data', '数据目录', true, store.paths.dataDir));
27
+ } catch (error) {
28
+ checks.push(check('data', '数据目录', false, String(error?.message ?? error), '把 dataDir 改到可写目录'));
29
+ }
30
+
31
+ for (const [id, label, file] of [
32
+ ['consciousness', '意识卡', store.paths.docs.consciousness],
33
+ ['diary', '观察日记', store.paths.docs.diary],
34
+ ]) {
35
+ try {
36
+ const size = Math.round(fs.statSync(file).size / 1024);
37
+ checks.push(check(id, label, true, `${size} KB`));
38
+ } catch {
39
+ checks.push(check(id, label, false, '文件不存在', '插件启动时会自动种一份'));
40
+ }
41
+ }
42
+
43
+ const pythonPath = String(config.pythonPath || '');
44
+ if (!pythonPath) {
45
+ checks.push(check('python', 'Python / edge-tts', false, '没配 pythonPath', '填一个装了 edge-tts 的 python.exe', true));
46
+ } else if (!fs.existsSync(pythonPath)) {
47
+ checks.push(check('python', 'Python / edge-tts', false, `找不到 ${pythonPath}`, '检查路径'));
48
+ } else {
49
+ const r = await run(pythonPath, ['-c', 'import edge_tts; print(getattr(edge_tts, "__version__", "ok"))']);
50
+ checks.push(check('python', 'Python / edge-tts', r.ok, r.ok ? `edge-tts ${r.out}` : (r.err || r.error || '导入失败'), r.ok ? '' : 'pip install edge-tts'));
51
+ }
52
+
53
+ const credentialFile = String(config.credentialFile || '');
54
+ if (!credentialFile) {
55
+ checks.push(check('cred', 'B站凭据', false, '没配 credentialFile', '导出 cookie JSON 后填路径', true));
56
+ } else if (!fs.existsSync(credentialFile)) {
57
+ checks.push(check('cred', 'B站凭据', false, `找不到 ${credentialFile}`, '检查路径'));
58
+ } else {
59
+ try {
60
+ const data = JSON.parse(fs.readFileSync(credentialFile, 'utf8'));
61
+ const missing = ['sessdata', 'bili_jct', 'buvid3', 'dedeuserid'].filter((key) => !data[key]);
62
+ checks.push(check('cred', 'B站凭据', missing.length === 0, missing.length ? `缺 ${missing.join(',')}` : '字段齐全'));
63
+ } catch {
64
+ checks.push(check('cred', 'B站凭据', false, 'JSON 解析失败', '重新导出 cookie'));
65
+ }
66
+ }
67
+
68
+ const room = await danmaku.roomInfo();
69
+ if (room?.roomId) {
70
+ checks.push(check('room', '直播间', true, `${room.title || '(无标题)'} · ${room.liveStatus ? '直播中' : '未开播'} · 在线 ${room.online ?? '?'}`));
71
+ } else {
72
+ checks.push(check('room', '直播间', false, '房号没配或接口不可达', '检查 roomId / 网络', true));
73
+ }
74
+
75
+ const skin = await avatar.status();
76
+ checks.push(check('vts', 'VTube Studio', skin.up, skin.up ? `8001 端口可用` : '还没开', '面板点「架好设备」会拉起', true));
77
+
78
+ const vtubeDir = String(config.vtubeDir || '');
79
+ if (!vtubeDir) {
80
+ checks.push(check('bridge', 'VTS 嘴型驱动', false, '没配 vtubeDir', '可选:有驱动目录才能动嘴', true));
81
+ } else {
82
+ const missing = ['vts_speak.py', 'vts_client.py'].filter((file) => !fs.existsSync(path.join(vtubeDir, file)));
83
+ checks.push(check('bridge', 'VTS 嘴型驱动', missing.length === 0, missing.length ? `缺 ${missing.join(',')}` : '就绪', '', true));
84
+ }
85
+
86
+ const apps = await stage.appStatus();
87
+ checks.push(check('obs', 'OBS', apps.obs.running || (apps.obs.configured && apps.obs.exists),
88
+ apps.obs.running ? '运行中' : apps.obs.configured ? (apps.obs.exists ? '没运行(架设备时会拉起)' : '路径不存在') : '没配 obsPath',
89
+ '可选;填 obs64.exe 路径', true));
90
+ checks.push(check('livehime', '直播姬', apps.livehime.running || (apps.livehime.configured && apps.livehime.exists),
91
+ apps.livehime.running ? '运行中' : apps.livehime.configured ? (apps.livehime.exists ? '没运行(架设备时会拉起)' : '路径不存在') : '没配 livehimePath',
92
+ '可选;填 livehime.exe 路径', true));
93
+
94
+ const ok = checks.filter((item) => item.ok).length;
95
+ const failed = checks.filter((item) => !item.ok && !item.optional).length;
96
+ const warned = checks.filter((item) => !item.ok && item.optional).length;
97
+ return { checks, summary: { total: checks.length, ok, failed, warned } };
98
+ }
package/lib/http.js CHANGED
@@ -83,7 +83,7 @@ function sendJson(response, status, value) {
83
83
  response.end(JSON.stringify(value));
84
84
  }
85
85
 
86
- export function registerHttp(ctx, { store, danmaku, voice, avatar, config, logger }) {
86
+ export function registerHttp(ctx, { store, danmaku, voice, avatar, stage, diagnose, config, logger }) {
87
87
  async function snapshot() {
88
88
  const state = store.state();
89
89
  const skin = await avatar.status();
@@ -147,6 +147,12 @@ export function registerHttp(ctx, { store, danmaku, voice, avatar, config, logge
147
147
  }
148
148
  return { avatar: await avatar.status() };
149
149
  }
150
+ if (action === 'stage') {
151
+ return { stage: await stage.stage() };
152
+ }
153
+ if (action === 'diagnose') {
154
+ return { diagnose: await diagnose({ store, danmaku, avatar, stage, config }) };
155
+ }
150
156
  if (action === 'note') {
151
157
  const text = String(body.text ?? '').trim();
152
158
  if (!text) throw Object.assign(new Error('没有要记的内容'), { code: 'INVALID_INPUT' });
@@ -212,6 +218,10 @@ export function registerHttp(ctx, { store, danmaku, voice, avatar, config, logge
212
218
  sendJson(response, 200, { ok: true, data: info });
213
219
  return;
214
220
  }
221
+ if (route === '/diagnose') {
222
+ sendJson(response, 200, { ok: true, data: await diagnose({ store, danmaku, avatar, stage, config }) });
223
+ return;
224
+ }
215
225
  sendJson(response, 404, { ok: false, error: 'not found' });
216
226
  return;
217
227
  }
package/lib/stage.js ADDED
@@ -0,0 +1,123 @@
1
+ /**
2
+ * 开播前的「架设备」:Steam / VTube Studio / OBS / 直播姬 / 直播间页面。
3
+ * 只负责“准备好”,不开始推流;已经在跑的不会重复拉。
4
+ */
5
+ import { execFile, spawn } from 'node:child_process';
6
+ import fs from 'node:fs';
7
+
8
+ function run(file, args, timeoutMs = 12000) {
9
+ return new Promise((resolve) => {
10
+ execFile(file, args, { timeout: timeoutMs, windowsHide: true }, (error, stdout, stderr) => {
11
+ resolve({ ok: !error, out: String(stdout ?? ''), err: String(stderr ?? ''), error: error ? error.message : null });
12
+ });
13
+ });
14
+ }
15
+
16
+ function tasklistHas(imageName) {
17
+ return run('tasklist', ['/FI', `IMAGENAME eq ${imageName}`, '/NH']).then((result) => {
18
+ if (!result.ok) return false;
19
+ return result.out.toLowerCase().includes(imageName.toLowerCase());
20
+ });
21
+ }
22
+
23
+ export function createStage(config = {}, avatar, logger = null) {
24
+ const obsPath = String(config.obsPath || '');
25
+ const livehimePath = String(config.livehimePath || '');
26
+ const vtsPath = String(config.vtsPath || '');
27
+ const stageWaitSec = Math.max(20, Math.min(180, Number(config.stageWaitSec) || 90));
28
+ const roomId = Number(config.roomId) || 0;
29
+ const roomUrl = String(config.roomUrl || (roomId ? `https://live.bilibili.com/${roomId}` : ''));
30
+ const openRoomPage = config.openRoomPage !== false;
31
+
32
+ async function appStatus() {
33
+ const [skin, obs, livehime, steam] = await Promise.all([
34
+ avatar.status(),
35
+ tasklistHas('obs64.exe'),
36
+ tasklistHas('livehime.exe'),
37
+ tasklistHas('steam.exe'),
38
+ ]);
39
+ return {
40
+ steam: { running: steam },
41
+ vts: { running: skin.up },
42
+ obs: { configured: Boolean(obsPath), exists: Boolean(obsPath) && fs.existsSync(obsPath), running: obs, path: obsPath },
43
+ livehime: {
44
+ configured: Boolean(livehimePath),
45
+ exists: Boolean(livehimePath) && fs.existsSync(livehimePath),
46
+ running: livehime,
47
+ path: livehimePath,
48
+ },
49
+ };
50
+ }
51
+
52
+ function launch(file, args, label) {
53
+ try {
54
+ const child = spawn(file, args, { detached: true, stdio: 'ignore', windowsHide: true });
55
+ child.unref();
56
+ logger?.info?.(`whale-live: 已启动 ${label || file}`);
57
+ return { ok: true };
58
+ } catch (error) {
59
+ return { ok: false, error: error?.message ?? String(error) };
60
+ }
61
+ }
62
+
63
+ /** 架镜头:四件软件 + 直播间页面,全部只“准备好”。 */
64
+ async function stage() {
65
+ const steps = [];
66
+ const apps = await appStatus();
67
+
68
+ steps.push({
69
+ id: 'steam',
70
+ ok: apps.steam.running,
71
+ optional: true,
72
+ text: apps.steam.running ? 'Steam 已在运行' : 'Steam 没开(VTS 要靠它拉起)',
73
+ });
74
+
75
+ if (apps.vts.running) {
76
+ steps.push({ id: 'vts', ok: true, text: 'VTube Studio 已在运行' });
77
+ } else if (vtsPath && fs.existsSync(vtsPath)) {
78
+ const started = launch(vtsPath, [], 'VTube Studio');
79
+ steps.push({ id: 'vts', ok: started.ok, text: started.ok ? 'VTube Studio 已直接拉起(等它开 API)' : `VTS 启动失败:${started.error}` });
80
+ } else {
81
+ const opened = avatar.open();
82
+ steps.push({
83
+ id: 'vts',
84
+ ok: opened.launched === true,
85
+ text: opened.launched ? '已请 Steam 拉起 VTube Studio(冷启动会慢)' : `VTS 拉不起来:${opened.error || '未知'}`,
86
+ });
87
+ }
88
+
89
+ if (!apps.obs.configured) {
90
+ steps.push({ id: 'obs', ok: false, optional: true, text: '没配 obsPath,跳过 OBS' });
91
+ } else if (!apps.obs.exists) {
92
+ steps.push({ id: 'obs', ok: false, text: `OBS 路径不存在:${obsPath}` });
93
+ } else if (apps.obs.running) {
94
+ steps.push({ id: 'obs', ok: true, text: 'OBS 已在运行(没推流)' });
95
+ } else {
96
+ const started = launch(obsPath, [], 'OBS');
97
+ steps.push({ id: 'obs', ok: started.ok, text: started.ok ? 'OBS 已拉起(没推流)' : `OBS 启动失败:${started.error}` });
98
+ }
99
+
100
+ if (!apps.livehime.configured) {
101
+ steps.push({ id: 'livehime', ok: false, optional: true, text: '没配 livehimePath,跳过直播姬' });
102
+ } else if (!apps.livehime.exists) {
103
+ steps.push({ id: 'livehime', ok: false, text: `直播姬路径不存在:${livehimePath}` });
104
+ } else if (apps.livehime.running) {
105
+ steps.push({ id: 'livehime', ok: true, text: '直播姬已在运行' });
106
+ } else {
107
+ const started = launch(livehimePath, [], '直播姬');
108
+ steps.push({ id: 'livehime', ok: started.ok, text: started.ok ? '直播姬已拉起' : `直播姬启动失败:${started.error}` });
109
+ }
110
+
111
+ if (openRoomPage && roomUrl) {
112
+ const page = launch('cmd.exe', ['/c', 'start', '', roomUrl], '直播间页面');
113
+ steps.push({ id: 'room', ok: page.ok, text: page.ok ? `已打开直播间页面:${roomUrl}` : `打开页面失败:${page.error}` });
114
+ } else {
115
+ steps.push({ id: 'room', ok: false, optional: true, text: '没开直播间页面(房号为空或 openRoomPage=false)' });
116
+ }
117
+
118
+ const ready = await avatar.wait(stageWaitSec);
119
+ return { steps, avatar: ready, apps };
120
+ }
121
+
122
+ return { stage, appStatus, roomUrl };
123
+ }
package/lib/store.js CHANGED
@@ -23,7 +23,7 @@ const DOC_FILES = {
23
23
  const CONSCIOUSNESS_SEED = `# 意识
24
24
 
25
25
  > 这是我自己的经历。不是聊天记录,是我记得、会影响我的那些事。
26
- > 每场直播结束,我会挑几件最核心的写进来。主人也可以改。
26
+ > 每场直播结束,我会挑几件最核心的写进来。你也可以改。
27
27
 
28
28
  ## 我是谁
29
29
 
package/lib/tools.js CHANGED
@@ -31,7 +31,7 @@ function timeOf(iso) {
31
31
  }
32
32
  }
33
33
 
34
- export function createTools({ ctx, store, danmaku, voice, avatar, config }) {
34
+ export function createTools({ ctx, store, danmaku, voice, avatar, stage, diagnose, config }) {
35
35
  const roomName = config.roomName || '直播间';
36
36
  const roomId = config.roomId;
37
37
 
@@ -72,6 +72,8 @@ export function createTools({ ctx, store, danmaku, voice, avatar, config }) {
72
72
  - close:下播,给你本场摘要,让你自己挑 1-3 件最想留下的。
73
73
  - propose / memory_list / commit / reject:入忆候选的流转。propose 只是排队,不是写记忆;真正写记忆要你自己调用 cortex_write。
74
74
  - avatar:皮套(VTube Studio)——status 看开没开,open 让 Steam 拉起来,wait 等它的 8001 就绪。
75
+ - stage:架设备——拉起 VTS / OBS / 直播间页面,但不开始推流,方便开播前调试。
76
+ - diagnose:自检——数据目录、两本戏本、Python/edge-tts、B站凭据、直播间、VTS、嘴型驱动、OBS 逐项报告。
75
77
  - read / status:翻文件、看状态。
76
78
 
77
79
  那条线要记住:直播里说的话、聊的人,只活在 dataDir 的两个文件里;cortex 只收你下播后亲手挑过、写过的几件事。`,
@@ -82,7 +84,7 @@ export function createTools({ ctx, store, danmaku, voice, avatar, config }) {
82
84
  properties: {
83
85
  action: {
84
86
  type: 'string',
85
- enum: ['open', 'status', 'read', 'note', 'listen', 'close', 'propose', 'memory_list', 'commit', 'reject', 'avatar'],
87
+ enum: ['open', 'status', 'read', 'note', 'listen', 'close', 'propose', 'memory_list', 'commit', 'reject', 'avatar', 'stage', 'diagnose'],
86
88
  description: '要做什么。',
87
89
  },
88
90
  kind: {
@@ -303,6 +305,34 @@ export function createTools({ ctx, store, danmaku, voice, avatar, config }) {
303
305
  }
304
306
  }
305
307
 
308
+ if (action === 'stage') {
309
+ const result = await stage.stage();
310
+ const lines = result.steps.map((step) => `${step.ok ? '✓' : step.optional ? '·' : '✗'} ${step.text}`);
311
+ return {
312
+ text: [
313
+ '架设备完成(不会开始推流,OBS 里还要你自己点「开始推流」):',
314
+ ...lines,
315
+ '',
316
+ `VTS:${result.avatar?.up ? '就绪' : '还没就绪(可能要等几十秒)'};OBS:${result.apps?.obs?.running ? '在运行' : '未运行'};直播姬:${result.apps?.livehime?.running ? '在运行' : '未运行'}`,
317
+ ].join('\n'),
318
+ };
319
+ }
320
+
321
+ if (action === 'diagnose') {
322
+ const result = await diagnose({ store, danmaku, avatar, stage, config });
323
+ const lines = result.checks.map((item) => {
324
+ const mark = item.ok ? '✓' : item.optional ? '!' : '✗';
325
+ const hint = item.ok || !item.hint ? '' : `(${item.hint})`;
326
+ return `${mark} ${item.label}:${item.detail}${hint}`;
327
+ });
328
+ return {
329
+ text: [
330
+ `自检:${result.summary.ok}/${result.summary.total} 通过 · ${result.summary.failed} 项必须修 · ${result.summary.warned} 项可选`,
331
+ ...lines,
332
+ ].join('\n'),
333
+ };
334
+ }
335
+
306
336
  return { text: `不认识的动作:${String(action)}` };
307
337
  },
308
338
  };
package/package.json CHANGED
@@ -1,76 +1,78 @@
1
- {
2
- "name": "dsh-whale-live",
3
- "version": "0.1.0",
4
- "description": "A left-sidebar live console for DeepSeek Harness: persona script, per-stream log, Bilibili danmaku, edge-tts speech and a hand-picked memory inbox. Windows only.",
5
- "type": "module",
6
- "main": "./index.js",
7
- "icon": "./icon.svg",
8
- "exports": {
9
- ".": "./index.js",
10
- "./client": "./client.js",
11
- "./package.json": "./package.json",
12
- "./locale/*.json": "./locale/*.json",
13
- "./icon": "./icon.svg"
14
- },
15
- "files": [
16
- "index.js",
17
- "client.js",
18
- "lib",
19
- "runtime",
20
- "cordis.patch.yml",
21
- "locale/*.json",
22
- "icon.svg",
23
- "README.md",
24
- "LICENSE"
25
- ],
26
- "dsh": {
27
- "manifestVersion": 1,
28
- "bundle": {
29
- "patch": "./cordis.patch.yml"
30
- },
31
- "client": {
32
- "platform": "web",
33
- "immediately": true,
34
- "inject": []
35
- },
36
- "compatibility": {
37
- "dsh": ">=0.2.1-rc.2 <0.4.0",
38
- "node": ">=20",
39
- "profiles": [
40
- "desktop",
41
- "web"
42
- ]
43
- }
44
- },
45
- "engines": {
46
- "node": ">=20"
47
- },
48
- "os": [
49
- "win32"
50
- ],
51
- "scripts": {
52
- "check": "node --check index.js && node --check client.js",
53
- "test": "node --test test/store.test.mjs && node test/apply.test.mjs"
54
- },
55
- "keywords": [
56
- "dsh",
57
- "dsh-plugin",
58
- "deepseek-harness",
59
- "cordis-plugin",
60
- "bilibili",
61
- "live",
62
- "persona",
63
- "edge-tts",
64
- "vtuber"
65
- ],
66
- "repository": {
67
- "type": "git",
68
- "url": "git+https://github.com/Lh1314-China/dsh-whale-live.git"
69
- },
70
- "homepage": "https://github.com/Lh1314-China/dsh-whale-live#readme",
71
- "bugs": {
72
- "url": "https://github.com/Lh1314-China/dsh-whale-live/issues"
73
- },
74
- "author": "Lh1314-China",
75
- "license": "MIT"
76
- }
1
+ {
2
+ "name": "dsh-whale-live",
3
+ "version": "0.2.0",
4
+ "description": "Left-sidebar live console for DeepSeek Harness: persona script, stream log, Bilibili danmaku, edge-tts speech, optional VTube Studio/OBS/直播姬 launch helpers and a memory inbox. Windows only.",
5
+ "type": "module",
6
+ "main": "./index.js",
7
+ "icon": "./icon.svg",
8
+ "exports": {
9
+ ".": "./index.js",
10
+ "./client": "./client.js",
11
+ "./package.json": "./package.json",
12
+ "./locale/*.json": "./locale/*.json",
13
+ "./icon": "./icon.svg"
14
+ },
15
+ "files": [
16
+ "index.js",
17
+ "client.js",
18
+ "lib",
19
+ "runtime",
20
+ "cordis.patch.yml",
21
+ "locale/*.json",
22
+ "icon.svg",
23
+ "README.md",
24
+ "LICENSE"
25
+ ],
26
+ "dsh": {
27
+ "manifestVersion": 1,
28
+ "bundle": {
29
+ "patch": "./cordis.patch.yml"
30
+ },
31
+ "client": {
32
+ "platform": "web",
33
+ "immediately": true,
34
+ "inject": [
35
+
36
+ ]
37
+ },
38
+ "compatibility": {
39
+ "dsh": "\u003e=0.2.1-rc.2 \u003c0.4.0",
40
+ "node": "\u003e=20",
41
+ "profiles": [
42
+ "desktop",
43
+ "web"
44
+ ]
45
+ }
46
+ },
47
+ "engines": {
48
+ "node": "\u003e=20"
49
+ },
50
+ "os": [
51
+ "win32"
52
+ ],
53
+ "scripts": {
54
+ "check": "node --check index.js \u0026\u0026 node --check client.js",
55
+ "test": "node --test test/store.test.mjs \u0026\u0026 node test/apply.test.mjs"
56
+ },
57
+ "keywords": [
58
+ "dsh",
59
+ "dsh-plugin",
60
+ "deepseek-harness",
61
+ "cordis-plugin",
62
+ "bilibili",
63
+ "live",
64
+ "persona",
65
+ "edge-tts",
66
+ "vtuber"
67
+ ],
68
+ "repository": {
69
+ "type": "git",
70
+ "url": "git+https://github.com/Lh1314-China/dsh-whale-live.git"
71
+ },
72
+ "homepage": "https://github.com/Lh1314-China/dsh-whale-live#readme",
73
+ "bugs": {
74
+ "url": "https://github.com/Lh1314-China/dsh-whale-live/issues"
75
+ },
76
+ "author": "Lh1314-China",
77
+ "license": "MIT"
78
+ }