@zhin.js/plugin-music 1.0.26 → 1.1.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.
Files changed (111) hide show
  1. package/README.md +28 -27
  2. package/commands/cookie/delete/[source]/index.js +23 -0
  3. package/commands/cookie/delete/[source]/index.ts +27 -0
  4. package/commands/cookie/get/[source]/index.js +29 -0
  5. package/commands/cookie/get/[source]/index.ts +33 -0
  6. package/commands/cookie/set/[source]/index.js +23 -0
  7. package/commands/cookie/set/[source]/index.ts +27 -0
  8. package/commands/cookie/status/index.js +25 -0
  9. package/commands/cookie/status/index.ts +25 -0
  10. package/commands/login/[platform]/index.js +66 -0
  11. package/commands/login/[platform]/index.ts +74 -0
  12. package/commands//347/202/271/346/255/214/[keyword]/index.js +63 -0
  13. package/commands//347/202/271/346/255/214/[keyword]/index.ts +75 -0
  14. package/components/share-music/index.js +24 -0
  15. package/components/share-music/index.ts +29 -0
  16. package/lib/config.d.ts +3 -3
  17. package/lib/config.d.ts.map +1 -1
  18. package/lib/config.js +36 -7
  19. package/lib/config.js.map +1 -1
  20. package/lib/credential-store.d.ts +37 -0
  21. package/lib/credential-store.d.ts.map +1 -0
  22. package/lib/credential-store.js +85 -0
  23. package/lib/credential-store.js.map +1 -0
  24. package/lib/index.d.ts +8 -1
  25. package/lib/index.d.ts.map +1 -1
  26. package/lib/index.js +7 -112
  27. package/lib/index.js.map +1 -1
  28. package/lib/login/index.d.ts +27 -0
  29. package/lib/login/index.d.ts.map +1 -0
  30. package/lib/login/index.js +124 -0
  31. package/lib/login/index.js.map +1 -0
  32. package/lib/login/netease.d.ts +6 -0
  33. package/lib/login/netease.d.ts.map +1 -0
  34. package/lib/login/netease.js +58 -0
  35. package/lib/login/netease.js.map +1 -0
  36. package/lib/login/qq.d.ts +7 -0
  37. package/lib/login/qq.d.ts.map +1 -0
  38. package/lib/login/qq.js +139 -0
  39. package/lib/login/qq.js.map +1 -0
  40. package/lib/login/types.d.ts +19 -0
  41. package/lib/login/types.d.ts.map +1 -0
  42. package/lib/login/types.js +11 -0
  43. package/lib/login/types.js.map +1 -0
  44. package/lib/music-lib.d.ts +32 -0
  45. package/lib/music-lib.d.ts.map +1 -0
  46. package/lib/music-lib.js +58 -0
  47. package/lib/music-lib.js.map +1 -0
  48. package/lib/runtime.d.ts +12 -0
  49. package/lib/runtime.d.ts.map +1 -0
  50. package/lib/runtime.js +3 -0
  51. package/lib/runtime.js.map +1 -0
  52. package/lib/session.d.ts +35 -0
  53. package/lib/session.d.ts.map +1 -0
  54. package/lib/session.js +51 -0
  55. package/lib/session.js.map +1 -0
  56. package/lib/sources/index.d.ts +4 -2
  57. package/lib/sources/index.d.ts.map +1 -1
  58. package/lib/sources/index.js +12 -6
  59. package/lib/sources/index.js.map +1 -1
  60. package/lib/sources/kugou.d.ts +6 -0
  61. package/lib/sources/kugou.d.ts.map +1 -0
  62. package/lib/sources/kugou.js +69 -0
  63. package/lib/sources/kugou.js.map +1 -0
  64. package/lib/sources/kuwo.d.ts +7 -0
  65. package/lib/sources/kuwo.d.ts.map +1 -0
  66. package/lib/sources/kuwo.js +69 -0
  67. package/lib/sources/kuwo.js.map +1 -0
  68. package/lib/sources/netease.d.ts +4 -1
  69. package/lib/sources/netease.d.ts.map +1 -1
  70. package/lib/sources/netease.js +25 -6
  71. package/lib/sources/netease.js.map +1 -1
  72. package/lib/sources/qq.d.ts +4 -14
  73. package/lib/sources/qq.d.ts.map +1 -1
  74. package/lib/sources/qq.js +117 -82
  75. package/lib/sources/qq.js.map +1 -1
  76. package/lib/types.d.ts +4 -24
  77. package/lib/types.d.ts.map +1 -1
  78. package/lib/types.js +0 -1
  79. package/lib/types.js.map +1 -1
  80. package/middlewares/music-selection/index.js +48 -0
  81. package/middlewares/music-selection/index.ts +62 -0
  82. package/package.json +66 -12
  83. package/plugin.js +45 -0
  84. package/schema.json +6 -0
  85. package/skills/music/SKILL.md +2 -2
  86. package/skills/music/tools/music-search/index.js +22 -0
  87. package/skills/music/tools/music-search/index.ts +33 -0
  88. package/skills/music/tools/music-share/index.js +21 -0
  89. package/skills/music/tools/music-share/index.ts +26 -0
  90. package/src/config.ts +54 -30
  91. package/src/credential-store.ts +125 -0
  92. package/src/index.ts +37 -0
  93. package/src/login/index.ts +154 -0
  94. package/src/login/netease.ts +69 -0
  95. package/src/login/qq.ts +154 -0
  96. package/src/login/types.ts +27 -0
  97. package/src/music-lib.ts +71 -0
  98. package/src/runtime.ts +17 -0
  99. package/src/session.ts +77 -0
  100. package/src/sources/index.ts +17 -10
  101. package/src/sources/kugou.ts +92 -0
  102. package/src/sources/kuwo.ts +94 -0
  103. package/src/sources/netease.ts +35 -11
  104. package/src/sources/qq.ts +164 -126
  105. package/src/types.ts +6 -28
  106. package/lib/async-jsx.d.ts +0 -19
  107. package/lib/async-jsx.d.ts.map +0 -1
  108. package/lib/async-jsx.js +0 -48
  109. package/lib/async-jsx.js.map +0 -1
  110. package/src/async-jsx.ts +0 -54
  111. package/src/index.tsx +0 -130
package/README.md CHANGED
@@ -17,21 +17,23 @@
17
17
  ## 📦 安装
18
18
 
19
19
  ```bash
20
- pnpm add @zhin.js/plugin-music
20
+ zhin install @zhin.js/plugin-music
21
21
  ```
22
22
 
23
23
  ## 🚀 使用
24
24
 
25
- ### 配置
25
+ ### 挂载插件
26
26
 
27
- 在 `zhin.config.ts` 中添加插件:
27
+ `zhin install` 会安装依赖,并把插件实例写入 `package.json#zhin.plugins`。手工挂载时使用同一份拓扑契约:
28
28
 
29
- ```typescript
30
- export default defineConfig({
31
- plugins: [
32
- 'music' // 添加音乐插件
33
- ]
34
- })
29
+ ```json
30
+ {
31
+ "zhin": {
32
+ "plugins": [
33
+ { "package": "@zhin.js/plugin-music", "instanceKey": "music" }
34
+ ]
35
+ }
36
+ }
35
37
  ```
36
38
 
37
39
  ### 命令列表
@@ -200,22 +202,18 @@ interface ShareContent {
200
202
  }
201
203
  ```
202
204
 
203
- ### 导出的服务
205
+ ### 运行时代码中使用服务
204
206
 
205
207
  ```typescript
206
- import { musicServices } from '@zhin.js/plugin-music'
207
-
208
- // 使用音乐搜索服务
209
- const qqMusic = musicServices.qq
210
- const results = await qqMusic.search('周杰伦', 10)
211
- const cover = await qqMusic.getCover('音乐ID')
212
- const detail = await qqMusic.getDetail('音乐ID')
213
-
214
- // 获取音频直链(需要 Meting API)
215
- const audioUrl = await qqMusic.getAudioUrl?.('音乐ID')
216
-
217
- // 获取歌词
218
- const lyric = await qqMusic.getLyric?.('音乐ID')
208
+ import { defineCommand } from 'zhin.js/command'
209
+ import { musicRuntimeToken } from '@zhin.js/plugin-music'
210
+
211
+ export default defineCommand({
212
+ async execute({ use }) {
213
+ const qqMusic = use(musicRuntimeToken).services.qq
214
+ return qqMusic.search('周杰伦', 10)
215
+ },
216
+ })
219
217
  ```
220
218
 
221
219
  ### 配置工具
@@ -266,10 +264,12 @@ export class MyMusicService implements MusicSearchService {
266
264
  // src/sources/index.ts
267
265
  import { MyMusicService } from './my-music.js'
268
266
 
269
- export const musicServices = {
270
- qq: new QQMusicService(),
271
- netease: new NeteaseMusicService(),
272
- mymusic: new MyMusicService(), // 添加新服务
267
+ export function createMusicServices(credentials: CredentialStore) {
268
+ return {
269
+ qq: new QQMusicService(credentials),
270
+ netease: new NeteaseMusicService(credentials),
271
+ mymusic: new MyMusicService(),
272
+ }
273
273
  }
274
274
  ```
275
275
 
@@ -286,6 +286,7 @@ export type MusicSource = 'qq' | 'netease' | 'mymusic'
286
286
  2. **版权问题**:仅用于搜索和分享链接,不提供下载功能
287
287
  3. **网络问题**:部分 API 可能需要稳定的网络连接
288
288
  4. **平台兼容**:ICQQ 特定功能仅在 ICQQ 适配器下可用
289
+ 5. **AI 工具平台限制**:旧版 `music-search` / `music-share` 工具曾通过 `.platform('icqq')` 限制仅 icqq 平台可用;现行 `defineAgentTool`(`@zhin.js/tool`)不支持 platform 约束,迁移后工具对所有平台可见
289
290
 
290
291
  ## 🐛 故障排除
291
292
 
@@ -0,0 +1,23 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { defineCommand } from 'zhin.js/command';
3
+ import { musicRuntimeToken } from "../../../../lib/runtime.js";
4
+ import { SOURCE_DISPLAY_NAME } from "../../../../lib/config.js";
5
+ const VALID_SOURCES = new Set(['qq', 'netease', 'kuwo', 'kugou']);
6
+ export default defineCommand({
7
+ description: '删除音乐平台凭证',
8
+ alias: ['删除'],
9
+ params: { source: { type: 'string', description: '音乐源(qq/netease/kuwo/kugou)' } },
10
+ permit: ['role(master)'],
11
+ async execute({ params, args, use }) {
12
+ const source = String(params.source ?? '').trim();
13
+ const key = String(args[0] ?? '').trim();
14
+ if (!VALID_SOURCES.has(source)) {
15
+ return `不支持的音乐源:${source}`;
16
+ }
17
+ if (!key) {
18
+ return `格式:cookie delete ${source} <key>`;
19
+ }
20
+ await use(musicRuntimeToken).credentials.delete(source, key);
21
+ return `[${SOURCE_DISPLAY_NAME[source]}] 凭证 ${key} 已删除`;
22
+ },
23
+ });
@@ -0,0 +1,27 @@
1
+ import { defineCommand } from 'zhin.js/command';
2
+ import { musicRuntimeToken } from '../../../../src/runtime.js';
3
+ import { SOURCE_DISPLAY_NAME } from '../../../../src/config.js';
4
+ import type { MusicSource } from '../../../../src/types.js';
5
+
6
+ const VALID_SOURCES = new Set<string>(['qq', 'netease', 'kuwo', 'kugou']);
7
+
8
+ export default defineCommand({
9
+ description: '删除音乐平台凭证',
10
+ alias: ['删除'],
11
+ params: { source: { type: 'string', description: '音乐源(qq/netease/kuwo/kugou)' } },
12
+ permit: ['role(master)'],
13
+ async execute({ params, args, use }) {
14
+ const source = String(params.source ?? '').trim();
15
+ const key = String(args[0] ?? '').trim();
16
+
17
+ if (!VALID_SOURCES.has(source)) {
18
+ return `不支持的音乐源:${source}`;
19
+ }
20
+ if (!key) {
21
+ return `格式:cookie delete ${source} <key>`;
22
+ }
23
+
24
+ await use(musicRuntimeToken).credentials.delete(source as MusicSource, key);
25
+ return `[${SOURCE_DISPLAY_NAME[source as MusicSource]}] 凭证 ${key} 已删除`;
26
+ },
27
+ });
@@ -0,0 +1,29 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { defineCommand } from 'zhin.js/command';
3
+ import { musicRuntimeToken } from "../../../../lib/runtime.js";
4
+ import { SOURCE_DISPLAY_NAME } from "../../../../lib/config.js";
5
+ const VALID_SOURCES = new Set(['qq', 'netease', 'kuwo', 'kugou']);
6
+ export default defineCommand({
7
+ description: '查看音乐平台凭证(脱敏)',
8
+ alias: ['查看'],
9
+ params: { source: { type: 'string', description: '音乐源(qq/netease/kuwo/kugou)' } },
10
+ permit: ['role(master)'],
11
+ async execute({ params, args, use }) {
12
+ const source = String(params.source ?? '').trim();
13
+ const key = String(args[0] ?? '').trim();
14
+ if (!VALID_SOURCES.has(source)) {
15
+ return `不支持的音乐源:${source}`;
16
+ }
17
+ if (!key) {
18
+ return `格式:cookie get ${source} <key>`;
19
+ }
20
+ const sourceName = SOURCE_DISPLAY_NAME[source];
21
+ const value = await use(musicRuntimeToken).credentials.get(source, key);
22
+ if (!value)
23
+ return `[${sourceName}] 凭证 ${key} 未设置`;
24
+ const masked = value.length > 10
25
+ ? `${value.slice(0, 4)}****${value.slice(-4)}`
26
+ : '****';
27
+ return `[${sourceName}] ${key} = ${masked}`;
28
+ },
29
+ });
@@ -0,0 +1,33 @@
1
+ import { defineCommand } from 'zhin.js/command';
2
+ import { musicRuntimeToken } from '../../../../src/runtime.js';
3
+ import { SOURCE_DISPLAY_NAME } from '../../../../src/config.js';
4
+ import type { MusicSource } from '../../../../src/types.js';
5
+
6
+ const VALID_SOURCES = new Set<string>(['qq', 'netease', 'kuwo', 'kugou']);
7
+
8
+ export default defineCommand({
9
+ description: '查看音乐平台凭证(脱敏)',
10
+ alias: ['查看'],
11
+ params: { source: { type: 'string', description: '音乐源(qq/netease/kuwo/kugou)' } },
12
+ permit: ['role(master)'],
13
+ async execute({ params, args, use }) {
14
+ const source = String(params.source ?? '').trim();
15
+ const key = String(args[0] ?? '').trim();
16
+
17
+ if (!VALID_SOURCES.has(source)) {
18
+ return `不支持的音乐源:${source}`;
19
+ }
20
+ if (!key) {
21
+ return `格式:cookie get ${source} <key>`;
22
+ }
23
+
24
+ const sourceName = SOURCE_DISPLAY_NAME[source as MusicSource];
25
+ const value = await use(musicRuntimeToken).credentials.get(source as MusicSource, key);
26
+ if (!value) return `[${sourceName}] 凭证 ${key} 未设置`;
27
+
28
+ const masked = value.length > 10
29
+ ? `${value.slice(0, 4)}****${value.slice(-4)}`
30
+ : '****';
31
+ return `[${sourceName}] ${key} = ${masked}`;
32
+ },
33
+ });
@@ -0,0 +1,23 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { defineCommand } from 'zhin.js/command';
3
+ import { musicRuntimeToken } from "../../../../lib/runtime.js";
4
+ import { SOURCE_DISPLAY_NAME } from "../../../../lib/config.js";
5
+ const VALID_SOURCES = new Set(['qq', 'netease', 'kuwo', 'kugou']);
6
+ export default defineCommand({
7
+ description: '保存音乐平台凭证',
8
+ alias: ['设置'],
9
+ params: { source: { type: 'string', description: '音乐源(qq/netease/kuwo/kugou)' } },
10
+ permit: ['role(master)'],
11
+ async execute({ params, args, use }) {
12
+ const source = String(params.source ?? '').trim();
13
+ const [key, ...valueParts] = args.map(String);
14
+ if (!VALID_SOURCES.has(source)) {
15
+ return `不支持的音乐源:${source}\n支持:${[...VALID_SOURCES].join(', ')}`;
16
+ }
17
+ if (!key || valueParts.length === 0) {
18
+ return `格式:cookie set ${source} <key> <value>\n示例:cookie set netease cookie MUSIC_U=xxx`;
19
+ }
20
+ await use(musicRuntimeToken).credentials.set(source, key, valueParts.join(' '));
21
+ return `[${SOURCE_DISPLAY_NAME[source]}] 凭证 ${key} 已保存`;
22
+ },
23
+ });
@@ -0,0 +1,27 @@
1
+ import { defineCommand } from 'zhin.js/command';
2
+ import { musicRuntimeToken } from '../../../../src/runtime.js';
3
+ import { SOURCE_DISPLAY_NAME } from '../../../../src/config.js';
4
+ import type { MusicSource } from '../../../../src/types.js';
5
+
6
+ const VALID_SOURCES = new Set<string>(['qq', 'netease', 'kuwo', 'kugou']);
7
+
8
+ export default defineCommand({
9
+ description: '保存音乐平台凭证',
10
+ alias: ['设置'],
11
+ params: { source: { type: 'string', description: '音乐源(qq/netease/kuwo/kugou)' } },
12
+ permit: ['role(master)'],
13
+ async execute({ params, args, use }) {
14
+ const source = String(params.source ?? '').trim();
15
+ const [key, ...valueParts] = args.map(String);
16
+
17
+ if (!VALID_SOURCES.has(source)) {
18
+ return `不支持的音乐源:${source}\n支持:${[...VALID_SOURCES].join(', ')}`;
19
+ }
20
+ if (!key || valueParts.length === 0) {
21
+ return `格式:cookie set ${source} <key> <value>\n示例:cookie set netease cookie MUSIC_U=xxx`;
22
+ }
23
+
24
+ await use(musicRuntimeToken).credentials.set(source as MusicSource, key, valueParts.join(' '));
25
+ return `[${SOURCE_DISPLAY_NAME[source as MusicSource]}] 凭证 ${key} 已保存`;
26
+ },
27
+ });
@@ -0,0 +1,25 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { defineCommand } from 'zhin.js/command';
3
+ import { musicRuntimeToken } from "../../../lib/runtime.js";
4
+ import { SOURCE_DISPLAY_NAME } from "../../../lib/config.js";
5
+ export default defineCommand({
6
+ description: '查看各平台凭证配置状态',
7
+ alias: ['状态'],
8
+ permit: ['role(master)'],
9
+ async execute({ use }) {
10
+ const allCreds = await use(musicRuntimeToken).credentials.list();
11
+ const lines = ['--- 音乐凭证状态 ---'];
12
+ for (const source of ['qq', 'netease', 'kuwo', 'kugou']) {
13
+ const name = SOURCE_DISPLAY_NAME[source];
14
+ const creds = allCreds.filter((c) => c.source === source);
15
+ if (creds.length === 0) {
16
+ lines.push(`${name}: 未配置`);
17
+ }
18
+ else {
19
+ const keys = creds.map((c) => c.key).join(', ');
20
+ lines.push(`${name}: ${keys} (${creds.length} 项)`);
21
+ }
22
+ }
23
+ return lines.join('\n');
24
+ },
25
+ });
@@ -0,0 +1,25 @@
1
+ import { defineCommand } from 'zhin.js/command';
2
+ import { musicRuntimeToken } from '../../../src/runtime.js';
3
+ import { SOURCE_DISPLAY_NAME } from '../../../src/config.js';
4
+ import type { MusicSource } from '../../../src/types.js';
5
+
6
+ export default defineCommand({
7
+ description: '查看各平台凭证配置状态',
8
+ alias: ['状态'],
9
+ permit: ['role(master)'],
10
+ async execute({ use }) {
11
+ const allCreds = await use(musicRuntimeToken).credentials.list();
12
+ const lines: string[] = ['--- 音乐凭证状态 ---'];
13
+ for (const source of ['qq', 'netease', 'kuwo', 'kugou'] as MusicSource[]) {
14
+ const name = SOURCE_DISPLAY_NAME[source];
15
+ const creds = allCreds.filter((c) => c.source === source);
16
+ if (creds.length === 0) {
17
+ lines.push(`${name}: 未配置`);
18
+ } else {
19
+ const keys = creds.map((c) => c.key).join(', ');
20
+ lines.push(`${name}: ${keys} (${creds.length} 项)`);
21
+ }
22
+ }
23
+ return lines.join('\n');
24
+ },
25
+ });
@@ -0,0 +1,66 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { defineCommand } from 'zhin.js/command';
3
+ import { loginSessionKey, } from "../../../lib/login/index.js";
4
+ import { resolveMessageIds } from "../../../lib/session.js";
5
+ import { SOURCE_DISPLAY_NAME } from "../../../lib/config.js";
6
+ import { musicRuntimeToken } from "../../../lib/runtime.js";
7
+ const QR_LOGIN_SOURCES = {
8
+ qq: 'qq',
9
+ netease: 'netease',
10
+ };
11
+ export default defineCommand({
12
+ description: '扫码登录音乐平台获取凭证(master 专用)',
13
+ params: {
14
+ platform: { type: 'string', description: '音乐平台(qq / netease / 取消)' },
15
+ },
16
+ permit: ['role(master)'],
17
+ async execute({ params, input, use }) {
18
+ const platform = String(params.platform ?? '').trim();
19
+ const ids = resolveMessageIds(input);
20
+ if (!ids)
21
+ return '无法识别会话信息,请在群聊或私聊中使用';
22
+ const key = loginSessionKey(ids.endpointId, ids.conversationId, ids.senderId);
23
+ const runtime = use(musicRuntimeToken);
24
+ if (platform === '取消' || platform === 'cancel') {
25
+ return runtime.logins.cancel(key) ? '已取消登录' : '没有进行中的登录会话';
26
+ }
27
+ const source = QR_LOGIN_SOURCES[platform];
28
+ if (!source) {
29
+ return `不支持的平台:${platform || '(空)'}\n支持:qq, netease`;
30
+ }
31
+ const existing = runtime.logins.get(key);
32
+ if (existing) {
33
+ return `正在进行 ${SOURCE_DISPLAY_NAME[existing.source]} 登录,请先完成或发送"音乐登录 取消"`;
34
+ }
35
+ const sourceName = SOURCE_DISPLAY_NAME[source];
36
+ let qrResult;
37
+ try {
38
+ qrResult = await runtime.logins.start(source, key);
39
+ }
40
+ catch (err) {
41
+ return `[${sourceName}] 获取二维码失败:${err instanceof Error ? err.message : String(err)}`;
42
+ }
43
+ const replyFn = input?.$reply;
44
+ if (!replyFn)
45
+ return '无法发送消息';
46
+ await replyFn([
47
+ `[${sourceName}] 请使用 ${sourceName} App 扫描二维码登录:\n`,
48
+ qrResult.imageSegment,
49
+ `\n二维码有效期 2 分钟,发送"音乐登录 取消"可中止`,
50
+ ]);
51
+ const finalResult = await runtime.logins.poll(key, async (result) => {
52
+ if (result.status === 'scanned') {
53
+ await replyFn(`[${sourceName}] ${result.message}`);
54
+ }
55
+ else if (result.status === 'confirmed') {
56
+ await replyFn(`[${sourceName}] 登录成功!cookie 已自动保存,现在可以搜索和播放高品质音乐了`);
57
+ }
58
+ else if (result.status === 'expired' || result.status === 'error') {
59
+ await replyFn(`[${sourceName}] ${result.message}`);
60
+ }
61
+ });
62
+ if (finalResult.status === 'confirmed')
63
+ return undefined;
64
+ return `[${sourceName}] ${finalResult.message}`;
65
+ },
66
+ });
@@ -0,0 +1,74 @@
1
+ import { defineCommand } from 'zhin.js/command';
2
+ import type { Message } from '@zhin.js/core/runtime';
3
+ import {
4
+ loginSessionKey,
5
+ type QrLoginSource,
6
+ } from '../../../src/login/index.js';
7
+ import { resolveMessageIds } from '../../../src/session.js';
8
+ import { SOURCE_DISPLAY_NAME } from '../../../src/config.js';
9
+ import { musicRuntimeToken } from '../../../src/runtime.js';
10
+
11
+ const QR_LOGIN_SOURCES: Record<string, QrLoginSource> = {
12
+ qq: 'qq',
13
+ netease: 'netease',
14
+ };
15
+
16
+ export default defineCommand<unknown, string, Message>({
17
+ description: '扫码登录音乐平台获取凭证(master 专用)',
18
+ params: {
19
+ platform: { type: 'string', description: '音乐平台(qq / netease / 取消)' },
20
+ },
21
+ permit: ['role(master)'],
22
+ async execute({ params, input, use }) {
23
+ const platform = String(params.platform ?? '').trim();
24
+
25
+ const ids = resolveMessageIds(input!);
26
+ if (!ids) return '无法识别会话信息,请在群聊或私聊中使用';
27
+ const key = loginSessionKey(ids.endpointId, ids.conversationId, ids.senderId);
28
+ const runtime = use(musicRuntimeToken);
29
+
30
+ if (platform === '取消' || platform === 'cancel') {
31
+ return runtime.logins.cancel(key) ? '已取消登录' : '没有进行中的登录会话';
32
+ }
33
+
34
+ const source = QR_LOGIN_SOURCES[platform];
35
+ if (!source) {
36
+ return `不支持的平台:${platform || '(空)'}\n支持:qq, netease`;
37
+ }
38
+
39
+ const existing = runtime.logins.get(key);
40
+ if (existing) {
41
+ return `正在进行 ${SOURCE_DISPLAY_NAME[existing.source]} 登录,请先完成或发送"音乐登录 取消"`;
42
+ }
43
+
44
+ const sourceName = SOURCE_DISPLAY_NAME[source];
45
+ let qrResult;
46
+ try {
47
+ qrResult = await runtime.logins.start(source, key);
48
+ } catch (err) {
49
+ return `[${sourceName}] 获取二维码失败:${err instanceof Error ? err.message : String(err)}`;
50
+ }
51
+
52
+ const replyFn = input?.$reply;
53
+ if (!replyFn) return '无法发送消息';
54
+
55
+ await replyFn([
56
+ `[${sourceName}] 请使用 ${sourceName} App 扫描二维码登录:\n`,
57
+ qrResult.imageSegment,
58
+ `\n二维码有效期 2 分钟,发送"音乐登录 取消"可中止`,
59
+ ]);
60
+
61
+ const finalResult = await runtime.logins.poll(key, async (result) => {
62
+ if (result.status === 'scanned') {
63
+ await replyFn(`[${sourceName}] ${result.message}`);
64
+ } else if (result.status === 'confirmed') {
65
+ await replyFn(`[${sourceName}] 登录成功!cookie 已自动保存,现在可以搜索和播放高品质音乐了`);
66
+ } else if (result.status === 'expired' || result.status === 'error') {
67
+ await replyFn(`[${sourceName}] ${result.message}`);
68
+ }
69
+ });
70
+
71
+ if (finalResult.status === 'confirmed') return undefined as unknown as string;
72
+ return `[${sourceName}] ${finalResult.message}`;
73
+ },
74
+ });
@@ -0,0 +1,63 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { defineCommand } from 'zhin.js/command';
3
+ import { searchMusic, formatSearchResults } from "../../../lib/music-lib.js";
4
+ import { resolveSourceAlias, SOURCE_DISPLAY_NAME } from "../../../lib/config.js";
5
+ import { sessionKey, resolveMessageIds } from "../../../lib/session.js";
6
+ import { musicRuntimeToken } from "../../../lib/runtime.js";
7
+ export default defineCommand({
8
+ description: '搜索音乐并点歌(支持QQ/网易云/酷我/酷狗)',
9
+ params: { keyword: { type: 'string' } },
10
+ shortcut: {
11
+ 'QQ点歌': { keyword: '' },
12
+ 'qq点歌': { keyword: '' },
13
+ '网易云点歌': { keyword: '' },
14
+ '酷我点歌': { keyword: '' },
15
+ '酷狗点歌': { keyword: '' },
16
+ },
17
+ async execute({ params, config, input, use }) {
18
+ const rawKeyword = String(params.keyword ?? '').trim();
19
+ if (!rawKeyword) {
20
+ const sources = Object.values(SOURCE_DISPLAY_NAME).join('/');
21
+ return `请输入歌曲名称,如:点歌 稻香\n支持音乐源:${sources}`;
22
+ }
23
+ let source = config?.defaultSource ?? 'qq';
24
+ let keyword = rawKeyword;
25
+ const triggerText = input?.content?.trim() ?? '';
26
+ for (const [prefix, s] of Object.entries({
27
+ 'QQ点歌': 'qq',
28
+ 'qq点歌': 'qq',
29
+ '网易云点歌': 'netease',
30
+ '酷我点歌': 'kuwo',
31
+ '酷狗点歌': 'kugou',
32
+ })) {
33
+ if (triggerText.startsWith(prefix)) {
34
+ source = s;
35
+ keyword = triggerText.slice(prefix.length).trim() || rawKeyword;
36
+ break;
37
+ }
38
+ }
39
+ const sourceFromKeyword = resolveSourceAlias(keyword.split(/\s+/)[0] ?? '');
40
+ if (sourceFromKeyword) {
41
+ source = sourceFromKeyword;
42
+ keyword = keyword.slice(keyword.indexOf(' ') + 1).trim();
43
+ if (!keyword)
44
+ return '请输入歌曲名称';
45
+ }
46
+ const pageSize = config?.pageSize ?? 5;
47
+ const result = await searchMusic(use(musicRuntimeToken), keyword, source, pageSize);
48
+ if (result.total === 0) {
49
+ return `[${SOURCE_DISPLAY_NAME[source]}] 未找到"${keyword}"相关歌曲`;
50
+ }
51
+ const ids = input ? resolveMessageIds(input) : null;
52
+ if (ids) {
53
+ const key = sessionKey(ids.endpointId, ids.conversationId, ids.senderId);
54
+ use(musicRuntimeToken).sessions.set(key, {
55
+ results: result.results,
56
+ source: result.source,
57
+ keyword,
58
+ timestamp: Date.now(),
59
+ });
60
+ }
61
+ return formatSearchResults(result.results, result.source);
62
+ },
63
+ });
@@ -0,0 +1,75 @@
1
+ import { defineCommand } from 'zhin.js/command';
2
+ import type { Message } from '@zhin.js/core/runtime';
3
+ import { searchMusic, formatSearchResults } from '../../../src/music-lib.js';
4
+ import { resolveSourceAlias, SOURCE_DISPLAY_NAME } from '../../../src/config.js';
5
+ import { sessionKey, resolveMessageIds } from '../../../src/session.js';
6
+ import type { MusicSource } from '../../../src/types.js';
7
+ import { musicRuntimeToken } from '../../../src/runtime.js';
8
+
9
+ interface MusicConfig {
10
+ defaultSource?: MusicSource;
11
+ pageSize?: number;
12
+ }
13
+
14
+ export default defineCommand<MusicConfig, string, Message>({
15
+ description: '搜索音乐并点歌(支持QQ/网易云/酷我/酷狗)',
16
+ params: { keyword: { type: 'string' } },
17
+ shortcut: {
18
+ 'QQ点歌': { keyword: '' },
19
+ 'qq点歌': { keyword: '' },
20
+ '网易云点歌': { keyword: '' },
21
+ '酷我点歌': { keyword: '' },
22
+ '酷狗点歌': { keyword: '' },
23
+ },
24
+ async execute({ params, config, input, use }) {
25
+ const rawKeyword = String(params.keyword ?? '').trim();
26
+ if (!rawKeyword) {
27
+ const sources = Object.values(SOURCE_DISPLAY_NAME).join('/');
28
+ return `请输入歌曲名称,如:点歌 稻香\n支持音乐源:${sources}`;
29
+ }
30
+
31
+ let source: MusicSource = (config as MusicConfig | undefined)?.defaultSource ?? 'qq';
32
+ let keyword = rawKeyword;
33
+
34
+ const triggerText = input?.content?.trim() ?? '';
35
+ for (const [prefix, s] of Object.entries({
36
+ 'QQ点歌': 'qq',
37
+ 'qq点歌': 'qq',
38
+ '网易云点歌': 'netease',
39
+ '酷我点歌': 'kuwo',
40
+ '酷狗点歌': 'kugou',
41
+ } as Record<string, MusicSource>)) {
42
+ if (triggerText.startsWith(prefix)) {
43
+ source = s;
44
+ keyword = triggerText.slice(prefix.length).trim() || rawKeyword;
45
+ break;
46
+ }
47
+ }
48
+
49
+ const sourceFromKeyword = resolveSourceAlias(keyword.split(/\s+/)[0] ?? '');
50
+ if (sourceFromKeyword) {
51
+ source = sourceFromKeyword;
52
+ keyword = keyword.slice(keyword.indexOf(' ') + 1).trim();
53
+ if (!keyword) return '请输入歌曲名称';
54
+ }
55
+
56
+ const pageSize = (config as MusicConfig | undefined)?.pageSize ?? 5;
57
+ const result = await searchMusic(use(musicRuntimeToken), keyword, source, pageSize);
58
+ if (result.total === 0) {
59
+ return `[${SOURCE_DISPLAY_NAME[source]}] 未找到"${keyword}"相关歌曲`;
60
+ }
61
+
62
+ const ids = input ? resolveMessageIds(input) : null;
63
+ if (ids) {
64
+ const key = sessionKey(ids.endpointId, ids.conversationId, ids.senderId);
65
+ use(musicRuntimeToken).sessions.set(key, {
66
+ results: result.results,
67
+ source: result.source,
68
+ keyword,
69
+ timestamp: Date.now(),
70
+ });
71
+ }
72
+
73
+ return formatSearchResults(result.results, result.source);
74
+ },
75
+ });
@@ -0,0 +1,24 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { defineComponent } from 'zhin.js/component';
3
+ import { sourceConfigMap } from "../../lib/config.js";
4
+ import { musicRuntimeToken } from "../../lib/runtime.js";
5
+ export default defineComponent({
6
+ async render({ platform, musicId }, context) {
7
+ const service = context.use(musicRuntimeToken).services[platform];
8
+ if (!service)
9
+ return 'unsupported music source';
10
+ const detail = await service.getDetail(musicId);
11
+ return {
12
+ type: 'share',
13
+ data: {
14
+ title: detail.title,
15
+ url: detail.url,
16
+ image: detail.image,
17
+ audio: detail.audio,
18
+ duration: detail.duration,
19
+ artist: detail.artist,
20
+ config: sourceConfigMap[platform],
21
+ },
22
+ };
23
+ },
24
+ });
@@ -0,0 +1,29 @@
1
+ import { defineComponent } from 'zhin.js/component';
2
+ import { sourceConfigMap } from '../../src/config.js';
3
+ import type { MusicSource } from '../../src/types.js';
4
+ import { musicRuntimeToken } from '../../src/runtime.js';
5
+
6
+ interface ShareMusicProps {
7
+ readonly platform: MusicSource;
8
+ readonly musicId: string;
9
+ }
10
+
11
+ export default defineComponent<ShareMusicProps>({
12
+ async render({ platform, musicId }, context) {
13
+ const service = context.use(musicRuntimeToken).services[platform];
14
+ if (!service) return 'unsupported music source';
15
+ const detail = await service.getDetail(musicId);
16
+ return {
17
+ type: 'share',
18
+ data: {
19
+ title: detail.title,
20
+ url: detail.url,
21
+ image: detail.image,
22
+ audio: detail.audio,
23
+ duration: detail.duration,
24
+ artist: detail.artist,
25
+ config: sourceConfigMap[platform],
26
+ },
27
+ };
28
+ },
29
+ });
package/lib/config.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import type { MusicSourceConfig, MusicSource } from './types.js';
2
- /** 音乐源配置映射 */
3
2
  export declare const sourceConfigMap: Record<MusicSource, MusicSourceConfig>;
4
- /** 格式化时长 */
3
+ export declare const SOURCE_ALIASES: Record<string, MusicSource>;
4
+ export declare const SOURCE_DISPLAY_NAME: Record<MusicSource, string>;
5
+ export declare function resolveSourceAlias(input: string): MusicSource | undefined;
5
6
  export declare function formatDuration(seconds?: number): string;
6
- /** 格式化音乐信息 */
7
7
  export declare function formatMusicInfo(music: {
8
8
  title: string;
9
9
  artist?: string;