@zhin.js/plugin-music 1.1.15 → 1.1.17
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/commands/cookie/delete/[source]/index.js +8 -7
- package/commands/cookie/delete/[source]/index.ts +8 -7
- package/commands/cookie/get/[source]/index.js +10 -9
- package/commands/cookie/get/[source]/index.ts +10 -9
- package/commands/cookie/set/[source]/index.js +8 -7
- package/commands/cookie/set/[source]/index.ts +8 -7
- package/commands/cookie/status/index.js +7 -7
- package/commands/cookie/status/index.ts +7 -7
- package/commands/login/[platform]/index.js +22 -18
- package/commands/login/[platform]/index.ts +23 -19
- package/commands//347/202/271/346/255/214/[keyword]/index.js +13 -9
- package/commands//347/202/271/346/255/214/[keyword]/index.ts +13 -9
- package/lib/i18n.d.ts +6 -0
- package/lib/i18n.d.ts.map +1 -0
- package/lib/i18n.js +50 -0
- package/lib/i18n.js.map +1 -0
- package/lib/login/index.d.ts +5 -2
- package/lib/login/index.d.ts.map +1 -1
- package/lib/login/index.js +13 -10
- package/lib/login/index.js.map +1 -1
- package/lib/login/netease.d.ts +3 -2
- package/lib/login/netease.d.ts.map +1 -1
- package/lib/login/netease.js +10 -9
- package/lib/login/netease.js.map +1 -1
- package/lib/login/qq.d.ts +2 -1
- package/lib/login/qq.d.ts.map +1 -1
- package/lib/login/qq.js +12 -11
- package/lib/login/qq.js.map +1 -1
- package/lib/login/types.d.ts +3 -2
- package/lib/login/types.d.ts.map +1 -1
- package/lib/login/types.js.map +1 -1
- package/lib/music-lib.d.ts +3 -2
- package/lib/music-lib.d.ts.map +1 -1
- package/lib/music-lib.js +8 -7
- package/lib/music-lib.js.map +1 -1
- package/lib/session.d.ts +7 -10
- package/lib/session.d.ts.map +1 -1
- package/lib/session.js +9 -8
- package/lib/session.js.map +1 -1
- package/middlewares/music-selection/index.js +9 -7
- package/middlewares/music-selection/index.ts +9 -6
- package/package.json +11 -10
- package/skills/music/tools/music-share/index.js +1 -1
- package/skills/music/tools/music-share/index.ts +1 -1
- package/src/i18n.ts +50 -0
- package/src/login/index.ts +16 -10
- package/src/login/netease.ts +11 -9
- package/src/login/qq.ts +13 -11
- package/src/login/types.ts +3 -2
- package/src/music-lib.ts +9 -7
- package/src/session.ts +13 -11
package/lib/session.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAwB,MAAM,sBAAsB,CAAC;AAWhF,MAAM,iBAAiB,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;AAExC,MAAM,UAAU,UAAU,CACxB,YAA6B,EAC7B,QAAgB;IAEhB,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC,kBAAkB,CAAC,YAAY,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC;AACtE,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,KAIrB;IACX,MAAM,YAAY,GAAG,KAAK,EAAE,YAAY,CAAC;IACzC,MAAM,QAAQ,GAAG,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC;IACnC,IAAI,CAAC,YAAY,EAAE,QAAQ,EAAE,EAAE,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,OAAO,IAAI,CAAC,YAAY,CAAC,EAAE;WAChF,CAAC,CAAC,SAAS,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC,QAAQ,CAAC,YAAY,CAAC,IAAI,CAAC;WAC5D,OAAO,QAAQ,KAAK,QAAQ,IAAI,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC5D,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,CAAC;AACpC,CAAC;AAED,iDAAiD;AACjD,MAAM,OAAO,mBAAmB;IACrB,QAAQ,GAAG,IAAI,GAAG,EAAyB,CAAC;IAC5C,UAAU,CAAS;IACnB,IAAI,CAAe;IAE5B,YACE,SAAS,GAAG,iBAAiB,EAC7B,MAAoB,IAAI,CAAC,GAAG;QAE5B,IAAI,CAAC,UAAU,GAAG,SAAS,CAAC;QAC5B,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC;IAClB,CAAC;IAED,GAAG,CAAC,GAAW,EAAE,MAAqB;QACpC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IACjC,CAAC;IAED,GAAG,CAAC,GAAW;QACb,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACvC,IAAI,CAAC,OAAO;YAAE,OAAO,SAAS,CAAC;QAC/B,IAAI,IAAI,CAAC,IAAI,EAAE,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,UAAU,EAAE,CAAC;YACtD,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAC1B,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,MAAM,CAAC,GAAW;QAChB,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC5B,CAAC;IAED,YAAY;QACV,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QACxB,KAAK,MAAM,CAAC,GAAG,EAAE,OAAO,CAAC,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC3C,IAAI,GAAG,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,UAAU,EAAE,CAAC;gBAC9C,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAC5B,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO;QACL,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;IACxB,CAAC;CACF"}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
import { resolveLocale } from 'zhin.js/i18n';
|
|
3
|
+
import { messages as t, sourceName as displayName } from "../../lib/i18n.js";
|
|
2
4
|
import { defineMiddleware } from 'zhin.js/middleware';
|
|
3
5
|
import { sessionKey, resolveMessageIds } from "../../lib/session.js";
|
|
4
6
|
import { shareMusicDetail, buildMusicShareSegment } from "../../lib/music-lib.js";
|
|
5
|
-
import { SOURCE_DISPLAY_NAME } from "../../lib/config.js";
|
|
6
7
|
import { musicRuntimeToken } from "../../lib/runtime.js";
|
|
7
8
|
export default defineMiddleware({
|
|
8
9
|
target: 'inbound',
|
|
@@ -17,16 +18,17 @@ export default defineMiddleware({
|
|
|
17
18
|
await next();
|
|
18
19
|
return;
|
|
19
20
|
}
|
|
20
|
-
const key = sessionKey(ids.
|
|
21
|
+
const key = sessionKey(ids.conversation, ids.senderId);
|
|
21
22
|
const runtime = context.use(musicRuntimeToken);
|
|
22
23
|
const pending = runtime.sessions.get(key);
|
|
23
24
|
if (!pending) {
|
|
24
25
|
await next();
|
|
25
26
|
return;
|
|
26
27
|
}
|
|
27
|
-
|
|
28
|
+
const locale = resolveLocale(pending.locale);
|
|
29
|
+
if (raw === (locale === 'en' ? 'cancel' : '取消')) {
|
|
28
30
|
runtime.sessions.delete(key);
|
|
29
|
-
await context.input.$reply('
|
|
31
|
+
await context.input.$reply(t(locale, 'cancelSelection'));
|
|
30
32
|
return;
|
|
31
33
|
}
|
|
32
34
|
const num = parseInt(raw, 10);
|
|
@@ -36,10 +38,10 @@ export default defineMiddleware({
|
|
|
36
38
|
}
|
|
37
39
|
runtime.sessions.delete(key);
|
|
38
40
|
const selected = pending.results[num - 1];
|
|
39
|
-
const sourceName =
|
|
40
|
-
const detailResult = await shareMusicDetail(runtime, selected.id, selected.source);
|
|
41
|
+
const sourceName = displayName(selected.source, locale) ?? selected.source;
|
|
42
|
+
const detailResult = await shareMusicDetail(runtime, selected.id, selected.source, locale);
|
|
41
43
|
if (!detailResult.success) {
|
|
42
|
-
await context.input.$reply(
|
|
44
|
+
await context.input.$reply(t(locale, 'detailFailed', { source: sourceName, title: selected.title, error: detailResult.error }));
|
|
43
45
|
return;
|
|
44
46
|
}
|
|
45
47
|
const segment = buildMusicShareSegment(detailResult.music);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { resolveLocale } from 'zhin.js/i18n';
|
|
2
|
+
import { messages as t, sourceName as displayName } from '../../src/i18n.js';
|
|
1
3
|
import { defineMiddleware } from 'zhin.js/middleware';
|
|
2
4
|
import type { Message } from '@zhin.js/core/runtime';
|
|
3
5
|
import { sessionKey, resolveMessageIds } from '../../src/session.js';
|
|
4
6
|
import { shareMusicDetail, buildMusicShareSegment } from '../../src/music-lib.js';
|
|
5
|
-
import { SOURCE_DISPLAY_NAME } from '../../src/config.js';
|
|
6
7
|
import { musicRuntimeToken } from '../../src/runtime.js';
|
|
7
8
|
|
|
8
9
|
export default defineMiddleware<Message>({
|
|
@@ -20,7 +21,7 @@ export default defineMiddleware<Message>({
|
|
|
20
21
|
return;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
const key = sessionKey(ids.
|
|
24
|
+
const key = sessionKey(ids.conversation, ids.senderId);
|
|
24
25
|
const runtime = context.use(musicRuntimeToken);
|
|
25
26
|
const pending = runtime.sessions.get(key);
|
|
26
27
|
if (!pending) {
|
|
@@ -28,9 +29,10 @@ export default defineMiddleware<Message>({
|
|
|
28
29
|
return;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
const locale = resolveLocale(pending.locale);
|
|
33
|
+
if (raw === (locale === 'en' ? 'cancel' : '取消')) {
|
|
32
34
|
runtime.sessions.delete(key);
|
|
33
|
-
await context.input.$reply('
|
|
35
|
+
await context.input.$reply(t(locale, 'cancelSelection'));
|
|
34
36
|
return;
|
|
35
37
|
}
|
|
36
38
|
|
|
@@ -42,16 +44,17 @@ export default defineMiddleware<Message>({
|
|
|
42
44
|
|
|
43
45
|
runtime.sessions.delete(key);
|
|
44
46
|
const selected = pending.results[num - 1]!;
|
|
45
|
-
const sourceName =
|
|
47
|
+
const sourceName = displayName(selected.source, locale) ?? selected.source;
|
|
46
48
|
|
|
47
49
|
const detailResult = await shareMusicDetail(
|
|
48
50
|
runtime,
|
|
49
51
|
selected.id,
|
|
50
52
|
selected.source,
|
|
53
|
+
locale,
|
|
51
54
|
);
|
|
52
55
|
if (!detailResult.success) {
|
|
53
56
|
await context.input.$reply(
|
|
54
|
-
|
|
57
|
+
t(locale, 'detailFailed', { source: sourceName, title: selected.title, error: detailResult.error }),
|
|
55
58
|
);
|
|
56
59
|
return;
|
|
57
60
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/plugin-music",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.17",
|
|
4
4
|
"description": "Music search and sharing plugin for Zhin.js (Plugin Runtime)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -42,21 +42,22 @@
|
|
|
42
42
|
},
|
|
43
43
|
"license": "MIT",
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@zhin.js/core": "1.1.
|
|
46
|
-
"@zhin.js/
|
|
47
|
-
"@zhin.js/
|
|
45
|
+
"@zhin.js/core": "1.1.52",
|
|
46
|
+
"@zhin.js/im-contract": "1.1.8",
|
|
47
|
+
"@zhin.js/tool": "1.1.11",
|
|
48
|
+
"@zhin.js/skill": "1.1.10"
|
|
48
49
|
},
|
|
49
50
|
"devDependencies": {
|
|
50
51
|
"typescript": "^6.0.3",
|
|
51
52
|
"vitest": "^4.1.10",
|
|
52
|
-
"zhin.js": "1.1.
|
|
53
|
+
"zhin.js": "1.1.17"
|
|
53
54
|
},
|
|
54
55
|
"peerDependencies": {
|
|
55
|
-
"@zhin.js/adapter-icqq": "^1.1.
|
|
56
|
-
"@zhin.js/command": "^1.1.
|
|
57
|
-
"@zhin.js/component": "^1.1.
|
|
58
|
-
"@zhin.js/middleware": "^1.1.
|
|
59
|
-
"zhin.js": "^1.1.
|
|
56
|
+
"@zhin.js/adapter-icqq": "^1.1.17",
|
|
57
|
+
"@zhin.js/command": "^1.1.11",
|
|
58
|
+
"@zhin.js/component": "^1.1.10",
|
|
59
|
+
"@zhin.js/middleware": "^1.1.10",
|
|
60
|
+
"zhin.js": "^1.1.17"
|
|
60
61
|
},
|
|
61
62
|
"peerDependenciesMeta": {
|
|
62
63
|
"@zhin.js/adapter-icqq": {
|
|
@@ -17,5 +17,5 @@ export default defineAgentTool({
|
|
|
17
17
|
required: ['id', 'source'],
|
|
18
18
|
},
|
|
19
19
|
requiresApproval: 'never',
|
|
20
|
-
execute: ({ id, source }, context) => shareMusicDetail(context.use(musicRuntimeToken), String(id), source),
|
|
20
|
+
execute: ({ id, source }, context) => shareMusicDetail(context.use(musicRuntimeToken), String(id), source, context.locale),
|
|
21
21
|
});
|
|
@@ -22,5 +22,5 @@ export default defineAgentTool<{
|
|
|
22
22
|
},
|
|
23
23
|
requiresApproval: 'never',
|
|
24
24
|
execute: ({ id, source }, context) =>
|
|
25
|
-
shareMusicDetail(context.use(musicRuntimeToken), String(id), source),
|
|
25
|
+
shareMusicDetail(context.use(musicRuntimeToken), String(id), source, context.locale),
|
|
26
26
|
});
|
package/src/i18n.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createMessages, type Locale } from 'zhin.js/i18n';
|
|
2
|
+
import type { MusicSource } from './types.js';
|
|
3
|
+
import { SOURCE_DISPLAY_NAME } from './config.js';
|
|
4
|
+
export const messages = createMessages({
|
|
5
|
+
loginAborted: { 'zh-CN': '登录已取消', en: 'Login cancelled.' },
|
|
6
|
+
keyword: { 'zh-CN': "请输入歌曲名称", en: "Enter a song name." },
|
|
7
|
+
usage: { 'zh-CN': "用法:{command}", en: "Usage: {command}" },
|
|
8
|
+
sources: { 'zh-CN': "支持音乐源:{sources}", en: "Music sources: {sources}" },
|
|
9
|
+
noResults: { 'zh-CN': "[{source}] 未找到相关歌曲", en: "[{source}] No matching songs found." },
|
|
10
|
+
noMatch: { 'zh-CN': "[{source}] 未找到\"{keyword}\"相关歌曲", en: "[{source}] No songs found for \"{keyword}\"." },
|
|
11
|
+
results: { 'zh-CN': "[{source}] 搜索结果:\n{lines}\n\n回复序号播放,回复\"取消\"取消选择", en: "[{source}] Search results:\n{lines}\n\nReply with a number to play, or \"cancel\" to cancel." },
|
|
12
|
+
unsupported: { 'zh-CN': "不支持的音乐源:{source}", en: "Unsupported music source: {source}" },
|
|
13
|
+
cancelSelection: { 'zh-CN': "已取消点歌", en: "Music selection cancelled." },
|
|
14
|
+
detailFailed: { 'zh-CN': "[{source}] 获取\"{title}\"播放信息失败:{error}", en: "[{source}] Could not load \"{title}\": {error}" },
|
|
15
|
+
credentials: { 'zh-CN': "--- 音乐凭证状态 ---", en: "--- Music credentials ---" },
|
|
16
|
+
unset: { 'zh-CN': "未配置", en: "Not configured" },
|
|
17
|
+
count: { 'zh-CN': "{keys} ({count} 项)", en: "{keys} ({count} entries)" },
|
|
18
|
+
credentialUnset: { 'zh-CN': "[{source}] 凭证 {key} 未设置", en: "[{source}] Credential {key} is not set." },
|
|
19
|
+
saved: { 'zh-CN': "[{source}] 凭证 {key} 已保存", en: "[{source}] Credential {key} saved." },
|
|
20
|
+
deleted: { 'zh-CN': "[{source}] 凭证 {key} 已删除", en: "[{source}] Credential {key} deleted." },
|
|
21
|
+
conversation: { 'zh-CN': "无法识别会话信息,请在群聊或私聊中使用", en: "Cannot identify this conversation. Use a group or private chat." },
|
|
22
|
+
cancelLogin: { 'zh-CN': "已取消登录", en: "Login cancelled." },
|
|
23
|
+
noLogin: { 'zh-CN': "没有进行中的登录会话", en: "There is no active login session." },
|
|
24
|
+
platform: { 'zh-CN': "不支持的平台:{platform}\n支持:qq, netease", en: "Unsupported platform: {platform}\nSupported: qq, netease" },
|
|
25
|
+
active: { 'zh-CN': "正在进行 {source} 登录,请先完成", en: "A {source} login is in progress. Complete it first." },
|
|
26
|
+
cancelHint: { 'zh-CN': "发送\"{command}\"可中止", en: "Send \"{command}\" to cancel." },
|
|
27
|
+
qrFailed: { 'zh-CN': "[{source}] 获取二维码失败:{error}", en: "[{source}] Could not obtain a QR code: {error}" },
|
|
28
|
+
noReply: { 'zh-CN': "无法发送消息", en: "Cannot send a reply." },
|
|
29
|
+
scan: { 'zh-CN': "[{source}] 请使用 {source} App 扫描二维码登录:", en: "[{source}] Scan the QR code with the {source} app to sign in:" },
|
|
30
|
+
valid: { 'zh-CN': "二维码有效期 2 分钟", en: "The QR code is valid for 2 minutes." },
|
|
31
|
+
success: { 'zh-CN': "登录成功!cookie 已自动保存,现在可以搜索和播放高品质音乐了", en: "Signed in. The cookie has been saved; you can now search and play high-quality music." },
|
|
32
|
+
waiting: { 'zh-CN': "等待扫码...", en: "Waiting for scan..." },
|
|
33
|
+
scanned: { 'zh-CN': "已扫码,请在手机上确认登录", en: "Scanned. Confirm the login on your phone." },
|
|
34
|
+
expired: { 'zh-CN': "二维码已过期,请重新发起登录", en: "The QR code has expired. Start a new login." },
|
|
35
|
+
confirmed: { 'zh-CN': "登录成功", en: "Signed in." },
|
|
36
|
+
unknown: { 'zh-CN': "未知状态", en: "Unknown status" },
|
|
37
|
+
pollFailed: { 'zh-CN': "轮询失败:{error}", en: "Login polling failed: {error}" },
|
|
38
|
+
pollError: { 'zh-CN': "轮询异常:{error}", en: "Login polling error: {error}" },
|
|
39
|
+
timeout: { 'zh-CN': "登录超时,请重试", en: "Login timed out. Try again." },
|
|
40
|
+
neteaseQr: { 'zh-CN': "获取网易云登录二维码失败", en: "Could not obtain a NetEase login QR code." },
|
|
41
|
+
emptyResponse: { 'zh-CN': "服务端返回空响应 (HTTP {status})", en: "The server returned an empty response (HTTP {status})." },
|
|
42
|
+
cookieFailed: { 'zh-CN': "登录成功但获取 cookie 失败", en: "Signed in, but could not retrieve the cookie." },
|
|
43
|
+
});
|
|
44
|
+
const EN_SOURCE_NAMES: Record<MusicSource, string> = { qq: 'QQ Music', netease: 'NetEase Cloud Music', kuwo: 'Kuwo', kugou: 'Kugou' };
|
|
45
|
+
export function sourceName(source: MusicSource, locale: Locale = 'zh-CN'): string {
|
|
46
|
+
return locale === 'en' ? EN_SOURCE_NAMES[source] : SOURCE_DISPLAY_NAME[source];
|
|
47
|
+
}
|
|
48
|
+
export function usage(locale: Locale, command: string | undefined): string {
|
|
49
|
+
return command === undefined ? '' : messages(locale, 'usage', { command });
|
|
50
|
+
}
|
package/src/login/index.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import type { Locale } from 'zhin.js/i18n';
|
|
2
|
+
import type { ConversationRef } from '@zhin.js/im-contract';
|
|
3
|
+
import { sessionKey } from '../session.js';
|
|
4
|
+
import { messages as t } from '../i18n.js';
|
|
1
5
|
import type { QrLoginProvider, QrLoginSource, QrPollResult } from './types.js';
|
|
2
6
|
import { QQLoginProvider } from './qq.js';
|
|
3
7
|
import { NeteaseLoginProvider } from './netease.js';
|
|
@@ -6,6 +10,7 @@ import type { CredentialStore } from '../credential-store.js';
|
|
|
6
10
|
export type { QrLoginSource, QrPollResult, QrLoginProvider } from './types.js';
|
|
7
11
|
|
|
8
12
|
export interface QrLoginSession {
|
|
13
|
+
locale: Locale;
|
|
9
14
|
source: QrLoginSource;
|
|
10
15
|
pollData: Record<string, string>;
|
|
11
16
|
timestamp: number;
|
|
@@ -17,11 +22,10 @@ const POLL_INTERVAL_MS = 3_000;
|
|
|
17
22
|
const MAX_POLLS = Math.ceil(LOGIN_TIMEOUT_MS / POLL_INTERVAL_MS);
|
|
18
23
|
|
|
19
24
|
export function loginSessionKey(
|
|
20
|
-
|
|
21
|
-
conversationId: string,
|
|
25
|
+
conversation: ConversationRef,
|
|
22
26
|
senderId: string,
|
|
23
27
|
): string {
|
|
24
|
-
return `login:${
|
|
28
|
+
return `login:${sessionKey(conversation, senderId)}`;
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
/** Generation-owned QR login coordinator. */
|
|
@@ -69,11 +73,13 @@ export class QrLoginRuntime {
|
|
|
69
73
|
async start(
|
|
70
74
|
source: QrLoginSource,
|
|
71
75
|
key: string,
|
|
76
|
+
locale: Locale = 'zh-CN',
|
|
72
77
|
): Promise<{ imageSegment: { type: 'image'; data: Record<string, unknown> } }> {
|
|
73
78
|
const existing = this.#active.get(key);
|
|
74
79
|
if (existing && !existing.aborted) existing.aborted = true;
|
|
75
80
|
const session: QrLoginSession = {
|
|
76
81
|
source,
|
|
82
|
+
locale,
|
|
77
83
|
pollData: {},
|
|
78
84
|
timestamp: this.#now(),
|
|
79
85
|
aborted: false,
|
|
@@ -81,9 +87,9 @@ export class QrLoginRuntime {
|
|
|
81
87
|
this.#active.set(key, session);
|
|
82
88
|
|
|
83
89
|
try {
|
|
84
|
-
const result = await this.#providers[source].createQr();
|
|
90
|
+
const result = await this.#providers[source].createQr(locale);
|
|
85
91
|
if (session.aborted || this.#active.get(key) !== session) {
|
|
86
|
-
throw new Error('
|
|
92
|
+
throw new Error(t(locale, 'loginAborted'));
|
|
87
93
|
}
|
|
88
94
|
session.pollData = result.pollData;
|
|
89
95
|
return { imageSegment: result.imageSegment };
|
|
@@ -98,7 +104,7 @@ export class QrLoginRuntime {
|
|
|
98
104
|
onStatus: (result: QrPollResult) => Promise<void>,
|
|
99
105
|
): Promise<QrPollResult> {
|
|
100
106
|
const session = this.#active.get(key);
|
|
101
|
-
if (!session) return { status: 'error', message: '
|
|
107
|
+
if (!session) return { status: 'error', message: t('zh-CN', 'noLogin') };
|
|
102
108
|
|
|
103
109
|
const provider = this.#providers[session.source];
|
|
104
110
|
let lastStatus = '';
|
|
@@ -106,10 +112,10 @@ export class QrLoginRuntime {
|
|
|
106
112
|
for (let i = 0; i < MAX_POLLS; i++) {
|
|
107
113
|
if (session.aborted) {
|
|
108
114
|
this.#active.delete(key);
|
|
109
|
-
return { status: 'error', message: '
|
|
115
|
+
return { status: 'error', message: t(session.locale, 'loginAborted') };
|
|
110
116
|
}
|
|
111
117
|
|
|
112
|
-
const result = await provider.pollQr(session.pollData);
|
|
118
|
+
const result = await provider.pollQr(session.pollData, session.locale);
|
|
113
119
|
if (result.status !== lastStatus) {
|
|
114
120
|
lastStatus = result.status;
|
|
115
121
|
await onStatus(result);
|
|
@@ -129,12 +135,12 @@ export class QrLoginRuntime {
|
|
|
129
135
|
this.#active.delete(key);
|
|
130
136
|
return {
|
|
131
137
|
status: 'error',
|
|
132
|
-
message:
|
|
138
|
+
message: t(session.locale, 'pollError', { error: error instanceof Error ? error.message : String(error) }),
|
|
133
139
|
};
|
|
134
140
|
}
|
|
135
141
|
|
|
136
142
|
this.#active.delete(key);
|
|
137
|
-
return { status: 'expired', message: '
|
|
143
|
+
return { status: 'expired', message: t(session.locale, 'timeout') };
|
|
138
144
|
}
|
|
139
145
|
|
|
140
146
|
pruneExpired(): void {
|
package/src/login/netease.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import type { Locale } from 'zhin.js/i18n';
|
|
2
|
+
import { messages as t } from '../i18n.js';
|
|
1
3
|
import type { QrLoginProvider, QrCreateResult, QrPollResult } from './types.js';
|
|
2
4
|
import { extractSetCookies } from './types.js';
|
|
3
5
|
|
|
4
6
|
const QR_IMAGE_API = 'https://api.qrserver.com/v1/create-qr-code/';
|
|
5
7
|
|
|
6
8
|
export class NeteaseLoginProvider implements QrLoginProvider {
|
|
7
|
-
async createQr(): Promise<QrCreateResult> {
|
|
9
|
+
async createQr(locale: Locale = 'zh-CN'): Promise<QrCreateResult> {
|
|
8
10
|
const timestampMs = Date.now();
|
|
9
11
|
const keyResp = await fetch(
|
|
10
12
|
`https://music.163.com/api/login/qrcode/unikey?type=1×tamp=${timestampMs}`,
|
|
@@ -16,7 +18,7 @@ export class NeteaseLoginProvider implements QrLoginProvider {
|
|
|
16
18
|
);
|
|
17
19
|
const keyData = (await keyResp.json()) as { code: number; unikey?: string };
|
|
18
20
|
if (keyData.code !== 200 || !keyData.unikey) {
|
|
19
|
-
throw new Error('
|
|
21
|
+
throw new Error(t(locale, 'neteaseQr'));
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
const loginUrl = `https://music.163.com/login?codekey=${keyData.unikey}`;
|
|
@@ -28,7 +30,7 @@ export class NeteaseLoginProvider implements QrLoginProvider {
|
|
|
28
30
|
};
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
async pollQr(pollData: Record<string, string
|
|
33
|
+
async pollQr(pollData: Record<string, string>, locale: Locale = 'zh-CN'): Promise<QrPollResult> {
|
|
32
34
|
const { unikey } = pollData;
|
|
33
35
|
try {
|
|
34
36
|
const response = await fetch(
|
|
@@ -43,26 +45,26 @@ export class NeteaseLoginProvider implements QrLoginProvider {
|
|
|
43
45
|
|
|
44
46
|
switch (data.code) {
|
|
45
47
|
case 801:
|
|
46
|
-
return { status: 'waiting', message: '
|
|
48
|
+
return { status: 'waiting', message: t(locale, 'waiting') };
|
|
47
49
|
case 802:
|
|
48
|
-
return { status: 'scanned', message: '
|
|
50
|
+
return { status: 'scanned', message: t(locale, 'scanned') };
|
|
49
51
|
case 800:
|
|
50
|
-
return { status: 'expired', message: '
|
|
52
|
+
return { status: 'expired', message: t(locale, 'expired') };
|
|
51
53
|
case 803: {
|
|
52
54
|
const cookie = extractSetCookies(response.headers);
|
|
53
55
|
return {
|
|
54
56
|
status: 'confirmed',
|
|
55
|
-
message: '
|
|
57
|
+
message: t(locale, 'confirmed'),
|
|
56
58
|
cookie: cookie || undefined,
|
|
57
59
|
};
|
|
58
60
|
}
|
|
59
61
|
default:
|
|
60
|
-
return { status: 'error', message: data.message ?? '
|
|
62
|
+
return { status: 'error', message: data.message ?? t(locale, 'unknown') };
|
|
61
63
|
}
|
|
62
64
|
} catch (err) {
|
|
63
65
|
return {
|
|
64
66
|
status: 'error',
|
|
65
|
-
message:
|
|
67
|
+
message: t(locale, 'pollFailed', { error: err instanceof Error ? err.message : String(err) }),
|
|
66
68
|
};
|
|
67
69
|
}
|
|
68
70
|
}
|
package/src/login/qq.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { Locale } from 'zhin.js/i18n';
|
|
2
|
+
import { messages as t } from '../i18n.js';
|
|
1
3
|
import type { QrLoginProvider, QrCreateResult, QrPollResult } from './types.js';
|
|
2
4
|
import { extractSetCookies } from './types.js';
|
|
3
5
|
|
|
@@ -75,7 +77,7 @@ export class QQLoginProvider implements QrLoginProvider {
|
|
|
75
77
|
};
|
|
76
78
|
}
|
|
77
79
|
|
|
78
|
-
async pollQr(pollData: Record<string, string
|
|
80
|
+
async pollQr(pollData: Record<string, string>, locale: Locale = 'zh-CN'): Promise<QrPollResult> {
|
|
79
81
|
const { ptqrtoken, qrsig, loginSig } = pollData;
|
|
80
82
|
const url = new URL('https://ssl.ptlogin2.qq.com/ptqrlogin');
|
|
81
83
|
url.searchParams.set('u1', 'https://y.qq.com/');
|
|
@@ -107,32 +109,32 @@ export class QQLoginProvider implements QrLoginProvider {
|
|
|
107
109
|
const text = await response.text();
|
|
108
110
|
|
|
109
111
|
if (text.includes('二维码未失效')) {
|
|
110
|
-
return { status: 'waiting', message: '
|
|
112
|
+
return { status: 'waiting', message: t(locale, 'waiting') };
|
|
111
113
|
}
|
|
112
114
|
if (text.includes('二维码认证中') || text.includes('已扫码')) {
|
|
113
|
-
return { status: 'scanned', message: '
|
|
115
|
+
return { status: 'scanned', message: t(locale, 'scanned') };
|
|
114
116
|
}
|
|
115
117
|
if (text.includes('二维码已失效')) {
|
|
116
|
-
return { status: 'expired', message: '
|
|
118
|
+
return { status: 'expired', message: t(locale, 'expired') };
|
|
117
119
|
}
|
|
118
120
|
if (text.includes('登录成功') || text.includes('ptsigx')) {
|
|
119
|
-
return this.handleLoginSuccess(text, response.headers);
|
|
121
|
+
return this.handleLoginSuccess(text, response.headers, locale);
|
|
120
122
|
}
|
|
121
123
|
|
|
122
124
|
if (!text) {
|
|
123
|
-
return { status: 'error', message:
|
|
125
|
+
return { status: 'error', message: t(locale, 'emptyResponse', { status: response.status }) };
|
|
124
126
|
}
|
|
125
127
|
|
|
126
|
-
return { status: 'waiting', message: '
|
|
128
|
+
return { status: 'waiting', message: t(locale, 'waiting') };
|
|
127
129
|
} catch (err) {
|
|
128
130
|
return {
|
|
129
131
|
status: 'error',
|
|
130
|
-
message:
|
|
132
|
+
message: t(locale, 'pollFailed', { error: err instanceof Error ? err.message : String(err) }),
|
|
131
133
|
};
|
|
132
134
|
}
|
|
133
135
|
}
|
|
134
136
|
|
|
135
|
-
private async handleLoginSuccess(text: string, headers: Headers): Promise<QrPollResult> {
|
|
137
|
+
private async handleLoginSuccess(text: string, headers: Headers, locale: Locale): Promise<QrPollResult> {
|
|
136
138
|
const redirectMatch = text.match(/'(https?:\/\/[^']+)'/);
|
|
137
139
|
if (redirectMatch) {
|
|
138
140
|
const redirectResp = await fetch(redirectMatch[1]!, {
|
|
@@ -141,13 +143,13 @@ export class QQLoginProvider implements QrLoginProvider {
|
|
|
141
143
|
});
|
|
142
144
|
const cookie = extractSetCookies(redirectResp.headers);
|
|
143
145
|
if (cookie) {
|
|
144
|
-
return { status: 'confirmed', message: '
|
|
146
|
+
return { status: 'confirmed', message: t(locale, 'confirmed'), cookie };
|
|
145
147
|
}
|
|
146
148
|
}
|
|
147
149
|
const cookie = extractSetCookies(headers);
|
|
148
150
|
return {
|
|
149
151
|
status: cookie ? 'confirmed' : 'error',
|
|
150
|
-
message: cookie ? '
|
|
152
|
+
message: t(locale, cookie ? 'confirmed' : 'cookieFailed'),
|
|
151
153
|
cookie: cookie || undefined,
|
|
152
154
|
};
|
|
153
155
|
}
|
package/src/login/types.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Locale } from 'zhin.js/i18n';
|
|
1
2
|
export type QrLoginSource = 'qq' | 'netease';
|
|
2
3
|
|
|
3
4
|
export interface QrPollResult {
|
|
@@ -12,8 +13,8 @@ export interface QrCreateResult {
|
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export interface QrLoginProvider {
|
|
15
|
-
createQr(): Promise<QrCreateResult>;
|
|
16
|
-
pollQr(pollData: Record<string, string
|
|
16
|
+
createQr(locale?: Locale): Promise<QrCreateResult>;
|
|
17
|
+
pollQr(pollData: Record<string, string>, locale?: Locale): Promise<QrPollResult>;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
export function extractSetCookies(headers: Headers): string {
|
package/src/music-lib.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Locale } from 'zhin.js/i18n';
|
|
2
|
+
import { messages as t, sourceName as displayName } from './i18n.js';
|
|
3
|
+
import { sourceConfigMap, formatDuration } from './config.js';
|
|
2
4
|
import type { MusicInfo, MusicDetail, MusicSource } from './types.js';
|
|
3
5
|
import type { MusicRuntime } from './runtime.js';
|
|
4
6
|
|
|
@@ -26,10 +28,10 @@ export async function searchMusic(
|
|
|
26
28
|
};
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
export async function shareMusicDetail(runtime: MusicRuntime, id: string, source: MusicSource) {
|
|
31
|
+
export async function shareMusicDetail(runtime: MusicRuntime, id: string, source: MusicSource, locale: Locale = 'zh-CN') {
|
|
30
32
|
const service = runtime.services[source];
|
|
31
33
|
if (!service) {
|
|
32
|
-
return { success: false as const, error:
|
|
34
|
+
return { success: false as const, error: t(locale, 'unsupported', { source }) };
|
|
33
35
|
}
|
|
34
36
|
try {
|
|
35
37
|
const detail = await service.getDetail(id);
|
|
@@ -42,16 +44,16 @@ export async function shareMusicDetail(runtime: MusicRuntime, id: string, source
|
|
|
42
44
|
}
|
|
43
45
|
}
|
|
44
46
|
|
|
45
|
-
export function formatSearchResults(results: readonly MusicInfo[], source: MusicSource): string {
|
|
46
|
-
const sourceName =
|
|
47
|
-
if (results.length === 0) return
|
|
47
|
+
export function formatSearchResults(results: readonly MusicInfo[], source: MusicSource, locale: Locale = 'zh-CN'): string {
|
|
48
|
+
const sourceName = displayName(source, locale) ?? source;
|
|
49
|
+
if (results.length === 0) return t(locale, 'noResults', { source: sourceName });
|
|
48
50
|
const lines = results.map((m, i) => {
|
|
49
51
|
const parts = [m.title];
|
|
50
52
|
if (m.artist) parts.push(m.artist);
|
|
51
53
|
const duration = m.duration ? ` [${formatDuration(m.duration)}]` : '';
|
|
52
54
|
return `${i + 1}. ${parts.join(' - ')}${duration}`;
|
|
53
55
|
});
|
|
54
|
-
return
|
|
56
|
+
return t(locale, 'results', { source: sourceName, lines: lines.join('\n') });
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
export function buildMusicShareSegment(detail: MusicDetail) {
|
package/src/session.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import type { Locale } from 'zhin.js/i18n';
|
|
2
|
+
import { conversationRefKey, type ConversationRef } from '@zhin.js/im-contract';
|
|
1
3
|
import type { MusicInfo, MusicSource } from './types.js';
|
|
2
4
|
|
|
3
5
|
export interface PendingSearch {
|
|
6
|
+
readonly locale?: Locale;
|
|
4
7
|
readonly results: readonly MusicInfo[];
|
|
5
8
|
readonly source: MusicSource;
|
|
6
9
|
readonly keyword: string;
|
|
@@ -10,24 +13,23 @@ export interface PendingSearch {
|
|
|
10
13
|
const SEARCH_TIMEOUT_MS = 3 * 60 * 1000;
|
|
11
14
|
|
|
12
15
|
export function sessionKey(
|
|
13
|
-
|
|
14
|
-
conversationId: string,
|
|
16
|
+
conversation: ConversationRef,
|
|
15
17
|
senderId: string,
|
|
16
18
|
): string {
|
|
17
|
-
return
|
|
19
|
+
return JSON.stringify([conversationRefKey(conversation), senderId]);
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
export function resolveMessageIds(input: {
|
|
21
23
|
metadata?: Record<string, unknown>;
|
|
22
|
-
conversation?:
|
|
24
|
+
conversation?: ConversationRef;
|
|
23
25
|
sender?: { id?: unknown };
|
|
24
|
-
}): {
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return {
|
|
26
|
+
} | undefined): { conversation: ConversationRef; senderId: string } | null {
|
|
27
|
+
const conversation = input?.conversation;
|
|
28
|
+
const senderId = input?.sender?.id;
|
|
29
|
+
if (!conversation?.endpoint?.id || !conversation.endpoint.adapter || !conversation.id
|
|
30
|
+
|| !['private', 'group', 'channel'].includes(conversation.kind)
|
|
31
|
+
|| typeof senderId !== 'string' || !senderId) return null;
|
|
32
|
+
return { conversation, senderId };
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
/** Generation-owned pending music selections. */
|