@zhin.js/plugin-music 1.1.14 → 1.1.16

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 (51) hide show
  1. package/commands/cookie/delete/[source]/index.js +8 -7
  2. package/commands/cookie/delete/[source]/index.ts +8 -7
  3. package/commands/cookie/get/[source]/index.js +10 -9
  4. package/commands/cookie/get/[source]/index.ts +10 -9
  5. package/commands/cookie/set/[source]/index.js +8 -7
  6. package/commands/cookie/set/[source]/index.ts +8 -7
  7. package/commands/cookie/status/index.js +7 -7
  8. package/commands/cookie/status/index.ts +7 -7
  9. package/commands/login/[platform]/index.js +22 -18
  10. package/commands/login/[platform]/index.ts +23 -19
  11. package/commands//347/202/271/346/255/214/[keyword]/index.js +13 -9
  12. package/commands//347/202/271/346/255/214/[keyword]/index.ts +13 -9
  13. package/lib/i18n.d.ts +6 -0
  14. package/lib/i18n.d.ts.map +1 -0
  15. package/lib/i18n.js +50 -0
  16. package/lib/i18n.js.map +1 -0
  17. package/lib/login/index.d.ts +5 -2
  18. package/lib/login/index.d.ts.map +1 -1
  19. package/lib/login/index.js +13 -10
  20. package/lib/login/index.js.map +1 -1
  21. package/lib/login/netease.d.ts +3 -2
  22. package/lib/login/netease.d.ts.map +1 -1
  23. package/lib/login/netease.js +10 -9
  24. package/lib/login/netease.js.map +1 -1
  25. package/lib/login/qq.d.ts +2 -1
  26. package/lib/login/qq.d.ts.map +1 -1
  27. package/lib/login/qq.js +12 -11
  28. package/lib/login/qq.js.map +1 -1
  29. package/lib/login/types.d.ts +3 -2
  30. package/lib/login/types.d.ts.map +1 -1
  31. package/lib/login/types.js.map +1 -1
  32. package/lib/music-lib.d.ts +3 -2
  33. package/lib/music-lib.d.ts.map +1 -1
  34. package/lib/music-lib.js +8 -7
  35. package/lib/music-lib.js.map +1 -1
  36. package/lib/session.d.ts +7 -10
  37. package/lib/session.d.ts.map +1 -1
  38. package/lib/session.js +9 -8
  39. package/lib/session.js.map +1 -1
  40. package/middlewares/music-selection/index.js +9 -7
  41. package/middlewares/music-selection/index.ts +9 -6
  42. package/package.json +11 -10
  43. package/skills/music/tools/music-share/index.js +1 -1
  44. package/skills/music/tools/music-share/index.ts +1 -1
  45. package/src/i18n.ts +50 -0
  46. package/src/login/index.ts +16 -10
  47. package/src/login/netease.ts +11 -9
  48. package/src/login/qq.ts +13 -11
  49. package/src/login/types.ts +3 -2
  50. package/src/music-lib.ts +9 -7
  51. package/src/session.ts +13 -11
@@ -1 +1 @@
1
- {"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AASA,MAAM,iBAAiB,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;AAExC,MAAM,UAAU,UAAU,CACxB,UAAkB,EAClB,cAAsB,EACtB,QAAgB;IAEhB,OAAO,GAAG,UAAU,IAAI,cAAc,IAAI,QAAQ,EAAE,CAAC;AACvD,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,KAIjC;IACC,MAAM,IAAI,GAAG,KAAK,CAAC,QAAQ,IAAI,EAAE,CAAC;IAClC,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,UAAU,IAAI,KAAK,CAAC,YAAY,EAAE,QAAQ,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;IACrF,MAAM,cAAc,GAAG,MAAM,CAAC,KAAK,CAAC,YAAY,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;IAC5D,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;IAChD,IAAI,CAAC,UAAU,IAAI,CAAC,cAAc,IAAI,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC7D,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,QAAQ,EAAE,CAAC;AAClD,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
+ {"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.endpointId, ids.conversationId, ids.senderId);
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
- if (raw === '取消') {
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 = SOURCE_DISPLAY_NAME[selected.source] ?? selected.source;
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(`[${sourceName}] 获取"${selected.title}"播放信息失败:${detailResult.error}`);
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.endpointId, ids.conversationId, ids.senderId);
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
- if (raw === '取消') {
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 = SOURCE_DISPLAY_NAME[selected.source] ?? selected.source;
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
- `[${sourceName}] 获取"${selected.title}"播放信息失败:${detailResult.error}`,
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.14",
3
+ "version": "1.1.16",
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.49",
46
- "@zhin.js/tool": "1.1.8",
47
- "@zhin.js/skill": "1.1.8"
45
+ "@zhin.js/core": "1.1.51",
46
+ "@zhin.js/im-contract": "1.1.7",
47
+ "@zhin.js/tool": "1.1.10",
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.14"
53
+ "zhin.js": "1.1.16"
53
54
  },
54
55
  "peerDependencies": {
55
- "@zhin.js/adapter-icqq": "^1.1.14",
56
- "@zhin.js/command": "^1.1.9",
57
- "@zhin.js/component": "^1.1.8",
58
- "@zhin.js/middleware": "^1.1.8",
59
- "zhin.js": "^1.1.14"
56
+ "@zhin.js/adapter-icqq": "^1.1.16",
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.16"
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
+ }
@@ -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
- endpointId: string,
21
- conversationId: string,
25
+ conversation: ConversationRef,
22
26
  senderId: string,
23
27
  ): string {
24
- return `login:${endpointId}:${conversationId}:${senderId}`;
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: `轮询异常:${error instanceof Error ? error.message : String(error)}`,
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 {
@@ -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&timestamp=${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>): Promise<QrPollResult> {
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: `轮询失败:${err instanceof Error ? err.message : String(err)}`,
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>): Promise<QrPollResult> {
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: `服务端返回空响应 (HTTP ${response.status})` };
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: `轮询失败:${err instanceof Error ? err.message : String(err)}`,
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: '登录成功', cookie };
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 ? '登录成功' : '登录成功但获取 cookie 失败',
152
+ message: t(locale, cookie ? 'confirmed' : 'cookieFailed'),
151
153
  cookie: cookie || undefined,
152
154
  };
153
155
  }
@@ -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>): Promise<QrPollResult>;
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 { sourceConfigMap, SOURCE_DISPLAY_NAME, formatDuration } from './config.js';
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: `不支持的音乐源: ${source}` };
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 = SOURCE_DISPLAY_NAME[source] ?? source;
47
- if (results.length === 0) return `[${sourceName}] 未找到相关歌曲`;
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 `[${sourceName}] 搜索结果:\n${lines.join('\n')}\n\n回复序号播放,回复"取消"取消选择`;
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
- endpointId: string,
14
- conversationId: string,
16
+ conversation: ConversationRef,
15
17
  senderId: string,
16
18
  ): string {
17
- return `${endpointId}:${conversationId}:${senderId}`;
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?: { id?: unknown; endpoint?: { id?: unknown } };
24
+ conversation?: ConversationRef;
23
25
  sender?: { id?: unknown };
24
- }): { endpointId: string; conversationId: string; senderId: string } | null {
25
- const meta = input.metadata ?? {};
26
- const endpointId = String(meta.endpointId ?? input.conversation?.endpoint?.id ?? '');
27
- const conversationId = String(input.conversation?.id ?? '');
28
- const senderId = String(input.sender?.id ?? '');
29
- if (!endpointId || !conversationId || !senderId) return null;
30
- return { endpointId, conversationId, senderId };
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. */