dsh-theme-palette 1.3.2 → 1.4.0

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/client.js CHANGED
@@ -27,7 +27,7 @@ window.__ModuleLoader__.load({
27
27
  const React = require('react')
28
28
  const h = React.createElement
29
29
 
30
- const PLUGIN_VERSION = '1.3.2'
30
+ const PLUGIN_VERSION = '1.4.0'
31
31
  const STORE_KEY = 'dsh-theme-palette.v1'
32
32
  const OVERRIDE_SOURCE = 'dsh-theme-palette'
33
33
 
@@ -977,26 +977,63 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
977
977
  }
978
978
 
979
979
  /* ================= 文案 ================= */
980
- function isZh() {
980
+ /* ================= 多语言 ================= */
981
+ /**
982
+ * 语言来源优先级:用户手选 > 宿主 documentElement.lang(locale 服务写的)> navigator.language。
983
+ * 宿主本身只有 zh/en 两套,但插件设置页可以多给几种;「自动」永远跟随宿主。
984
+ */
985
+ const LOCALE_KEY = 'dsh-theme-palette.lang.v1'
986
+ const LOCALES = ['zh-CN', 'zh-TW', 'en', 'ja']
987
+ // 语言名用它自己的文字写(中/繁/EN/JA),只有「自动」需要按界面语言翻译。
988
+ const LANG_OPTIONS = [
989
+ { id: 'auto' },
990
+ { id: 'zh-CN', label: '中' },
991
+ { id: 'zh-TW', label: '繁' },
992
+ { id: 'en', label: 'EN' },
993
+ { id: 'ja', label: 'JA' },
994
+ ]
995
+
996
+ function readLangPref() {
997
+ try {
998
+ const raw = localStorage.getItem(LOCALE_KEY)
999
+ if (raw === 'auto' || LOCALES.indexOf(raw) !== -1) return raw
1000
+ } catch (err) { /* 退回自动 */ }
1001
+ return 'auto'
1002
+ }
1003
+
1004
+ function writeLangPref(id) {
1005
+ try { localStorage.setItem(LOCALE_KEY, id) } catch (err) { /* 忽略 */ }
1006
+ }
1007
+
1008
+ function hostLocale() {
981
1009
  try {
982
- const lang = document.documentElement.lang || navigator.language || ''
983
- return /^zh/i.test(lang)
984
- } catch (err) { return true }
1010
+ const lang = (document.documentElement.lang || '').trim() || navigator.language || ''
1011
+ if (/^zh-(hant|tw|hk|mo)/i.test(lang)) return 'zh-TW'
1012
+ if (/^zh/i.test(lang)) return 'zh-CN'
1013
+ if (/^ja/i.test(lang)) return 'ja'
1014
+ if (/^en/i.test(lang)) return 'en'
1015
+ return null
1016
+ } catch (err) { return null }
1017
+ }
1018
+
1019
+ function pickLocale(pref) {
1020
+ if (pref !== 'auto') return pref
1021
+ return hostLocale() || 'zh-CN'
985
1022
  }
986
1023
 
987
1024
  const TEXT = {
988
- zh: {
1025
+ 'zh-CN': {
989
1026
  nav: '主题色',
990
1027
  hint: '配色与明暗模式独立;切换后立即生效并保存在 localStorage。',
991
1028
  md: 'Markdown 强调',
992
- mdHint: '标题与加粗用「正文色混强调色」,比正文更有色;关掉则与正文同色。「更多选项」里。',
1029
+ mdHint: '标题与加粗用「正文色混强调色」,比正文更有色;关掉则与正文同色。',
993
1030
  control: '开关 / 勾选',
994
- controlHint: '宿主的开关、复选框、单选都压在近黑/近白上,所以开态是黑白拨杆;开启后改用强调色。',
1031
+ controlHint: '宿主的开关、复选框、单选都压在近黑/近白上;开启后改用强调色。',
995
1032
  scroll: '滚动条',
996
- scrollHint: '滚动条默认是中性灰;「跟随」上配色色相,「强调」用强调色。明暗关系不变。',
1033
+ scrollHint: '默认中性灰;「跟随」上配色色相,「强调」用强调色。明暗关系不变。',
997
1034
  scrollHintMono: '当前是黑白配色,没有色相可跟随,滚动条保持中性灰。',
998
1035
  rightbar: '右侧栏底色',
999
- rightbarHint: '右栏本来没有自己的背景,露的是会话区画布色;改为「同左栏」后与左侧栏一致。',
1036
+ rightbarHint: '右栏本来没有自己的背景,露的是会话区画布色。',
1000
1037
  advanced: '更多选项',
1001
1038
  textSub: '正文与强调的着色程度',
1002
1039
  sidebarSub: '相对会话区的层次',
@@ -1004,14 +1041,50 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
1004
1041
  mdSub: '标题与加粗是否着色',
1005
1042
  scrollSub: '滚动条与滑轨的色相',
1006
1043
  controlSub: '开关与复选框是否跟强调色',
1044
+ langSub: '自动跟随宿主界面语言',
1007
1045
  surface: '面板表面',
1008
- surfaceHint: '决定侧栏与右栏相对会话区是「浮起」还是「凹陷」;两者都随配色变。',
1046
+ surfaceHint: '决定侧栏与右栏相对会话区是「浮起」还是「凹陷」。',
1009
1047
  ok: '✓ 配色已生效',
1010
1048
  bad: '⚠ 部分 token 未生效(宿主可能改名),请查看控制台',
1049
+ healed: '配色层被宿主丢过一次,已自动恢复 ',
1050
+ healedSuffix: ' 次',
1011
1051
  text: '强度',
1012
- textHint: '让正文/次级文字带配色色味;对比度由求解器兜底,三档都满足 AA。',
1052
+ textHint: '让正文/次级文字带配色色味;三档都满足 AA。',
1013
1053
  sidebar: '左侧栏底色',
1014
- sidebarHint: '让侧栏比会话区更浅(或更深),做出区块层次;深浅两个模式同时生效。',
1054
+ sidebarHint: '让侧栏比会话区更浅(或更深),做出区块层次。',
1055
+ lang: '语言',
1056
+ },
1057
+ 'zh-TW': {
1058
+ nav: '主題色',
1059
+ hint: '配色與明暗模式獨立;切換後立即生效並存在 localStorage。',
1060
+ md: 'Markdown 強調',
1061
+ mdHint: '標題與粗體用「本文色混強調色」,比本文更有色;關掉則與本文同色。',
1062
+ control: '開關 / 勾選',
1063
+ controlHint: '宿主開關、核取方塊、單選都用近黑/近白;開啟後改用強調色。',
1064
+ scroll: '捲軸',
1065
+ scrollHint: '預設中性灰;「跟隨」上配色色相,「強調」用強調色。明暗關係不變。',
1066
+ scrollHintMono: '目前是黑白配色,沒有色相可跟隨,捲軸保持中性灰。',
1067
+ rightbar: '右側欄底色',
1068
+ rightbarHint: '右欄本來沒有自己的背景,露的是會話區畫布色。',
1069
+ advanced: '更多選項',
1070
+ textSub: '本文與強調的著色程度',
1071
+ sidebarSub: '相對會話區的層次',
1072
+ rightbarSub: '面板底色',
1073
+ mdSub: '標題與粗體是否著色',
1074
+ scrollSub: '捲軸與軌道的色相',
1075
+ controlSub: '開關與核取方塊是否跟強調色',
1076
+ langSub: '自動跟隨宿主介面語言',
1077
+ surface: '面板表面',
1078
+ surfaceHint: '決定側欄與右欄相對會話區是「浮起」還是「凹陷」。',
1079
+ ok: '✓ 配色已生效',
1080
+ bad: '⚠ 部分 token 未生效(宿主可能改名),請看主控台',
1081
+ healed: '配色層被宿主丟棄過一次,已自動恢復 ',
1082
+ healedSuffix: ' 次',
1083
+ text: '強度',
1084
+ textHint: '讓本文/次級文字帶配色色味;三檔都符合 AA。',
1085
+ sidebar: '左側欄底色',
1086
+ sidebarHint: '讓側欄比會話區更淺(或更深),做出區塊層次。',
1087
+ lang: '語言',
1015
1088
  },
1016
1089
  en: {
1017
1090
  nav: 'Color theme',
@@ -1019,12 +1092,12 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
1019
1092
  md: 'Markdown emphasis',
1020
1093
  mdHint: 'Headings and bold mix the accent into body colour; off keeps them identical to body text.',
1021
1094
  control: 'Switches / checks',
1022
- controlHint: 'Switches, checkboxes and radios all use near-black/white in the host. Turn this on to use the accent colour.',
1095
+ controlHint: 'Switches, checkboxes and radios use near-black/white in the host. Turn this on to use the accent colour.',
1023
1096
  scroll: 'Scrollbars',
1024
- scrollHint: 'Scrollbars ship neutral grey. Tint follows the palette hue; Accent uses the accent colour. Lightness is unchanged.',
1097
+ scrollHint: 'Neutral grey by default. Tint follows the palette hue; Accent uses the accent colour. Lightness is unchanged.',
1025
1098
  scrollHintMono: 'Monochrome has no hue to follow; scrollbars stay neutral.',
1026
1099
  rightbar: 'Right panel surface',
1027
- rightbarHint: 'The right panel has no fill of its own; it shows the canvas. Set it to the left sidebar colour to match.',
1100
+ rightbarHint: 'The right panel has no fill of its own; it shows the canvas.',
1028
1101
  advanced: 'More options',
1029
1102
  textSub: 'How strongly text follows the palette',
1030
1103
  sidebarSub: 'Layer relative to the conversation',
@@ -1032,14 +1105,98 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
1032
1105
  mdSub: 'Tint headings and bold',
1033
1106
  scrollSub: 'Hue of the scrollbar',
1034
1107
  controlSub: 'Switches and checkboxes',
1108
+ langSub: 'Follow the host interface language',
1035
1109
  surface: 'Panel surfaces',
1036
- surfaceHint: 'Whether the sidebars sit above or below the conversation area; both follow the palette.',
1110
+ surfaceHint: 'Whether the sidebars sit above or below the conversation area.',
1037
1111
  ok: '✓ Palette applied',
1038
1112
  bad: '⚠ Some tokens did not apply (host may have renamed them); check the console',
1113
+ healed: 'Palette layer was dropped by the host; restored ',
1114
+ healedSuffix: 'x',
1039
1115
  text: 'Strength',
1040
- textHint: 'Tints body and secondary text toward the palette; all three levels still meet AA.',
1116
+ textHint: 'Tints body and secondary text toward the palette; all three levels meet AA.',
1041
1117
  sidebar: 'Sidebar surface',
1042
- sidebarHint: 'Makes the sidebar lighter (or deeper) than the conversation area, in both color modes.',
1118
+ sidebarHint: 'Makes the sidebar lighter (or deeper) than the conversation area.',
1119
+ lang: 'Language',
1120
+ },
1121
+ ja: {
1122
+ nav: 'カラーテーマ',
1123
+ hint: 'ライト/ダークとは独立です。変更は即時反映され、localStorage に保存されます。',
1124
+ md: 'Markdown の強調',
1125
+ mdHint: '見出しと太字にアクセント色を混ぜます。オフにすると本文と同じ色になります。',
1126
+ control: 'スイッチ / チェック',
1127
+ controlHint: 'ホストのスイッチ・チェックボックスはほぼ白黒です。オンにするとアクセント色になります。',
1128
+ scroll: 'スクロールバー',
1129
+ scrollHint: '既定はニュートラルグレー。「追従」でテーマの色相、「アクセント」でアクセント色になります。',
1130
+ scrollHintMono: 'モノクロには色相がないため、スクロールバーはグレーのままです。',
1131
+ rightbar: '右パネルの背景',
1132
+ rightbarHint: '右パネルは独自の背景を持たず、キャンバス色が見えています。',
1133
+ advanced: 'その他のオプション',
1134
+ textSub: '本文と強調の色づき具合',
1135
+ sidebarSub: '会話エリアとの段差',
1136
+ rightbarSub: 'パネルの背景',
1137
+ mdSub: '見出しと太字を色づけする',
1138
+ scrollSub: 'スクロールバーの色相',
1139
+ controlSub: 'スイッチとチェックの色',
1140
+ langSub: 'ホストの表示言語に合わせます',
1141
+ surface: 'パネル表面',
1142
+ surfaceHint: 'サイドバーが会話エリアより浮くか沈むかを決めます。',
1143
+ ok: '✓ 配色を適用しました',
1144
+ bad: '⚠ 一部の token が適用されていません(ホスト側で改名された可能性)。コンソールを確認してください',
1145
+ healed: '配色レイヤーがホストに破棄されたため、自動で復元しました(',
1146
+ healedSuffix: ' 回)',
1147
+ text: '強さ',
1148
+ textHint: '本文と副次テキストをテーマ色に寄せます。3 段階とも AA を満たします。',
1149
+ sidebar: '左サイドバーの背景',
1150
+ sidebarHint: 'サイドバーを会話エリアより明るく(または暗く)して段差を作ります。',
1151
+ lang: '言語',
1152
+ },
1153
+ }
1154
+
1155
+ /** 配色名与各档位的显示名:id 不变,只换文案。 */
1156
+ const LABELS = {
1157
+ 'zh-CN': {
1158
+ scheme: { mono: '黑白', warm: '暖砂', indigo: '靛蓝', moss: '苔绿' },
1159
+ desc: { mono: '无色调 · 默认', warm: '米白 + 棕调', indigo: '冷白 + 蓝调', moss: '浅灰绿 + 绿调' },
1160
+ text: { neutral: '静', tinted: '标准', deep: '浓' },
1161
+ sidebar: { light: '更浅', match: '同色', deep: '更深' },
1162
+ rightbar: { sidebar: '同左栏', canvas: '跟随画布' },
1163
+ scroll: { neutral: '中性', tinted: '跟随', accent: '强调' },
1164
+ control: { accent: '主题色', mono: '黑白' },
1165
+ md: { on: '着色', off: '同正文' },
1166
+ lang: { auto: '自动' },
1167
+ },
1168
+ 'zh-TW': {
1169
+ scheme: { mono: '黑白', warm: '暖砂', indigo: '靛藍', moss: '苔綠' },
1170
+ desc: { mono: '無色調 · 預設', warm: '米白 + 棕調', indigo: '冷白 + 藍調', moss: '淺灰綠 + 綠調' },
1171
+ text: { neutral: '靜', tinted: '標準', deep: '濃' },
1172
+ sidebar: { light: '更淺', match: '同色', deep: '更深' },
1173
+ rightbar: { sidebar: '同左欄', canvas: '跟隨畫布' },
1174
+ scroll: { neutral: '中性', tinted: '跟隨', accent: '強調' },
1175
+ control: { accent: '主題色', mono: '黑白' },
1176
+ md: { on: '著色', off: '同本文' },
1177
+ lang: { auto: '自動' },
1178
+ },
1179
+ en: {
1180
+ scheme: { mono: 'Mono', warm: 'Warm sand', indigo: 'Indigo', moss: 'Moss' },
1181
+ desc: { mono: 'Neutral · default', warm: 'Cream + brown', indigo: 'Cool white + blue', moss: 'Grey-green' },
1182
+ text: { neutral: 'Quiet', tinted: 'Standard', deep: 'Strong' },
1183
+ sidebar: { light: 'Lighter', match: 'Same', deep: 'Deeper' },
1184
+ rightbar: { sidebar: 'Match sidebar', canvas: 'Follow canvas' },
1185
+ scroll: { neutral: 'Neutral', tinted: 'Tint', accent: 'Accent' },
1186
+ control: { accent: 'Accent', mono: 'Mono' },
1187
+ md: { on: 'Tinted', off: 'Body colour' },
1188
+ lang: { auto: 'Auto' },
1189
+ },
1190
+ ja: {
1191
+ scheme: { mono: 'モノクロ', warm: 'ウォームサンド', indigo: 'インディゴ', moss: 'モスグリーン' },
1192
+ desc: { mono: '色なし · 既定', warm: 'クリーム + ブラウン', indigo: 'クールホワイト + ブルー', moss: 'グレーグリーン' },
1193
+ text: { neutral: '控えめ', tinted: '標準', deep: '強め' },
1194
+ sidebar: { light: '明るく', match: '同じ', deep: '暗く' },
1195
+ rightbar: { sidebar: 'サイドバーに合わせる', canvas: 'キャンバスに従う' },
1196
+ scroll: { neutral: 'ニュートラル', tinted: '追従', accent: 'アクセント' },
1197
+ control: { accent: 'アクセント', mono: 'モノクロ' },
1198
+ md: { on: '色づけ', off: '本文と同じ' },
1199
+ lang: { auto: '自動' },
1043
1200
  },
1044
1201
  }
1045
1202
 
@@ -1054,9 +1211,12 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
1054
1211
  const mdMode = React.useSyncExternalStore(props.subscribeMd, props.getMdMode)
1055
1212
  const health = React.useSyncExternalStore(props.subscribeHealth, props.getHealth)
1056
1213
  const [advanced, setAdvanced] = React.useState(false)
1057
- const txt = isZh() ? TEXT.zh : TEXT.en
1214
+ const locale = React.useSyncExternalStore(props.subscribeLocale, props.getLocale)
1215
+ const langPref = React.useSyncExternalStore(props.subscribeLocale, props.getLangPref)
1216
+ const txt = TEXT[locale] || TEXT['zh-CN']
1217
+ const lb = LABELS[locale] || LABELS['zh-CN']
1058
1218
 
1059
- const seg = (modes, value, onPick) => h(
1219
+ const seg = (modes, value, onPick, labels) => h(
1060
1220
  'div',
1061
1221
  { className: 'dsh-tp-seg', role: 'group' },
1062
1222
  modes.map((mode) => h(
@@ -1068,7 +1228,7 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
1068
1228
  'aria-pressed': mode.id === value,
1069
1229
  onClick: () => onPick(mode.id),
1070
1230
  },
1071
- mode.label,
1231
+ (labels && labels[mode.id]) || mode.label,
1072
1232
  )),
1073
1233
  )
1074
1234
 
@@ -1101,8 +1261,8 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
1101
1261
  h('span', { className: 'dsh-tp-thumb-side' }),
1102
1262
  h('span', { className: 'dsh-tp-thumb-main' }, h('b', null), h('b', null), h('b', null))),
1103
1263
  h('span', { className: 'dsh-tp-card-meta' },
1104
- h('span', { className: 'dsh-tp-card-name' }, scheme.label),
1105
- h('span', { className: 'dsh-tp-card-desc' }, scheme.desc)),
1264
+ h('span', { className: 'dsh-tp-card-name' }, lb.scheme[scheme.id] || scheme.label),
1265
+ h('span', { className: 'dsh-tp-card-desc' }, lb.desc[scheme.id] || scheme.desc)),
1106
1266
  h('span', { className: 'dsh-tp-check', 'aria-hidden': 'true' }, '✓'),
1107
1267
  )
1108
1268
  }
@@ -1115,14 +1275,14 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
1115
1275
  h('span', { className: 'dsh-tp-badge' }, 'v' + PLUGIN_VERSION)),
1116
1276
  h('p', { className: 'dsh-tp-sub' }, txt.hint),
1117
1277
  h('div', { className: 'dsh-tp-grid' }, SCHEMES.map(card)),
1118
- row(txt.text, txt.textSub, seg(TEXT_MODES, textMode, props.setTextMode)),
1119
- row(txt.sidebar, txt.sidebarSub, seg(SIDEBAR_MODES, sidebarMode, props.setSidebarMode)),
1120
- row(txt.rightbar, txt.rightbarSub, seg(RIGHTBAR_MODES, rightbarMode, props.setRightbarMode)),
1278
+ row(txt.text, txt.textSub, seg(TEXT_MODES, textMode, props.setTextMode, lb.text)),
1279
+ row(txt.sidebar, txt.sidebarSub, seg(SIDEBAR_MODES, sidebarMode, props.setSidebarMode, lb.sidebar)),
1280
+ row(txt.rightbar, txt.rightbarSub, seg(RIGHTBAR_MODES, rightbarMode, props.setRightbarMode, lb.rightbar)),
1121
1281
  health && !health.ok && h('p', { className: 'dsh-tp-status is-bad' }, txt.bad),
1122
1282
  health && health.ok && health.reapplies > 0 && h(
1123
1283
  'p',
1124
1284
  { className: 'dsh-tp-status' },
1125
- (isZh() ? '配色层被宿主丢过一次,已自动恢复 ' : 'Palette layer was dropped by the host; restored ') + health.reapplies + (isZh() ? ' 次' : 'x'),
1285
+ txt.healed + health.reapplies + txt.healedSuffix,
1126
1286
  ),
1127
1287
  h('button', {
1128
1288
  type: 'button',
@@ -1133,9 +1293,10 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
1133
1293
  advanced && h(
1134
1294
  'div',
1135
1295
  { className: 'dsh-tp-adv' },
1136
- row(txt.md, txt.mdSub, seg(MD_MODES, mdMode, props.setMdMode)),
1137
- row(txt.scroll, current === 'mono' ? txt.scrollHintMono : txt.scrollSub, seg(SCROLL_MODES, scrollMode, props.setScrollMode)),
1138
- row(txt.control, txt.controlSub, seg(CONTROL_MODES, controlMode, props.setControlMode)),
1296
+ row(txt.lang, txt.langSub, seg(LANG_OPTIONS, langPref, props.setLangPref, lb.lang)),
1297
+ row(txt.md, txt.mdSub, seg(MD_MODES, mdMode, props.setMdMode, lb.md)),
1298
+ row(txt.scroll, current === 'mono' ? txt.scrollHintMono : txt.scrollSub, seg(SCROLL_MODES, scrollMode, props.setScrollMode, lb.scroll)),
1299
+ row(txt.control, txt.controlSub, seg(CONTROL_MODES, controlMode, props.setControlMode, lb.control)),
1139
1300
  ),
1140
1301
  )
1141
1302
  }
@@ -1162,6 +1323,10 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
1162
1323
  let scrollMode = readScrollStored()
1163
1324
  let controlMode = readControlStored()
1164
1325
  let mdMode = readMdStored()
1326
+ let langPref = readLangPref()
1327
+ const localeListeners = new Set()
1328
+ const currentLocale = () => pickLocale(langPref)
1329
+ const emitLocale = () => { for (const fn of localeListeners) { try { fn() } catch (err) { /* 忽略 */ } } }
1165
1330
  const listeners = new Set()
1166
1331
  const sidebarListeners = new Set()
1167
1332
  const textListeners = new Set()
@@ -1250,9 +1415,18 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
1250
1415
  * 这一层就没了 —— 表现正是"配色自己跳回默认",而 localStorage 里的选择还在。
1251
1416
  * 这里定期(以及窗口重新可见/获焦时)回读 body:token 不在了就把最后一层重放。
1252
1417
  */
1418
+ // 连续失败要退避:宿主真把 token 改名时,每 4 秒重放一次既刷日志、又会堆 override 层。
1419
+ const HEAL_BACKOFF = [4000, 15000, 60000, 180000, 600000]
1420
+ let healFails = 0
1421
+ let lastHealAt = 0
1422
+ let healDisabled = false
1423
+
1253
1424
  const ensureApplied = (reason) => {
1254
- if (lastTokens === null) return
1425
+ if (lastTokens === null || healDisabled) return
1255
1426
  if (Date.now() - lastAppliedAt < 900) return
1427
+ const wait = HEAL_BACKOFF[Math.min(healFails, HEAL_BACKOFF.length - 1)]
1428
+ if (healFails > 0 && Date.now() - lastHealAt < wait) return
1429
+ lastHealAt = Date.now()
1256
1430
  let missing = false
1257
1431
  try {
1258
1432
  const style = getComputedStyle(document.body)
@@ -1263,7 +1437,16 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
1263
1437
  } catch (err) {
1264
1438
  missing = true
1265
1439
  }
1266
- if (!missing) return
1440
+ if (!missing) {
1441
+ healFails = 0
1442
+ return
1443
+ }
1444
+ if (healFails >= HEAL_BACKOFF.length) {
1445
+ healDisabled = true
1446
+ console.error('[dsh-theme-palette] 配色层反复失效(' + reapplyCount + ' 次),已停止自动重放;请检查宿主 token 是否改名')
1447
+ return
1448
+ }
1449
+ healFails += 1
1267
1450
  reapplyCount += 1
1268
1451
  console.info('[dsh-theme-palette] 配色层不见了(' + reason + '),已重放第 ' + reapplyCount + ' 次')
1269
1452
  applyAll('self-heal:' + reason)
@@ -1340,6 +1523,24 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
1340
1523
  applyAll()
1341
1524
  emitMd()
1342
1525
  }
1526
+ const getLangPref = () => langPref
1527
+ const getLocale = () => currentLocale()
1528
+ const subscribeLocale = (fn) => {
1529
+ localeListeners.add(fn)
1530
+ return () => { localeListeners.delete(fn) }
1531
+ }
1532
+ const setLangPref = (id) => {
1533
+ if (id !== 'auto' && LOCALES.indexOf(id) === -1) return
1534
+ langPref = id
1535
+ writeLangPref(id)
1536
+ emitLocale()
1537
+ }
1538
+ // 宿主切换界面语言时(locale 服务会改 documentElement.lang),设置页跟着重渲染。
1539
+ let langObserver = null
1540
+ try {
1541
+ langObserver = new MutationObserver(() => emitLocale())
1542
+ langObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['lang'] })
1543
+ } catch (err) { /* 没有 MutationObserver 就只在打开设置页时读一次 */ }
1343
1544
 
1344
1545
  // 开发期自动化接口:截图回归要从外部驱动「换配色 / 换明暗」,而这两件事
1345
1546
  // 平时只能点界面。暴露一组 setter,让 CDP 的 Runtime.evaluate 能直接调用。
@@ -1354,6 +1555,8 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
1354
1555
  setScrollMode,
1355
1556
  setControlMode,
1356
1557
  getPreference: () => theme.getTheme().preference,
1558
+ setLangPref,
1559
+ locales: LOCALES,
1357
1560
  setTheme: (id) => theme.setTheme(id),
1358
1561
  schemes: SCHEMES.map((s) => s.id),
1359
1562
  })
@@ -1375,7 +1578,10 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
1375
1578
  window.addEventListener('pageshow', healOnShow)
1376
1579
  } catch (err) { /* 无 DOM 时跳过 */ }
1377
1580
  try { ctx.on('theme/change', healOnTheme) } catch (err) { /* 事件名变了就只靠定时器 */ }
1378
- try { healTimer = setInterval(() => ensureApplied('interval'), 4000) } catch (err) { /* 无定时器环境 */ }
1581
+ try {
1582
+ healTimer = setInterval(() => ensureApplied('interval'), 4000)
1583
+ if (healTimer && typeof healTimer.unref === 'function') healTimer.unref()
1584
+ } catch (err) { /* 无定时器环境 */ }
1379
1585
 
1380
1586
  ctx.effect(() => {
1381
1587
  applyAll('boot')
@@ -1391,6 +1597,7 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
1391
1597
  window.removeEventListener('pageshow', healOnShow)
1392
1598
  } catch (err) { /* 同上 */ }
1393
1599
  if (healTimer !== null) { clearInterval(healTimer); healTimer = null }
1600
+ if (langObserver !== null) { langObserver.disconnect(); langObserver = null }
1394
1601
  }
1395
1602
  }, 'dsh-theme-palette: token layer + 右栏样式')
1396
1603
 
@@ -1402,7 +1609,7 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
1402
1609
  // 内置节顺序:account -10 / general 0 / models 10 / plugins 15 / agent-presets 20;
1403
1610
  // 5 = 紧跟「通用」之后。
1404
1611
  order: 5,
1405
- label: () => (isZh() ? TEXT.zh.nav : TEXT.en.nav),
1612
+ label: () => TEXT[pickLocale(readLangPref())].nav,
1406
1613
  inject: () => ({
1407
1614
  getCurrent,
1408
1615
  subscribe,
@@ -1425,6 +1632,10 @@ body[data-ds-dark-theme] .dsh-tp-check{background:var(--t-accent-d);color:var(--
1425
1632
  getMdMode,
1426
1633
  subscribeMd,
1427
1634
  setMdMode,
1635
+ getLangPref,
1636
+ getLocale,
1637
+ subscribeLocale,
1638
+ setLangPref,
1428
1639
  getHealth,
1429
1640
  subscribeHealth,
1430
1641
  }),