coopani-world-music 0.3.13 → 0.3.14

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,40 @@
1
1
  # 更新日志
2
2
 
3
+ ## 0.3.14
4
+
5
+ **换台机器装扩展,动作自动补齐了**
6
+
7
+ 十一个自定义动作(蹦迪 / 跟着哼 / 坐着哼 / 怔住 …)一直住在**宿主桌宠包里**,
8
+ 不是本扩展的代码 —— 所以从市场装到一台新机器时,它们本来就是缺的。
9
+ 缺了会静默降级:`蹦迪` 退回原生 `dance`,原地踩拍、不横移。
10
+ 用户在另一台机器上的原话是「**蹦是蹦了,就是没走**」,这七个字就是降级的指纹。
11
+
12
+ 现在每次启动都会检查宿主词表,缺什么补什么:
13
+
14
+ - **动手前先备份**到 `<data>/host-backup/<时间戳>/`,只留最近三份
15
+ - **一处锚点对不上就一处都不写** —— 半截补丁比没打更难查
16
+ (白名单里多了词、派发里没这一段,她收到会当成不认识的动作用掉,表现得像"偶尔站住不动")
17
+ - 补完**要重启一次才生效**:桌宠页面已经把旧的 `body.js` 读进内存了,
18
+ 这一轮仍然按旧词表降级,日志会写明
19
+ - 任何异常都不许把扩展带崩:打不上就打不上,降级照样能听歌
20
+
21
+ 设置页「记录与位置」多了开关 **自动补齐桌宠动作**(默认开)。关掉就只用宿主原生的那几个。
22
+
23
+ **顺带修了两个「她整首歌站着不动」的原因**
24
+
25
+ 1. `plain`(没标路数、字面也猜不出)原来被当成「这首淡,不该动」,动作返回空。
26
+ 于是整首歌一个动作都没有,而且 `lastMove` 不更新,**避让机制整条失效**。
27
+ 现在给中性的「打拍子」——不挑曲风、不带路数色彩。
28
+ 2. 表情那一格空了,会把整句动作候选一起吞掉。现在两件事解耦:
29
+ 没有表情就只写动作。
30
+
31
+ **已知偏差(没有假装验证过)**:补丁表里部分条目的注释文本与本机实际文件不完全一致
32
+ (开发期直接改过宿主文件,脚本没逐字同步)。功能不受影响,但换台机器补出来的文件
33
+ 在注释上会有细微差别。要彻底对齐,得按当前宿主重新生成一遍补丁表。
34
+
35
+ 测试 89 项(88 通过 / 1 跳过),新增 6 项专测补丁层:
36
+ 打上、幂等、锚点失败一处不写、CRLF 宿主、宿主目录不存在、备份只留三份。
37
+
3
38
  ## 0.3.13
4
39
 
5
40
  **动作从 4 个扩到 11 个,而且不再由她自己挑**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coopani-world-music",
3
- "version": "0.3.13",
3
+ "version": "0.3.14",
4
4
  "type": "module",
5
5
  "description": "陪着你听歌:知道 QQ音乐 / 网易云音乐 在放什么。换歌必说一句,一首歌里分几段接话,还能就着刚唱过的词评两句;表情动作跟着曲风变,另有律动、打拍子、闭眼听、蹦迪四个专属动作。记得住你听过什么——记忆可以存到你指定的文件夹,公司和家里接同一份。能替你暂停切歌,也能自己搜歌挑给你。和「陪打炉石」一起装更有意思:打牌时听的每一首她都知道。嫌吵能让其中一个先安静。",
6
6
  "author": {
package/src/config.ts CHANGED
@@ -1,283 +1,301 @@
1
- /**
2
- * 配置段与设置页。
3
- *
4
- * 每一个字段都必须有读取点(见 world.ts 里的 this.cfg.xxx);只声明不接线,
5
- * 使用者改了没效果而且不报错——这是这个框架最容易骗人的一处。
6
- *
7
- * 设置页按「会改的东西」分组,不按代码结构分组:说话节奏 / 读哪边 / 特殊时刻 / 记录与权限。
8
- * 顺序就是看见的顺序,最常动的放最前。
9
- */
10
- import type { ConfigGroup } from 'cortico/core/config-schema.ts';
11
- import type { WorldSection } from 'cortico/world.ts';
12
- import type { PlayerPref } from './players.ts';
13
- import { suggestedMemoryDir } from './storage.ts';
14
-
15
- export const MUSIC_ID = 'music';
16
-
17
- /** 开口频率。把"别太吵"做成代码里的规则,而不是指望模型自律。 */
18
- export type Chatinness = 'quiet' | 'normal';
19
-
20
- export interface MusicConfigSection extends WorldSection {
21
- /** 她多爱接话。 */
22
- chatiness: Chatinness;
23
- /** 同时开着两个播放器时听谁的。 */
24
- player: PlayerPref;
25
- /** 采样间隔(毫秒)。改完要重启扩展实例。 */
26
- intervalMs: number;
27
- /** 额外认作播放器的 AUMID 片段,逗号分隔。 */
28
- extraPlayers: string;
29
- /** 深夜起点小时(0–23),窗口到 05:00。 */
30
- nightFrom: number;
31
- /** 连续听多久算"久听"。 */
32
- longMinutes: number;
33
- /** 听歌记录保留多少天。 */
34
- keepDays: number;
35
- /**
36
- * 记忆存到哪个文件夹。留空=跟着这台机器。
37
- *
38
- * 填一个两边都看得见的目录(OneDrive、NAS、移动硬盘),公司和家里就是同一份记忆。
39
- * 只影响**记忆**类数据;当前播放状态和歌词缓存永远在本机,见 storage.ts。
40
- */
41
- memoryDir: string;
42
- /** 允许她代为暂停/切歌。 */
43
- controlAllowed: boolean;
44
- /** 一首歌切成多长一段(秒)。每段结束推一条事件,让她就这一段说一句。0 = 不评论。 */
45
- segmentEverySec: number;
46
- /** 每首最多说几段。短歌靠它压次数,长曲靠它把段长拉开。 */
47
- maxSegmentsPerTrack: number;
48
- /** 允许她自己搜歌、挑歌。关了就只有看着的份。 */
49
- selfPick: boolean;
50
- /** 她上哪找歌。auto = 两个库都搜。 */
51
- pickSource: 'auto' | 'qq' | 'netease';
52
- /** 她什么时候可以主动挑:ask = 你问或话题合适时;idle = 静下来没歌时自己挑一首。 */
53
- pickWhen: 'ask' | 'idle';
54
- /** 允许她读这首歌的歌词。关了就只有听感,说不出词里在讲什么。 */
55
- lyricAware: boolean;
56
- /** 一次给她几行歌词。给多了她会复述,给少了没法评。 */
57
- lyricLines: number;
58
- /**
59
- * 打炉石时让路。
60
- *
61
- * 打开后:检测到 `Hearthstone.exe` 在跑就不主动开口(换歌、段落都不说),
62
- * 你戳她她还是照答 —— 让的是「主动播报」,不是「拒绝回答」。
63
- *
64
- * **默认关。** 两件事同时进行、各说各的,这是很多人喜欢的状态;
65
- * 只有嫌吵的人才需要打开。看到进程也不等于正在对局,挂在后台也会让路。
66
- */
67
- yieldToHearthstone: boolean;
68
- }
69
-
70
- export const MUSIC_DEFAULTS = (): MusicConfigSection => ({
71
- enabled: false, // 由 WorldSection 提供,扩展不能自己决定装上就开
72
- chatiness: 'normal',
73
- player: 'auto',
74
- intervalMs: 1000,
75
- extraPlayers: '',
76
- nightFrom: 23,
77
- longMinutes: 100,
78
- keepDays: 90,
79
- memoryDir: '',
80
- controlAllowed: true,
81
- segmentEverySec: 45,
82
- maxSegmentsPerTrack: 3,
83
- selfPick: false,
84
- pickSource: 'auto',
85
- pickWhen: 'ask',
86
- lyricAware: true,
87
- lyricLines: 2,
88
- yieldToHearthstone: false,
89
- });
90
-
91
- const P = (field: string): string => `worlds.${MUSIC_ID}.${field}`;
92
-
93
- /**
94
- * 设置页。四组,每组一句话说清"改了会怎样"。
95
- *
96
- * 为什么不再堆成一组:十个字段一屏,最该调的(说话多不多)和最不该动的(采样间隔)
97
- * 混在一起,等于没分组。
98
- */
99
- export function musicConfigGroups(): ConfigGroup[] {
100
- return [
101
- {
102
- id: 'music.talk',
103
- owner: `world:${MUSIC_ID}`,
104
- schema: {
105
- type: 'object',
106
- title: '她怎么开口',
107
- description: '换歌必说一句;一首歌里说几句、隔多久说,由这两项决定。',
108
- properties: {
109
- [P('chatiness')]: {
110
- type: 'string',
111
- title: '接话频率',
112
- description: '正常=换歌说一句,每段再说一句;安静=只在深夜、久听这类时刻开口,平时陪着你听,不评论。',
113
- enum: ['normal', 'quiet'],
114
- },
115
- [P('maxSegmentsPerTrack')]: {
116
- type: 'integer',
117
- title: '一首歌说几次',
118
- description: '不含开头那次。3 段的话:3 分半的歌约 52 秒一段,15 分钟的古典约 3 分 45 一段。调大就变密。',
119
- minimum: 0,
120
- maximum: 8,
121
- 'x-suffix': '段',
122
- },
123
- [P('yieldToHearthstone')]: {
124
- type: 'boolean',
125
- title: '打炉石时让路',
126
- description: '打开后:检测到炉石在跑就不主动开口(换歌、段落都不说),你戳她还是照答;打完自动恢复。默认关——两件事同时进行、各说各的,很多人反而喜欢。注意:炉石挂在后台不动也算「在跑」,这时同样会安静。',
127
- },
128
- [P('segmentEverySec')]: {
129
- type: 'integer',
130
- title: '一段至少多长',
131
- description: '秒。段长的下限,也是第一段的长度——换完歌要尽快表态,这一段不参与均分。填 0 就只报换歌。',
132
- minimum: 0,
133
- maximum: 180,
134
- 'x-suffix': '秒',
135
- },
136
- },
137
- },
138
- },
139
- {
140
- id: 'music.source',
141
- owner: `world:${MUSIC_ID}`,
142
- schema: {
143
- type: 'object',
144
- title: '从哪读',
145
- description: '读 Windows 的系统媒体会话(SMTC)。只有播放器不上报时才退回读窗口标题。',
146
- properties: {
147
- [P('player')]: {
148
- type: 'string',
149
- title: '两个播放器同时在放',
150
- description: '自动=谁在放听谁;也可以固定只听某一个。',
151
- enum: ['auto', 'qq', 'netease'],
152
- },
153
- [P('extraPlayers')]: {
154
- type: 'string',
155
- title: '还认哪些播放器',
156
- description: '逗号分隔的进程名片段,例如 spotify,foobar。留空则只认 QQ音乐和网易云。',
157
- },
158
- [P('intervalMs')]: {
159
- type: 'integer',
160
- title: '多久看一次',
161
- description: '毫秒。默认 1000,够用就别动。改完要重启扩展才生效。',
162
- minimum: 400,
163
- maximum: 5000,
164
- 'x-suffix': 'ms',
165
- 'x-hot': false,
166
- },
167
- },
168
- },
169
- },
170
- {
171
- id: 'music.pick',
172
- owner: `world:${MUSIC_ID}`,
173
- schema: {
174
- type: 'object',
175
- title: '她自己找歌',
176
- description: '打开之后她会自己去搜歌、挑一首、说清为什么想听,并给出链接。播放那一下仍要你来点——两个客户端都不接受从外部点播,原因写在 README 里。',
177
- properties: {
178
- [P('selfPick')]: {
179
- type: 'boolean',
180
- title: '允许她自己去搜歌挑歌',
181
- description: '关着的时候她只评价你在放的,不会去翻别的。打开后她想听什么会直接说,并记进想听清单。',
182
- },
183
- [P('pickSource')]: {
184
- type: 'string',
185
- title: '上哪个曲库找',
186
- description: '自动=两个库都搜再挑,有些冷门歌只有一边有;也可以固定只搜一家。',
187
- enum: ['auto', 'qq', 'netease'],
188
- },
189
- [P('pickWhen')]: {
190
- type: 'string',
191
- title: '什么时候可以挑',
192
- description: '问我才挑=你问她、或聊到想听什么的时候;自己挑=没歌在放的时候她可以主动挑一首,不用等你开口。',
193
- enum: ['ask', 'idle'],
194
- },
195
- },
196
- },
197
- },
198
- {
199
- id: 'music.lyric',
200
- owner: `world:${MUSIC_ID}`,
201
- schema: {
202
- type: 'object',
203
- title: '歌词',
204
- description: '打开之后她能知道这首歌此刻唱到哪一句,评价能落到词上,而不是只会说"鼓点不错"。歌词按歌名歌手去曲库反查,对不上就不给——宁可没有,也不能拿错歌的词去评。',
205
- properties: {
206
- [P('lyricAware')]: {
207
- type: 'boolean',
208
- title: '让她看歌词',
209
- description: '关掉之后她只有听感:能说编曲、唱腔、节奏,说不出词里在讲什么。打开时每段会附上刚唱过的一两句,需要联网查一次词。',
210
- },
211
- [P('lyricLines')]: {
212
- type: 'integer',
213
- title: '一次给她几行',
214
- description: '1–3。给两行她还能评,给三行以上她就容易照着念——那样听着像在念歌词,不是在聊天。',
215
- minimum: 1,
216
- maximum: 3,
217
- 'x-suffix': '行',
218
- },
219
- },
220
- },
221
- },
222
- {
223
- id: 'music.moments',
224
- owner: `world:${MUSIC_ID}`,
225
- schema: {
226
- type: 'object',
227
- title: '特殊时刻',
228
- description: '两个只在特定条件下才触发的提醒,各只说一次,不反复念。',
229
- properties: {
230
- [P('nightFrom')]: {
231
- type: 'integer',
232
- title: '深夜从几点算',
233
- description: '0–23,窗口到清晨 5 点。这段时间还在听,她会开口一次。',
234
- minimum: 0,
235
- maximum: 23,
236
- 'x-suffix': '点',
237
- },
238
- [P('longMinutes')]: {
239
- type: 'integer',
240
- title: '连续听多久算久',
241
- description: '分钟。达标时提醒一次休息。设很大等于关掉这条。',
242
- minimum: 20,
243
- maximum: 480,
244
- 'x-suffix': '分',
245
- },
246
- },
247
- },
248
- },
249
- {
250
- id: 'music.data',
251
- owner: `world:${MUSIC_ID}`,
252
- schema: {
253
- type: 'object',
254
- title: '记录与位置',
255
- description: '听歌记录存在哪、留多久,以及她能不能替你按键。',
256
- properties: {
257
- [P('memoryDir')]: {
258
- type: 'string',
259
- title: '记忆保存位置',
260
- description:
261
- '留空=跟着这台机器。填一个同步盘或移动硬盘里的文件夹,公司和家里的桌宠就是同一份记忆(听过什么、给哪首写过笔记、欠你哪几首)。'
262
- + '换位置不会丢旧记录——本机原来那份仍然一起读。'
263
- + '两点要注意:别两台机器同时开着往同一个目录里写,网盘同步会打架;歌词缓存和「现在在放什么」永远留在本机,不跟着走。',
264
- 'x-path': { kind: 'directory', recommendedDir: suggestedMemoryDir() },
265
- },
266
- [P('keepDays')]: {
267
- type: 'integer',
268
- title: '听歌记录保留',
269
- description: '天。超出的旧记录在下次启动时删掉。',
270
- minimum: 1,
271
- maximum: 365,
272
- 'x-suffix': '天',
273
- },
274
- [P('controlAllowed')]: {
275
- type: 'boolean',
276
- title: '允许她替你按暂停/切歌',
277
- description: '关掉之后她只能看着,不能动你的播放器。',
278
- },
279
- },
280
- },
281
- },
282
- ];
283
- }
1
+ /**
2
+ * 配置段与设置页。
3
+ *
4
+ * 每一个字段都必须有读取点(见 world.ts 里的 this.cfg.xxx);只声明不接线,
5
+ * 使用者改了没效果而且不报错——这是这个框架最容易骗人的一处。
6
+ *
7
+ * 设置页按「会改的东西」分组,不按代码结构分组:说话节奏 / 读哪边 / 特殊时刻 / 记录与权限。
8
+ * 顺序就是看见的顺序,最常动的放最前。
9
+ */
10
+ import type { ConfigGroup } from 'cortico/core/config-schema.ts';
11
+ import type { WorldSection } from 'cortico/world.ts';
12
+ import type { PlayerPref } from './players.ts';
13
+ import { suggestedMemoryDir } from './storage.ts';
14
+
15
+ export const MUSIC_ID = 'music';
16
+
17
+ /** 开口频率。把"别太吵"做成代码里的规则,而不是指望模型自律。 */
18
+ export type Chatinness = 'quiet' | 'normal';
19
+
20
+ export interface MusicConfigSection extends WorldSection {
21
+ /** 她多爱接话。 */
22
+ chatiness: Chatinness;
23
+ /** 同时开着两个播放器时听谁的。 */
24
+ player: PlayerPref;
25
+ /** 采样间隔(毫秒)。改完要重启扩展实例。 */
26
+ intervalMs: number;
27
+ /** 额外认作播放器的 AUMID 片段,逗号分隔。 */
28
+ extraPlayers: string;
29
+ /** 深夜起点小时(0–23),窗口到 05:00。 */
30
+ nightFrom: number;
31
+ /** 连续听多久算"久听"。 */
32
+ longMinutes: number;
33
+ /** 听歌记录保留多少天。 */
34
+ keepDays: number;
35
+ /**
36
+ * 记忆存到哪个文件夹。留空=跟着这台机器。
37
+ *
38
+ * 填一个两边都看得见的目录(OneDrive、NAS、移动硬盘),公司和家里就是同一份记忆。
39
+ * 只影响**记忆**类数据;当前播放状态和歌词缓存永远在本机,见 storage.ts。
40
+ */
41
+ memoryDir: string;
42
+ /** 允许她代为暂停/切歌。 */
43
+ controlAllowed: boolean;
44
+ /**
45
+ * 启动时自动给宿主桌宠补齐自定义动作。
46
+ *
47
+ * 那十一个动作住在宿主包里,npm 带不过来 —— 关掉这个开关,换台机器就只剩原生动作,
48
+ * 「蹦迪」会退回原地跳舞(不横移)。默认开。
49
+ */
50
+ autoPatchHost: boolean;
51
+ /** 一首歌切成多长一段(秒)。每段结束推一条事件,让她就这一段说一句。0 = 不评论。 */
52
+ segmentEverySec: number;
53
+ /** 每首最多说几段。短歌靠它压次数,长曲靠它把段长拉开。 */
54
+ maxSegmentsPerTrack: number;
55
+ /** 允许她自己搜歌、挑歌。关了就只有看着的份。 */
56
+ selfPick: boolean;
57
+ /** 她上哪找歌。auto = 两个库都搜。 */
58
+ pickSource: 'auto' | 'qq' | 'netease';
59
+ /** 她什么时候可以主动挑:ask = 你问或话题合适时;idle = 静下来没歌时自己挑一首。 */
60
+ pickWhen: 'ask' | 'idle';
61
+ /** 允许她读这首歌的歌词。关了就只有听感,说不出词里在讲什么。 */
62
+ lyricAware: boolean;
63
+ /** 一次给她几行歌词。给多了她会复述,给少了没法评。 */
64
+ lyricLines: number;
65
+ /**
66
+ * 打炉石时让路。
67
+ *
68
+ * 打开后:检测到 `Hearthstone.exe` 在跑就不主动开口(换歌、段落都不说),
69
+ * 你戳她她还是照答 —— 让的是「主动播报」,不是「拒绝回答」。
70
+ *
71
+ * **默认关。** 两件事同时进行、各说各的,这是很多人喜欢的状态;
72
+ * 只有嫌吵的人才需要打开。看到进程也不等于正在对局,挂在后台也会让路。
73
+ */
74
+ yieldToHearthstone: boolean;
75
+ }
76
+
77
+ export const MUSIC_DEFAULTS = (): MusicConfigSection => ({
78
+ enabled: false, // 由 WorldSection 提供,扩展不能自己决定装上就开
79
+ chatiness: 'normal',
80
+ player: 'auto',
81
+ intervalMs: 1000,
82
+ extraPlayers: '',
83
+ nightFrom: 23,
84
+ longMinutes: 100,
85
+ keepDays: 90,
86
+ memoryDir: '',
87
+ controlAllowed: true,
88
+ autoPatchHost: true,
89
+ segmentEverySec: 45,
90
+ maxSegmentsPerTrack: 3,
91
+ selfPick: false,
92
+ pickSource: 'auto',
93
+ pickWhen: 'ask',
94
+ lyricAware: true,
95
+ lyricLines: 2,
96
+ yieldToHearthstone: false,
97
+ });
98
+
99
+ const P = (field: string): string => `worlds.${MUSIC_ID}.${field}`;
100
+
101
+ /**
102
+ * 设置页。四组,每组一句话说清"改了会怎样"。
103
+ *
104
+ * 为什么不再堆成一组:十个字段一屏,最该调的(说话多不多)和最不该动的(采样间隔)
105
+ * 混在一起,等于没分组。
106
+ */
107
+ export function musicConfigGroups(): ConfigGroup[] {
108
+ return [
109
+ {
110
+ id: 'music.talk',
111
+ owner: `world:${MUSIC_ID}`,
112
+ schema: {
113
+ type: 'object',
114
+ title: '她怎么开口',
115
+ description: '换歌必说一句;一首歌里说几句、隔多久说,由这两项决定。',
116
+ properties: {
117
+ [P('chatiness')]: {
118
+ type: 'string',
119
+ title: '接话频率',
120
+ description: '正常=换歌说一句,每段再说一句;安静=只在深夜、久听这类时刻开口,平时陪着你听,不评论。',
121
+ enum: ['normal', 'quiet'],
122
+ },
123
+ [P('maxSegmentsPerTrack')]: {
124
+ type: 'integer',
125
+ title: '一首歌说几次',
126
+ description: '不含开头那次。3 段的话:3 分半的歌约 52 秒一段,15 分钟的古典约 3 分 45 一段。调大就变密。',
127
+ minimum: 0,
128
+ maximum: 8,
129
+ 'x-suffix': '段',
130
+ },
131
+ [P('yieldToHearthstone')]: {
132
+ type: 'boolean',
133
+ title: '打炉石时让路',
134
+ description: '打开后:检测到炉石在跑就不主动开口(换歌、段落都不说),你戳她还是照答;打完自动恢复。默认关——两件事同时进行、各说各的,很多人反而喜欢。注意:炉石挂在后台不动也算「在跑」,这时同样会安静。',
135
+ },
136
+ [P('segmentEverySec')]: {
137
+ type: 'integer',
138
+ title: '一段至少多长',
139
+ description: '秒。段长的下限,也是第一段的长度——换完歌要尽快表态,这一段不参与均分。填 0 就只报换歌。',
140
+ minimum: 0,
141
+ maximum: 180,
142
+ 'x-suffix': '秒',
143
+ },
144
+ },
145
+ },
146
+ },
147
+ {
148
+ id: 'music.source',
149
+ owner: `world:${MUSIC_ID}`,
150
+ schema: {
151
+ type: 'object',
152
+ title: '从哪读',
153
+ description: '读 Windows 的系统媒体会话(SMTC)。只有播放器不上报时才退回读窗口标题。',
154
+ properties: {
155
+ [P('player')]: {
156
+ type: 'string',
157
+ title: '两个播放器同时在放',
158
+ description: '自动=谁在放听谁;也可以固定只听某一个。',
159
+ enum: ['auto', 'qq', 'netease'],
160
+ },
161
+ [P('extraPlayers')]: {
162
+ type: 'string',
163
+ title: '还认哪些播放器',
164
+ description: '逗号分隔的进程名片段,例如 spotify,foobar。留空则只认 QQ音乐和网易云。',
165
+ },
166
+ [P('intervalMs')]: {
167
+ type: 'integer',
168
+ title: '多久看一次',
169
+ description: '毫秒。默认 1000,够用就别动。改完要重启扩展才生效。',
170
+ minimum: 400,
171
+ maximum: 5000,
172
+ 'x-suffix': 'ms',
173
+ 'x-hot': false,
174
+ },
175
+ },
176
+ },
177
+ },
178
+ {
179
+ id: 'music.pick',
180
+ owner: `world:${MUSIC_ID}`,
181
+ schema: {
182
+ type: 'object',
183
+ title: '她自己找歌',
184
+ description: '打开之后她会自己去搜歌、挑一首、说清为什么想听,并给出链接。播放那一下仍要你来点——两个客户端都不接受从外部点播,原因写在 README 里。',
185
+ properties: {
186
+ [P('selfPick')]: {
187
+ type: 'boolean',
188
+ title: '允许她自己去搜歌挑歌',
189
+ description: '关着的时候她只评价你在放的,不会去翻别的。打开后她想听什么会直接说,并记进想听清单。',
190
+ },
191
+ [P('pickSource')]: {
192
+ type: 'string',
193
+ title: '上哪个曲库找',
194
+ description: '自动=两个库都搜再挑,有些冷门歌只有一边有;也可以固定只搜一家。',
195
+ enum: ['auto', 'qq', 'netease'],
196
+ },
197
+ [P('pickWhen')]: {
198
+ type: 'string',
199
+ title: '什么时候可以挑',
200
+ description: '问我才挑=你问她、或聊到想听什么的时候;自己挑=没歌在放的时候她可以主动挑一首,不用等你开口。',
201
+ enum: ['ask', 'idle'],
202
+ },
203
+ },
204
+ },
205
+ },
206
+ {
207
+ id: 'music.lyric',
208
+ owner: `world:${MUSIC_ID}`,
209
+ schema: {
210
+ type: 'object',
211
+ title: '歌词',
212
+ description: '打开之后她能知道这首歌此刻唱到哪一句,评价能落到词上,而不是只会说"鼓点不错"。歌词按歌名歌手去曲库反查,对不上就不给——宁可没有,也不能拿错歌的词去评。',
213
+ properties: {
214
+ [P('lyricAware')]: {
215
+ type: 'boolean',
216
+ title: '让她看歌词',
217
+ description: '关掉之后她只有听感:能说编曲、唱腔、节奏,说不出词里在讲什么。打开时每段会附上刚唱过的一两句,需要联网查一次词。',
218
+ },
219
+ [P('lyricLines')]: {
220
+ type: 'integer',
221
+ title: '一次给她几行',
222
+ description: '1–3。给两行她还能评,给三行以上她就容易照着念——那样听着像在念歌词,不是在聊天。',
223
+ minimum: 1,
224
+ maximum: 3,
225
+ 'x-suffix': '行',
226
+ },
227
+ },
228
+ },
229
+ },
230
+ {
231
+ id: 'music.moments',
232
+ owner: `world:${MUSIC_ID}`,
233
+ schema: {
234
+ type: 'object',
235
+ title: '特殊时刻',
236
+ description: '两个只在特定条件下才触发的提醒,各只说一次,不反复念。',
237
+ properties: {
238
+ [P('nightFrom')]: {
239
+ type: 'integer',
240
+ title: '深夜从几点算',
241
+ description: '0–23,窗口到清晨 5 点。这段时间还在听,她会开口一次。',
242
+ minimum: 0,
243
+ maximum: 23,
244
+ 'x-suffix': '点',
245
+ },
246
+ [P('longMinutes')]: {
247
+ type: 'integer',
248
+ title: '连续听多久算久',
249
+ description: '分钟。达标时提醒一次休息。设很大等于关掉这条。',
250
+ minimum: 20,
251
+ maximum: 480,
252
+ 'x-suffix': '分',
253
+ },
254
+ },
255
+ },
256
+ },
257
+ {
258
+ id: 'music.data',
259
+ owner: `world:${MUSIC_ID}`,
260
+ schema: {
261
+ type: 'object',
262
+ title: '记录与位置',
263
+ description: '听歌记录存在哪、留多久、她能不能替你按键,以及要不要自动补齐桌宠动作。',
264
+ properties: {
265
+ [P('memoryDir')]: {
266
+ type: 'string',
267
+ title: '记忆保存位置',
268
+ description:
269
+ '留空=跟着这台机器。填一个同步盘或移动硬盘里的文件夹,公司和家里的桌宠就是同一份记忆(听过什么、给哪首写过笔记、欠你哪几首)。'
270
+ + '换位置不会丢旧记录——本机原来那份仍然一起读。'
271
+ + '两点要注意:别两台机器同时开着往同一个目录里写,网盘同步会打架;歌词缓存和「现在在放什么」永远留在本机,不跟着走。',
272
+ 'x-path': { kind: 'directory', recommendedDir: suggestedMemoryDir() },
273
+ },
274
+ [P('keepDays')]: {
275
+ type: 'integer',
276
+ title: '听歌记录保留',
277
+ description: '天。超出的旧记录在下次启动时删掉。',
278
+ minimum: 1,
279
+ maximum: 365,
280
+ 'x-suffix': '天',
281
+ },
282
+ [P('controlAllowed')]: {
283
+ type: 'boolean',
284
+ title: '允许她替你按暂停/切歌',
285
+ description: '关掉之后她只能看着,不能动你的播放器。',
286
+ },
287
+ [P('autoPatchHost')]: {
288
+ type: 'boolean',
289
+ title: '自动补齐桌宠动作',
290
+ description:
291
+ '十一个自定义动作(蹦迪 / 跟着哼 / 坐着哼 等)不在本扩展里,而在桌宠宿主包里 —— '
292
+ + '所以从市场装到一台新机器时,它们本来是缺的,「蹦迪」会退回原地跳舞、不横着走。'
293
+ + '打开后每次启动都会检查宿主,缺什么补什么:动手前先备份,补完要重启一次才生效。'
294
+ + '关掉就只能用宿主原生的那几个动作。',
295
+ 'x-hot': true,
296
+ },
297
+ },
298
+ },
299
+ },
300
+ ];
301
+ }