dsh-whale-live 0.2.1 → 0.2.2

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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.2 — 2026-11-12
4
+
5
+ ### Added
6
+ - **Real-time watching.** `whale_live action=watch` starts a 4-second danmaku poll; when new messages arrive the plugin calls the Agent's `followup()` to wake the model, so it can answer while the stream is live. `watch step=stop` (or closing the session) stops it.
7
+ - The plugin now captures the calling Agent from tool execution so it can be woken later.
8
+
9
+ ### Notes
10
+ - If `followup` is unavailable in a future DSH version, the plugin falls back to `inject` (context only, no wake) instead of failing the loop.
3
11
  ## 0.2.1 — 2026-11-11
4
12
 
5
13
  ### Fixed
package/lib/tools.js CHANGED
@@ -7,6 +7,9 @@
7
7
  * 这里只写直播那个“她”的东西;cortex 由本鲸自己动手写。
8
8
  */
9
9
 
10
+ import fs from 'node:fs';
11
+ import path from 'node:path';
12
+
10
13
  const TEXT_OUTPUT = Object.freeze({
11
14
  schema: {
12
15
  type: 'object',
@@ -32,6 +35,83 @@ function timeOf(iso) {
32
35
  }
33
36
 
34
37
  export function createTools({ ctx, store, danmaku, voice, avatar, vts, stage, diagnose, config }) {
38
+ let lastAgent = null;
39
+ let watchTimer = null;
40
+ let watchBusy = false;
41
+
42
+ function stopWatch() {
43
+ if (watchTimer) {
44
+ clearInterval(watchTimer);
45
+ watchTimer = null;
46
+ }
47
+ }
48
+
49
+ /** 叫醒本鲸:优先 followup;签名不对就换一种;都不行就 inject(只进上下文不唤醒)。 */
50
+ function wakeAgent(text) {
51
+ const agent = lastAgent;
52
+ if (!agent) return false;
53
+ try {
54
+ agent.followup(text);
55
+ return true;
56
+ } catch {
57
+ try {
58
+ agent.followup({ role: 'user', content: text });
59
+ return true;
60
+ } catch {
61
+ try {
62
+ agent.inject(text);
63
+ } catch {
64
+ /* 叫不醒也不能影响轮询 */
65
+ }
66
+ return false;
67
+ }
68
+ }
69
+ }
70
+
71
+ async function watchTick() {
72
+ if (watchBusy || !store.state().session) return;
73
+ watchBusy = true;
74
+ try {
75
+ const { messages } = await danmaku.pollOnce();
76
+ if (!messages.length) return;
77
+ for (const message of messages) {
78
+ store.appendEvent({ kind: 'danmaku', user: message.user, text: message.text, self: message.self === true });
79
+ }
80
+ const lines = messages.map((message) => `${message.user}:${message.text}`).join('\n');
81
+ wakeAgent(`[直播间新弹幕]\n${lines}\n\n(去看看,想接就接;不想接也可以记一笔。)`);
82
+ } catch {
83
+ /* 轮询失败下一轮再来 */
84
+ } finally {
85
+ watchBusy = false;
86
+ }
87
+ }
88
+
89
+ /** 探测 Agent 上有什么方法(只记录名字和类型,不调用)。 */
90
+ function probeAgent(agent) {
91
+ if (!agent) return;
92
+ lastAgent = agent;
93
+ try {
94
+ const own = Object.getOwnPropertyNames(agent);
95
+ const proto = Object.getPrototypeOf(agent);
96
+ const protoNames = proto ? Object.getOwnPropertyNames(proto) : [];
97
+ const types = {};
98
+ for (const name of [...new Set([...own, ...protoNames])]) {
99
+ try {
100
+ types[name] = typeof agent[name];
101
+ } catch {
102
+ types[name] = 'unreadable';
103
+ }
104
+ }
105
+ fs.writeFileSync(
106
+ path.join(store.paths.dataDir, 'agent-probe.json'),
107
+ JSON.stringify({ when: new Date().toISOString(), own, proto: protoNames, types }, null, 2),
108
+ 'utf8',
109
+ );
110
+ } catch {
111
+ /* 探测失败不影响正常使用 */
112
+ }
113
+ }
114
+
35
115
  const roomName = config.roomName || '直播间';
36
116
  const roomId = config.roomId;
37
117
 
@@ -84,7 +164,7 @@ export function createTools({ ctx, store, danmaku, voice, avatar, vts, stage, di
84
164
  properties: {
85
165
  action: {
86
166
  type: 'string',
87
- enum: ['open', 'status', 'read', 'note', 'listen', 'close', 'propose', 'memory_list', 'commit', 'reject', 'avatar', 'vts', 'stage', 'diagnose'],
167
+ enum: ['open', 'status', 'read', 'note', 'listen', 'close', 'propose', 'memory_list', 'commit', 'reject', 'avatar', 'vts', 'stage', 'diagnose', 'watch'],
88
168
  description: '要做什么。',
89
169
  },
90
170
  kind: {
@@ -166,7 +246,8 @@ export function createTools({ ctx, store, danmaku, voice, avatar, vts, stage, di
166
246
  timeoutMs: 130000,
167
247
  presentCall: () => ({ card: 'generic', title: '鲸鱼直播台', kind: 'other' }),
168
248
 
169
- async execute(args = {}) {
249
+ async execute(args = {}, exec = {}) {
250
+ probeAgent(exec?.agent);
170
251
  const action = args.action;
171
252
 
172
253
  if (action === 'open') {
@@ -264,6 +345,22 @@ export function createTools({ ctx, store, danmaku, voice, avatar, vts, stage, di
264
345
  return { text: `收到 ${result.messages.length} 条:\n${lines.join('\n')}\n\n接下来看你怎么接。` };
265
346
  }
266
347
 
348
+ if (action === 'watch') {
349
+ const step = args.step || 'start';
350
+ if (step === 'stop') {
351
+ stopWatch();
352
+ return { text: 'watch 已停。' };
353
+ }
354
+ if (step === 'test') {
355
+ const ok = wakeAgent('[测试] 本鲸被叫醒了。');
356
+ return { text: ok ? '叫醒测试已发出,看看本鲸会不会醒。' : '叫醒失败:没有 agent 或 followup 不可用。' };
357
+ }
358
+ if (!store.state().session) return { text: '先 action=open 开一场再 watch。' };
359
+ stopWatch();
360
+ watchTimer = setInterval(watchTick, 4000);
361
+ return { text: 'watch 已开:每 4 秒看一次弹幕,有新话就叫醒本鲸;下播/stop 会停。' };
362
+ }
363
+
267
364
  if (action === 'close') {
268
365
  const meta = store.closeSession({ text: args.text });
269
366
  if (!meta) return { text: '现在没有开着的场。' };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-whale-live",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
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",