@audiofool/moondog 0.1.0 → 0.2.0-beta.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.
- package/LICENSE +202 -0
- package/README.md +153 -68
- package/contracts/v1/definitions.schema.json +1 -169
- package/package.json +7 -23
- package/scripts/contract-lib.mjs +0 -7
- package/scripts/contract-semantics.mjs +0 -1904
- package/scripts/moondog.mjs +285 -74
- package/src/core/apple-library-source-status.mjs +0 -16
- package/src/core/apple-projection-domain-services.mjs +168 -36
- package/src/core/application-update.mjs +320 -0
- package/src/core/capability-catalog.mjs +278 -55
- package/src/core/listening-now.mjs +108 -0
- package/src/core/listening-profile-domain-services.mjs +153 -41
- package/src/core/listening-upkeep.mjs +31 -0
- package/src/core/moondog-application.mjs +1908 -104
- package/src/core/playback-preferences.mjs +97 -0
- package/src/core/spotify-listening-intent.mjs +121 -0
- package/src/core/synthetic-domain-services.mjs +24 -3
- package/src/demo/moondog-demo.mjs +4 -22
- package/src/i18n/index.mjs +285 -0
- package/src/i18n/messages/de.mjs +128 -0
- package/src/i18n/messages/en.mjs +129 -0
- package/src/i18n/messages/es.mjs +128 -0
- package/src/i18n/messages/fr.mjs +128 -0
- package/src/i18n/messages/it.mjs +128 -0
- package/src/i18n/messages/ja.mjs +128 -0
- package/src/i18n/messages/ko.mjs +128 -0
- package/src/i18n/messages/pt.mjs +128 -0
- package/src/i18n/messages/zh-Hant.mjs +128 -0
- package/src/i18n/messages/zh.mjs +128 -0
- package/src/i18n/preferences.mjs +34 -0
- package/src/i18n/screen/es.mjs +966 -0
- package/src/i18n/screen/ja.mjs +966 -0
- package/src/i18n/screen/pt.mjs +966 -0
- package/src/i18n/screen/zh.mjs +966 -0
- package/src/importers/apple-music-library/index.mjs +0 -16
- package/src/importers/apple-music-library/sqlite-projection.mjs +51 -9
- package/src/integrations/apple-music/catalog.mjs +123 -14
- package/src/integrations/apple-music/music-app.mjs +232 -0
- package/src/integrations/apple-music/storefront.mjs +28 -0
- package/src/integrations/apple-music/terminal-preview-player.mjs +221 -0
- package/src/integrations/lastfm/history.mjs +221 -0
- package/src/integrations/lastfm/settings.mjs +75 -0
- package/src/integrations/netease/ncm-cli-player.mjs +321 -0
- package/src/integrations/open-music-similarity/artist-radio.mjs +26 -18
- package/src/integrations/public-playlists.mjs +2 -1
- package/src/integrations/spotify/account-data-profile.mjs +14 -6
- package/src/integrations/spotify/authentication.mjs +149 -15
- package/src/integrations/spotify/catalog-resolver.mjs +8 -4
- package/src/integrations/spotify/connection.mjs +2 -1
- package/src/integrations/spotify/credential-store.mjs +4 -0
- package/src/integrations/spotify/extended-streaming-history.mjs +11 -11
- package/src/integrations/spotify/history-archive.mjs +1 -1
- package/src/integrations/spotify/library-snapshot.mjs +227 -0
- package/src/integrations/spotify/recent-activity.mjs +2 -0
- package/src/integrations/spotify/service.mjs +873 -69
- package/src/integrations/spotify/web-api-client.mjs +407 -127
- package/src/integrations/web/brave-web.mjs +45 -0
- package/src/integrations/web/codex-web.mjs +15 -82
- package/src/integrations/web/deepseek-web.mjs +81 -0
- package/src/integrations/web/web-research.mjs +119 -0
- package/src/integrations/web/web-result.mjs +215 -0
- package/src/integrations/youtube-music/takeout.mjs +3 -1
- package/src/memory/local-memory-store.mjs +469 -41
- package/src/profile/history-import.mjs +42 -6
- package/src/profile/listener-profile-build.mjs +378 -0
- package/src/profile/listener-profile-store.mjs +130 -0
- package/src/profile/listener-profile-verification.mjs +68 -0
- package/src/profile/listener-profile-worker.mjs +172 -0
- package/src/profile/listening-history-store.mjs +424 -8
- package/src/profile/listening-profile-projection.mjs +242 -76
- package/src/profile/local-music-data.mjs +1 -1
- package/src/profile/music-providers.mjs +11 -7
- package/src/profile/profile-exploration.mjs +84 -0
- package/src/profile/recorded-measures.mjs +24 -0
- package/src/profile/spotify-archive-taste.mjs +0 -4
- package/src/runtime/pi/agent-runtime.mjs +2204 -605
- package/src/runtime/pi/authentication.mjs +34 -1
- package/src/runtime/pi/configured-runtime.mjs +22 -6
- package/src/runtime/pi/conversation-name.mjs +53 -0
- package/src/runtime/pi/memory-agent-runtime.mjs +2 -2
- package/src/runtime/pi/model-catalog.mjs +28 -5
- package/src/runtime/pi/model-settings.mjs +202 -0
- package/src/runtime/pi/model-transport.mjs +10 -0
- package/src/runtime/pi/persistent-credential-store.mjs +67 -31
- package/src/runtime/pi/profile-build-runtime.mjs +160 -0
- package/src/runtime/pi/public-model-catalog.mjs +288 -0
- package/src/runtime/pi/runtime-settings.mjs +10 -2
- package/src/runtime/pi/skills/listener-profile/SKILL.md +46 -0
- package/src/runtime/pi/tool-trace.mjs +37 -0
- package/src/surfaces/cli/auth-command.mjs +12 -4
- package/src/surfaces/cli/brand-components.mjs +310 -132
- package/src/surfaces/cli/brand-theme.mjs +51 -28
- package/src/surfaces/cli/command-completions.mjs +58 -42
- package/src/surfaces/cli/format-output.mjs +199 -388
- package/src/surfaces/cli/history-import-view.mjs +292 -139
- package/src/surfaces/cli/import-guides.mjs +2 -0
- package/src/surfaces/cli/listener-profile-command.mjs +92 -0
- package/src/surfaces/cli/listening-menu.mjs +7 -4
- package/src/surfaces/cli/profile-command.mjs +30 -21
- package/src/surfaces/cli/seasons-chart.mjs +87 -0
- package/src/surfaces/cli/spotify-command.mjs +54 -6
- package/src/surfaces/cli/taste-profile-model.mjs +78 -79
- package/src/surfaces/cli/taste-profile-view.mjs +33 -26
- package/src/surfaces/cli/terminal-art.mjs +140 -35
- package/src/surfaces/cli/tui.mjs +1066 -357
- package/src/surfaces/cli/update-command.mjs +32 -0
- package/src/surfaces/cli/web-command.mjs +53 -14
- package/src/surfaces/html/tasteprint.mjs +43 -70
- package/contracts/v1/capability-policy.schema.json +0 -572
- package/contracts/v1/profile-evidence.schema.json +0 -237
- package/contracts/v1/tool-invocation.schema.json +0 -318
- package/contracts/v1/tool-receipt.schema.json +0 -1066
- package/scripts/demo-studio.mjs +0 -58
- package/src/core/apple-data-migration.mjs +0 -50
- package/src/demo/moondog-interactive-tasteprint-demo.mjs +0 -293
- package/src/demo/moondog-tasteprint-demo.mjs +0 -631
- package/src/integrations/spotify/index.mjs +0 -80
- package/src/surfaces/web/showcase.mjs +0 -272
- package/src/surfaces/web/studio-page.mjs +0 -4148
- package/src/surfaces/web/studio.mjs +0 -2288
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// 繁體中文(台灣用語)。Keys match messages/en.mjs; a missing key falls back to English.
|
|
2
|
+
export default {
|
|
3
|
+
"discovery.reason.listenbrainz": ({ adjacent, seed, release }) => `ListenBrainz 收聽資料將 ${adjacent} 與 ${seed} 連在一起;選自《${release}》。`,
|
|
4
|
+
"discovery.reason.appleMusic": ({ queries, release, genre }) => `Apple Music 目錄${queries ? `符合搜尋詞 ${queries}` : "候選"};選自《${release}》${genre ? `,目錄曲風為 ${genre}` : ""}。`,
|
|
5
|
+
"discovery.reason.external": ({ release }) => `來自已檢索的外部目錄,選自《${release}》。`,
|
|
6
|
+
"discovery.order": ({ path }) => `依序試聽:${path}。`,
|
|
7
|
+
"discovery.unavailable": "音樂探索服務暫時無法連線,這次沒有產生推薦。請重新送出這個請求再試一次。",
|
|
8
|
+
|
|
9
|
+
"citations.heading": "公開音樂來源(與私人聆聽證據分開):",
|
|
10
|
+
"citations.item": ({ label, url, storefront, date }) => `- [${label}](<${url}>) - ${storefront} 商店,檢索於 ${date}。`,
|
|
11
|
+
"citations.scope": "範圍:這些公開目錄頁面無法證明個人偏好、完整聆聽紀錄或跨平台可用性。",
|
|
12
|
+
"web.sources.heading": ({ providers }) => `公開網頁來源(${providers})`,
|
|
13
|
+
"web.sources.headingUnnamed": "公開網頁來源",
|
|
14
|
+
"web.sources.published": ({ date }) => `來源日期:${date}`,
|
|
15
|
+
"web.sources.retrieved": ({ date }) => `檢索於 ${date}`,
|
|
16
|
+
|
|
17
|
+
"external.neighbors": ({ seed }) => seed ? `ListenBrainz 聽眾相鄰,從 ${seed} 出發` : "ListenBrainz 聽眾相鄰",
|
|
18
|
+
"external.listenbrainz": ({ neighbor, date }) => `${neighbor}(Wikidata 身分,CC0,檢索於 ${date};這不是音訊相似度)`,
|
|
19
|
+
"external.appleMusic": ({ date }) => `Apple Music 美國曲名比對(檢索於 ${date})`,
|
|
20
|
+
"external.limit": "唯一排除的是匯入曲庫裡同名同藝人的歌,所以這不代表你從沒聽過。這些是供試聽的建議。",
|
|
21
|
+
"external.from": ({ parts, limit }) => `來源:${parts.join(",以及")}。${limit}`,
|
|
22
|
+
|
|
23
|
+
"failure.deviceList": ({ noMatch, names, code }) => `裝置名稱${noMatch ? "沒有符合" : "符合多個裝置"}。Spotify 目前列出:${names}。請用顯示的名稱說「在這台裝置上播放」,選好的歌曲會保留。尚未送出歌曲播放請求。(${code})`,
|
|
24
|
+
"failure.networkRead": ({ notSent, code }) => `讀取 Spotify 裝置或播放狀態時發生網路錯誤,裝置準備已停止。${notSent ? "歌曲播放請求尚未送出。" : ""}請檢查與 Spotify 的連線後重試。(${code})`,
|
|
25
|
+
"failure.readiness.spotify_device_selection_required": "選好的歌曲仍保留,但目標裝置還沒確定。請說出要用的裝置名稱,不會改用之前的裝置。",
|
|
26
|
+
"failure.readiness.spotify_device_not_found": "Spotify 看不到目標裝置。請在那台裝置上開啟 Spotify,再請求播放;沒有改用其他裝置。",
|
|
27
|
+
"failure.readiness.spotify_device_ambiguous": "可能有多台 Spotify 裝置可用,請指定要用的裝置名稱。",
|
|
28
|
+
"failure.readiness.spotify_device_restricted": "目標裝置沒有確認允許 Spotify Web API 控制,請選擇其他可用的裝置。",
|
|
29
|
+
"failure.readiness.spotify_device_changed": "另一台 Spotify 裝置變成使用中。請明確選擇裝置,播放沒有自動轉移。",
|
|
30
|
+
"failure.readiness.spotify_device_state_unconfirmed": "Spotify 的裝置與播放狀態不完整或不一致。請先在 Spotify 確認目前的裝置再試。",
|
|
31
|
+
"failure.readiness.spotify_device_not_ready": "目標裝置在等待時間內沒有就緒。請在那台裝置上開啟 Spotify,再請求播放同一首歌。",
|
|
32
|
+
"failure.readiness.spotify_playback_restricted": "Spotify 目前不允許繼續播放,請檢查目標裝置。",
|
|
33
|
+
"failure.songNotSent": " 尚未送出歌曲播放請求。",
|
|
34
|
+
"failure.preparationStopped": ({ evidence }) => `播放前的裝置檢查沒有完成(${evidence}),沒有送出歌曲播放請求。請檢查 Spotify 裝置狀態後再試。`,
|
|
35
|
+
"failure.queueRequestRequired": "這次沒有加入歌曲:我沒辦法把這則回覆對應到加入佇列的請求。需要明確請我加入播放佇列,才能繼續。",
|
|
36
|
+
"failure.notSent": ({ code }) => `沒有向 Spotify 送出任何內容:我沒辦法把它對應到剛才找到的歌曲(${code})。請帶上歌名或歌手再說一次,我會重新搜尋。`,
|
|
37
|
+
"failure.outcome.lookup": "Spotify 查詢沒有完成",
|
|
38
|
+
"failure.outcome.unknown": "Spotify 操作結果尚不確定",
|
|
39
|
+
"failure.outcome.unconfirmed": "Spotify 播放操作未獲確認",
|
|
40
|
+
"failure.reason": ({ reason }) => `Spotify 回報的原因:${reason}。`,
|
|
41
|
+
"failure.reasonUnknown": "具體原因尚未證實。",
|
|
42
|
+
"failure.lookup": ({ outcome, evidence, reason }) => `${outcome}(${evidence})。${reason}沒有送出播放或加入佇列操作。`,
|
|
43
|
+
"failure.general": ({ targets, outcome, evidence, reason, playbackNotSent, recovered }) =>
|
|
44
|
+
`${targets ? `目標:${targets}。\n` : ""}${outcome}(${evidence})。${reason}${playbackNotSent ? "歌曲播放請求尚未送出。" : ""}${recovered ? "已結束一次有限的裝置復原流程,不再重試。" : "沒有自動重試。"}可以讀取播放狀態和裝置清單繼續診斷。`,
|
|
45
|
+
|
|
46
|
+
"queue.unverified": () => "Spotify 已接受,但我沒辦法讀取你的佇列再確認一次。",
|
|
47
|
+
"queue.confirmedPlaying": ({ one }) => one ? "已在 Spotify 確認正在播放。" : "已在 Spotify 確認:第一首正在播放,其餘都在佇列裡。",
|
|
48
|
+
"queue.confirmed": ({ count }) => count === 1 ? "已在 Spotify 佇列裡確認。" : `已在 Spotify 佇列裡確認這 ${count} 首。`,
|
|
49
|
+
"queue.partlyVisible": ({ accepted, confirmed, truncated }) => `Spotify 接受了 ${accepted} 首,目前佇列裡看得到 ${confirmed} 首${truncated ? "(Spotify 只顯示長佇列的一部分)" : ""}。`,
|
|
50
|
+
"queue.noPlayback": "Spotify 目前沒有可當作起點的歌曲,沒有加入任何歌曲。",
|
|
51
|
+
"queue.noCandidates": "篩選後沒有可加入的候選,沒有加入任何歌曲。",
|
|
52
|
+
"queue.requested": ({ requested, accepted, shortfall }) => `請求 ${requested} 首;Spotify 已接受 ${accepted} 首${shortfall ? `,還差 ${shortfall} 首` : ""}。`,
|
|
53
|
+
"queue.similarSeed": ({ artist }) => `根據聽眾資料,與 ${artist} 相近的藝人:`,
|
|
54
|
+
"queue.cancelled": "已停止繼續加入佇列;已接受的歌曲仍在佇列裡。",
|
|
55
|
+
"queue.stopped": ({ count, title, artist }) => `已把 ${count} 首加入 Spotify 佇列,停在《${title}》- ${artist}。`,
|
|
56
|
+
"queue.queued": ({ count }) => `已把這 ${count} 首加入 Spotify 佇列:`,
|
|
57
|
+
"queue.queuedSome": ({ count, unmatched }) => `已把 ${count} 首加入 Spotify 佇列,另有 ${unmatched} 首對不上:`,
|
|
58
|
+
"queue.outcomeUnknown": "Spotify 沒有確認停下的那首歌是否已加入。請先檢查佇列再試;這次寫入沒有重送。",
|
|
59
|
+
"queue.started": ({ title, device, more }) => `剛才沒有在播放,所以我先在${device ? `「${device}」` : " Spotify "}上播放《${title}》${more ? ",其餘排在後面" : ""}。`,
|
|
60
|
+
"queue.unmatchedHeading": "在 Spotify 上對不上:",
|
|
61
|
+
"queue.skippedDuplicates": ({ count }) => `略過 ${count} 首重複曲目。`,
|
|
62
|
+
"queue.skippedUncertain": ({ count }) => `略過 ${count} 首先前加入結果不明的曲目,避免重複;請檢查 Spotify 佇列。`,
|
|
63
|
+
"queue.skippedAvoided": ({ count }) => `略過 ${count} 首你要求避開的曲目。`,
|
|
64
|
+
"queue.skippedKnown": ({ count }) => `依你的要求,略過 ${count} 首聆聽紀錄裡已有的曲目。`,
|
|
65
|
+
"queue.observationTruncated": "只檢查了 Spotify 回傳的有限佇列片段,沒顯示的部分仍可能重複。",
|
|
66
|
+
"queue.notAddedHeading": "沒有加入:",
|
|
67
|
+
"queue.earlierReceipt": "這是上一次佇列操作的結果;這次說明沒有再次加入歌曲。",
|
|
68
|
+
|
|
69
|
+
"playlist.partialUnknown": ({ name }) => `Spotify 已建立私人播放清單「${name}」,但加入曲目的結果尚未確認。請先檢查再決定是否重試。`,
|
|
70
|
+
"playlist.partialEmpty": ({ name }) => `Spotify 已建立私人播放清單「${name}」,但沒有加入曲目;這個空的播放清單已經存在,請先檢查再決定是否重試。`,
|
|
71
|
+
"playlist.saved": ({ name, count }) => `已儲存為 Spotify 私人播放清單「${name}」(${count} 首)。`,
|
|
72
|
+
|
|
73
|
+
"action.playback.resume": "播放請求",
|
|
74
|
+
"action.playback.pause": "暫停請求",
|
|
75
|
+
"action.playback.next": "下一首請求",
|
|
76
|
+
"action.playback.previous": "上一首請求",
|
|
77
|
+
"action.playback.volume.set": "音量調整",
|
|
78
|
+
"action.playback.seek": "播放位置調整",
|
|
79
|
+
"action.playback.shuffle.set": "隨機播放設定",
|
|
80
|
+
"action.playback.repeat.set": "重複播放設定",
|
|
81
|
+
"action.playback.transfer": "裝置切換",
|
|
82
|
+
"action.playback.queue.add": "加入佇列",
|
|
83
|
+
"action.library.remove": "從音樂庫移除",
|
|
84
|
+
"action.library.save": "儲存到音樂庫",
|
|
85
|
+
"action.playlist.write": "建立播放清單",
|
|
86
|
+
"action.playlist.edit": "播放清單變更",
|
|
87
|
+
"action.playlist.rename": "播放清單重新命名",
|
|
88
|
+
"action.playlist.unfollow": "從音樂庫移除播放清單",
|
|
89
|
+
"action.playlist.remove_track": "移除播放清單曲目",
|
|
90
|
+
"action.other": "要求的變更",
|
|
91
|
+
"write.unknown": ({ action, target }) => `Spotify 的${action}${target ? `(「${target}」)` : ""}結果尚未確認。請先檢查狀態,再決定是否重試。`,
|
|
92
|
+
"write.unfollowed": ({ name }) => `已從你的 Spotify 音樂庫移除「${name}」(取消追蹤)。其他聽眾可能仍看得到這個播放清單;它沒有被永久刪除。`,
|
|
93
|
+
"write.renamed": ({ name }) => `Spotify 已接受把播放清單重新命名為「${name}」。`,
|
|
94
|
+
"write.trackRemoved": ({ name }) => `Spotify 已接受從「${name}」移除 1 首曲目。`,
|
|
95
|
+
"write.accepted": ({ action, count, targets }) => `Spotify 已接受${action}${count === null ? "" : `(${count} 首)`}${targets ? `:${targets}` : ""}。`,
|
|
96
|
+
|
|
97
|
+
"plan.heading.private_library": ({ count }) => `來自你曲庫的 ${count} 首方案:`,
|
|
98
|
+
"plan.heading.private_history": ({ count }) => `從你的聆聽紀錄挑出、值得再聽的 ${count} 首:`,
|
|
99
|
+
"plan.heading.external_catalog": ({ count }) => `這 ${count} 首:`,
|
|
100
|
+
"plan.heading.mixed": ({ count }) => `這 ${count} 首:`,
|
|
101
|
+
"plan.heading.timeCapsule": ({ count }) => `穿越你 ${count} 個聆聽年份的時光機:`,
|
|
102
|
+
"plan.heading.historicalReturn": ({ count }) => `${count} 首隔了很久又回來的歌:`,
|
|
103
|
+
"plan.heading.backToBack": ({ count }) => `${count} 首曾被你連著播放的歌:`,
|
|
104
|
+
"plan.externalTrack": ({ line, release }) => `${line},選自《${release}》`,
|
|
105
|
+
"plan.rationale": ({ text }) => ` 選曲理由:${text}`,
|
|
106
|
+
"plan.ordering": ({ text }) => `排序理由:${text}`,
|
|
107
|
+
"plan.notSaved": "還沒有儲存,也沒有加入佇列。",
|
|
108
|
+
"plan.previewing": "30 秒試聽正在播放器裡播放,完整歌曲在 Apple Music 上。",
|
|
109
|
+
"plan.pending": "說明:這個方案只在這次執行中等待確認,還沒寫入 Spotify,也沒有其他外部影響。",
|
|
110
|
+
"plan.backToBackBoundary": "說明:相鄰的播放紀錄不能證明當時開了重複播放、是刻意重播,或你喜歡這首歌。",
|
|
111
|
+
"plan.validationScope": "驗證範圍:本地規劃器驗證了曲目身分、候選範圍、數量和順序;選曲理由與情境適配仍是模型根據現有資訊的判斷。",
|
|
112
|
+
"plan.unvalidated": "我沒辦法驗證這份播放清單方案,所以不會回傳沒經過可信候選比對的曲目清單。請縮小或放寬搜尋範圍再試一次。",
|
|
113
|
+
|
|
114
|
+
"edit.previewHeading": ({ name, before, after }) => `Spotify 私人播放清單「${name}」編輯預覽(${before} -> ${after} 首):`,
|
|
115
|
+
"edit.status.added": "新增",
|
|
116
|
+
"edit.status.moved": "移動",
|
|
117
|
+
"edit.status.retained": "保留",
|
|
118
|
+
"edit.removeHeading": "將移除:",
|
|
119
|
+
"edit.summary": ({ added, removed, moved, retained }) => `變更摘要:新增 ${added},移除 ${removed},移動 ${moved},原位保留 ${retained}。`,
|
|
120
|
+
"edit.boundary": "說明:Spotify 還沒有變更。請在下一則訊息明確確認,Moondog 才會寫入這份保留下來的版本。",
|
|
121
|
+
"edit.updated": ({ name, before, after }) => `已依照確認過的預覽更新 Spotify 私人播放清單「${name}」${before === null ? `(現在 ${after} 首)` : `(${before} -> ${after} 首)`}。`,
|
|
122
|
+
|
|
123
|
+
"selection.note": ({ playlist, track }) => `\n\n接下來的編輯會用:播放清單 ${playlist};歌曲 ${track}。`,
|
|
124
|
+
"memory.spotifyExcluded": "\n\nSpotify 的結果不會進入一般記憶;只有可驗證的你的原話會被記住,其他內容沒有儲存。",
|
|
125
|
+
"memory.notSaved": "\n\n這則回覆已完成,但記憶沒有儲存成功。",
|
|
126
|
+
"turn.cancelled": "已取消後續操作。",
|
|
127
|
+
"turn.interrupted": "回覆中斷了。已記錄的 Spotify 結果如下;沒有重新執行任何操作。",
|
|
128
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// 简体中文。Keys match messages/en.mjs; a missing key falls back to English.
|
|
2
|
+
export default {
|
|
3
|
+
"discovery.reason.listenbrainz": ({ adjacent, seed, release }) => `ListenBrainz 收听数据将 ${adjacent} 与 ${seed} 关联;选自《${release}》。`,
|
|
4
|
+
"discovery.reason.appleMusic": ({ queries, release, genre }) => `Apple Music 目录${queries ? `匹配检索词 ${queries}` : "候选"};选自《${release}》${genre ? `,目录流派为 ${genre}` : ""}。`,
|
|
5
|
+
"discovery.reason.external": ({ release }) => `来自已检索的外部目录,选自《${release}》。`,
|
|
6
|
+
"discovery.order": ({ path }) => `依次试听:${path}。`,
|
|
7
|
+
"discovery.unavailable": "音乐发现服务暂时无法连接,这次没有生成推荐。请重新提交这条请求再试。",
|
|
8
|
+
|
|
9
|
+
"citations.heading": "公开音乐来源(与私人听歌证据分开):",
|
|
10
|
+
"citations.item": ({ label, url, storefront, date }) => `- [${label}](<${url}>) - ${storefront} storefront,检索于 ${date}。`,
|
|
11
|
+
"citations.scope": "范围:这些公开 catalog 页面不证明个人偏好、完整听歌历史或全平台可用性。",
|
|
12
|
+
"web.sources.heading": ({ providers }) => `公开网页来源(${providers})`,
|
|
13
|
+
"web.sources.headingUnnamed": "公开网页来源",
|
|
14
|
+
"web.sources.published": ({ date }) => `来源日期:${date}`,
|
|
15
|
+
"web.sources.retrieved": ({ date }) => `检索于 ${date}`,
|
|
16
|
+
|
|
17
|
+
"external.neighbors": ({ seed }) => seed ? `ListenBrainz 听友相邻,从 ${seed} 出发` : "ListenBrainz 听友相邻",
|
|
18
|
+
"external.listenbrainz": ({ neighbor, date }) => `${neighbor}(Wikidata 身份,CC0,检索于 ${date};这不是音频相似)`,
|
|
19
|
+
"external.appleMusic": ({ date }) => `Apple Music US 曲名匹配(检索于 ${date})`,
|
|
20
|
+
"external.limit": "导入曲库里的精确同名同艺人是唯一排除项,所以这不代表你从未听过。这些是供试听的建议。",
|
|
21
|
+
"external.from": ({ parts, limit }) => `来源:${parts.join(",以及")}。${limit}`,
|
|
22
|
+
|
|
23
|
+
"failure.deviceList": ({ noMatch, names, code }) => `设备名称${noMatch ? "没有匹配成功" : "匹配到多个设备"}。Spotify 当前可见:${names}。请用显示的名称说“在该设备上播放它”,所选歌曲会保留。尚未发送歌曲播放请求。 (${code})`,
|
|
24
|
+
"failure.networkRead": ({ notSent, code }) => `读取 Spotify 设备或播放状态时发生网络错误,设备准备已停止。${notSent ? "歌曲播放请求尚未发送。" : ""}请检查与 Spotify 的连接后重试。 (${code})`,
|
|
25
|
+
"failure.readiness.spotify_device_selection_required": "所选歌曲仍保留,但目标设备尚未确定。请明确指定设备名称,不会改用之前的设备。",
|
|
26
|
+
"failure.readiness.spotify_device_not_found": "Spotify 没有列出目标设备。请在该设备上打开 Spotify,再请求播放;没有改用其他设备。",
|
|
27
|
+
"failure.readiness.spotify_device_ambiguous": "有多个可能的 Spotify 设备,请指定要使用的设备名称。",
|
|
28
|
+
"failure.readiness.spotify_device_restricted": "目标设备未确认允许 Spotify Web API 控制,请选择其他可用设备。",
|
|
29
|
+
"failure.readiness.spotify_device_changed": "另一个 Spotify 设备已变为活跃状态。请明确选择设备,没有自动转移播放。",
|
|
30
|
+
"failure.readiness.spotify_device_state_unconfirmed": "Spotify 的设备与播放状态不完整或不一致。请检查 Spotify 中的当前设备后再试。",
|
|
31
|
+
"failure.readiness.spotify_device_not_ready": "目标设备尚未就绪,已停止有限等待。请在该设备上打开 Spotify,再请求播放同一首歌。",
|
|
32
|
+
"failure.readiness.spotify_playback_restricted": "Spotify 当前不允许恢复播放,请检查目标设备。",
|
|
33
|
+
"failure.songNotSent": " 尚未发送歌曲播放请求。",
|
|
34
|
+
"failure.preparationStopped": ({ evidence }) => `播放前的设备检查未完成(${evidence}),没有发送歌曲播放请求。请检查 Spotify 设备状态后再试。`,
|
|
35
|
+
"failure.queueRequestRequired": "这次没有加入歌曲:我没能把这条回复关联到入队请求。需要明确要加入播放队列,才能继续。",
|
|
36
|
+
"failure.notSent": ({ code }) => `没有向 Spotify 发送任何内容:我没能把它对应到刚才找到的歌曲(${code})。请带上歌名或歌手再说一次,我会重新搜索。`,
|
|
37
|
+
"failure.outcome.lookup": "Spotify 查询未完成",
|
|
38
|
+
"failure.outcome.unknown": "Spotify 操作结果尚不确定",
|
|
39
|
+
"failure.outcome.unconfirmed": "Spotify 播放操作未获确认",
|
|
40
|
+
"failure.reason": ({ reason }) => `Spotify 返回的原因:${reason}。`,
|
|
41
|
+
"failure.reasonUnknown": "具体原因未获证实。",
|
|
42
|
+
"failure.lookup": ({ outcome, evidence, reason }) => `${outcome}(${evidence})。${reason}未发送播放或加入队列操作。`,
|
|
43
|
+
"failure.general": ({ targets, outcome, evidence, reason, playbackNotSent, recovered }) =>
|
|
44
|
+
`${targets ? `目标:${targets}。\n` : ""}${outcome}(${evidence})。${reason}${playbackNotSent ? "歌曲播放请求尚未发送。" : ""}${recovered ? "已结束一次有限的设备恢复流程,不再重试。" : "未自动重试。"}可以读取播放状态和设备列表继续诊断。`,
|
|
45
|
+
|
|
46
|
+
"queue.unverified": () => "Spotify 已接受,但我没能读取你的队列来再次确认。",
|
|
47
|
+
"queue.confirmedPlaying": ({ one }) => one ? "已在 Spotify 上确认正在播放。" : "已在 Spotify 上确认:第一首正在播放,其余都在队列里。",
|
|
48
|
+
"queue.confirmed": ({ count }) => count === 1 ? "已在 Spotify 队列里确认。" : `已在 Spotify 队列里确认这 ${count} 首。`,
|
|
49
|
+
"queue.partlyVisible": ({ accepted, confirmed, truncated }) => `Spotify 接受了 ${accepted} 首,目前队列里能看到 ${confirmed} 首${truncated ? "(Spotify 只显示长队列的一部分)" : ""}。`,
|
|
50
|
+
"queue.noPlayback": "Spotify 当前没有可用的歌曲播放信息,未加入任何歌曲。",
|
|
51
|
+
"queue.noCandidates": "筛选后没有可加入的候选,未加入任何歌曲。",
|
|
52
|
+
"queue.requested": ({ requested, accepted, shortfall }) => `请求 ${requested} 首;Spotify 已接受 ${accepted} 首${shortfall ? `,还差 ${shortfall} 首` : ""}。`,
|
|
53
|
+
"queue.similarSeed": ({ artist }) => `基于艺人 ${artist} 的听众相似性:`,
|
|
54
|
+
"queue.cancelled": "已取消继续加入队列;已接受的歌曲仍在队列中。",
|
|
55
|
+
"queue.stopped": ({ count, title, artist }) => `已把 ${count} 首加入 Spotify 队列,停在《${title}》- ${artist}。`,
|
|
56
|
+
"queue.queued": ({ count }) => `已把这 ${count} 首加入 Spotify 队列:`,
|
|
57
|
+
"queue.queuedSome": ({ count, unmatched }) => `已把 ${count} 首加入 Spotify 队列,另有 ${unmatched} 首对不上:`,
|
|
58
|
+
"queue.outcomeUnknown": "Spotify 未确认停止处这首歌是否已加入;请先检查队列,不要自动重试。",
|
|
59
|
+
"queue.started": ({ title, device, more }) => `刚才没有在播放,所以我先在${device ? `「${device}」` : " Spotify "}上播放《${title}》${more ? ",其余排在后面" : ""}。`,
|
|
60
|
+
"queue.unmatchedHeading": "Spotify 上对不上:",
|
|
61
|
+
"queue.skippedDuplicates": ({ count }) => `跳过 ${count} 首重复曲目。`,
|
|
62
|
+
"queue.skippedUncertain": ({ count }) => `跳过 ${count} 首此前入队结果不明的曲目,避免重复;请先检查 Spotify 队列。`,
|
|
63
|
+
"queue.skippedAvoided": ({ count }) => `跳过 ${count} 首 Avoid 曲目。`,
|
|
64
|
+
"queue.skippedKnown": ({ count }) => `按你的要求跳过 ${count} 首有保留听歌记录的曲目。`,
|
|
65
|
+
"queue.observationTruncated": "仅检查了 Spotify 返回的有限队列片段,无法保证未显示部分没有重复。",
|
|
66
|
+
"queue.notAddedHeading": "还没加入:",
|
|
67
|
+
"queue.earlierReceipt": "这是上次队列操作的结果;这次澄清没有再次添加歌曲。",
|
|
68
|
+
|
|
69
|
+
"playlist.partialUnknown": ({ name }) => `Spotify 已创建私有歌单「${name}」,但加入曲目的结果尚未确认。请检查歌单后再决定是否重试。`,
|
|
70
|
+
"playlist.partialEmpty": ({ name }) => `Spotify 已创建私有歌单「${name}」,但未能加入曲目;这个空歌单已经存在,请检查后再决定是否重试。`,
|
|
71
|
+
"playlist.saved": ({ name, count }) => `已保存为 Spotify 私有歌单「${name}」(${count} 首)。`,
|
|
72
|
+
|
|
73
|
+
"action.playback.resume": "播放请求",
|
|
74
|
+
"action.playback.pause": "暂停请求",
|
|
75
|
+
"action.playback.next": "下一首请求",
|
|
76
|
+
"action.playback.previous": "上一首请求",
|
|
77
|
+
"action.playback.volume.set": "音量调整",
|
|
78
|
+
"action.playback.seek": "播放位置调整",
|
|
79
|
+
"action.playback.shuffle.set": "随机播放设置",
|
|
80
|
+
"action.playback.repeat.set": "循环播放设置",
|
|
81
|
+
"action.playback.transfer": "设备切换请求",
|
|
82
|
+
"action.playback.queue.add": "加入队列请求",
|
|
83
|
+
"action.library.remove": "取消收藏请求",
|
|
84
|
+
"action.library.save": "曲目保存请求",
|
|
85
|
+
"action.playlist.write": "歌单创建请求",
|
|
86
|
+
"action.playlist.edit": "歌单更改请求",
|
|
87
|
+
"action.playlist.rename": "歌单重命名",
|
|
88
|
+
"action.playlist.unfollow": "歌单取消收藏",
|
|
89
|
+
"action.playlist.remove_track": "歌单曲目移除",
|
|
90
|
+
"action.other": "更改请求",
|
|
91
|
+
"write.unknown": ({ action, target }) => `Spotify 的${action}${target ? `(「${target}」)` : ""}结果尚未确认。请先检查,再决定是否重试。`,
|
|
92
|
+
"write.unfollowed": ({ name }) => `已从你的 Spotify 音乐库移除歌单「${name}」(取消收藏)。歌单可能仍对其他听众存在;这不是全局删除。`,
|
|
93
|
+
"write.renamed": ({ name }) => `Spotify 已接受歌单重命名为「${name}」。`,
|
|
94
|
+
"write.trackRemoved": ({ name }) => `Spotify 已接受从「${name}」移除 1 首曲目。`,
|
|
95
|
+
"write.accepted": ({ action, count, targets }) => `Spotify 已接受${action}${count === null ? "" : `(${count} 首)`}${targets ? `:${targets}` : ""}。`,
|
|
96
|
+
|
|
97
|
+
"plan.heading.private_library": ({ count }) => `来自个人曲库的 ${count} 首方案:`,
|
|
98
|
+
"plan.heading.private_history": ({ count }) => `值得再听一次的 ${count} 首历史重逢方案:`,
|
|
99
|
+
"plan.heading.external_catalog": ({ count }) => `这 ${count} 首:`,
|
|
100
|
+
"plan.heading.mixed": ({ count }) => `这 ${count} 首:`,
|
|
101
|
+
"plan.heading.timeCapsule": ({ count }) => `穿过 ${count} 个听歌年份的时间机器方案:`,
|
|
102
|
+
"plan.heading.historicalReturn": ({ count }) => `${count} 首曾在长久空档后重新出现的回归方案:`,
|
|
103
|
+
"plan.heading.backToBack": ({ count }) => `${count} 首曾被连续播放的历史片段方案:`,
|
|
104
|
+
"plan.externalTrack": ({ line, release }) => `${line},选自《${release}》`,
|
|
105
|
+
"plan.rationale": ({ text }) => ` 策展判断:${text}`,
|
|
106
|
+
"plan.ordering": ({ text }) => `排序逻辑:${text}`,
|
|
107
|
+
"plan.notSaved": "还没有保存,也没有加入队列。",
|
|
108
|
+
"plan.previewing": "30 秒试听正在播放器里播放,完整歌曲在 Apple Music 上。",
|
|
109
|
+
"plan.pending": "边界:此方案仅在当前进程内待确认,尚未写入 Spotify,也没有外部副作用。",
|
|
110
|
+
"plan.backToBackBoundary": "解释边界:相邻的保留播放事件不证明当时开启了循环、重播是有意的,或你喜欢这首歌。",
|
|
111
|
+
"plan.validationScope": "校验范围:本地 planner 校验了曲目身份、候选集归属、数量和输出顺序;策展理由与情境适配度仍是基于现有元数据的模型判断。",
|
|
112
|
+
"plan.unvalidated": "我无法验证这次 playlist plan,因此不会返回未经可信候选集校验的曲目列表。请缩小或扩大搜索范围后重试。",
|
|
113
|
+
|
|
114
|
+
"edit.previewHeading": ({ name, before, after }) => `Spotify 私有歌单「${name}」编辑预览(${before} -> ${after} 首):`,
|
|
115
|
+
"edit.status.added": "新增",
|
|
116
|
+
"edit.status.moved": "移动",
|
|
117
|
+
"edit.status.retained": "保留",
|
|
118
|
+
"edit.removeHeading": "将移除:",
|
|
119
|
+
"edit.summary": ({ added, removed, moved, retained }) => `变更摘要:新增 ${added},移除 ${removed},移动 ${moved},原位保留 ${retained}。`,
|
|
120
|
+
"edit.boundary": "边界:Spotify 尚未发生变化。请在下一条消息中明确确认,Moondog 才会写入这份宿主层保留的精确版本。",
|
|
121
|
+
"edit.updated": ({ name, before, after }) => `已按确认过的精确预览更新 Spotify 私有歌单「${name}」${before === null ? `(现为 ${after} 首)` : `(${before} -> ${after} 首)`}。`,
|
|
122
|
+
|
|
123
|
+
"selection.note": ({ playlist, track }) => `\n\n后续编辑选择:歌单 ${playlist};歌曲 ${track}。`,
|
|
124
|
+
"memory.spotifyExcluded": "\n\nSpotify 查询结果不进入通用记忆;仅保存可验证的用户原话,其他记忆未保存。",
|
|
125
|
+
"memory.notSaved": "\n\n这条回复已经完成,但记忆没有成功保存。",
|
|
126
|
+
"turn.cancelled": "已取消后续操作。",
|
|
127
|
+
"turn.interrupted": "回复中断;已确认的 Spotify 操作结果如下。未重新执行任何操作。",
|
|
128
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { resolveMoondogAuthFile } from "../runtime/pi/persistent-credential-store.mjs";
|
|
5
|
+
import { normalizeLocale, systemLocale } from "./index.mjs";
|
|
6
|
+
|
|
7
|
+
// Kept apart from runtime settings, whose strict shape older versions enforce.
|
|
8
|
+
export function resolveLanguagePreferenceFile(environment = process.env) {
|
|
9
|
+
return path.join(path.dirname(resolveMoondogAuthFile(environment)), "language.json");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function readLanguagePreference(environment = process.env) {
|
|
13
|
+
try {
|
|
14
|
+
const document = JSON.parse(await readFile(resolveLanguagePreferenceFile(environment), "utf8"));
|
|
15
|
+
return document?.version === 1 ? normalizeLocale(document.language) : null;
|
|
16
|
+
} catch {
|
|
17
|
+
// A missing or unreadable preference falls back to the system language.
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function writeLanguagePreference(locale, environment = process.env) {
|
|
23
|
+
const language = normalizeLocale(locale);
|
|
24
|
+
if (!language) throw new Error(`Unsupported language: ${locale}`);
|
|
25
|
+
const file = resolveLanguagePreferenceFile(environment);
|
|
26
|
+
await mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
27
|
+
await writeFile(file, `${JSON.stringify({ version: 1, language }, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
28
|
+
return language;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** MOONDOG_LANGUAGE wins, then the saved choice, then the system language. */
|
|
32
|
+
export async function resolveListenerLocale(environment = process.env) {
|
|
33
|
+
return normalizeLocale(environment.MOONDOG_LANGUAGE) ?? await readLanguagePreference(environment) ?? systemLocale(environment);
|
|
34
|
+
}
|