dsh-whale-live 0.2.2 → 0.2.3
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/CHANGELOG.md +9 -0
- package/client.js +11 -0
- package/index.js +3 -2
- package/lib/http.js +6 -1
- package/lib/tools.js +23 -11
- package/lib/watch.js +70 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.3 — 2026-11-12
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **Watch toggle in the panel.** The Prepare tab now has a "Watch chat / Stop watching" button, so the host can start or stop the 4-second danmaku poll without asking the model. The toggle reads the live state from `/state` and writes through `action=watch`.
|
|
7
|
+
- The watch timer is shared between the model tool and the panel, so either side can start/stop it; the model is still the one woken by `followup()`.
|
|
8
|
+
|
|
9
|
+
### Notes
|
|
10
|
+
- Starting watch requires an open session; it stops when the session closes or the plugin reloads.
|
|
11
|
+
- Falls back to `inject` (context only) if `followup` is unavailable.
|
|
3
12
|
## 0.2.2 — 2026-11-12
|
|
4
13
|
|
|
5
14
|
### Added
|
package/client.js
CHANGED
|
@@ -27,6 +27,8 @@ window.__ModuleLoader__.load({
|
|
|
27
27
|
prepareHint: '开播前先把设备架好:拉起 VTube Studio / OBS / 直播间页面,只准备,不推流;自检会逐项报告缺什么。',
|
|
28
28
|
stageBtn: '架好设备',
|
|
29
29
|
diagnoseBtn: '自检',
|
|
30
|
+
watchStart: '蹲点',
|
|
31
|
+
watchStop: '停止蹲点',
|
|
30
32
|
prepareSummary: '{ok}/{total} 通过 · {failed} 项必须修 · {warned} 项可选',
|
|
31
33
|
stageDone: '设备已拉起(没有自动推流)。',
|
|
32
34
|
stateLive: '直播中',
|
|
@@ -75,6 +77,8 @@ window.__ModuleLoader__.load({
|
|
|
75
77
|
prepareHint: 'Set the gear up before going live: VTube Studio, OBS and the room page. Preparation only — nothing starts streaming.',
|
|
76
78
|
stageBtn: 'Set up',
|
|
77
79
|
diagnoseBtn: 'Self-check',
|
|
80
|
+
watchStart: 'Watch chat',
|
|
81
|
+
watchStop: 'Stop watching',
|
|
78
82
|
prepareSummary: '{ok}/{total} passed · {failed} required · {warned} optional',
|
|
79
83
|
stageDone: 'Gear is up (streaming was not started).',
|
|
80
84
|
stateLive: 'Live',
|
|
@@ -452,6 +456,13 @@ window.__ModuleLoader__.load({
|
|
|
452
456
|
disabled: diagLoading,
|
|
453
457
|
onClick: loadDiag,
|
|
454
458
|
}, diagLoading ? '…' : t('diagnoseBtn')),
|
|
459
|
+
h('button', {
|
|
460
|
+
key: 'watch',
|
|
461
|
+
type: 'button',
|
|
462
|
+
className: 'dshwl-btn',
|
|
463
|
+
disabled: busy !== '' || !session,
|
|
464
|
+
onClick: () => run('watch', 'watch', { step: state?.watch?.running ? 'stop' : 'start' }),
|
|
465
|
+
}, state?.watch?.running ? t('watchStop') : t('watchStart')),
|
|
455
466
|
session ? null : h('button', {
|
|
456
467
|
key: 'live',
|
|
457
468
|
type: 'button',
|
package/index.js
CHANGED
|
@@ -57,13 +57,14 @@ export async function apply(ctx, config = {}) {
|
|
|
57
57
|
const vts = createVts(settings, ctx.logger);
|
|
58
58
|
const stage = createStage(settings, avatar, ctx.logger);
|
|
59
59
|
const voice = createVoice(settings, store, ctx.logger, avatar);
|
|
60
|
-
const
|
|
60
|
+
const watchApi = {};
|
|
61
|
+
const tools = createTools({ ctx, store, danmaku, voice, avatar, vts, stage, diagnose, watchApi, config: settings });
|
|
61
62
|
|
|
62
63
|
ctx.effect(() => {
|
|
63
64
|
const disposers = [];
|
|
64
65
|
try {
|
|
65
66
|
for (const tool of tools) disposers.push(ctx.tools.register(tool));
|
|
66
|
-
const disposeHttp = registerHttp(ctx, { store, danmaku, voice, avatar, stage, diagnose, config: settings, logger: ctx.logger });
|
|
67
|
+
const disposeHttp = registerHttp(ctx, { store, danmaku, voice, avatar, stage, watchApi, diagnose, config: settings, logger: ctx.logger });
|
|
67
68
|
if (disposeHttp) disposers.push(disposeHttp);
|
|
68
69
|
disposers.push(() => voice.dispose());
|
|
69
70
|
} catch (error) {
|
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, stage, diagnose, config, logger }) {
|
|
86
|
+
export function registerHttp(ctx, { store, danmaku, voice, avatar, stage, watchApi, diagnose, config, logger }) {
|
|
87
87
|
async function snapshot() {
|
|
88
88
|
const state = store.state();
|
|
89
89
|
const skin = await avatar.status();
|
|
@@ -105,6 +105,7 @@ export function registerHttp(ctx, { store, danmaku, voice, avatar, stage, diagno
|
|
|
105
105
|
recent: state.session ? store.recentEvents(state.session.id, 80) : [],
|
|
106
106
|
voice: voice.status(),
|
|
107
107
|
danmaku: danmaku.status(),
|
|
108
|
+
watch: watchApi?.status?.() ?? null,
|
|
108
109
|
avatar: skin,
|
|
109
110
|
config: {
|
|
110
111
|
roomId: config.roomId,
|
|
@@ -150,6 +151,10 @@ export function registerHttp(ctx, { store, danmaku, voice, avatar, stage, diagno
|
|
|
150
151
|
if (action === 'stage') {
|
|
151
152
|
return { stage: await stage.stage() };
|
|
152
153
|
}
|
|
154
|
+
if (action === 'watch') {
|
|
155
|
+
if (!watchApi?.start) throw Object.assign(new Error('watch 还没就绪'), { code: 'NOT_READY' });
|
|
156
|
+
return { watch: body.step === 'stop' ? watchApi.stop() : watchApi.start() };
|
|
157
|
+
}
|
|
153
158
|
if (action === 'diagnose') {
|
|
154
159
|
return { diagnose: await diagnose({ store, danmaku, avatar, stage, config }) };
|
|
155
160
|
}
|
package/lib/tools.js
CHANGED
|
@@ -34,7 +34,7 @@ function timeOf(iso) {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
export function createTools({ ctx, store, danmaku, voice, avatar, vts, stage, diagnose, config }) {
|
|
37
|
+
export function createTools({ ctx, store, danmaku, voice, avatar, vts, stage, diagnose, watchApi, config }) {
|
|
38
38
|
let lastAgent = null;
|
|
39
39
|
let watchTimer = null;
|
|
40
40
|
let watchBusy = false;
|
|
@@ -46,25 +46,21 @@ export function createTools({ ctx, store, danmaku, voice, avatar, vts, stage, di
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
/**
|
|
49
|
+
/** 叫醒本鲸:followup/inject 都收 UserMessage 对象,绝不能再塞裸字符串(会写坏会话日志)。 */
|
|
50
50
|
function wakeAgent(text) {
|
|
51
51
|
const agent = lastAgent;
|
|
52
52
|
if (!agent) return false;
|
|
53
|
+
const message = { content: [{ type: 'text', text }], source: { kind: 'user' } };
|
|
53
54
|
try {
|
|
54
|
-
agent.followup(
|
|
55
|
+
agent.followup(message);
|
|
55
56
|
return true;
|
|
56
57
|
} catch {
|
|
57
58
|
try {
|
|
58
|
-
agent.
|
|
59
|
-
return true;
|
|
59
|
+
agent.inject(message);
|
|
60
60
|
} catch {
|
|
61
|
-
|
|
62
|
-
agent.inject(text);
|
|
63
|
-
} catch {
|
|
64
|
-
/* 叫不醒也不能影响轮询 */
|
|
65
|
-
}
|
|
66
|
-
return false;
|
|
61
|
+
/* 叫不醒也不能影响轮询 */
|
|
67
62
|
}
|
|
63
|
+
return false;
|
|
68
64
|
}
|
|
69
65
|
}
|
|
70
66
|
|
|
@@ -86,6 +82,22 @@ export function createTools({ ctx, store, danmaku, voice, avatar, vts, stage, di
|
|
|
86
82
|
}
|
|
87
83
|
}
|
|
88
84
|
|
|
85
|
+
// 面板也用它开/关蹲点。
|
|
86
|
+
if (watchApi) {
|
|
87
|
+
watchApi.status = () => ({ running: Boolean(watchTimer), hasAgent: Boolean(lastAgent) });
|
|
88
|
+
watchApi.start = () => {
|
|
89
|
+
if (watchTimer) return { running: true, already: true };
|
|
90
|
+
if (!store.state().session) return { running: false, error: '还没开播' };
|
|
91
|
+
watchTimer = setInterval(watchTick, 4000);
|
|
92
|
+
return { running: true };
|
|
93
|
+
};
|
|
94
|
+
watchApi.stop = () => {
|
|
95
|
+
stopWatch();
|
|
96
|
+
return { running: false };
|
|
97
|
+
};
|
|
98
|
+
watchApi.wake = wakeAgent;
|
|
99
|
+
}
|
|
100
|
+
|
|
89
101
|
/** 探测 Agent 上有什么方法(只记录名字和类型,不调用)。 */
|
|
90
102
|
function probeAgent(agent) {
|
|
91
103
|
if (!agent) return;
|
package/lib/watch.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 实时蹲点:轮询弹幕 + 叫醒 Agent。
|
|
3
|
+
* 共享给工具(模型侧)和面板(用户点开关)。
|
|
4
|
+
*/
|
|
5
|
+
export function createWatch({ danmaku, store, logger = null }) {
|
|
6
|
+
let timer = null;
|
|
7
|
+
let agent = null;
|
|
8
|
+
let busy = false;
|
|
9
|
+
|
|
10
|
+
function setAgent(value) {
|
|
11
|
+
if (value) agent = value;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function wake(text) {
|
|
15
|
+
if (!agent) return false;
|
|
16
|
+
try {
|
|
17
|
+
agent.followup(text);
|
|
18
|
+
return true;
|
|
19
|
+
} catch {
|
|
20
|
+
try {
|
|
21
|
+
agent.followup({ role: 'user', content: text });
|
|
22
|
+
return true;
|
|
23
|
+
} catch {
|
|
24
|
+
try {
|
|
25
|
+
agent.inject(text);
|
|
26
|
+
} catch {
|
|
27
|
+
/* 叫不醒也不能影响轮询 */
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function tick() {
|
|
35
|
+
if (busy || !store.state().session) return;
|
|
36
|
+
busy = true;
|
|
37
|
+
try {
|
|
38
|
+
const { messages } = await danmaku.pollOnce();
|
|
39
|
+
if (!messages.length) return;
|
|
40
|
+
for (const message of messages) {
|
|
41
|
+
store.appendEvent({ kind: 'danmaku', user: message.user, text: message.text, self: message.self === true });
|
|
42
|
+
}
|
|
43
|
+
const lines = messages.map((message) => `${message.user}:${message.text}`).join('\n');
|
|
44
|
+
wake(`[直播间新弹幕]\n${lines}\n\n(去看看,想接就接;不想接也可以记一笔。)`);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
logger?.warn?.(`whale-live: watch 轮询失败 ${error instanceof Error ? error.message : String(error)}`);
|
|
47
|
+
} finally {
|
|
48
|
+
busy = false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function start() {
|
|
53
|
+
if (timer) return { running: true, already: true };
|
|
54
|
+
if (!store.state().session) return { running: false, error: '还没开播,先 open 或点「开播」' };
|
|
55
|
+
timer = setInterval(tick, 4000);
|
|
56
|
+
return { running: true };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function stop() {
|
|
60
|
+
if (timer) clearInterval(timer);
|
|
61
|
+
timer = null;
|
|
62
|
+
return { running: false };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function status() {
|
|
66
|
+
return { running: Boolean(timer), hasAgent: Boolean(agent) };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { setAgent, wake, start, stop, status, tick };
|
|
70
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-whale-live",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
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
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|