coopani-world-music 0.3.2 → 0.3.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 +13 -0
- package/package.json +1 -1
- package/src/config.ts +16 -0
- package/src/peer.ts +56 -0
- package/src/world.ts +11 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# 更新日志
|
|
2
2
|
|
|
3
|
+
## 0.3.3
|
|
4
|
+
|
|
5
|
+
**打炉石时让路(可选,默认关)**
|
|
6
|
+
|
|
7
|
+
设置页「她怎么开口」里多了一个开关。打开后,检测到炉石在跑,就不主动开口
|
|
8
|
+
(换歌、段落都不说);你戳她还是照答,关掉炉石自动恢复。
|
|
9
|
+
|
|
10
|
+
默认关是有意的 —— 两件事同时进行、各说各的,很多人反而喜欢,不该替人决定。
|
|
11
|
+
注意:炉石挂在后台不动也算「在跑」,这时同样会安静。
|
|
12
|
+
|
|
13
|
+
实现上不碰新接口:两个扩展之间本来就没有互相调用的通道(宿主只给了「往同一个对话
|
|
14
|
+
推事件」这一条),所以让路靠读进程表,不需要跨扩展通信。
|
|
15
|
+
|
|
3
16
|
## 0.3.2
|
|
4
17
|
|
|
5
18
|
**市场简介补上听歌专用的四个动作**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coopani-world-music",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "陪着你听歌:读 Windows 的媒体会话,知道 QQ音乐 / 网易云音乐在放什么——换歌必说一句,一首歌里分几段接话,能看到当前这一段的歌词、就着词说两句,表情和动作跟着曲风与段落变,为听歌另做了律动、打拍子、闭眼听、蹦迪四个小动作(宿主没有时会自动降级);记得住你听过什么,能替你按暂停、切下一首,也能自己去曲库里搜一首想听的挑给你。",
|
|
6
6
|
"author": {
|
package/src/config.ts
CHANGED
|
@@ -47,6 +47,16 @@ export interface MusicConfigSection extends WorldSection {
|
|
|
47
47
|
lyricAware: boolean;
|
|
48
48
|
/** 一次给她几行歌词。给多了她会复述,给少了没法评。 */
|
|
49
49
|
lyricLines: number;
|
|
50
|
+
/**
|
|
51
|
+
* 打炉石时让路。
|
|
52
|
+
*
|
|
53
|
+
* 打开后:检测到 `Hearthstone.exe` 在跑就不主动开口(换歌、段落都不说),
|
|
54
|
+
* 你戳她她还是照答 —— 让的是「主动播报」,不是「拒绝回答」。
|
|
55
|
+
*
|
|
56
|
+
* **默认关。** 两件事同时进行、各说各的,这是很多人喜欢的状态;
|
|
57
|
+
* 只有嫌吵的人才需要打开。看到进程也不等于正在对局,挂在后台也会让路。
|
|
58
|
+
*/
|
|
59
|
+
yieldToHearthstone: boolean;
|
|
50
60
|
}
|
|
51
61
|
|
|
52
62
|
export const MUSIC_DEFAULTS = (): MusicConfigSection => ({
|
|
@@ -66,6 +76,7 @@ export const MUSIC_DEFAULTS = (): MusicConfigSection => ({
|
|
|
66
76
|
pickWhen: 'ask',
|
|
67
77
|
lyricAware: true,
|
|
68
78
|
lyricLines: 2,
|
|
79
|
+
yieldToHearthstone: false,
|
|
69
80
|
});
|
|
70
81
|
|
|
71
82
|
const P = (field: string): string => `worlds.${MUSIC_ID}.${field}`;
|
|
@@ -100,6 +111,11 @@ export function musicConfigGroups(): ConfigGroup[] {
|
|
|
100
111
|
maximum: 8,
|
|
101
112
|
'x-suffix': '段',
|
|
102
113
|
},
|
|
114
|
+
[P('yieldToHearthstone')]: {
|
|
115
|
+
type: 'boolean',
|
|
116
|
+
title: '打炉石时让路',
|
|
117
|
+
description: '打开后:检测到炉石在跑就不主动开口(换歌、段落都不说),你戳她还是照答;打完自动恢复。默认关——两件事同时进行、各说各的,很多人反而喜欢。注意:炉石挂在后台不动也算「在跑」,这时同样会安静。',
|
|
118
|
+
},
|
|
103
119
|
[P('segmentEverySec')]: {
|
|
104
120
|
type: 'integer',
|
|
105
121
|
title: '一段至少多长',
|
package/src/peer.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 对端扩展对应的程序在不在跑。
|
|
3
|
+
*
|
|
4
|
+
* 为什么需要这一层:两个扩展装在同一个宿主里,各推各的事件,**互相不认识** ——
|
|
5
|
+
* 宿主只给了「往同一个对话推事件」这一条路,没有 world 之间互查状态的接口。
|
|
6
|
+
* 而「打炉石的时候别聊歌」这种需求是真的,进程列表就是唯一不需要跨扩展通信、
|
|
7
|
+
* 也不用改宿主就能拿到的信号。
|
|
8
|
+
*
|
|
9
|
+
* 代价要说清楚:**看到进程 ≠ 正在对局**。炉石挂在后台不动也算在跑,这时音乐同样会安静。
|
|
10
|
+
* 所以它只能躲在用户自己打开的开关后面,不能默认开。
|
|
11
|
+
*/
|
|
12
|
+
import { execFile } from 'node:child_process';
|
|
13
|
+
import { promisify } from 'node:util';
|
|
14
|
+
|
|
15
|
+
const run = promisify(execFile);
|
|
16
|
+
|
|
17
|
+
/** 缓存时长:tasklist 一次要一两百毫秒,不能每次要判断都跑一遍。 */
|
|
18
|
+
const TTL_MS = 15_000;
|
|
19
|
+
|
|
20
|
+
let lastAt = 0;
|
|
21
|
+
let lastHit = false;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 名单里任一进程在跑就返回 true。
|
|
25
|
+
*
|
|
26
|
+
* 查询失败时返回 false(当作没在跑)—— 这个方向是刻意的:宁可多陪几句,
|
|
27
|
+
* 也不要因为一次 tasklist 失败就让她整段安静下来,那种"突然不说话"最难排查。
|
|
28
|
+
*/
|
|
29
|
+
export async function isPeerRunning(names: readonly string[]): Promise<boolean> {
|
|
30
|
+
const now = Date.now();
|
|
31
|
+
if (now - lastAt < TTL_MS) return lastHit;
|
|
32
|
+
|
|
33
|
+
let hit = false;
|
|
34
|
+
try {
|
|
35
|
+
// /FO CSV 一行一个进程,形如 "Hearthstone.exe","1234","Console","1","1,234 K"
|
|
36
|
+
const { stdout } = await run('tasklist', ['/NH', '/FO', 'CSV'], {
|
|
37
|
+
timeout: 4000,
|
|
38
|
+
windowsHide: true,
|
|
39
|
+
maxBuffer: 4 << 20,
|
|
40
|
+
});
|
|
41
|
+
const lower = stdout.toLowerCase();
|
|
42
|
+
hit = names.some((name) => lower.includes(`"${name.toLowerCase()}"`));
|
|
43
|
+
} catch {
|
|
44
|
+
hit = false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
lastAt = now;
|
|
48
|
+
lastHit = hit;
|
|
49
|
+
return hit;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 测试用:把缓存清掉。 */
|
|
53
|
+
export function resetPeerCache(): void {
|
|
54
|
+
lastAt = 0;
|
|
55
|
+
lastHit = false;
|
|
56
|
+
}
|
package/src/world.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
Logger, StoragePart, ToolDef, World, WorldHost, WorldWebUiDecl, WorldLamp,
|
|
15
15
|
} from 'cortico/core/types.ts';
|
|
16
16
|
import { MUSIC_ID, musicConfigGroups, type MusicConfigSection } from './config.ts';
|
|
17
|
+
import { isPeerRunning } from './peer.ts';
|
|
17
18
|
import { readHostVocab } from './host-vocab.ts';
|
|
18
19
|
import { NowPlayingBridge, type TickPayload } from './nowplaying.ts';
|
|
19
20
|
import { pickSession, playerLabel, type PickResult } from './players.ts';
|
|
@@ -960,6 +961,16 @@ export class MusicWorld implements World {
|
|
|
960
961
|
}
|
|
961
962
|
if (!text) return;
|
|
962
963
|
|
|
964
|
+
// 让路:开关打开、并且炉石在跑,就不主动开口。
|
|
965
|
+
//
|
|
966
|
+
// 让的是「主动播报」这一层 —— 被戳时的回答走工具调用,不经过这里,所以照样会答。
|
|
967
|
+
// 默认关:两件事同时进行、各说各的,很多人就喜欢那样,不该由我们替他们决定。
|
|
968
|
+
// 顺带记一行日志:以后有人说「她怎么不说话了」,先看这里是不是让路了。
|
|
969
|
+
if (this.cfg.yieldToHearthstone && await isPeerRunning(['Hearthstone.exe'])) {
|
|
970
|
+
this.log?.info(`炉石在跑,${decl.type} 让路(不主动开口)。`);
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
|
|
963
974
|
try {
|
|
964
975
|
await host.pushEvent({
|
|
965
976
|
type: decl.type,
|