creditdaddy 1.3.2 → 1.4.1

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/src/panel.html CHANGED
@@ -43,11 +43,56 @@
43
43
  .brand { display: flex; align-items: center; gap: 10px; min-width: 0; }
44
44
  .logo { width: 36px; height: 36px; flex-shrink: 0; display: block; }
45
45
  .title { font-family: "Avenir Next", Avenir, "Segoe UI Variable Display", "Segoe UI", sans-serif; font-size: 17px; font-weight: 750; letter-spacing: -.5px; line-height: 1.15; }
46
- .ver { color: var(--text-3); font-size: 11px; line-height: 1.3; font-variant-numeric: tabular-nums; }
47
- .head-right { display: flex; align-items: center; gap: 10px; color: var(--text-3); font-size: 12px; }
46
+ .ver { color: var(--text-3); font-size: 11px; line-height: 1.55; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
47
+ .head-right { display: flex; align-items: center; gap: 8px; color: var(--text-3); font-size: 12px; }
48
+ /* 状态胶囊 + 分组分隔线:把「下一次调度」这类只读状态与右侧操作按钮区分开 */
49
+ .sched-chip { display: inline-flex; align-items: center; gap: 6px; padding: 5px 10px 5px 9px; border-radius: 999px; background: var(--tertiary); color: var(--text-3); font-size: 12px; white-space: nowrap; }
50
+ .sched-chip svg { width: 13px; height: 13px; flex-shrink: 0; opacity: .8; }
51
+ .sched-chip b { font-weight: 650; color: var(--text-2); font-variant-numeric: tabular-nums; }
52
+ .sched-chip.busy { background: rgba(var(--accent-rgb), .14); color: var(--accent); }
53
+ .sched-chip.busy b { color: var(--accent); }
54
+ .sched-chip:empty { display: none; }
55
+ .head-sep { width: 1px; height: 18px; background: var(--border); flex: none; }
56
+ /* 右上角聚合菜单:锚定在按钮下方右对齐的下拉(不是居中弹窗)。
57
+ 宽高固定、内部分区(工作区 / 外观 / 关于)——一眼能分清「设置值」与「一次性动作」。 */
58
+ .hmenu { position: relative; display: inline-flex; }
59
+ /* 面板设置弹出层:与聚合菜单同一视觉语言,紧贴按钮右对齐展开,不再是全屏遮罩的居中弹窗 */
60
+ .head-setpop { position: relative; display: inline-flex; }
61
+ .setpop { position: absolute; right: 0; top: calc(100% + 10px); z-index: 60; width: min(560px, calc(100vw - 24px)); display: none; }
62
+ .setpop.show { display: block; }
63
+ .setpop .modal { width: 100%; max-height: calc(100vh - 120px); border: 1px solid var(--border-subtle); padding: 16px 18px; }
64
+ .hmenu-pop { position: absolute; right: 0; top: calc(100% + 10px); z-index: 60; width: 252px; background: var(--surface); border: 1px solid var(--border-subtle); border-radius: 14px; padding: 7px; box-shadow: var(--shadow); display: none; }
65
+ .hmenu-pop.show { display: block; }
66
+ /* 分区:块级流,不用 flex —— 否则分段控件会被当成 flex 子项压扁(曾出现与下一分区重叠) */
67
+ .hmenu-group { padding: 3px 0; }
68
+ .hmenu-group + .hmenu-group { margin-top: 4px; padding-top: 7px; border-top: 1px solid var(--border-subtle); }
69
+ .hmenu-title { padding: 4px 10px 5px; font-size: 10.5px; font-weight: 700; letter-spacing: .6px; color: var(--text-3); text-transform: uppercase; }
70
+ .hmenu-item { display: flex; align-items: center; gap: 9px; width: 100%; padding: 7px 10px; border: 0; border-radius: 8px; background: transparent; color: var(--text-2); font-size: 13px; font-weight: 600; cursor: pointer; text-align: left; font-family: inherit; text-decoration: none; }
71
+ .hmenu-item:hover { background: var(--hover); color: var(--text); }
72
+ .hmenu-item svg { width: 15px; height: 15px; flex-shrink: 0; }
73
+ .hmenu-item.danger { color: var(--err); }
74
+ .hmenu-item.danger:hover { background: var(--err-soft); color: var(--err); }
75
+ .hmenu-clip { margin-left: auto; color: var(--text-3); font-size: 11px; font-weight: 600; max-width: 116px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
76
+ /* 外观 / 语言:三态分段控件(跟随系统 / 亮色 / 暗色),当前态高亮 —— 主题只有这一个真相源。
77
+ inline-flex + 外层留白:分段是块级流里的一个整体,高度由按钮撑开(不能是 flex 子项被压扁)。
78
+ 类名必须是 mode-seg:.seg 已被额度水位条的分段占用(见下方 .bar 段),同名会互相覆盖——
79
+ 分段控件会被压成 5px 绿条,水位条的分段则会各带 12px 横向 margin 而溢出裁切。 */
80
+ .mode-seg { display: inline-flex; gap: 3px; margin: 2px 6px 4px; padding: 3px; border-radius: 9px; background: var(--tertiary); }
81
+ /* 语言五档:等宽铺满弹窗宽度,避免中/英/日/韩标签长短不一导致右侧留白 */
82
+ .lang-seg { display: flex; width: calc(100% - 12px); }
83
+ .lang-seg .seg-btn { flex: 1 1 0; padding: 6px 0; }
84
+ .seg-btn { display: inline-flex; align-items: center; justify-content: center; gap: 5px; padding: 6px 9px; border: 0; border-radius: 7px; background: transparent; color: var(--text-3); font-size: 12px; font-weight: 600; line-height: 1; cursor: pointer; font-family: inherit; white-space: nowrap; }
85
+ .seg-btn svg { width: 13px; height: 13px; flex-shrink: 0; display: block; }
86
+ .seg-btn:hover { color: var(--text); }
87
+ .seg-btn.on { background: var(--surface); color: var(--text); box-shadow: 0 1px 3px rgba(0, 0, 0, .12); }
88
+ html[data-theme="dark"] .seg-btn.on { background: var(--active); box-shadow: none; }
89
+ .hmenu-ver { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 7px 10px 4px; margin-top: 4px; border-top: 1px solid var(--border-subtle); color: var(--text-3); font-size: 11px; }
90
+ .hmenu-ver a { color: var(--text-3); text-decoration: none; }
91
+ .hmenu-ver a:hover { color: var(--text); }
92
+ @media (max-width: 720px) { .sched-chip { display: none; } .head-sep { display: none; } }
48
93
  /* 面板设置(右上角齿轮 → 弹窗):访问密码 + 组件显隐与排序 + 账号卡片排序 */
49
94
  .comp-menu { display: flex; flex-direction: column; gap: 8px; }
50
- .comp-menu-title { font-size: 11px; color: var(--text-3); margin: 10px 0 4px; padding-top: 8px; border-top: 1px dashed var(--border-subtle); }
95
+ .comp-menu-title { font-size: 12px; color: var(--text-2); font-weight: 650; margin: 12px 0 4px; padding-top: 10px; border-top: 1px dashed var(--border-subtle); }
51
96
  .comp-menu-title:first-child { margin-top: 0; padding-top: 0; border-top: 0; }
52
97
  .comp-menu .check { margin-right: 0; font-size: 12.5px; color: var(--text); }
53
98
  /* 产品页 notice 的功能分区行 */
@@ -65,12 +110,25 @@
65
110
  .comp-row { display: flex; align-items: center; justify-content: space-between; gap: 6px; }
66
111
  .comp-row.off .check { opacity: .45; }
67
112
  .comp-ops { display: inline-flex; gap: 2px; flex: none; }
113
+ /* 横向组件 chips:一行放下全部产品,不再一条一行占半屏 */
114
+ .comp-chips { display: flex; flex-wrap: wrap; gap: 6px; }
115
+ .comp-chip { display: inline-flex; align-items: center; gap: 0; padding: 3px 4px 3px 10px; border-radius: 999px; background: var(--hover); font-size: 12px; font-weight: 600; color: var(--text); }
116
+ .comp-chip.off { opacity: .5; filter: grayscale(.9); }
117
+ .comp-chip[data-comp-toggle] { cursor: pointer; }
118
+ .comp-chip .sort-name { max-width: 130px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
119
+ .comp-chip .icon-btn.mini { width: 20px; height: 20px; }
120
+ .comp-chip[draggable] { cursor: grab; }
121
+ .comp-chip.dragging { opacity: .4; }
122
+ .comp-chip.drop-before { box-shadow: -3px 0 0 0 var(--ok); }
123
+ .comp-chip.drop-after { box-shadow: 3px 0 0 0 var(--ok); }
124
+ .drag-grip { color: var(--text-3); display: inline-flex; }
125
+ .drag-grip svg { width: 12px; height: 12px; }
68
126
  .icon-btn.mini { width: 20px; height: 20px; border-radius: 6px; font-size: 11px; line-height: 1; padding: 0; }
69
127
  .icon-btn.mini svg { width: 12px; height: 12px; }
70
128
  .sort-sec { border-top: 1px dashed var(--border-subtle); padding-top: 6px; font-size: 12px; }
71
129
  .sort-sec summary { cursor: pointer; color: var(--text-2); user-select: none; display: flex; align-items: center; gap: 6px; }
72
130
  .sort-sec summary::-webkit-details-marker { display: none; }
73
- .sort-hint { font-size: 11px; color: var(--text-3); margin: 5px 0 3px; }
131
+ .sort-hint { font-size: 11px; color: var(--text-3); line-height: 1.65; margin: 4px 0 3px; }
74
132
  .sort-row { display: flex; align-items: center; justify-content: space-between; gap: 6px; padding: 2px 0; }
75
133
  .sort-row .sort-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-variant-numeric: tabular-nums; }
76
134
  .card.dragging, .tr-card.dragging { opacity: .45; }
@@ -98,6 +156,8 @@
98
156
  .btn svg, .icon-btn svg { width: 15px; height: 15px; flex-shrink: 0; }
99
157
  .icon-btn { width: 28px; height: 28px; border-radius: 8px; display: inline-flex; align-items: center; justify-content: center; border: 1px solid transparent; background: var(--hover); color: var(--text-2); cursor: pointer; transition: all .15s; padding: 0; }
100
158
  .icon-btn:hover { background: var(--active); color: var(--text); }
159
+ .icon-btn:active { transform: scale(.9); }
160
+ .btn:active, .seg-btn:active, .hmenu-item:active { transform: translateY(1px); }
101
161
  .icon-btn.del:hover { background: #ffecec; color: #f53f3f; }
102
162
  html[data-theme="dark"] .icon-btn.del:hover { background: rgba(245,63,63,.15); }
103
163
  .icon-btn.del.armed { background: #f53f3f; color: #fff; }
@@ -195,11 +255,23 @@
195
255
  html[data-theme="dark"] .log-filter button.active { background: rgba(255,255,255,.1); }
196
256
  .collapsed .logs { display: none; }
197
257
 
258
+ /* 首次加载态:HTML 静态可见(无需等 JS/词条),数据首屏后淡出 */
259
+ #boot-busy { position: fixed; inset: 0; z-index: 95; display: flex; align-items: center; justify-content: center; background: var(--bg); transition: opacity .25s; }
260
+ #boot-busy.fade { opacity: 0; pointer-events: none; }
261
+ .lb-card.boot { padding: 18px 26px; font-size: 15px; }
262
+
263
+ /* 语言切换加载态:点击分段按钮立即给反馈,落盘 + 整页刷新期间盖住 */
264
+ #locale-busy { position: fixed; inset: 0; z-index: 90; display: flex; align-items: center; justify-content: center; background: var(--mask); }
265
+ #locale-busy[hidden] { display: none; }
266
+ .lb-card { display: flex; align-items: center; gap: 10px; background: var(--surface); border: 1px solid var(--border-subtle); border-radius: 14px; padding: 14px 20px; box-shadow: var(--shadow); font-size: 13px; font-weight: 600; color: var(--text); }
267
+ .lb-spin { width: 16px; height: 16px; border-radius: 50%; border: 2px solid var(--border-subtle); border-top-color: var(--ok); animation: spin .8s linear infinite; }
268
+
198
269
  /* modal */
199
- .mask { position: fixed; inset: 0; z-index: 50; background: var(--mask); display: none; align-items: center; justify-content: center; padding: 16px; }
270
+ .mask { position: fixed; inset: 0; z-index: 96; background: var(--mask); display: none; align-items: center; justify-content: center; padding: 16px; }
200
271
  .mask.show { display: flex; }
201
272
  .modal { width: 560px; max-width: 100%; max-height: 90vh; overflow-y: auto; background: var(--surface); border-radius: 14px; padding: 16px 18px; box-shadow: var(--shadow); }
202
273
  .modal.sm { width: 360px; }
274
+ .changelog-sec + .changelog-sec { border-top: 1px dashed var(--border-subtle); margin-top: 10px; padding-top: 10px; }
203
275
  .modal-title { display: flex; align-items: center; justify-content: space-between; font-size: 14px; font-weight: 700; margin-bottom: 12px; }
204
276
  .modal-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 14px; }
205
277
  .close { border: none; background: none; color: var(--text-3); font-size: 16px; cursor: pointer; padding: 2px 6px; border-radius: 6px; }
@@ -209,15 +281,15 @@
209
281
  .tab:hover { color: var(--text); }
210
282
  .tab.active { background: var(--btn-bg); color: var(--btn-fg); }
211
283
  .pane { display: none; } .pane.active { display: block; }
212
- .field { display: flex; flex-direction: column; gap: 5px; margin-bottom: 10px; }
213
- .field label { font-size: 12px; color: var(--text-2); font-weight: 600; }
284
+ .field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px; }
285
+ .field label { font-size: 12.5px; color: var(--text-2); font-weight: 650; }
214
286
  .input { width: 100%; padding: 8px 10px; border: 1px solid var(--border); border-radius: 9px; background: var(--surface-2); color: var(--text); outline: none; transition: border-color .15s; }
215
287
  .input::placeholder { color: var(--text-3); opacity: .7; }
216
288
  .input:focus { border-color: #4f86ff; }
217
289
  .input:disabled { opacity: .6; cursor: not-allowed; }
218
290
  .input.mono { font-family: ui-monospace, Consolas, monospace; }
219
291
  .row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
220
- .hint { font-size: 12px; color: var(--text-3); line-height: 1.7; }
292
+ .hint { font-size: 11px; color: var(--text-3); line-height: 1.65; }
221
293
  .hint a { color: #4f86ff; word-break: break-all; }
222
294
  .box { border: 1px solid var(--border-subtle); border-radius: 10px; padding: 10px 12px; margin-top: 10px; background: var(--surface-2); }
223
295
  .cand { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 10px 12px; border: 1px solid var(--border-subtle); border-radius: 10px; background: var(--surface); }
@@ -353,30 +425,132 @@
353
425
  </style>
354
426
  </head>
355
427
  <body>
428
+ <!-- 首次加载态:数据首屏后由 boot 隐藏 -->
429
+ <div id="boot-busy"><div class="lb-card boot"><span class="lb-spin"></span><strong>CreditDaddy</strong></div></div>
356
430
  <div class="wrap">
357
431
  <header class="head">
358
432
  <div class="brand">
359
433
  <img class="logo" alt="" src="data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDI0IiBoZWlnaHQ9IjEwMjQiPjxwYXRoIGQ9Ik0zNzIuNDggMTM4LjY2NjY2N0ExMjggMTI4IDAgMCAwIDI5OC42NjY2NjcgMTAxLjU0NjY2N2ExMTcuNzYgMTE3Ljc2IDAgMCAwLTYyLjA4IDAgMTAuODggMTAuODggMCAwIDAtNS4xMiAxNi44NTMzMzNsNzEuMjUzMzMzIDkxLjUyLTgxLjI4IDI4LjM3MzMzM2ExMC42NjY2NjcgMTAuNjY2NjY3IDAgMCAwLTIuMzQ2NjY3IDE4Ljk4NjY2NyAyNDIuNTYgMjQyLjU2IDAgMCAwIDY5Ljc2IDMyLjg1MzMzMyAyNzMuNzA2NjY3IDI3My43MDY2NjcgMCAwIDAgNTYuOTYgOC4zMnoiIGZpbGw9IiNGRjhBMTQiPjwvcGF0aD48cGF0aCBkPSJNMjQ1Ljc2IDEzOC4wMjY2NjdhMTMwLjk4NjY2NyAxMzAuOTg2NjY3IDAgMCAxIDUyLjA1MzMzMyAyLjEzMzMzM0ExNDguMDUzMzMzIDE0OC4wNTMzMzMgMCAwIDEgMzY2LjcyIDE3MC42NjY2NjdsNS43Ni0zMi44NTMzMzRBMTI4IDEyOCAwIDAgMCAyOTguNjY2NjY3IDEwMS41NDY2NjdhMTE3Ljk3MzMzMyAxMTcuOTczMzMzIDAgMCAwLTYyLjA4IDAgMTAuODggMTAuODggMCAwIDAtNS4xMiAxNi44NTMzMzN6IiBmaWxsPSIjRkZBQTU0Ij48L3BhdGg+PHBhdGggZD0iTTIzNC42NjY2NjcgOTcwLjY2NjY2N2EyNzcuMzMzMzMzIDMyIDAgMSAwIDU1NC42NjY2NjYgMCAyNzcuMzMzMzMzIDMyIDAgMSAwLTU1NC42NjY2NjYgMFoiIGZpbGw9IiM0NTQxM0MiIG9wYWNpdHk9Ii4xNSI+PC9wYXRoPjxwYXRoIGQ9Ik02MjAuMzczMzMzIDQ2MC41ODY2NjdzMTUxLjA0LTkwLjY2NjY2NyAxODEuMzMzMzM0LTc0LjY2NjY2Ny0xMi41ODY2NjcgMTc0LjI5MzMzMy0xMDYuNjY2NjY3IDIxMy4zMzMzMzMtNzQuNjY2NjY3LTEzOC42NjY2NjctNzQuNjY2NjY3LTEzOC42NjY2NjZ6IiBmaWxsPSIjRkZFNTAwIj48L3BhdGg+PHBhdGggZD0iTTYyMC4zNzMzMzMgNTEzLjkyczE1MS4wNC05MC42NjY2NjcgMTgxLjMzMzMzNC03NC42NjY2NjdhMTQuNzIgMTQuNzIgMCAwIDEgNS4zMzMzMzMgNS45NzMzMzRjNi44MjY2NjctMjkuODY2NjY3IDUuMzMzMzMzLTUzLjU0NjY2Ny01LjMzMzMzMy01OS4zMDY2NjctMzAuMjkzMzMzLTE2LTE4MS4zMzMzMzMgNzQuNjY2NjY3LTE4MS4zMzMzMzQgNzQuNjY2NjY3YTM4Ni45ODY2NjcgMzg2Ljk4NjY2NyAwIDAgMCAwIDYxLjg2NjY2NmMtMC40MjY2NjctNS4zMzMzMzMgMC04LjUzMzMzMyAwLTguNTMzMzMzeiIgZmlsbD0iI0ZGRjQ4QyI+PC9wYXRoPjxwYXRoIGQ9Ik00MDMuNjI2NjY3IDQ2MC41ODY2NjdzLTE1MS4wNC05MC42NjY2NjctMTgxLjMzMzMzNC03NC42NjY2NjcgMTIuMzczMzMzIDE3NC4yOTMzMzMgMTA2LjY2NjY2NyAyMTMuMzMzMzMzIDc0LjY2NjY2Ny0xMzguNjY2NjY3IDc0LjY2NjY2Ny0xMzguNjY2NjY2eiIgZmlsbD0iI0ZGRTUwMCI+PC9wYXRoPjxwYXRoIGQ9Ik0yMjIuMjkzMzMzIDQzOS4yNTMzMzNjMzAuMjkzMzMzLTE2IDE4MS4zMzMzMzMgNzQuNjY2NjY3IDE4MS4zMzMzMzQgNzQuNjY2NjY3djguNTMzMzMzYTM3My41NDY2NjcgMzczLjU0NjY2NyAwIDAgMCAwLTYxLjg2NjY2NnMtMTUxLjA0LTkwLjY2NjY2Ny0xODEuMzMzMzM0LTc0LjY2NjY2N2MtMTAuNjY2NjY3IDUuNzYtMTIuMTYgMjkuNDQtNS4zMzMzMzMgNTkuMzA2NjY3YTEzLjQ0IDEzLjQ0IDAgMCAxIDUuMzMzMzMzLTUuOTczMzM0eiIgZmlsbD0iI0ZGRjQ4QyI+PC9wYXRoPjxwYXRoIGQ9Ik0yODggNjUwLjY2NjY2N2EyNjYuNjY2NjY3IDIyNCA5MCAxIDAgNDQ4IDAgMjY2LjY2NjY2NyAyMjQgOTAgMSAwLTQ0OCAwWiIgZmlsbD0iI0ZGRTUwMCI+PC9wYXRoPjxwYXRoIGQ9Ik02ODAuMTA2NjY3IDQ3NC4yNGEyMjQgMjI0IDAgMCAxLTMzNi4yMTMzMzQgMEEyMTAuNTYgMjEwLjU2IDAgMCAxIDUxMiAzODRhMjEwLjU2IDIxMC41NiAwIDAgMSAxNjguMTA2NjY3IDkwLjI0eiIgZmlsbD0iI0VCQ0IwMCI+PC9wYXRoPjxwYXRoIGQ9Ik01MTIgMjY2LjY2NjY2N20tMjI0IDBhMjI0IDIyNCAwIDEgMCA0NDggMCAyMjQgMjI0IDAgMSAwLTQ0OCAwWiIgZmlsbD0iI0ZGRTUwMCI+PC9wYXRoPjxwYXRoIGQ9Ik01MTIgMTAxLjk3MzMzM2EyMjQuMjEzMzMzIDIyNC4yMTMzMzMgMCAwIDEgMjIxLjg2NjY2NyAxOTQuMzQ2NjY3IDIyNCAyMjQgMCAxIDAtNDQzLjczMzMzNCAwQTIyMy43ODY2NjcgMjIzLjc4NjY2NyAwIDAgMSA1MTIgMTAxLjk3MzMzM3oiIGZpbGw9IiNGRkY0OEMiPjwvcGF0aD48cGF0aCBkPSJNODMzLjQ5MzMzMyA1OTguODI2NjY3bC03MC40IDcwLjRhMTguMzQ2NjY3IDE4LjM0NjY2NyAwIDAgMS0yNS44MTMzMzMgMEw2NDQuMDUzMzMzIDU3NkExOC4zNDY2NjcgMTguMzQ2NjY3IDAgMCAwIDYxOC42NjY2NjcgNTc2bC05My4wMTMzMzQgOTMuMjI2NjY3YTE4Ljc3MzMzMyAxOC43NzMzMzMgMCAwIDEtMjYuMDI2NjY2IDBMNDA1LjMzMzMzMyA1NzZhMTguMTMzMzMzIDE4LjEzMzMzMyAwIDAgMC0yNS44MTMzMzMgMGwtOTMuMjI2NjY3IDkzLjIyNjY2N2ExOC4zNDY2NjcgMTguMzQ2NjY3IDAgMCAxLTI1LjgxMzMzMyAwbC03MC40LTcwLjRhOS4xNzMzMzMgOS4xNzMzMzMgMCAwIDAtMTUuNzg2NjY3IDYuODI2NjY2QzE4OS44NjY2NjcgODQxLjgxMzMzMyAzMzQuOTMzMzMzIDk2MCA1MTIgOTYwczMyMi4xMzMzMzMtMTE4LjE4NjY2NyAzMzcuMjgtMzU0LjM0NjY2N2E5LjE3MzMzMyA5LjE3MzMzMyAwIDAgMC0xNS43ODY2NjctNi44MjY2NjZ6IiBmaWxsPSIjRjBGMEYwIj48L3BhdGg+PHBhdGggZD0iTTE5MC41MDY2NjcgNjY1LjgxMzMzM2w3MC40IDYxLjIyNjY2N2EyMS4zMzMzMzMgMjEuMzMzMzMzIDAgMCAwIDI1LjgxMzMzMyAwbDkzLjIyNjY2Ny04MS4wNjY2NjdhMjEuMzMzMzMzIDIxLjMzMzMzMyAwIDAgMSAyNS44MTMzMzMgMGw5My4yMjY2NjcgODEuMDY2NjY3YTIxLjMzMzMzMyAyMS4zMzMzMzMgMCAwIDAgMjYuMDI2NjY2IDBsOTMuNjUzMzM0LTgxLjA2NjY2N2EyMS4zMzMzMzMgMjEuMzMzMzMzIDAgMCAxIDI2LjAyNjY2NiAwbDkzLjIyNjY2NyA4MS4wNjY2NjdhMjEuMzMzMzMzIDIxLjMzMzMzMyAwIDAgMCAyNS44MTMzMzMgMGw3MC40LTYxLjIyNjY2N2E5LjgxMzMzMyA5LjgxMzMzMyAwIDAgMSA4LjUzMzMzNC0xLjkyIDUzNi4xMDY2NjcgNTM2LjEwNjY2NyAwIDAgMCA3LjI1MzMzMy01OC4yNCA5LjE3MzMzMyA5LjE3MzMzMyAwIDAgMC0xNS43ODY2NjctNi44MjY2NjZsLTcwLjQgNzAuNGExOC4zNDY2NjcgMTguMzQ2NjY3IDAgMCAxLTI1LjgxMzMzMyAwTDY0NC4wNTMzMzMgNTc2QTE4LjM0NjY2NyAxOC4zNDY2NjcgMCAwIDAgNjE4LjY2NjY2NyA1NzZsLTkzLjAxMzMzNCA5My4yMjY2NjdhMTguNzczMzMzIDE4Ljc3MzMzMyAwIDAgMS0yNi4wMjY2NjYgMEw0MDUuMzMzMzMzIDU3NmExOC4xMzMzMzMgMTguMTMzMzMzIDAgMCAwLTI1LjgxMzMzMyAwbC05My4yMjY2NjcgOTMuMjI2NjY3YTE4LjM0NjY2NyAxOC4zNDY2NjcgMCAwIDEtMjUuODEzMzMzIDBsLTcwLjQtNzAuNGE5LjE3MzMzMyA5LjE3MzMzMyAwIDAgMC0xNS43ODY2NjcgNi44MjY2NjYgNTM3LjE3MzMzMyA1MzcuMTczMzMzIDAgMCAwIDcuMDQgNTguMjQgMTAuMDI2NjY3IDEwLjAyNjY2NyAwIDAgMSA5LjE3MzMzNCAxLjkyeiIgZmlsbD0iI0ZGRkZGRiI+PC9wYXRoPjxwYXRoIGQ9Ik00OTguMTMzMzMzIDE4NC41MzMzMzNtLTI5LjAxMzMzMyAwYTI5LjAxMzMzMyAyOS4wMTMzMzMgMCAxIDAgNTguMDI2NjY3IDAgMjkuMDEzMzMzIDI5LjAxMzMzMyAwIDEgMC01OC4wMjY2NjcgMFoiIGZpbGw9IiM0NTQxM0MiPjwvcGF0aD48cGF0aCBkPSJNNTc2IDI5MC45ODY2NjdjMCAxMS43MzMzMzMtMTUuNzg2NjY3IDIxLjMzMzMzMy0zNC45ODY2NjcgMjEuMzMzMzMzcy0zNS4yLTkuNi0zNS4yLTIxLjMzMzMzMyAxNS43ODY2NjctMjEuMzMzMzMzIDM1LjItMjEuMzMzMzM0IDM0Ljk4NjY2NyA5LjYgMzQuOTg2NjY3IDIxLjMzMzMzNHoiIGZpbGw9IiNGRkY0OEMiPjwvcGF0aD48L3N2Zz4=">
360
434
  <div>
361
435
  <div class="title">CreditDaddy</div>
362
- <div class="ver" id="ver">Qoder / WorkBuddy / ZCode 多账号管理 · 每日积分自动领取</div>
436
+ <div class="ver" id="ver" data-i18n="Qoder / WorkBuddy / ZCode 多账号管理 · 每日积分自动领取">Qoder / WorkBuddy / ZCode 多账号管理 · 每日积分自动领取</div>
363
437
  </div>
364
438
  </div>
365
439
  <div class="head-right">
366
- <span id="sched" class="num"></span>
367
- <button class="icon-btn" id="btn-comp" title="面板设置:访问密码、组件显隐与排序、账号卡片排序"></button>
368
- <a class="icon-btn" id="btn-home" href="https://github.com/techysy/CreditDaddy" target="_blank" rel="noopener" title="项目主页(GitHub)"></a>
369
- <button class="icon-btn" id="btn-theme" title="切换亮色 / 暗色"></button>
440
+ <!-- 只读状态:下一次自动领取(领取中变强调色)。窄屏自动隐藏,操作按钮优先 -->
441
+ <span id="sched" class="sched-chip"></span>
442
+ <span class="head-sep"></span>
443
+ <button class="icon-btn" id="btn-theme" title="主题"></button>
444
+ <div class="head-setpop">
445
+ <button class="icon-btn" id="btn-comp" title="面板设置:访问密码、组件显隐与排序、账号卡片排序"></button>
446
+ <!-- 面板设置(右上角齿轮) -->
447
+ <div class="setpop" id="m-settings">
448
+ <div class="modal">
449
+ <div class="modal-title"><span data-i18n="面板设置">面板设置</span><button class="close" data-close>✕</button></div>
450
+ <div class="tabs" id="set-tabs">
451
+ <button class="tab active" data-set-tab="net" data-i18n="通用">通用</button>
452
+ <button class="tab" data-set-tab="comp" data-i18n="组件与排序">组件与排序</button>
453
+ </div>
454
+
455
+ <div class="pane active" data-set-pane="net">
456
+ <div class="field">
457
+ <label data-i18n="全局自动签到">全局自动签到</label>
458
+ <label class="check"><input type="checkbox" id="cfg-auto-checkin"> <span data-i18n="开启自动签到">开启自动签到</span></label>
459
+ <div style="display:flex;gap:8px;align-items:center;margin-top:6px">
460
+ <span class="hint" data-i18n="签到频率">签到频率</span>
461
+ <select class="input" id="cfg-checkin-interval" style="width:auto">
462
+ <option value="60" data-i18n="每 1 小时">每 1 小时</option>
463
+ <option value="120" data-i18n="每 2 小时">每 2 小时</option>
464
+ <option value="240" data-i18n="每 4 小时">每 4 小时</option>
465
+ <option value="480" data-i18n="每 8 小时">每 8 小时</option>
466
+ </select>
467
+ </div>
468
+ <div class="hint" data-i18n="作用于全部产品的定时签到轮(ZCode 活动领取在自身页签单独配置);修改立即生效,下一轮按新间隔执行。">作用于全部产品的定时签到轮(ZCode 活动领取在自身页签单独配置);修改立即生效,下一轮按新间隔执行。</div>
469
+ </div>
470
+ <div class="field">
471
+ <label data-i18n="WorkBuddy 国际版 活跃请求">WorkBuddy 国际版 活跃请求</label>
472
+ <label class="check"><input type="checkbox" id="cfg-wb-activity"> <span data-i18n="定期发送活跃请求(保持资格,不保证奖励)">定期发送活跃请求(保持资格,不保证奖励)</span></label>
473
+ <div class="hint" data-i18n="关闭后本机不再对国际版账号自动/手动发活跃请求;卡片与仪表盘的「活跃」入口一同隐藏。">关闭后本机不再对国际版账号自动/手动发活跃请求;卡片与仪表盘的「活跃」入口一同隐藏。</div>
474
+ </div>
475
+ <div class="field">
476
+ <label data-i18n="面板访问密码">面板访问密码</label>
477
+ <div id="sec-block"></div>
478
+ </div>
479
+ <div class="field">
480
+ <label data-i18n="日志显示">日志显示</label>
481
+ <select class="input" id="cfg-logs-display" style="width:auto">
482
+ <option value="expanded" data-i18n="默认展开">默认展开</option>
483
+ <option value="collapsed" data-i18n="默认收起">默认收起</option>
484
+ </select>
485
+ </div>
486
+ <div class="field">
487
+ <label data-i18n="网络出口(全局)">网络出口(全局)</label>
488
+ <label class="check"><input type="radio" name="znet-order" id="znet-direct"> 直连优先(代理兜底)</label>
489
+ <label class="check"><input type="radio" name="znet-order" id="znet-proxy"> 代理优先(直连兜底)</label>
490
+ <label style="margin-top:6px" data-i18n="出口代理(http://host:port,支持口令 http://user:pass@host:port)">出口代理(http://host:port,支持口令 http://user:pass@host:port)</label>
491
+ <input class="input" id="znet-url" placeholder="http://192.168.31.101:7890">
492
+ <div class="hint" data-i18n="作用于所有出网请求:ZCode 上游、Qoder 国际版登录与凭据探测(按 IP 识别地区,国内直连会被拦)等。留空 = 清除代理,退回 HTTPS_PROXY 环境变量。">作用于所有出网请求:ZCode 上游、Qoder 国际版登录与凭据探测(按 IP 识别地区,国内直连会被拦)等。留空 = 清除代理,退回 HTTPS_PROXY 环境变量。</div>
493
+ <div style="display:flex;gap:8px;align-items:center;margin-top:6px"><button class="btn primary" id="znet-ok" data-i18n="保存出口">保存出口</button><span class="hint" id="znet-state"></span></div>
494
+ </div>
495
+ </div>
496
+
497
+ <div class="pane" data-set-pane="comp">
498
+ <div class="comp-menu" id="comp-menu"></div>
499
+ </div>
500
+
501
+ <div class="modal-actions"><button class="btn" data-close>关闭</button></div>
502
+ </div>
503
+ </div>
504
+
505
+ </div>
506
+ <!-- 聚合菜单(最右侧):工作区 / 外观 / 语言 / 关于四分区,下拉紧贴按钮(图标由 JS 注入,Lucide 描边同款)。
507
+ 主题两处入口联动:菜单分段控件(跟随系统/亮色/暗色) + 头部独立按钮一键循环,都经 applyThemeMode 同一存储 -->
508
+ <div class="hmenu">
509
+ <button class="icon-btn" id="btn-header-menu" title="更多操作:更新、主题、重启、反馈"></button>
510
+ <div class="hmenu-pop" id="hmenu-pop">
511
+ <div class="hmenu-group">
512
+ <div class="hmenu-title" data-i18n="外观">外观</div>
513
+ <div class="mode-seg" id="theme-seg">
514
+ <button class="seg-btn" data-theme-mode="auto" title="跟随系统亮色 / 暗色设置" data-i18n="跟随系统">跟随系统</button>
515
+ <button class="seg-btn" data-theme-mode="light" title="始终使用亮色" data-i18n="亮色">亮色</button>
516
+ <button class="seg-btn" data-theme-mode="dark" title="始终使用暗色" data-i18n="暗色">暗色</button>
517
+ </div>
518
+ </div>
519
+ <div class="hmenu-group">
520
+ <div class="hmenu-title" data-i18n="语言">语言</div>
521
+ <div class="mode-seg lang-seg" id="locale-seg">
522
+ <button class="seg-btn" data-locale="zh-CN" title="简体中文">简</button>
523
+ <button class="seg-btn" data-locale="zh-TW" title="繁體中文">繁</button>
524
+ <button class="seg-btn" data-locale="en" title="English">EN</button>
525
+ <button class="seg-btn" data-locale="ja" title="日本語">日</button>
526
+ <button class="seg-btn" data-locale="ko" title="한국어">한</button>
527
+ </div>
528
+ </div>
529
+ <div class="hmenu-group">
530
+ <div class="hmenu-title" data-i18n="工作区">工作区</div>
531
+ <button class="hmenu-item" id="hm-update">检查更新</button>
532
+ <button class="hmenu-item" id="changelog-brief-btn">更新日志</button>
533
+ </div>
534
+ <div class="hmenu-group">
535
+ <div class="hmenu-title" data-i18n="关于">关于</div>
536
+ <a class="hmenu-item" id="hm-home" href="https://github.com/techysy/CreditDaddy" target="_blank" rel="noopener" data-i18n="项目主页">项目主页</a>
537
+ <button class="hmenu-item" id="feedback-btn" data-i18n="建议反馈">建议反馈</button>
538
+ <button class="hmenu-item" id="donate-btn" data-i18n="捐赠支持">捐赠支持</button>
539
+ <button class="hmenu-item danger" id="hm-restart" data-i18n="重启 CreditDaddy">重启 CreditDaddy</button>
540
+ </div>
541
+ <div class="hmenu-ver"><span id="hmenu-version">CreditDaddy</span><a id="hmenu-channel" href="https://github.com/techysy/CreditDaddy/releases/latest" target="_blank" rel="noopener" hidden>Releases</a></div>
542
+ </div>
543
+ </div>
370
544
  </div>
371
545
  </header>
372
546
 
373
547
  <nav class="nav" id="nav">
374
- <button data-view="dash" class="active">仪表盘</button>
548
+ <button data-view="dash" class="active" data-i18n="仪表盘">仪表盘</button>
375
549
  <button data-view="qoder">Qoder<span class="cnt" id="cnt-qoder">0</span></button>
376
550
  <button data-view="workbuddy">WorkBuddy<span class="cnt" id="cnt-workbuddy">0</span></button>
377
551
  <button data-view="zcode">ZCode<span class="cnt" id="cnt-zcode">0</span></button>
378
552
  <button data-view="mirasim">mirasim<span class="cnt" id="cnt-mirasim">0</span></button>
379
- <button data-view="catpaw">妙手<span class="cnt" id="cnt-catpaw">0</span></button>
553
+ <button data-view="catpaw"><span data-i18n="妙手">妙手</span><span class="cnt" id="cnt-catpaw">0</span></button>
380
554
  <button data-view="trae">Trae<span class="cnt" id="cnt-trae">0</span></button>
381
555
  <button data-view="minimax">MiniMax<span class="cnt" id="cnt-minimax">0</span></button>
382
556
  <button data-view="tenrouter">10Router<span class="cnt" id="cnt-tenrouter">–</span></button>
@@ -386,7 +560,7 @@
386
560
  <section class="view active" id="view-dash">
387
561
  <div class="panel dash-panel">
388
562
  <div class="toolbar">
389
- <div class="summary"><span class="dash-title">总览</span></div>
563
+ <div class="summary"><span class="dash-title" data-i18n="总览">总览</span></div>
390
564
  <div class="actions">
391
565
  <button class="btn primary" id="btn-checkin-all"></button>
392
566
  <button class="btn" id="btn-add-dash"></button>
@@ -398,8 +572,8 @@
398
572
  </div>
399
573
  <div class="prods" id="prods"></div>
400
574
  <div class="dash-cols">
401
- <section class="pcard" data-fold="recent"><div class="pcard-title fold-head"><span>最近记录</span><span class="fold-side"><span class="hint" id="last-tick"></span><span class="fold-ico"></span></span></div><div class="fold-body"><div id="recent"></div></div></section>
402
- <section class="pcard" data-fold="attention"><div class="pcard-title fold-head"><span>需要处理</span><span class="fold-side"><span class="hint" id="att-count"></span><span class="fold-ico"></span></span></div><div class="fold-body"><div id="attention"></div></div></section>
575
+ <section class="pcard" data-fold="recent"><div class="pcard-title fold-head"><span data-i18n="最近记录">最近记录</span><span class="fold-side"><span class="hint" id="last-tick"></span><span class="fold-ico"></span></span></div><div class="fold-body"><div id="recent"></div></div></section>
576
+ <section class="pcard" data-fold="attention"><div class="pcard-title fold-head"><span data-i18n="需要处理">需要处理</span><span class="fold-side"><span class="hint" id="att-count"></span><span class="fold-ico"></span></span></div><div class="fold-body"><div id="attention"></div></div></section>
403
577
  </div>
404
578
  </section>
405
579
 
@@ -408,23 +582,22 @@
408
582
  <div class="panel">
409
583
  <div class="toolbar">
410
584
  <div class="summary">
411
- <div class="stat"><span>账号</span><strong id="s-count">0</strong></div>
585
+ <div class="stat"><span data-i18n="账号">账号</span><strong id="s-count">0</strong></div>
412
586
  <div class="divider"></div>
413
587
  <div id="s-done-wrap" style="display:contents">
414
- <div class="stat"><span>今日已领</span><strong id="s-done">0</strong></div>
588
+ <div class="stat"><span data-i18n="今日已领">今日已领</span><strong id="s-done">0</strong></div>
415
589
  <div class="divider"></div>
416
590
  </div>
417
- <div class="stat"><span id="s-credits-label">剩余积分</span><strong id="s-credits">-</strong></div>
591
+ <div class="stat"><span id="s-credits-label" data-i18n="剩余积分">剩余积分</span><strong id="s-credits">-</strong></div>
418
592
  <div class="divider" id="region-div"></div>
419
593
  <div class="seg-ctl" id="region">
420
- <button data-r="" class="active">全部</button><button data-r="intl">国际版</button><button data-r="cn">国内版</button>
594
+ <button data-r="" class="active">全部</button><button data-r="intl" data-i18n="国际版">国际版</button><button data-r="cn" data-i18n="国内版">国内版</button>
421
595
  </div>
422
596
  </div>
423
597
  <div class="actions">
424
598
  <button class="btn primary" id="btn-checkin"></button>
425
599
  <span id="z-actions" style="display:none">
426
600
  <button class="btn" id="zt-ac"></button>
427
- <button class="btn" id="zt-net"></button>
428
601
  <button class="btn" id="zt-gw"></button>
429
602
  <button class="btn" id="zt-open" style="display:none"></button>
430
603
  </span>
@@ -442,62 +615,65 @@
442
615
  </div>
443
616
  </div>
444
617
  <div class="notice" id="notice" style="display:none"></div>
445
- <div class="list" id="list"><div class="empty">加载中…</div></div>
618
+ <div class="list" id="list"><div class="empty" data-i18n="加载中…">加载中…</div></div>
446
619
  </div>
447
620
  </section>
448
621
 
449
622
  <!-- 10Router:其他供应商额度 + 用量同步 -->
450
623
  <section class="view" id="view-10r">
451
- <div class="panel" id="tr-panel"><div class="empty">加载中…</div></div>
624
+ <div class="panel" id="tr-panel"><div class="empty" data-i18n="加载中…">加载中…</div></div>
452
625
  </section>
453
626
 
454
627
  <!-- 日志卡:运行日志 + 各网关调用日志,卡内标签切换(默认跟随当前页,可手动切) -->
455
628
  <section class="pcard collapsed" id="log-card">
456
629
  <div class="pcard-title">
457
630
  <div class="seg-ctl" id="log-tabs">
458
- <button data-log-tab="run" class="active">运行日志</button>
459
- <button data-log-tab="zcode">ZCode 网关</button>
460
- <button data-log-tab="minimax">MiniMax 网关</button>
461
- <button data-log-tab="trae">Trae 网关</button>
631
+ <button data-log-tab="run" class="active" data-i18n="运行日志">运行日志</button>
632
+ <button data-log-tab="zcode" data-i18n="ZCode 网关">ZCode 网关</button>
633
+ <button data-log-tab="minimax" data-i18n="MiniMax 网关">MiniMax 网关</button>
634
+ <button data-log-tab="trae" data-i18n="Trae 网关">Trae 网关</button>
462
635
  </div>
463
- <div class="log-filter" id="log-filter">
464
- <button data-log-level="all" class="active">全部</button>
465
- <button data-log-level="warn">只看提醒</button>
636
+ <div style="display:flex;align-items:center;gap:8px">
637
+ <div class="log-filter" id="log-filter">
638
+ <button data-log-level="all" class="active">全部</button>
639
+ <button data-log-level="warn" data-i18n="只看提醒">只看提醒</button>
640
+ </div>
641
+ <button class="btn" id="btn-logs-history" data-i18n="加载历史">加载历史</button>
642
+ <button class="btn" id="btn-logs" data-i18n="展开">展开</button>
466
643
  </div>
467
- <button class="btn" id="btn-logs">展开</button>
468
644
  </div>
469
- <div class="logs" id="logs">(暂无)</div>
645
+ <div class="logs" id="logs" data-i18n="(暂无)">(暂无)</div>
470
646
  </section>
471
647
  </div>
472
648
 
473
649
  <!-- 添加账号 -->
474
650
  <div class="mask" id="m-add">
475
651
  <div class="modal">
476
- <div class="modal-title"><span>添加账号</span><button class="close" data-close>✕</button></div>
652
+ <div class="modal-title"><span data-i18n="添加账号">添加账号</span><button class="close" data-close>✕</button></div>
477
653
  <div class="tabs">
478
- <button class="tab active" data-tab="local">本机导入</button>
479
- <button class="tab" data-tab="device">浏览器登录</button>
480
- <button class="tab" data-tab="token">粘贴 Token</button>
654
+ <button class="tab active" data-tab="local" data-i18n="本机导入">本机导入</button>
655
+ <button class="tab" data-tab="device" data-i18n="浏览器登录">浏览器登录</button>
656
+ <button class="tab" data-tab="token" data-i18n="粘贴 Token">粘贴 Token</button>
481
657
  </div>
482
658
 
483
659
  <div class="pane active" data-pane="local">
484
660
  <div class="hint" id="local-hint"></div>
485
- <div class="row" style="margin-top:10px"><button class="btn primary" id="btn-scan">重新扫描</button><span class="hint" id="scan-status"></span></div>
486
- <div class="box" id="scan-result"><div class="hint">点击「重新扫描」</div></div>
661
+ <div class="row" style="margin-top:10px"><button class="btn primary" id="btn-scan" data-i18n="重新扫描">重新扫描</button><span class="hint" id="scan-status"></span></div>
662
+ <div class="box" id="scan-result"><div class="hint" data-i18n="点击「重新扫描」">点击「重新扫描」</div></div>
487
663
  </div>
488
664
 
489
665
  <div class="pane" data-pane="device">
490
666
  <div class="hint" id="dev-hint" style="margin-bottom:10px"></div>
491
667
  <div class="field">
492
- <label>登录到</label>
668
+ <label data-i18n="登录到">登录到</label>
493
669
  <select class="input" id="dev-provider">
494
670
  <option value="qoder">Qoder 国际版</option><option value="qoder-cn">Qoder 国内版</option>
495
- <option value="workbuddy">WorkBuddy / CodeBuddy 国内版</option><option value="workbuddy-intl">WorkBuddy / CodeBuddy 国际版</option>
496
- <option value="zcode-bigmodel">ZCode · BigModel(智谱)</option><option value="zcode-zai">ZCode · Z.ai(国际)</option>
671
+ <option value="workbuddy" data-i18n="WorkBuddy / CodeBuddy 国内版">WorkBuddy / CodeBuddy 国内版</option><option value="workbuddy-intl" data-i18n="WorkBuddy / CodeBuddy 国际版">WorkBuddy / CodeBuddy 国际版</option>
672
+ <option value="zcode-bigmodel" data-i18n="ZCode · BigModel(智谱)">ZCode · BigModel(智谱)</option><option value="zcode-zai" data-i18n="ZCode · Z.ai(国际)">ZCode · Z.ai(国际)</option>
497
673
  <option value="minimax">MiniMax Code</option>
498
674
  </select>
499
675
  </div>
500
- <div class="row"><button class="btn primary" id="btn-device">生成授权链接</button><button class="btn" id="dev-cancel" style="display:none">取消</button></div>
676
+ <div class="row"><button class="btn primary" id="btn-device" data-i18n="生成授权链接">生成授权链接</button><button class="btn" id="dev-cancel" style="display:none">取消</button></div>
501
677
  <div class="box" id="device-box" style="display:none">
502
678
  <div class="hint" id="dev-status"></div>
503
679
  <div class="hint" style="margin-top:6px"><a id="dev-link" href="#" target="_blank" rel="noopener"></a></div>
@@ -506,12 +682,12 @@
506
682
 
507
683
  <div class="pane" data-pane="token">
508
684
  <div class="field">
509
- <label>版本</label>
510
- <select class="input" id="f-provider"><option value="qoder">Qoder 国际版</option><option value="qoder-cn">Qoder 国内版</option><option value="workbuddy">WorkBuddy 国内版</option><option value="workbuddy-intl">WorkBuddy 国际版</option><option value="minimax">MiniMax</option></select>
685
+ <label data-i18n="版本">版本</label>
686
+ <select class="input" id="f-provider"><option value="qoder">Qoder 国际版</option><option value="qoder-cn">Qoder 国内版</option><option value="workbuddy" data-i18n="WorkBuddy 国内版">WorkBuddy 国内版</option><option value="workbuddy-intl" data-i18n="WorkBuddy 国际版">WorkBuddy 国际版</option><option value="minimax">MiniMax</option></select>
511
687
  </div>
512
- <div class="field"><label>备注名(可选)</label><input class="input" id="f-name" placeholder="留空则自动读取昵称"></div>
688
+ <div class="field"><label data-i18n="备注名(可选)">备注名(可选)</label><input class="input" id="f-name" placeholder="留空则自动读取昵称"></div>
513
689
  <div class="field"><label>Token</label><input class="input mono" id="f-token" placeholder="dt-… / jt-… 或 PAT(pt-…,自动兑换)"></div>
514
- <div class="modal-actions"><button class="btn primary" id="btn-add-token">添加</button></div>
690
+ <div class="modal-actions"><button class="btn primary" id="btn-add-token" data-i18n="添加">添加</button></div>
515
691
  </div>
516
692
  </div>
517
693
  </div>
@@ -519,47 +695,28 @@
519
695
  <!-- 导出 -->
520
696
  <div class="mask" id="m-export">
521
697
  <div class="modal sm">
522
- <div class="modal-title"><span>导出账号</span><button class="close" data-close>✕</button></div>
698
+ <div class="modal-title"><span data-i18n="导出账号">导出账号</span><button class="close" data-close>✕</button></div>
523
699
  <div class="field">
524
- <label>范围</label>
525
- <select class="input" id="ex-provider"><option value="">全部账号</option><option value="p:qoder">Qoder(全部)</option><option value="qoder">仅 Qoder 国际版</option><option value="qoder-cn">仅 Qoder 国内版</option><option value="p:workbuddy">WorkBuddy(全部)</option><option value="workbuddy">仅 WorkBuddy 国内版</option><option value="workbuddy-intl">仅 WorkBuddy 国际版</option><option value="zcode">ZCode</option><option value="p:mirasim">mirasim</option><option value="p:catpaw">妙手</option><option value="p:trae">Trae</option><option value="p:minimax">MiniMax</option></select>
700
+ <label data-i18n="范围">范围</label>
701
+ <select class="input" id="ex-provider"><option value="" data-i18n="全部账号">全部账号</option><option value="p:qoder" data-i18n="Qoder(全部)">Qoder(全部)</option><option value="qoder" data-i18n="仅 Qoder 国际版">仅 Qoder 国际版</option><option value="qoder-cn" data-i18n="仅 Qoder 国内版">仅 Qoder 国内版</option><option value="p:workbuddy" data-i18n="WorkBuddy(全部)">WorkBuddy(全部)</option><option value="workbuddy" data-i18n="仅 WorkBuddy 国内版">仅 WorkBuddy 国内版</option><option value="workbuddy-intl" data-i18n="仅 WorkBuddy 国际版">仅 WorkBuddy 国际版</option><option value="zcode">ZCode</option><option value="p:mirasim">mirasim</option><option value="p:catpaw" data-i18n="妙手">妙手</option><option value="p:trae">Trae</option><option value="p:minimax">MiniMax</option></select>
526
702
  </div>
527
703
  <div class="field">
528
- <label>加密口令(必填)</label>
704
+ <label data-i18n="加密口令(必填)">加密口令(必填)</label>
529
705
  <input class="input" id="ex-password" type="password" placeholder="至少 4 位" autocomplete="new-password">
530
706
  </div>
531
- <div class="hint">导出文件一律加密(10router 迁移格式,scrypt + AES-256-GCM),可直接在 10router 对应提供商页导入(WorkBuddy 对应 10router 的 CodeBuddy)。10router 按提供商导入,建议按单个版本导出。<b>口令不会被保存</b>,请牢记——遗失后文件无法解开、无法导入。</div>
707
+ <div class="hint">导出文件一律加密(10router 迁移格式,scrypt + AES-256-GCM),可直接在 10router 对应提供商页导入(WorkBuddy 对应 10router 的 CodeBuddy)。10router 按提供商导入,建议按单个版本导出。<b data-i18n="口令不会被保存">口令不会被保存</b>,请牢记——遗失后文件无法解开、无法导入。</div>
532
708
  <div class="notice" id="ex-session-note" style="display:none"></div>
533
- <div class="modal-actions"><button class="btn" data-close>取消</button><button class="btn primary" id="btn-do-export">导出</button></div>
709
+ <div class="modal-actions"><button class="btn" data-close>取消</button><button class="btn primary" id="btn-do-export" data-i18n="导出">导出</button></div>
534
710
  </div>
535
711
  </div>
536
712
 
537
713
  <!-- 口令输入 -->
538
714
  <div class="mask" id="m-pass">
539
715
  <div class="modal sm">
540
- <div class="modal-title"><span>输入口令</span><button class="close" data-close>✕</button></div>
541
- <div class="hint" id="pass-hint">该文件已加密,请输入导出时设置的口令。</div>
716
+ <div class="modal-title"><span data-i18n="输入口令">输入口令</span><button class="close" data-close>✕</button></div>
717
+ <div class="hint" id="pass-hint" data-i18n="该文件已加密,请输入导出时设置的口令。">该文件已加密,请输入导出时设置的口令。</div>
542
718
  <div class="field" style="margin-top:10px"><input class="input" id="pass-input" type="password"></div>
543
- <div class="modal-actions"><button class="btn" data-close>取消</button><button class="btn primary" id="btn-pass-ok">导入</button></div>
544
- </div>
545
- </div>
546
-
547
- <!-- 面板设置(右上角齿轮) -->
548
- <div class="mask" id="m-settings">
549
- <div class="modal">
550
- <div class="modal-title"><span>面板设置</span><button class="close" data-close>✕</button></div>
551
- <div class="comp-menu" id="comp-menu"></div>
552
- <div class="field" id="upd-entry" style="display:none;margin-top:12px">
553
- <label>版本与更新</label>
554
- <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap"><span class="dim" id="upd-ver"></span><button class="btn" id="btn-update">检查更新</button></div>
555
- </div>
556
- <div class="field" style="margin-top:12px;padding-top:10px;border-top:1px dashed var(--border-subtle)">
557
- <label>服务维护</label>
558
- <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
559
- <button class="btn" id="btn-restart-app">重启 CreditDaddy</button>
560
- </div>
561
- </div>
562
- <div class="modal-actions"><button class="btn" data-close>关闭</button></div>
719
+ <div class="modal-actions"><button class="btn" data-close>取消</button><button class="btn primary" id="btn-pass-ok" data-i18n="导入">导入</button></div>
563
720
  </div>
564
721
  </div>
565
722
 
@@ -572,24 +729,33 @@
572
729
  </div>
573
730
  </div>
574
731
 
732
+ <!-- 更新日志(简要版):解析随包 CHANGELOG.md,主题行 + 板块标题 -->
733
+ <div class="mask" id="m-changelog">
734
+ <div class="modal sm">
735
+ <div class="modal-title"><span>更新日志</span><button class="close" data-close>✕</button></div>
736
+ <div id="changelog-body" style="max-height:56vh;overflow-y:auto"></div>
737
+ <div class="modal-actions"><button class="btn" id="btn-changelog-full" data-i18n="GitHub 完整日志">GitHub 完整日志</button><button class="btn primary" data-close>关闭</button></div>
738
+ </div>
739
+ </div>
740
+
575
741
  <!-- 10Router 设置 -->
576
742
  <div class="mask" id="m-10r">
577
743
  <div class="modal">
578
- <div class="modal-title"><span>10Router 连接设置</span><button class="close" data-close>✕</button></div>
579
- <div class="hint">填 10Router 的访问地址和仪表盘里创建的<b>虚拟 key(sk-…)</b>。key 只保存在 CreditDaddy 守护进程的数据目录(0600),面板里只显示脱敏值。</div>
580
- <div class="field" style="margin-top:12px"><label>10Router 地址</label><input class="input mono" id="tr-endpoint" placeholder="http://127.0.0.1:20128(站点根地址,不要带 /v1)"></div>
581
- <div class="field"><label>虚拟 key</label><input class="input mono" id="tr-key" placeholder="sk-…" autocomplete="off"></div>
582
- <div class="field"><label>10Router 面板密码(可选)</label><input class="input mono" id="tr-admin-pw" type="password" placeholder="配置后「同步账号」走 OAuth 通道(带 refreshToken 自动续期);留空保持不变" autocomplete="new-password"></div>
583
- <div class="field"><label>账号同步</label>
584
- <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap"><button class="btn" id="tr-sync-accounts">同步账号到 10R</button><span class="hint">把本机 Qoder / WorkBuddy 账号推为 10Router 上游连接;zcode 待反代就绪后支持</span></div>
744
+ <div class="modal-title"><span data-i18n="10Router 连接设置">10Router 连接设置</span><button class="close" data-close>✕</button></div>
745
+ <div class="hint">填 10Router 的访问地址和仪表盘里创建的<b data-i18n="虚拟 key(sk-…)">虚拟 key(sk-…)</b>。key 只保存在 CreditDaddy 守护进程的数据目录(0600),面板里只显示脱敏值。</div>
746
+ <div class="field" style="margin-top:12px"><label data-i18n="10Router 地址">10Router 地址</label><input class="input mono" id="tr-endpoint" placeholder="http://127.0.0.1:20128(站点根地址,不要带 /v1)"></div>
747
+ <div class="field"><label data-i18n="虚拟 key">虚拟 key</label><input class="input mono" id="tr-key" placeholder="sk-…" autocomplete="off"></div>
748
+ <div class="field"><label data-i18n="10Router 面板密码(可选)">10Router 面板密码(可选)</label><input class="input mono" id="tr-admin-pw" type="password" placeholder="配置后「同步账号」走 OAuth 通道(带 refreshToken 自动续期);留空保持不变" autocomplete="new-password"></div>
749
+ <div class="field"><label data-i18n="账号同步">账号同步</label>
750
+ <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap"><button class="btn" id="tr-sync-accounts" data-i18n="同步账号到 10R">同步账号到 10R</button><span class="hint" data-i18n="把本机 Qoder / WorkBuddy 账号推为 10Router 上游连接;zcode 待反代就绪后支持">把本机 Qoder / WorkBuddy 账号推为 10Router 上游连接;zcode 待反代就绪后支持</span></div>
585
751
  </div>
586
- <div class="field"><label>显示</label>
752
+ <div class="field"><label data-i18n="显示">显示</label>
587
753
  <div><label class="check"><input type="checkbox" id="tr-hide-empty"> 「全部」视图隐藏没有额度 / 余额为 0 的供应商汇总卡</label></div>
588
754
  </div>
589
755
  <div class="hint" id="tr-test-msg"></div>
590
756
  <div class="modal-actions">
591
- <button class="btn" id="tr-disconnect" style="margin-right:auto">断开</button>
592
- <button class="btn" id="tr-test">测试连接</button>
757
+ <button class="btn" id="tr-disconnect" style="margin-right:auto" data-i18n="断开">断开</button>
758
+ <button class="btn" id="tr-test" data-i18n="测试连接">测试连接</button>
593
759
  <button class="btn primary" id="tr-save">保存</button>
594
760
  </div>
595
761
  </div>
@@ -598,20 +764,20 @@
598
764
  <!-- 10Router 用量同步 -->
599
765
  <div class="mask" id="m-trsync">
600
766
  <div class="modal sm">
601
- <div class="modal-title"><span>用量同步(同 10router-sync 插件)</span><button class="close" data-close>✕</button></div>
767
+ <div class="modal-title"><span data-i18n="用量同步(同 10router-sync 插件)">用量同步(同 10router-sync 插件)</span><button class="close" data-close>✕</button></div>
602
768
  <div class="field"><label class="check"><input type="checkbox" id="tr-auto"> 每小时自动同步本机用量到 10Router</label></div>
603
- <div class="field"><label>同步来源</label><div id="tr-sources"></div></div>
604
- <div class="hint">ZCode 只同步官方渠道;经 10Router 中转的 mirasim 调用会跳过,避免重复计数。「10Router 本机」只把实例原生产生的行标记为网关同步。10Router 按行签名去重,重复同步不会产生重复数据。</div>
769
+ <div class="field"><label data-i18n="同步来源">同步来源</label><div id="tr-sources"></div></div>
770
+ <div class="hint" data-i18n="ZCode 只同步官方渠道;经 10Router 中转的 mirasim 调用会跳过,避免重复计数。「10Router 本机」只把实例原生产生的行标记为网关同步。10Router 按行签名去重,重复同步不会产生重复数据。">ZCode 只同步官方渠道;经 10Router 中转的 mirasim 调用会跳过,避免重复计数。「10Router 本机」只把实例原生产生的行标记为网关同步。10Router 按行签名去重,重复同步不会产生重复数据。</div>
605
771
  <div class="hint" id="tr-sync-status" style="margin-top:8px"></div>
606
- <div class="modal-actions"><button class="btn" style="margin-right:auto" id="tr-sync-now">立即同步一次</button><button class="btn" data-close>取消</button><button class="btn primary" id="tr-sync-save">保存</button></div>
772
+ <div class="modal-actions"><button class="btn" style="margin-right:auto" id="tr-sync-now" data-i18n="立即同步一次">立即同步一次</button><button class="btn" data-close>取消</button><button class="btn primary" id="tr-sync-save">保存</button></div>
607
773
  </div>
608
774
  </div>
609
775
 
610
776
  <!-- ZCode 活动领取 -->
611
777
  <div class="mask" id="m-claim">
612
778
  <div class="modal">
613
- <div class="modal-title"><span>活动领取</span><button class="close" data-close>✕</button></div>
614
- <div class="hint">从 ZCode 官方活动接口领取(billing/preview → billing/claim)。领取时通常静默通过人机验证;触发风控时会弹出验证码,按提示完成即可。</div>
779
+ <div class="modal-title"><span data-i18n="活动领取">活动领取</span><button class="close" data-close>✕</button></div>
780
+ <div class="hint" data-i18n="从 ZCode 官方活动接口领取(billing/preview → billing/claim)。领取时通常静默通过人机验证;触发风控时会弹出验证码,按提示完成即可。">从 ZCode 官方活动接口领取(billing/preview → billing/claim)。领取时通常静默通过人机验证;触发风控时会弹出验证码,按提示完成即可。</div>
615
781
  <div id="claim-body" style="margin-top:10px"></div>
616
782
  </div>
617
783
  </div>
@@ -622,49 +788,31 @@
622
788
  <div class="modal-title"><span id="dlg-title"></span><button class="close" data-close>✕</button></div>
623
789
  <div class="hint" id="dlg-msg" style="white-space:pre-wrap"></div>
624
790
  <div class="field" id="dlg-field" style="margin-top:10px"><input class="input" id="dlg-input"></div>
625
- <div class="modal-actions"><button class="btn" data-close>取消</button><button class="btn primary" id="dlg-ok">确定</button></div>
791
+ <div class="modal-actions"><button class="btn" data-close>取消</button><button class="btn primary" id="dlg-ok" data-i18n="确定">确定</button></div>
626
792
  </div>
627
793
  </div>
628
794
 
629
795
  <!-- ZCode 自动领取计划 -->
630
796
  <div class="mask" id="m-zac">
631
797
  <div class="modal sm">
632
- <div class="modal-title"><span>ZCode 自动领取计划</span><button class="close" data-close>✕</button></div>
798
+ <div class="modal-title"><span data-i18n="ZCode 自动领取计划">ZCode 自动领取计划</span><button class="close" data-close>✕</button></div>
633
799
  <div class="field">
634
- <label>轮询周期(分钟,1–720)</label>
800
+ <label data-i18n="轮询周期(分钟,1–720)">轮询周期(分钟,1–720)</label>
635
801
  <input class="input" id="zac-interval" type="number" min="1" max="720" step="1">
636
802
  </div>
637
803
  <div class="field">
638
- <label>单次运行时长(分钟,0 = 一直运行直到手动关闭)</label>
804
+ <label data-i18n="单次运行时长(分钟,0 = 一直运行直到手动关闭)">单次运行时长(分钟,0 = 一直运行直到手动关闭)</label>
639
805
  <input class="input" id="zac-window" type="number" min="0" max="1440" step="5">
640
806
  </div>
641
- <div class="hint">开启后按此周期检查活动资格并自动领取(首次检查在开启 2 分钟后开始)。活动包由服务端在每日 0 点左右刷新放出——若运行时段没有覆盖 0 点,会错过当次领取,建议把运行时长设为 0,或让时段跨过 0 点。需要验证码时无法自动通过,卡片上会提示手动领取。</div>
642
- <div class="modal-actions"><button class="btn" id="zac-off" style="display:none">停用</button><button class="btn" data-close>取消</button><button class="btn primary" id="zac-ok">保存并启用</button></div>
643
- </div>
644
- </div>
645
-
646
- <!-- ZCode 网络出口 -->
647
- <div class="mask" id="m-znet">
648
- <div class="modal sm">
649
- <div class="modal-title"><span>ZCode 网络出口</span><button class="close" data-close>✕</button></div>
650
- <div class="field">
651
- <label>上游接口访问顺序(失败 / 被拦时自动回退另一条)</label>
652
- <label class="check"><input type="radio" name="znet-order" id="znet-direct"> 直连优先(代理兜底)</label>
653
- <label class="check"><input type="radio" name="znet-order" id="znet-proxy"> 代理优先(直连兜底)</label>
654
- </div>
655
- <div class="field">
656
- <label>出口代理(http://host:port,支持口令 http://user:pass@host:port)</label>
657
- <input class="input" id="znet-url" placeholder="http://192.168.31.101:7890">
658
- </div>
659
- <div class="hint">留空 = 清除已保存的代理,退回 HTTPS_PROXY 环境变量;取消 = 不改动。当前代理只显示脱敏后的地址。</div>
660
- <div class="modal-actions"><button class="btn" data-close>取消</button><button class="btn primary" id="znet-ok">保存</button></div>
807
+ <div class="hint" data-i18n="开启后按此周期检查活动资格并自动领取(首次检查在开启 2 分钟后开始)。活动包由服务端在每日 0 点左右刷新放出——若运行时段没有覆盖 0 点,会错过当次领取,建议把运行时长设为 0,或让时段跨过 0 点。需要验证码时无法自动通过,卡片上会提示手动领取。">开启后按此周期检查活动资格并自动领取(首次检查在开启 2 分钟后开始)。活动包由服务端在每日 0 点左右刷新放出——若运行时段没有覆盖 0 点,会错过当次领取,建议把运行时长设为 0,或让时段跨过 0 点。需要验证码时无法自动通过,卡片上会提示手动领取。</div>
808
+ <div class="modal-actions"><button class="btn" id="zac-off" style="display:none" data-i18n="停用">停用</button><button class="btn" data-close>取消</button><button class="btn primary" id="zac-ok" data-i18n="保存并启用">保存并启用</button></div>
661
809
  </div>
662
810
  </div>
663
811
 
664
812
  <!-- ZCode 体验包接口 -->
665
813
  <div class="mask" id="m-zgw">
666
814
  <div class="modal sm">
667
- <div class="modal-title"><span>ZCode 体验包接口</span><button class="close" data-close>✕</button></div>
815
+ <div class="modal-title"><span data-i18n="ZCode 体验包接口">ZCode 体验包接口</span><button class="close" data-close>✕</button></div>
668
816
  <div class="hint" id="zgw-url" style="margin-bottom:10px"></div>
669
817
  <div class="field">
670
818
  <label class="check"><input type="checkbox" id="zgw-on"> 启用体验包接口(/gateway/zcode/v1/messages)</label>
@@ -682,7 +830,7 @@
682
830
  <!-- MiniMax 本地接口 -->
683
831
  <div class="mask" id="m-mgw">
684
832
  <div class="modal sm">
685
- <div class="modal-title"><span>MiniMax 本地接口</span><button class="close" data-close>✕</button></div>
833
+ <div class="modal-title"><span data-i18n="MiniMax 本地接口">MiniMax 本地接口</span><button class="close" data-close>✕</button></div>
686
834
  <div class="hint" id="mgw-url" style="margin-bottom:10px"></div>
687
835
  <div class="field">
688
836
  <label class="check"><input type="checkbox" id="mgw-on"> 启用本地接口(/gateway/minimax/v1/messages)</label>
@@ -700,7 +848,7 @@
700
848
  <!-- Trae 本地接口 -->
701
849
  <div class="mask" id="m-tgw">
702
850
  <div class="modal sm">
703
- <div class="modal-title"><span>Trae 本地接口</span><button class="close" data-close>✕</button></div>
851
+ <div class="modal-title"><span data-i18n="Trae 本地接口">Trae 本地接口</span><button class="close" data-close>✕</button></div>
704
852
  <div class="hint" id="tgw-url" style="margin-bottom:10px"></div>
705
853
  <div class="field">
706
854
  <label class="check"><input type="checkbox" id="tgw-on"> 启用本地接口(/gateway/trae/v1/messages)</label>
@@ -710,7 +858,7 @@
710
858
  <label>IP 白名单(每行一个,支持 192.168.31.* 通配)</label>
711
859
  <textarea class="input" id="tgw-allow" rows="4" placeholder="192.168.31.101"></textarea>
712
860
  </div>
713
- <div class="hint">开启后,Trae SOLO 积分与模型能力(Doubao-Seed / GLM-5.1 / Kimi-K2.6 / DeepSeek-V4 等 12 款模型)变成本机的一个原生 Anthropic Messages 补全接口——在 10Router 或 Claude Code 等工具中建一个 anthropic-compatible 节点指向本机 <code>/gateway/trae/v1/messages</code> 即可调用。支持多账号自动加权轮转、429 限流自动冷却 5 分钟、流式增量与完整思考过程(thinking_delta)。白名单内的机器<b>免密直连</b>;本机回环(127.0.0.1)始终放行。</div>
861
+ <div class="hint">开启后,Trae SOLO 积分与模型能力(官方目录持续更新,网关透传任意合法模型 ID——如 GLM-5.3 / DeepSeek-V4.1-Flash / MiMo-V2.6 / Kimi-K3)变成本机的一个原生 Anthropic Messages 补全接口——在 10Router 或 Claude Code 等工具中建一个 anthropic-compatible 节点指向本机 <code>/gateway/trae/v1/messages</code> 即可调用。支持多账号自动加权轮转、429 限流自动冷却 5 分钟、流式增量与完整思考过程(thinking_delta)。白名单内的机器<b>免密直连</b>;本机回环(127.0.0.1)始终放行。</div>
714
862
  <div class="modal-actions"><button class="btn" data-close>取消</button><button class="btn primary" id="tgw-ok">保存</button></div>
715
863
  </div>
716
864
  </div>
@@ -721,6 +869,33 @@
721
869
  <script>
722
870
  'use strict';
723
871
 
872
+ let I18N_DICT = {};
873
+ function T(text, vars) {
874
+ const s = I18N_DICT[text] ?? text;
875
+ if (!vars) return s;
876
+ return s.replace(/\{(\w+)\}/g, (m, k) => (k in vars ? String(vars[k]) : m));
877
+ }
878
+
879
+ // 语言:顶栏菜单的五档分段与面板设置弹窗的下拉共用一个真相源。
880
+ // 切换必须先落盘(settings.locale 由守护进程读取,日志也按它实时重渲染),再刷新页面。
881
+ async function setLocale(locale) {
882
+ const busy = $('locale-busy');
883
+ if (busy) busy.hidden = false;
884
+ try {
885
+ await api('/api/settings', { method: 'PUT', json: { locale } });
886
+ location.reload();
887
+ } catch (e) { if (busy) busy.hidden = true; /* 失败时保留原选项 */ }
888
+ }
889
+ function renderLocale(cur) {
890
+ if (cur) state.locale = cur;
891
+ const l = state.locale || 'zh-CN';
892
+ const seg = $('locale-seg');
893
+ if (seg) for (const btn of seg.querySelectorAll('[data-locale]')) btn.classList.toggle('on', btn.dataset.locale === l);
894
+ const sel = $('set-locale');
895
+ if (sel && sel.value !== l) sel.value = l;
896
+ }
897
+
898
+
724
899
  // ── 图标(Lucide 风格,currentColor) ──
725
900
  const svg = (d) => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' + d + '</svg>';
726
901
  const I = {
@@ -737,14 +912,24 @@ const I = {
737
912
  credit: svg('<path d="M12 3l1.9 5.1L19 10l-5.1 1.9L12 17l-1.9-5.1L5 10l5.1-1.9z"/><path d="M19 15l.8 2.2L22 18l-2.2.8L19 21l-.8-2.2L16 18l2.2-.8z"/>'),
738
913
  gift: svg('<rect x="3" y="8" width="18" height="4"/><path d="M12 8v13"/><path d="M19 12v7a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-7"/><path d="M7.5 8a2.5 2.5 0 0 1 0-5C11 3 12 8 12 8s1-5 4.5-5a2.5 2.5 0 0 1 0 5"/>'),
739
914
  tune: svg('<path d="M4 21v-7M4 10V3M12 21v-9M12 8V3M20 21v-5M20 12V3"/><path d="M1 14h6M9 8h6M17 16h6"/>'),
915
+ // 齿轮:面板设置(比「推子」更通用地表达“设置”)
916
+ settings: svg('<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/>'),
740
917
  chevUp: svg('<path d="M18 15l-6-6-6 6"/>'),
918
+ grip: svg('<circle cx="9" cy="5" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="9" cy="19" r="1"/><circle cx="15" cy="5" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="15" cy="19" r="1"/>'),
741
919
  chevDown: svg('<path d="M6 9l6 6 6-6"/>'),
742
920
  chevRight: svg('<path d="M9 18l6-6-6-6"/>'),
743
921
  check: svg('<path d="M20 6L9 17l-5-5"/>'),
922
+ grid: svg('<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/>'),
923
+ book: svg('<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/>'),
924
+ power: svg('<path d="M12 2v10"/><path d="M18.4 6.6a9 9 0 1 1-12.77.04"/>'),
925
+ monitor: svg('<rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/>'),
926
+ clock: svg('<circle cx="12" cy="12" r="9"/><path d="M12 7.5v5l3 2"/>'),
927
+ msg: svg('<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>'),
928
+ heart: svg('<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/>'),
744
929
  };
745
930
 
746
931
  const $ = (id) => document.getElementById(id);
747
- const state = { accounts: [], quotas: {}, status: null, busy: {}, view: 'dash', region: '', addProduct: '', logTab: 'run', logLevel: 'all' };
932
+ const state = { accounts: [], quotas: {}, status: null, busy: {}, view: 'dash', region: '', addProduct: '', logTab: 'run', logLevel: 'all', locale: 'zh-CN' };
748
933
  const VIEWS = ['dash', 'qoder', 'workbuddy', 'zcode', 'mirasim', 'catpaw', 'trae', 'minimax', 'tenrouter'];
749
934
  // 10Router 集成:config 来自 /api/tenrouter,quotas 来自 /api/tenrouter/quotas
750
935
  const tr = { config: null, quotas: null, error: null, loading: false, syncing: false };
@@ -757,7 +942,7 @@ const PRODUCTS = [
757
942
  { id: 'trae', label: 'Trae' },
758
943
  { id: 'minimax', label: 'MiniMax' },
759
944
  ];
760
- const prodLabel = (id) => (PRODUCTS.find((x) => x.id === id) || { label: id }).label;
945
+ const prodLabel = (id) => T((PRODUCTS.find((x) => x.id === id) || { label: id }).label);
761
946
 
762
947
  // ── 全局组件开关:不用某个产品 / 10Router 时整块收起(偏好存浏览器本地) ──
763
948
  const COMPONENTS = [
@@ -906,8 +1091,8 @@ const provBadges = (p, regionOnly, flavor) => {
906
1091
  : flavor === 'zai' ? '<span class="badge intl">Z.ai</span>'
907
1092
  : '<span class="badge cn">BigModel</span>')
908
1093
  : '';
909
- return (regionOnly ? '' : '<span class="badge prod ' + v.prod + '">' + esc(v.prodLabel) + '</span>')
910
- + (v.region ? '<span class="badge ' + v.cls + '">' + v.region + '</span>' : '')
1094
+ return (regionOnly ? '' : '<span class="badge prod ' + v.prod + '">' + esc(T(v.prodLabel)) + '</span>')
1095
+ + (v.region ? '<span class="badge ' + v.cls + '">' + T(v.region) + '</span>' : '')
911
1096
  + zBadge;
912
1097
  };
913
1098
 
@@ -935,7 +1120,7 @@ let toastTimer = null;
935
1120
  const armed = {};
936
1121
  function toast(msg, ok) {
937
1122
  const t = $('toast');
938
- t.textContent = msg;
1123
+ t.textContent = T(msg);
939
1124
  t.classList.toggle('err', ok === false);
940
1125
  t.classList.add('show');
941
1126
  clearTimeout(toastTimer);
@@ -958,11 +1143,12 @@ let keyPrompt = null;
958
1143
  /** 弹一次密码输入框(并发的请求共用同一个);返回 true = 已输入 */
959
1144
  function promptKey(invalid) {
960
1145
  if (!keyPrompt) {
961
- keyPrompt = uiPrompt('输入面板访问密码', '', {
962
- type: 'password', okText: '进入',
963
- message: (invalid ? '密码不正确,请重新输入。\n' : '')
964
- + '面板访问密码在右上角「设置」里管理。\n'
965
- + 'fnOS:安装向导设置的密码(忘了可在 fnOS「应用设置」里重设)。\n命令行部署:环境变量 CREDITDADDY_PASSWORD 的值。',
1146
+ hideBootBusy(); // 需要密码说明守护已应答:首屏 loading 卡让位给密码门(否则 z95 卡片盖住 z50 密码窗,看着就像卡死)
1147
+ keyPrompt = uiPrompt(T('输入面板访问密码'), '', {
1148
+ type: 'password', okText: T('进入'),
1149
+ message: (invalid ? T('密码不正确,请重新输入。\n') : '')
1150
+ + T('面板访问密码在右上角「设置」里管理。\n')
1151
+ + T('fnOS:安装向导设置的密码(忘了可在 fnOS「应用设置」里重设)。\n命令行部署:环境变量 CREDITDADDY_PASSWORD 的值。'),
966
1152
  }).then((k) => { keyPrompt = null; if (k === null || !k.trim()) return false; setKey(k.trim()); return true; });
967
1153
  }
968
1154
  return keyPrompt;
@@ -996,14 +1182,85 @@ async function loadSettings() {
996
1182
  try {
997
1183
  const d = await api('/api/settings');
998
1184
  state.sec = { enabled: Boolean(d.panelKeyEnabled), loaded: true };
1185
+ state.wbActivity = d.workbuddyActivity !== false; // WB 国际版活跃请求总开关(默认开)
999
1186
  if (!d.panelKeyEnabled) setKey(''); // 关闭后清掉本地缓存的旧密钥
1187
+ if (d.locale) renderLocale(d.locale);
1000
1188
  } catch (e) { /* 401(密码开启但未输入)等状态由 promptKey 流程处理 */ }
1001
1189
  }
1002
1190
 
1191
+
1192
+ async function applyDictFromServer() {
1193
+ try {
1194
+ const { dict, locale } = await api('/api/i18n');
1195
+ I18N_DICT = dict || {};
1196
+ renderLocale(locale); // 语言分段的高亮态以服务端为准(比 /api/settings 更早可用)
1197
+ document.querySelectorAll('[data-i18n]').forEach((el) => {
1198
+ const src = el.getAttribute('data-i18n');
1199
+ if (dict[src]) el.textContent = dict[src];
1200
+ });
1201
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
1202
+ for (let n = walker.nextNode(); n; n = walker.nextNode()) {
1203
+ const key = n.nodeValue.trim();
1204
+ if (key && dict[key] && !n.parentElement?.closest('script,style')) n.nodeValue = n.nodeValue.replace(key, dict[key]);
1205
+ }
1206
+ document.querySelectorAll('[title],[placeholder]').forEach((el) => {
1207
+ for (const attr of ['title', 'placeholder']) {
1208
+ const v = el.getAttribute(attr);
1209
+ if (v && dict[v]) el.setAttribute(attr, dict[v]);
1210
+ }
1211
+ });
1212
+ } catch (e) { /* 字典不可用时保留中文原文 */ }
1213
+ }
1214
+
1215
+ document.getElementById('set-locale')?.addEventListener('change', (ev) => setLocale(ev.target.value));
1216
+
1217
+ // ── 通用设置:全局自动签到(开关 + 频率,落盘立刻重调度)、日志默认显示方式(本机偏好)──
1218
+ const wbActOn = () => state.wbActivity !== false;
1219
+ $('cfg-wb-activity').addEventListener('change', async (e) => {
1220
+ try {
1221
+ await api('/api/settings', { method: 'PUT', json: { workbuddyActivity: e.target.checked } });
1222
+ await loadSettings();
1223
+ render(); renderStatus();
1224
+ toast(T('已保存'), true);
1225
+ } catch (err) { toast(err.message, false); }
1226
+ });
1227
+ function fillAutoCheckin() {
1228
+ const sc = state.status && state.status.scheduler;
1229
+ if (!sc) return;
1230
+ $('cfg-auto-checkin').checked = sc.enabled !== false;
1231
+ $('cfg-checkin-interval').value = String(sc.intervalMin || 120);
1232
+ }
1233
+ function logsDisplayDefault() {
1234
+ try { return localStorage.getItem('qd_logs_expanded') !== '0' ? 'expanded' : 'collapsed'; } catch { return 'expanded'; }
1235
+ }
1236
+ function applyLogsDisplay(mode) {
1237
+ const collapsed = mode !== 'expanded';
1238
+ $('log-card').classList.toggle('collapsed', collapsed);
1239
+ $('btn-logs').textContent = collapsed ? T('展开') : T('收起');
1240
+ try { localStorage.setItem('qd_logs_expanded', collapsed ? '0' : '1'); } catch {}
1241
+ }
1242
+ $('cfg-auto-checkin').addEventListener('change', async (e) => {
1243
+ try {
1244
+ await api('/api/settings', { method: 'PUT', json: { autoCheckin: e.target.checked } });
1245
+ await loadStatus();
1246
+ toast(T('已保存'), true);
1247
+ } catch (err) { toast(err.message, false); fillAutoCheckin(); }
1248
+ });
1249
+ $('cfg-checkin-interval').addEventListener('change', async (e) => {
1250
+ try {
1251
+ await api('/api/settings', { method: 'PUT', json: { checkinIntervalMin: Number(e.target.value) } });
1252
+ await loadStatus();
1253
+ toast(T('已保存'), true);
1254
+ } catch (err) { toast(err.message, false); fillAutoCheckin(); }
1255
+ });
1256
+ $('cfg-logs-display').addEventListener('change', (e) => applyLogsDisplay(e.target.value));
1257
+ applyLogsDisplay(logsDisplayDefault());
1258
+
1003
1259
  // ── 主题 ──
1260
+ // 应用的是「解析后的」亮/暗;用户选的三态(跟随系统 / 亮色 / 暗色)另存于 qd_theme,
1261
+ // 由 renderThemeMode() 反映到菜单里的分段控件(主题只有那一个入口)。
1004
1262
  function applyTheme(t) {
1005
1263
  document.documentElement.dataset.theme = t;
1006
- $('btn-theme').innerHTML = t === 'dark' ? I.sun : I.moon;
1007
1264
  }
1008
1265
  function initTheme() {
1009
1266
  let t = null;
@@ -1022,10 +1279,120 @@ function initTheme() {
1022
1279
  if (mq.addEventListener) mq.addEventListener('change', onSysTheme);
1023
1280
  else if (mq.addListener) mq.addListener(onSysTheme); // 旧 Safari
1024
1281
  }
1025
- $('btn-theme').onclick = () => {
1026
- const n = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark';
1027
- applyTheme(n);
1028
- try { localStorage.setItem('qd_theme', n); } catch (e) {}
1282
+ // 右上角菜单按钮:打开聚合操作弹窗(openModal/遮罩点击/ESC/✕ 由通用弹窗机制处理)
1283
+ $('btn-header-menu').innerHTML = I.grid;
1284
+ const hmenuPop = $('hmenu-pop');
1285
+ const closeHmenu = () => hmenuPop.classList.remove('show');
1286
+ $('btn-header-menu').addEventListener('click', (e) => {
1287
+ e.stopPropagation();
1288
+ closeModal('m-settings'); // 设置弹层与聚合菜单同处一处,开一个收一个
1289
+ const willOpen = !hmenuPop.classList.contains('show');
1290
+ if (willOpen) renderThemeMode(); // 打开时同步一次当前主题态(系统可能已变过)
1291
+ hmenuPop.classList.toggle('show', willOpen);
1292
+ });
1293
+ // 点菜单外任意处 / ESC 收起下拉
1294
+ document.addEventListener('click', (e) => { if (!e.target.closest('.hmenu')) closeHmenu(); });
1295
+ document.addEventListener('click', (e) => {
1296
+ const st = $('m-settings');
1297
+ // target 已脱离 DOM = 点击者所在区域刚被重渲染(如组件显隐切换重建 comp-menu),不算「点外面」
1298
+ if (st && st.classList.contains('show') && e.target.isConnected && !e.target.closest('#m-settings') && !e.target.closest('.head-setpop')) closeModal('m-settings');
1299
+ });
1300
+ window.addEventListener('scroll', closeHmenu, true);
1301
+ // 菜单项图标:与头部按钮同款 Lucide 描边(静态文字前插图标)
1302
+ const withIcon = (id, icon) => { const b = $(id); if (b) b.innerHTML = icon + b.textContent; };
1303
+ withIcon('hm-update', I.refresh);
1304
+ withIcon('changelog-brief-btn', I.book);
1305
+ withIcon('hm-home', I.github); // 由头部独立图标收进「关于」分区
1306
+ withIcon('hm-restart', I.power);
1307
+ withIcon('feedback-btn', I.msg);
1308
+ withIcon('donate-btn', I.heart);
1309
+ // 检查更新:桌面版走应用内更新;纯浏览器(无 updater 桥)也保留入口——点开给「去 Releases 手动下载」的承接。
1310
+ $('hm-update').onclick = () => { closeHmenu(); openUpdateModal(true); };
1311
+ $('hm-restart').onclick = async () => {
1312
+ closeHmenu();
1313
+ const ok = await uiConfirm(T('确定要重启 CreditDaddy 服务吗?正在进行的任务可能会中断。'), T('重启服务'), T('重启'));
1314
+ if (ok) restartCreditDaddy();
1315
+ };
1316
+ $('changelog-brief-btn').onclick = () => { closeHmenu(); openChangelogModal(); };
1317
+ // 主题:菜单里的三态分段控件(跟随系统 / 亮色 / 暗色),当前态高亮。
1318
+ // 「跟随系统」= 清掉手动选择,matchMedia 实时跟跑;手动选过亮/暗则固定不再跟系统
1319
+ // (与 initTheme 里 onSysTheme 判定同一存储键 qd_theme)。菜单保持展开,连点可连续比较观感。
1320
+ const THEME_MODES = [
1321
+ ['auto', T('跟随系统'), I.monitor],
1322
+ ['light', T('亮色'), I.sun],
1323
+ ['dark', T('暗色'), I.moon],
1324
+ ];
1325
+ function currentThemeMode() {
1326
+ try { const t = localStorage.getItem('qd_theme'); return t === 'light' || t === 'dark' ? t : 'auto'; } catch (e) { return 'auto'; }
1327
+ }
1328
+ function applyThemeMode(mode) {
1329
+ try {
1330
+ if (mode === 'auto') localStorage.removeItem('qd_theme');
1331
+ else localStorage.setItem('qd_theme', mode);
1332
+ } catch (e) {}
1333
+ const sysDark = window.matchMedia && matchMedia('(prefers-color-scheme: dark)').matches;
1334
+ applyTheme(mode === 'auto' ? (sysDark ? 'dark' : 'light') : mode);
1335
+ renderThemeMode();
1336
+ }
1337
+ // 分段控件:注入图标 + 绑定一次点击;renderThemeMode 只切换 .on,不重建节点(保住焦点)
1338
+ // 与菜单项一样,点击不收起菜单——切主题本来就该能连着试。
1339
+ function renderThemeMode() {
1340
+ const seg = $('theme-seg');
1341
+ if (seg) {
1342
+ const cur = currentThemeMode();
1343
+ for (const btn of seg.querySelectorAll('[data-theme-mode]')) {
1344
+ const mode = btn.dataset.themeMode;
1345
+ if (!btn.dataset.ready) {
1346
+ const def = THEME_MODES.find((m) => m[0] === mode);
1347
+ btn.innerHTML = (def ? def[2] : '') + btn.textContent.trim();
1348
+ btn.dataset.ready = '1';
1349
+ }
1350
+ btn.classList.toggle('on', mode === cur);
1351
+ }
1352
+ }
1353
+ renderThemeButton(); // 所有改主题的通路(头部按钮 / 菜单分段 / 系统主题变化兜底)统一同步图标
1354
+ }
1355
+ // 头部独立主题按钮:只做亮↔暗一键反转(每次点击必有视觉反馈)。
1356
+ // 「跟随系统」不进循环——跟随系统与系统主题一致时,点那一档观感=没反应(用户反馈);
1357
+ // 需要跟随系统走菜单里的三态分段控件。
1358
+ const themeBtn = $('btn-theme');
1359
+ function resolvedTheme() { return document.documentElement.dataset.theme === 'dark' ? 'dark' : 'light'; }
1360
+ function renderThemeButton() {
1361
+ if (!themeBtn) return;
1362
+ const cur = resolvedTheme();
1363
+ themeBtn.innerHTML = cur === 'dark' ? I.moon : I.sun;
1364
+ themeBtn.title = T('主题:{mode}(点击切换)', { mode: T(cur === 'dark' ? '暗色' : '亮色') });
1365
+ }
1366
+ themeBtn.addEventListener('click', () => applyThemeMode(resolvedTheme() === 'dark' ? 'light' : 'dark'));
1367
+ // 字典到达后需要重渲染主题按钮(标题/图标):暴露出脚本级入口供 boot 调
1368
+ window.__syncTheme = renderThemeMode;
1369
+ renderThemeMode();
1370
+ $('theme-seg').addEventListener('click', (e) => {
1371
+ const btn = e.target.closest('[data-theme-mode]');
1372
+ if (btn) applyThemeMode(btn.dataset.themeMode);
1373
+ });
1374
+ renderLocale();
1375
+ $('locale-seg').addEventListener('click', (e) => {
1376
+ const btn = e.target.closest('[data-locale]');
1377
+ if (btn && btn.dataset.locale !== state.locale) setLocale(btn.dataset.locale);
1378
+ });
1379
+
1380
+ // 建议反馈:直达 GitHub issue 模板选择页(🐞 Bug 反馈 / ✨ 功能建议 / 🔒 安全问题,
1381
+ // 表单已含版本 / 安装渠道 / 平台字段,与 10Router 仓库同一套结构)
1382
+ $('feedback-btn').onclick = () => {
1383
+ closeHmenu();
1384
+ const url = 'https://github.com/techysy/CreditDaddy/issues/new/choose';
1385
+ if (window.creditdaddy && typeof window.creditdaddy.shellOpen === 'function')
1386
+ window.creditdaddy.shellOpen(url);
1387
+ else window.open(url, '_blank');
1388
+ };
1389
+
1390
+ // 捐赠(桌面版走 IPC 桥;网页直接开浏览器)
1391
+ $('donate-btn').onclick = () => {
1392
+ closeHmenu();
1393
+ if (window.creditdaddy && typeof window.creditdaddy.shellOpen === 'function')
1394
+ window.creditdaddy.shellOpen('https://github.com/sponsors/techysy');
1395
+ else window.open('https://github.com/sponsors/techysy', '_blank');
1029
1396
  };
1030
1397
  }
1031
1398
 
@@ -1039,17 +1406,17 @@ function statusOf(a) {
1039
1406
  const done = state.status && state.status.todayDone && state.status.todayDone[a.id] === today;
1040
1407
  const r = a.lastResult;
1041
1408
  const fresh = r && r.at && state.status && (Date.now() - new Date(r.at).getTime() < 26 * 3600e3);
1042
- if (r && fresh && r.status === 'limited' && done) return { cls: 'limited', text: '本机已领', msg: r.message };
1409
+ if (r && fresh && r.status === 'limited' && done) return { cls: 'limited', text: T('本机已领'), msg: r.message };
1043
1410
  if (done) {
1044
- if (intl) return { cls: 'ok', text: '今日活跃', msg: r && r.message ? r.message : '' };
1411
+ if (intl) return { cls: 'ok', text: T('今日活跃'), msg: r && r.message ? r.message : '' };
1045
1412
  const amt = r && r.status === 'checked-in' && fresh && r.amount ? ' +' + fmtNum(r.amount) : '';
1046
- const streak = r && !intl && r.streakDays ? ' · 连续 ' + r.streakDays + ' 天' : '';
1047
- return { cls: 'ok', text: '今日已领' + amt + streak, msg: r && r.message && !['checked-in', 'already'].includes(r.status) ? r.message : '' };
1413
+ const streak = r && !intl && r.streakDays ? T(' · 连续 ') + r.streakDays + T(' 天') : '';
1414
+ return { cls: 'ok', text: T('今日已领') + amt + streak, msg: r && r.message && !['checked-in', 'already'].includes(r.status) ? r.message : '' };
1048
1415
  }
1049
- const failText = intl ? '活跃失败' : '领取失败';
1416
+ const failText = intl ? T('活跃失败') : T('领取失败');
1050
1417
  if (r && fresh && r.status === 'failed') return { cls: 'fail', text: failText, msg: r.message, err: true };
1051
- if (r && fresh && r.status === 'no-activity') return { cls: 'pending', text: intl ? '待活跃' : '待领取', msg: r.message };
1052
- return { cls: 'pending', text: intl ? '未活跃' : '未领取', msg: '' };
1418
+ if (r && fresh && r.status === 'no-activity') return { cls: 'pending', text: intl ? T('待活跃') : T('待领取'), msg: r.message };
1419
+ return { cls: 'pending', text: intl ? T('未活跃') : T('未领取'), msg: '' };
1053
1420
  }
1054
1421
 
1055
1422
  function expiryCell(a) {
@@ -1058,11 +1425,11 @@ function expiryCell(a) {
1058
1425
  if (q && q.planExpiresAt) {
1059
1426
  const msPlan = new Date(q.planExpiresAt).getTime() - Date.now();
1060
1427
  const planName = q.plan ? esc(q.plan) + ' ' : '';
1061
- if (msPlan <= 0) return { label: planName + '已到期', warn: true, title: '套餐已到期' };
1428
+ if (msPlan <= 0) return { label: planName + T('已到期'), warn: true, title: T('套餐已到期') };
1062
1429
  return {
1063
1430
  label: planName + fmtTime(q.planExpiresAt).slice(0, 10),
1064
1431
  warn: msPlan < 3 * 86400e3,
1065
- title: '套餐有效期至 ' + fmtTime(q.planExpiresAt),
1432
+ title: T('套餐有效期至 ') + fmtTime(q.planExpiresAt),
1066
1433
  isPlan: true,
1067
1434
  };
1068
1435
  }
@@ -1070,17 +1437,17 @@ function expiryCell(a) {
1070
1437
  // 具有 refreshToken 的账号会自动在后台续期,短期 exp 属于正常轮换,不应显示刺眼红字误导用户
1071
1438
  if (a.hasRefreshToken) {
1072
1439
  return {
1073
- label: '凭据有效 · 自动续期',
1440
+ label: T('凭据有效 · 自动续期'),
1074
1441
  warn: false,
1075
- title: a.expiresAt ? '本次登录凭据有效期至 ' + fmtTime(a.expiresAt) + '(过期将自动通过 refreshToken 刷新续期,无需手动干预)' : '支持自动刷新续期',
1442
+ title: a.expiresAt ? T('本次登录凭据有效期至 ') + fmtTime(a.expiresAt) + T('(过期将自动通过 refreshToken 刷新续期,无需手动干预)') : T('支持自动刷新续期'),
1076
1443
  isAuto: true,
1077
1444
  };
1078
1445
  }
1079
1446
 
1080
- if (!a.expiresAt) return { label: a.isPat ? 'PAT 长期有效' : '—', warn: false };
1447
+ if (!a.expiresAt) return { label: a.isPat ? T('PAT 长期有效') : '—', warn: false };
1081
1448
  const ms = new Date(a.expiresAt).getTime() - Date.now();
1082
- if (ms <= 0) return { label: '已过期', warn: true, title: '登录凭据已过期' };
1083
- return { label: fmtTime(a.expiresAt), warn: ms < 48 * 3600e3, title: '有效期至 ' + fmtTime(a.expiresAt) };
1449
+ if (ms <= 0) return { label: T('已过期'), warn: true, title: T('登录凭据已过期') };
1450
+ return { label: fmtTime(a.expiresAt), warn: ms < 48 * 3600e3, title: T('有效期至 ') + fmtTime(a.expiresAt) };
1084
1451
  }
1085
1452
 
1086
1453
  // 通用相对重置标签:「3 天 后重置」「45 分钟 后到期」;一小时内标红,悬停看绝对时间
@@ -1089,11 +1456,11 @@ function relReset(q) {
1089
1456
  if (!at) return null;
1090
1457
  const ms = new Date(at).getTime() - Date.now();
1091
1458
  if (!Number.isFinite(ms)) return null;
1092
- if (ms <= 0) return { text: '即将' + (q.recurring === false ? '到期' : '重置'), soon: true };
1459
+ if (ms <= 0) return { text: T('即将') + (q.recurring === false ? T('到期') : T('重置')), soon: true };
1093
1460
  const m = Math.ceil(ms / 60000);
1094
1461
  const d = Math.floor(m / 1440), h = Math.floor((m % 1440) / 60), mm = m % 60;
1095
- const dur = d > 0 ? d + ' 天' + (h ? h + ' 小时' : '') : h > 0 ? h + ' 小时' + (mm ? mm + ' 分' : '') : Math.max(1, mm) + ' 分钟';
1096
- return { text: dur + ' 后' + (q.recurring === false ? '到期' : '重置'), soon: ms < 3600e3 };
1462
+ const dur = d > 0 ? d + T(' 天') + (h ? h + T(' 小时') : '') : h > 0 ? h + T(' 小时') + (mm ? mm + T(' 分') : '') : Math.max(1, mm) + T(' 分钟');
1463
+ return { text: dur + T(' 后') + (q.recurring === false ? T('到期') : T('重置')), soon: ms < 3600e3 };
1097
1464
  }
1098
1465
  function relResetHtml(q) {
1099
1466
  const r = relReset(q);
@@ -1113,38 +1480,38 @@ function trLine(name, valHtml, title) {
1113
1480
 
1114
1481
  function creditHtml(a) {
1115
1482
  const q = state.quotas[a.id];
1116
- if (!q) return '<div class="credit-line"><span class="dim">读取积分中…</span></div><div class="bar"></div>';
1117
- if (q.error) return '<div class="credit-line"><span class="dim" title="' + esc(q.error) + '">积分读取失败:' + esc(q.error.slice(0, 40)) + '</span></div><div class="bar"></div>';
1118
- if (q.empty) return '<div class="credit-line"><span class="dim">无有效套餐(仅免费额度)</span></div><div class="bar"></div>';
1483
+ if (!q) return T('<div class="credit-line"><span class="dim">读取积分中…</span></div><div class="bar"></div>');
1484
+ if (q.error) return '<div class="credit-line"><span class="dim" title="' + esc(T(q.error)) + '">' + T('积分读取失败:') + esc(q.error.slice(0, 40)) + '</span></div><div class="bar"></div>';
1485
+ if (q.empty) return T('<div class="credit-line"><span class="dim">无有效套餐(仅免费额度)</span></div><div class="bar"></div>');
1119
1486
 
1120
1487
  // 妙手:Credits 钱包余额 + 当前版本(体验版 / 专业版),无「总量」口径,不画水位条
1121
1488
  if (a.product === 'catpaw') {
1122
1489
  const planBadge = q.plan ? '<span class="badge">' + esc(q.plan) + '</span>' : '';
1123
- const expHtml = q.planExpiresAt ? '<span class="dim">' + fmtTime(q.planExpiresAt).slice(0, 10) + ' 到期</span>'
1124
- : q.nextRefreshAt ? '<span class="dim">' + fmtTime(q.nextRefreshAt).slice(0, 10) + ' 下次刷新</span>' : '';
1125
- return '<div class="credit-line"><span class="credit"><span class="lbl">可用额度</span><b>' + fmtNum(q.remaining) + '</b>Credits</span>'
1490
+ const expHtml = q.planExpiresAt ? '<span class="dim">' + fmtTime(q.planExpiresAt).slice(0, 10) + T(' 到期</span>')
1491
+ : q.nextRefreshAt ? '<span class="dim">' + fmtTime(q.nextRefreshAt).slice(0, 10) + T(' 下次刷新</span>') : '';
1492
+ return T('<div class="credit-line"><span class="credit"><span class="lbl">可用额度</span><b>') + fmtNum(q.remaining) + '</b>Credits</span>'
1126
1493
  + '<span class="credit dim">' + planBadge + (planBadge && expHtml ? ' · ' : '') + expHtml + '</span></div>';
1127
1494
  }
1128
1495
 
1129
1496
  // 多窗口配额体系(mirasim 等):每个窗口平等等权,独立一行带分散对齐的名称、数值与三档细水位条
1130
1497
  if (a.product === 'mirasim' && q.parts && q.parts.length) {
1131
1498
  const isExhausted = q.exceeded || (q.remaining <= 0);
1132
- const planBadge = q.plan ? '<span class="badge">' + esc(q.plan) + ' 套餐</span>' : '';
1133
- const expLabel = q.planExpiresAt ? '<span class="dim">' + fmtTime(q.planExpiresAt).slice(0, 10) + ' 到期</span>' : '';
1499
+ const planBadge = q.plan ? '<span class="badge">' + esc(T(q.plan)) + T(' 套餐</span>') : '';
1500
+ const expLabel = q.planExpiresAt ? '<span class="dim">' + fmtTime(q.planExpiresAt).slice(0, 10) + T(' 到期</span>') : '';
1134
1501
  // 状态采用温和的徽章,避免整行大红字制造恐慌
1135
1502
  const topStatus = isExhausted
1136
- ? '<span class="pill pending">' + esc(q.bottleneckLabel || '额度已用尽') + '</span>'
1137
- : '<span class="lbl">可用额度</span><b>' + fmtNum(q.remaining) + '%</b>';
1503
+ ? '<span class="pill pending">' + esc(T(q.bottleneckLabel || '') || T('额度已用尽')) + '</span>'
1504
+ : T('<span class="lbl">可用额度</span><b>') + fmtNum(q.remaining) + '%</b>';
1138
1505
 
1139
- const fmtUnits = (n) => (n >= 10000 ? (n / 10000).toFixed(2) + '万' : fmtNum(Math.round(n)));
1506
+ const fmtUnits = (n) => (n >= 10000 ? (n / 10000).toFixed(2) + T('万') : fmtNum(Math.round(n)));
1140
1507
  const rows = q.parts.map((p) => {
1141
1508
  const pct = p.remainingPercentage ?? (p.total > 0 ? Math.max(0, Math.min(100, (p.remaining / p.total) * 100)) : 0);
1142
1509
  const isZero = pct <= 0;
1143
1510
  const numTxt = p.total > 0 ? fmtUnits(p.remaining) + ' / ' + fmtUnits(p.total) : '';
1144
1511
  // 数值保持中性温和排版,不用鲜红大字恐吓用户
1145
1512
  const pctTxt = isZero
1146
- ? '<span class="dim">剩 0%</span>'
1147
- : '<span>剩 ' + fmtNum(pct) + '%</span>';
1513
+ ? T('<span class="dim">剩 0%</span>')
1514
+ : T('<span>剩 ') + fmtNum(pct) + '%</span>';
1148
1515
  const valHtml = pctTxt + (numTxt ? ' <small class="dim">(' + numTxt + ')</small>' : '') + ' ' + relResetHtml(p);
1149
1516
  return trLine(p.name, valHtml) + trMeter(pct);
1150
1517
  }).join('');
@@ -1154,16 +1521,16 @@ function creditHtml(a) {
1154
1521
  + '<div class="mirasim-windows">' + rows + '</div>';
1155
1522
  }
1156
1523
 
1157
- const unit = q.unit ? ' ' + q.unit : '';
1524
+ const unit = q.unit ? ' ' + T(q.unit) : '';
1158
1525
  if (q.unit) {
1159
1526
  const items = q.parts.map((x) => {
1160
- const base = esc(x.name) + ' ' + fmtNum(x.remaining) + '/' + fmtNum(x.total) + (x.unit ? ' ' + esc(x.unit) : '');
1161
- if (x.pending && x.startsAt) return base + '(' + fmtTime(x.startsAt).slice(5, 16) + ' 生效)';
1162
- return base + (x.expiresAt ? '(' + fmtTime(x.expiresAt).slice(5, 16) + (x.recurring ? ' 重置)' : ' 到期)') : '');
1527
+ const base = esc(T(x.name)) + ' ' + fmtNum(x.remaining) + '/' + fmtNum(x.total) + (x.unit ? ' ' + esc(T(x.unit)) : '');
1528
+ if (x.pending && x.startsAt) return base + T('(') + fmtTime(x.startsAt).slice(5, 16) + T(' 生效)');
1529
+ return base + (x.expiresAt ? T('(') + fmtTime(x.expiresAt).slice(5, 16) + (x.recurring ? T(' 重置)') : T(' 到期)')) : '');
1163
1530
  }).join(' · ');
1164
1531
  const pct = q.total > 0 ? Math.min(1, q.remaining / q.total) : 0;
1165
1532
  // 满额(100%)不再渲染灰色「已使用」尾段——它只占 0 宽也会造成几像素的灰边
1166
- return '<div class="credit-line"><span class="credit"><span class="lbl">剩余</span><b>' + fmtNum(q.remaining) + '</b>' + esc(unit) + '</span>'
1533
+ return T('<div class="credit-line"><span class="credit"><span class="lbl">剩余</span><b>') + fmtNum(q.remaining) + '</b>' + esc(unit) + '</span>'
1167
1534
  + '<span class="credit dim" title="' + items + '">' + (q.plan ? esc(q.plan) + ' · ' : '') + items + '</span></div>'
1168
1535
  + '<div class="bar"><span class="seg" style="flex:' + pct.toFixed(4) + ' 1 0"></span>'
1169
1536
  + (pct < 0.9999 ? '<span class="seg used" style="flex:' + (1 - pct).toFixed(4) + ' 1 0"></span>' : '')
@@ -1174,14 +1541,14 @@ function creditHtml(a) {
1174
1541
  if (total > 0) {
1175
1542
  q.parts.filter((x) => x.remaining > 0).forEach((x, i) => {
1176
1543
  bar += '<span class="seg" style="flex:' + (x.remaining / total).toFixed(4) + ' 1 0;--a:' + (x.recurring ? 1 : Math.max(.45, .85 - i * .05)) + '" title="'
1177
- + esc(x.name) + ':剩余 ' + fmtNum(x.remaining) + ' / ' + fmtNum(x.total) + (x.expiresAt ? (x.recurring ? ',' + fmtTime(x.expiresAt).slice(0, 10) + ' 重置' : ',' + fmtTime(x.expiresAt).slice(0, 10) + ' 到期') : '') + '"></span>';
1544
+ + esc(T(x.name)) + T(':剩余 ') + fmtNum(x.remaining) + ' / ' + fmtNum(x.total) + (x.expiresAt ? (x.recurring ? T(',') + fmtTime(x.expiresAt).slice(0, 10) + T(' 重置') : T(',') + fmtTime(x.expiresAt).slice(0, 10) + T(' 到期')) : '') + '"></span>';
1178
1545
  });
1179
- if (q.used > 0) bar += '<span class="seg used" style="flex:' + (q.used / total).toFixed(4) + ' 1 0" title="已使用 ' + fmtNum(q.used) + '"></span>';
1546
+ if (q.used > 0) bar += '<span class="seg used" style="flex:' + (q.used / total).toFixed(4) + ' 1 0" title="' + T('已使用 ') + fmtNum(q.used) + '"></span>';
1180
1547
  }
1181
1548
  const soon = q.parts.filter((x) => x.remaining > 0 && !x.recurring && x.expiresAt).sort((x, y) => x.expiresAt.localeCompare(y.expiresAt))[0];
1182
1549
  return '<div class="credit-line">'
1183
- + '<span class="credit"><span class="lbl">剩余</span>' + I.credit + '<b>' + fmtNum(q.remaining) + '</b></span>'
1184
- + '<span class="credit dim">' + (soon ? fmtNum(soon.remaining) + ' 于 ' + fmtTime(soon.expiresAt).slice(5, 10) + ' 到期 · ' : '') + '已用 ' + fmtNum(q.used) + ' / 共 ' + fmtNum(total) + (q.exceeded ? ' · <span style="color:var(--err)">已超额</span>' : '') + '</span>'
1550
+ + T('<span class="credit"><span class="lbl">剩余</span>') + I.credit + '<b>' + fmtNum(q.remaining) + '</b></span>'
1551
+ + '<span class="credit dim">' + (soon ? fmtNum(soon.remaining) + T(' 于 ') + fmtTime(soon.expiresAt).slice(5, 10) + T(' 到期 · ') : '') + T('已用 ') + fmtNum(q.used) + T(' / 共 ') + fmtNum(total) + (q.exceeded ? T(' · <span style="color:var(--err)">已超额</span>') : '') + '</span>'
1185
1552
  + '</div><div class="bar">' + bar + '</div>';
1186
1553
  }
1187
1554
 
@@ -1193,31 +1560,31 @@ function cardHtml(a) {
1193
1560
  const canCheckin = CHECKIN_PRODUCTS.has(a.product);
1194
1561
  return '<div class="card" draggable="true" data-id="' + esc(a.id) + '">'
1195
1562
  + '<div class="row1"><div class="name-group">'
1196
- + '<span class="name" data-act="rename" title="点击修改备注名">' + esc(a.name || '(未命名)') + '</span>'
1563
+ + T('<span class="name" data-act="rename" title="点击修改备注名">') + esc(a.name || T('(未命名)')) + '</span>'
1197
1564
  + provBadges(a.provider, true, a.flavor)
1198
- + (isCurrent ? '<span class="pill cur" title="' + esc(CLIENT_LABEL[a.product] || '') + ' 客户端当前登录的账号">客户端当前</span>' : '')
1199
- + (a.product === 'zcode' && isGatewayCurrent(a) ? '<span class="pill gw" title="体验包接口最近一次补全用的账号(多账号时按轮换自动选择,与客户端登录无关)">接口使用中</span>' : '')
1200
- + (a.product === 'minimax' && isMiniMaxGatewayCurrent(a) ? '<span class="pill gw" title="本地接口最近一次补全用的账号(多账号时按轮换自动选择,与客户端登录无关)">接口使用中</span>' : '')
1201
- + (a.product === 'trae' && isTraeGatewayCurrent(a) ? '<span class="pill gw" title="本地接口最近一次补全用的账号(多账号时按轮换自动选择,与客户端登录无关)">接口使用中</span>' : '')
1565
+ + (isCurrent ? '<span class="pill cur" title="' + esc(CLIENT_LABEL[a.product] ? T(CLIENT_LABEL[a.product]) : '') + T(' 客户端当前登录的账号">客户端当前</span>') : '')
1566
+ + (a.product === 'zcode' && isGatewayCurrent(a) ? T('<span class="pill gw" title="体验包接口最近一次补全用的账号(多账号时按轮换自动选择,与客户端登录无关)">接口使用中</span>') : '')
1567
+ + (a.product === 'minimax' && isMiniMaxGatewayCurrent(a) ? T('<span class="pill gw" title="本地接口最近一次补全用的账号(多账号时按轮换自动选择,与客户端登录无关)">接口使用中</span>') : '')
1568
+ + (a.product === 'trae' && isTraeGatewayCurrent(a) ? T('<span class="pill gw" title="本地接口最近一次补全用的账号(多账号时按轮换自动选择,与客户端登录无关)">接口使用中</span>') : '')
1202
1569
  + (a.isPat ? '<span class="badge">PAT</span>' : '')
1203
1570
  + qoderWebBadge(a)
1204
- + (a.verified === false ? '<span class="pill fail">token 无效</span>' : '')
1571
+ + (a.verified === false ? T('<span class="pill fail">token 无效</span>') : '')
1205
1572
  + (st.text ? '<span class="pill ' + st.cls + '" title="' + esc(a.lastResult ? fmtTime(a.lastResult.at) + ' ' + (a.lastResult.message || a.lastResult.status) : '') + '">' + esc(st.text) + '</span>' : '')
1206
1573
  + '</div><div class="ops">'
1207
- + (a.product === 'zcode' ? '<button class="icon-btn" data-act="claim" title="活动领取(billing/preview → claim,可能需过一次验证码)">' + I.gift + '</button>' : '')
1208
- + (canCheckin ? '<button class="icon-btn' + (state.busy[a.id] ? ' busy' : '') + '" data-act="checkin" title="' + (a.provider === 'workbuddy-intl' ? '发送活跃请求' : '立即领取') + '">' + (state.busy[a.id] ? I.refresh : I.play) + '</button>' : '')
1209
- + (a.canSwitch && !isCurrent && st.cls !== 'fail' ? '<button class="icon-btn" data-act="switch" title="切换 ' + (CLIENT_LABEL[a.product] || prodLabel(a.product)) + ' 客户端到此账号">' + I.swap + '</button>' : '')
1210
- + '<button class="icon-btn" data-act="quota" title="刷新积分">' + I.refresh + '</button>'
1211
- + '<button class="icon-btn del" data-act="del" title="删除(点两次确认)">' + I.trash + '</button>'
1574
+ + (a.product === 'zcode' ? T('<button class="icon-btn" data-act="claim" title="活动领取(billing/preview → claim,可能需过一次验证码)">') + I.gift + '</button>' : '')
1575
+ + (canCheckin && (a.provider !== 'workbuddy-intl' || wbActOn()) ? '<button class="icon-btn' + (state.busy[a.id] ? ' busy' : '') + '" data-act="checkin" title="' + (a.provider === 'workbuddy-intl' ? T('发送活跃请求') : T('立即领取')) + '">' + (state.busy[a.id] ? I.refresh : I.play) + '</button>' : '')
1576
+ + (a.canSwitch && !isCurrent && st.cls !== 'fail' ? T('<button class="icon-btn" data-act="switch" title="切换 ') + (CLIENT_LABEL[a.product] ? T(CLIENT_LABEL[a.product]) : prodLabel(a.product)) + T(' 客户端到此账号">') + I.swap + '</button>' : '')
1577
+ + T('<button class="icon-btn" data-act="quota" title="刷新积分">') + I.refresh + '</button>'
1578
+ + T('<button class="icon-btn del" data-act="del" title="删除(点两次确认)">') + I.trash + '</button>'
1212
1579
  + '</div></div>'
1213
1580
  + '<div class="meta">'
1214
- + '<div class="mi"><span class="lbl">账号</span><span class="val" title="' + esc(a.email || a.phone || a.uid || '') + '">' + esc(a.phone && !a.email ? ident : mask(ident)) + '</span></div>'
1215
- + '<div class="mi right"><span class="lbl">' + (ex.isPlan ? '套餐到期' : ex.isAuto ? '凭据状态' : '有效期至') + '</span>'
1581
+ + '<div class="mi"><span class="lbl">' + T('账号') + '</span><span class="val" title="' + esc(a.email || a.phone || a.uid || '') + '">' + esc(a.phone && !a.email ? ident : mask(ident)) + '</span></div>'
1582
+ + '<div class="mi right"><span class="lbl">' + (ex.isPlan ? T('套餐到期') : ex.isAuto ? T('凭据状态') : T('有效期至')) + '</span>'
1216
1583
  + '<span class="val' + (ex.warn ? ' warn' : '') + '" title="' + esc(ex.title || '') + '">' + esc(ex.label) + '</span></div>'
1217
1584
  + '</div>'
1218
1585
  + creditHtml(a)
1219
1586
  + (st.msg && !(state.quotas[a.id] && state.quotas[a.id].error && state.quotas[a.id].error.includes(st.msg.slice(-24)))
1220
- ? '<div class="msg' + (st.err ? ' err' : '') + '" title="' + esc(st.msg) + '">' + esc(st.msg) + '</div>' : '')
1587
+ ? '<div class="msg' + (st.err ? ' err' : '') + '" title="' + esc(T(st.msg)) + '">' + esc(T(st.msg)) + '</div>' : '')
1221
1588
  + '</div>';
1222
1589
  }
1223
1590
 
@@ -1298,7 +1665,7 @@ const EMPTY_TIP = {
1298
1665
  mirasim: '还没有 mirasim 账号。<br>点「添加账号」可读取本机 mirasim 客户端当前登录的账号(本地解密,token 不出本机)。',
1299
1666
  catpaw: '还没有妙手账号。<br>点「添加账号」可读取本机妙手(美团 CatPaw)客户端当前登录的账号(本地解密凭据,token 不出本机)。',
1300
1667
  trae: '还没有 Trae 账号。<br>点「添加账号 → 本机导入」可读取本机 Trae 客户端当前登录的账号(本地解密 storage.json 凭据,token 不出本机)。多个账号需先在 Trae 里分别登录并逐个导入。',
1301
- minimax: '还没有 MiniMax 账号。<br>点「添加账号 → 本机导入」可读取本机 MiniMax Code 客户端当前登录的账号,或粘贴 Bearer Token 添加。',
1668
+ minimax: '还没有 MiniMax 账号。<br>点「添加账号 → 本机导入」可读取本机 MiniMax Code 客户端当前登录的账号,或用浏览器登录 / 粘贴 Bearer Token 添加。',
1302
1669
  };
1303
1670
 
1304
1671
  function renderProd() {
@@ -1306,7 +1673,7 @@ function renderProd() {
1306
1673
  const all = ofProduct(prod);
1307
1674
  const acc = all.filter((a) => !state.region || provOf(a.provider).cls === state.region);
1308
1675
  const list = $('list');
1309
- if (!acc.length) list.innerHTML = '<div class="empty">' + (all.length ? '该地区下没有账号。' : EMPTY_TIP[prod]) + '</div>';
1676
+ if (!acc.length) list.innerHTML = '<div class="empty">' + (all.length ? T('该地区下没有账号。') : T(EMPTY_TIP[prod])) + '</div>';
1310
1677
  else list.innerHTML = sortedAccounts(prod, acc).map(cardHtml).join('');
1311
1678
  const canCheckin = CHECKIN_PRODUCTS.has(prod);
1312
1679
  $('s-count').textContent = acc.length;
@@ -1314,16 +1681,16 @@ function renderProd() {
1314
1681
  if (prod === 'workbuddy') {
1315
1682
  // 国内版 = 每日签到送积分;国际版 = 保持活跃(活跃不等于必有奖励)
1316
1683
  const cnAcc = acc.filter((a) => a.provider === 'workbuddy');
1317
- const intlAcc = acc.filter((a) => a.provider === 'workbuddy-intl');
1318
- $('s-done-wrap').innerHTML = (cnAcc.length ? '<div class="stat"><span title="国内版每日签到送积分">领取(国内)</span><strong>' + cnAcc.filter(isDone).length + ' / ' + cnAcc.length + '</strong></div><div class="divider"></div>' : '')
1319
- + (intlAcc.length ? '<div class="stat"><span title="国际版定期发送活跃请求保持资格;活跃不等于必有奖励">活跃(国际)</span><strong>' + intlAcc.filter(isDone).length + ' / ' + intlAcc.length + '</strong></div><div class="divider"></div>' : '');
1684
+ const intlAcc = acc.filter((a) => a.provider === 'workbuddy-intl' && wbActOn());
1685
+ $('s-done-wrap').innerHTML = (cnAcc.length ? T('<div class="stat"><span title="国内版每日签到送积分">领取(国内)</span><strong>') + cnAcc.filter(isDone).length + ' / ' + cnAcc.length + '</strong></div><div class="divider"></div>' : '')
1686
+ + (intlAcc.length ? T('<div class="stat"><span title="国际版定期发送活跃请求保持资格;活跃不等于必有奖励">活跃(国际)</span><strong>') + intlAcc.filter(isDone).length + ' / ' + intlAcc.length + '</strong></div><div class="divider"></div>' : '');
1320
1687
  } else {
1321
1688
  // 从 WorkBuddy 页切回来时 s-done 可能被 innerHTML 换没了,先重建结构再写值
1322
- if (!$('s-done')) $('s-done-wrap').innerHTML = '<div class="stat"><span>今日已领</span><strong id="s-done"></strong></div><div class="divider"></div>';
1689
+ if (!$('s-done')) $('s-done-wrap').innerHTML = T('<div class="stat"><span>今日已领</span><strong id="s-done"></strong></div><div class="divider"></div>');
1323
1690
  $('s-done').textContent = acc.filter(isDone).length + ' / ' + acc.length;
1324
1691
  }
1325
1692
  const c = creditSum(acc);
1326
- $('s-credits-label').textContent = CREDIT_VIEWS.has(prod) ? '有效套餐' : '剩余积分';
1693
+ $('s-credits-label').textContent = CREDIT_VIEWS.has(prod) ? T('有效套餐') : T('剩余积分');
1327
1694
  $('s-credits').textContent = CREDIT_VIEWS.has(prod)
1328
1695
  ? acc.filter((a) => { const q = state.quotas[a.id]; return q && !q.error && !q.empty; }).length + ' / ' + acc.length
1329
1696
  : c.loading ? '…' : (c.known ? fmtNum(c.sum) : '-');
@@ -1333,8 +1700,8 @@ function renderProd() {
1333
1700
  document.querySelectorAll('#region button').forEach((x) => x.classList.toggle('active', x.dataset.r === state.region));
1334
1701
  $('btn-checkin').style.display = canCheckin ? '' : 'none';
1335
1702
  if (!$('btn-checkin').disabled) {
1336
- const hasIntl = acc.some((a) => a.provider === 'workbuddy-intl');
1337
- $('btn-checkin').innerHTML = I.play + (prod === 'workbuddy' && hasIntl ? '领取 / 活跃全部' : '领取全部 ' + prodLabel(prod));
1703
+ const hasIntl = acc.some((a) => a.provider === 'workbuddy-intl') && wbActOn();
1704
+ $('btn-checkin').innerHTML = I.play + (prod === 'workbuddy' && hasIntl ? T('领取 / 活跃全部') : T('领取全部 ') + prodLabel(prod));
1338
1705
  }
1339
1706
  // 顶栏产品专属入口:ZCode 设置组、Trae 打开客户端,带状态灯(运行中绿点呼吸)
1340
1707
  const dot = (on, pulse) => '<span class="dot' + (on ? ' on' : '') + (on && pulse ? ' pulse' : '') + '"></span>';
@@ -1342,44 +1709,42 @@ function renderProd() {
1342
1709
  if (prod === 'zcode') {
1343
1710
  const acOn = state.zAutoClaim === true;
1344
1711
  const gwOn = state.zGateway === true;
1345
- $('zt-ac').innerHTML = dot(acOn) + '自动领取';
1346
- $('zt-ac').title = acOn ? '自动领取运行中——点我调整轮询周期 / 运行时长' : '自动领取已关闭——点我设置轮询周期与运行时长并开启';
1347
- $('zt-net').innerHTML = '出口:<b style="font-weight:700;color:var(--text)">' + (state.zProxyFirst === true ? '代理优先' : '直连优先') + '</b>';
1348
- $('zt-net').title = '网络出口:直连 / 代理顺序与代理地址';
1349
- $('zt-gw').innerHTML = dot(gwOn) + '接口' + (gwOn && state.zGatewayLan === true ? '·局域网' : '');
1350
- $('zt-gw').title = '体验包接口:开关、局域网开放与 IP 白名单';
1712
+ $('zt-ac').innerHTML = dot(acOn) + T('自动领取');
1713
+ $('zt-ac').title = acOn ? T('自动领取运行中——点我调整轮询周期 / 运行时长') : T('自动领取已关闭——点我设置轮询周期与运行时长并开启');
1714
+ $('zt-gw').innerHTML = dot(gwOn) + T('接口') + (gwOn && state.zGatewayLan === true ? T('·局域网') : '');
1715
+ $('zt-gw').title = T('体验包接口:开关、局域网开放与 IP 白名单');
1351
1716
  const zc = (state.status && state.status.zcodeClient) || {};
1352
1717
  const canOpen = zc.installed && window.creditdaddy && typeof window.creditdaddy.openZcodeClient === 'function';
1353
1718
  $('zt-open').style.display = canOpen ? '' : 'none';
1354
1719
  if (canOpen) {
1355
- $('zt-open').innerHTML = dot(zc.running === true, true) + '打开客户端';
1356
- $('zt-open').title = zc.running ? 'ZCode 客户端运行中,点我唤到前台' : 'ZCode 客户端未运行,点我启动';
1720
+ $('zt-open').innerHTML = dot(zc.running === true, true) + T('打开客户端');
1721
+ $('zt-open').title = zc.running ? T('ZCode 客户端运行中,点我唤到前台') : T('ZCode 客户端未运行,点我启动');
1357
1722
  }
1358
1723
  }
1359
1724
  $('t-actions').style.display = prod === 'trae' ? 'contents' : 'none';
1360
1725
  if (prod === 'trae') {
1361
1726
  const gwOn = state.tGateway === true;
1362
- $('tt-gw').innerHTML = dot(gwOn) + '接口' + (gwOn && state.tGatewayLan === true ? '·局域网' : '');
1363
- $('tt-gw').title = 'Trae 本地接口:开关、局域网开放与 IP 白名单';
1727
+ $('tt-gw').innerHTML = dot(gwOn) + T('接口') + (gwOn && state.tGatewayLan === true ? T('·局域网') : '');
1728
+ $('tt-gw').title = T('Trae 本地接口:开关、局域网开放与 IP 白名单');
1364
1729
  const tc = (state.status && state.status.traeClient) || {};
1365
1730
  const canOpen = tc.installed && window.creditdaddy && typeof window.creditdaddy.openTraeClient === 'function';
1366
1731
  $('tt-open').style.display = canOpen ? '' : 'none';
1367
1732
  if (canOpen) {
1368
- $('tt-open').innerHTML = dot(tc.running === true, true) + '打开客户端';
1369
- $('tt-open').title = tc.running ? 'Trae 客户端运行中,点我唤到前台' : 'Trae 客户端未运行,点我启动';
1733
+ $('tt-open').innerHTML = dot(tc.running === true, true) + T('打开客户端');
1734
+ $('tt-open').title = tc.running ? T('Trae 客户端运行中,点我唤到前台') : T('Trae 客户端未运行,点我启动');
1370
1735
  }
1371
1736
  }
1372
1737
  $('m-actions').style.display = prod === 'minimax' ? 'contents' : 'none';
1373
1738
  if (prod === 'minimax') {
1374
1739
  const gwOn = state.mGateway === true;
1375
- $('mt-gw').innerHTML = dot(gwOn) + '接口' + (gwOn && state.mGatewayLan === true ? '·局域网' : '');
1376
- $('mt-gw').title = 'MiniMax 本地接口:开关、局域网开放与 IP 白名单';
1740
+ $('mt-gw').innerHTML = dot(gwOn) + T('接口') + (gwOn && state.mGatewayLan === true ? T('·局域网') : '');
1741
+ $('mt-gw').title = T('MiniMax 本地接口:开关、局域网开放与 IP 白名单');
1377
1742
  const mc = (state.status && state.status.minimaxClient) || {};
1378
1743
  const canOpen = mc.installed && window.creditdaddy && typeof window.creditdaddy.openMiniMaxClient === 'function';
1379
1744
  $('mt-open').style.display = canOpen ? '' : 'none';
1380
1745
  if (canOpen) {
1381
- $('mt-open').innerHTML = dot(mc.running === true, true) + '打开客户端';
1382
- $('mt-open').title = mc.running ? 'MiniMax Code 客户端运行中,点我唤到前台' : 'MiniMax Code 客户端未运行,点我启动';
1746
+ $('mt-open').innerHTML = dot(mc.running === true, true) + T('打开客户端');
1747
+ $('mt-open').title = mc.running ? T('MiniMax Code 客户端运行中,点我唤到前台') : T('MiniMax Code 客户端未运行,点我启动');
1383
1748
  }
1384
1749
  }
1385
1750
  }
@@ -1400,29 +1765,29 @@ function renderDash() {
1400
1765
  const byProd = orderedVisibleComps().filter((id) => id !== 'tenrouter').map((id) => prodLabel(id) + ' ' + ofProduct(id).length).join(' · ');
1401
1766
  const creditByProd = ['qoder', 'workbuddy', 'minimax'].filter(compVisible).map((id) => { const c = creditSum(ofProduct(id)); return prodLabel(id) + ' ' + (c.known ? fmtNum(c.sum) : '-'); }).join(' · ');
1402
1767
  // 今日进度拆「国内领取 / 国际活跃」:国际版 WorkBuddy 是保持活跃(活跃不等于必有奖励)
1403
- const intlAcc = signable.filter((a) => a.provider === 'workbuddy-intl');
1768
+ const intlAcc = signable.filter((a) => a.provider === 'workbuddy-intl' && wbActOn());
1404
1769
  const domAcc = signable.filter((a) => a.provider !== 'workbuddy-intl');
1405
1770
  const doneDom = domAcc.filter(isDone).length;
1406
1771
  const doneIntl = intlAcc.filter(isDone).length;
1407
1772
  $('kpis').innerHTML =
1408
- kpi('账号', state.accounts.length, byProd)
1409
- + kpi('今日领取' + (intlAcc.length ? ' / 活跃' : ''), intlAcc.length
1410
- ? doneDom + ' <small>/ ' + domAcc.length + '</small><small class="kpi-side"> · 活跃 ' + doneIntl + ' / ' + intlAcc.length + '</small>'
1773
+ kpi(T('账号'), state.accounts.length, byProd)
1774
+ + kpi(T('今日领取') + (intlAcc.length ? T(' / 活跃') : ''), intlAcc.length
1775
+ ? doneDom + ' <small>/ ' + domAcc.length + '</small><small class="kpi-side">' + T(' · 活跃 {n} / {m}', { n: doneIntl, m: intlAcc.length }) + '</small>'
1411
1776
  : done + ' <small>/ ' + signable.length + '</small>',
1412
- !signable.length ? '暂无可领取的账号'
1413
- : (doneDom === domAcc.length && doneIntl === intlAcc.length ? (intlAcc.length ? '今天全部完成(国际为活跃请求,不保证奖励)' : '今天全部签完')
1414
- : '待办 ' + (signable.length - done) + (failed ? ' · 失败 ' + failed : '')),
1415
- '领取 = 国内版每日签到;活跃 = 国际版定期活跃请求(保持资格,不保证奖励)')
1416
- + kpi('剩余积分', credit.loading ? '…' : (credit.known ? fmtNum(credit.sum) : '-'), credit.loading ? '正在读取各账号额度…' : creditByProd)
1417
- + kpi('下次自动领取', sch.ticking ? '领取中…' : sch.nextTickAt ? fmtTime(sch.nextTickAt).slice(11) : '-',
1418
- sch.lastTick ? '上一轮 ' + fmtTime(sch.lastTick.at).slice(5) + ' · ' + esc(sch.lastTick.summary || '') : '每 ~2 小时一轮');
1777
+ !signable.length ? T('暂无可领取的账号')
1778
+ : (doneDom === domAcc.length && doneIntl === intlAcc.length ? (intlAcc.length ? T('今天全部完成(国际为活跃请求,不保证奖励)') : T('今天全部签完'))
1779
+ : T('待办 ') + (signable.length - done) + (failed ? T(' · 失败 ') + failed : '')),
1780
+ T('领取 = 国内版每日签到;活跃 = 国际版定期活跃请求(保持资格,不保证奖励)'))
1781
+ + kpi(T('剩余积分'), credit.loading ? '…' : (credit.known ? fmtNum(credit.sum) : '-'), credit.loading ? T('正在读取各账号额度…') : creditByProd)
1782
+ + kpi(T('下次自动领取'), sch.ticking ? T('领取中…') : sch.nextTickAt ? fmtTime(sch.nextTickAt).slice(11) : '-',
1783
+ sch.lastTick ? T('上一轮 ') + fmtTime(sch.lastTick.at).slice(5) + ' · ' + esc(sch.lastTick.summary || '') : T('每 ~2 小时一轮'));
1419
1784
 
1420
1785
  // 仪表盘产品卡按组件设置里的顺序渲染(与标签栏同步),10Router 汇总卡按其顺序插入
1421
1786
  const prodCardHtml = (p) => {
1422
1787
  const list = ofProduct(p.id);
1423
1788
  let rows = '';
1424
1789
  if (!list.length) {
1425
- rows = '<span class="lbl">账号</span><span class="val dim">还没有,点击进入添加</span>';
1790
+ rows = '<span class="lbl">' + T('账号') + '</span><span class="val dim">' + T('还没有,点击进入添加') + '</span>';
1426
1791
  } else if (CHECKIN_PRODUCTS.has(p.id)) {
1427
1792
  const d = list.filter(isDone).length;
1428
1793
  const c = creditSum(list);
@@ -1431,57 +1796,57 @@ function renderDash() {
1431
1796
  const cnList = list.filter((a) => a.provider === 'workbuddy');
1432
1797
  const intlList = list.filter((a) => a.provider === 'workbuddy-intl');
1433
1798
  const parts = [];
1434
- if (cnList.length) parts.push(cnList.filter(isDone).length + ' / ' + cnList.length + '(国内)');
1435
- if (intlList.length) parts.push(intlList.filter(isDone).length + ' / ' + intlList.length + '(国际 · 不保证奖励)');
1436
- rows += '<span class="lbl">领取 / 活跃</span><span class="val" title="' + esc(parts.join(',') + (intlList.length ? '(国际为活跃请求,不保证奖励)' : '')) + '">'
1437
- + (cnList.length ? cnList.filter(isDone).length + ' / ' + cnList.length + ' 国内' : '')
1799
+ if (cnList.length) parts.push(cnList.filter(isDone).length + ' / ' + cnList.length + T('(国内)'));
1800
+ if (intlList.length) parts.push(intlList.filter(isDone).length + ' / ' + intlList.length + T('(国际 · 不保证奖励)'));
1801
+ rows += T('<span class="lbl">领取 / 活跃</span><span class="val" title="') + esc(parts.join(T(',')) + (intlList.length ? T('(国际为活跃请求,不保证奖励)') : '')) + '">'
1802
+ + (cnList.length ? cnList.filter(isDone).length + ' / ' + cnList.length + T(' 国内') : '')
1438
1803
  + (cnList.length && intlList.length ? ' · ' : '')
1439
- + (intlList.length ? intlList.filter(isDone).length + ' / ' + intlList.length + ' 国际' : '')
1804
+ + (intlList.length ? intlList.filter(isDone).length + ' / ' + intlList.length + T(' 国际') : '')
1440
1805
  + '</span>';
1441
1806
  } else if (p.id === 'minimax') {
1442
1807
  const planned = list.filter((a) => { const q = state.quotas[a.id]; return q && !q.error && !q.empty; }).length;
1443
- rows = '<span class="lbl">今日领取</span><span class="val">' + d + ' / ' + list.length + '</span>'
1808
+ rows = T('<span class="lbl">今日领取</span><span class="val">') + d + ' / ' + list.length + '</span>'
1444
1809
  + '<span style="grid-column:1 / -1">' + meter(d, list.length) + '</span>'
1445
- + '<span class="lbl">剩余积分</span><span class="val">' + (c.known ? fmtNum(c.sum) : '-') + '</span>';
1810
+ + T('<span class="lbl">剩余积分</span><span class="val">') + (c.known ? fmtNum(c.sum) : '-') + '</span>';
1446
1811
  } else {
1447
- rows = '<span class="lbl">今日领取</span><span class="val">' + d + ' / ' + list.length + '</span>';
1812
+ rows = T('<span class="lbl">今日领取</span><span class="val">') + d + ' / ' + list.length + '</span>';
1448
1813
  }
1449
1814
  if (p.id !== 'minimax') {
1450
1815
  rows += '<span style="grid-column:1 / -1">' + meter(d, list.length) + '</span>'
1451
- + '<span class="lbl">剩余积分</span><span class="val">' + (c.known ? fmtNum(c.sum) : '-') + '</span>';
1816
+ + T('<span class="lbl">剩余积分</span><span class="val">') + (c.known ? fmtNum(c.sum) : '-') + '</span>';
1452
1817
  }
1453
1818
  } else if (p.id === 'mirasim') {
1454
1819
  const planned = list.filter((a) => { const q = state.quotas[a.id]; return q && !q.error && !q.empty; }).length;
1455
- rows = '<span class="lbl">额度体系</span><span class="val dim">5h / 7d 滚动窗口</span>'
1456
- + '<span class="lbl">有效套餐</span><span class="val">' + planned + ' / ' + list.length + '</span>';
1820
+ rows = T('<span class="lbl">额度体系</span><span class="val dim">5h / 7d 滚动窗口</span>')
1821
+ + T('<span class="lbl">有效套餐</span><span class="val">') + planned + ' / ' + list.length + '</span>';
1457
1822
  } else if (p.id === 'catpaw') {
1458
1823
  const withPlan = list.filter((a) => { const q = state.quotas[a.id]; return q && !q.error && q.plan; });
1459
1824
  const credits = withPlan.reduce((s, a) => s + (Number(state.quotas[a.id].remaining) || 0), 0);
1460
- rows = '<span class="lbl">额度体系</span><span class="val dim">Credits 余额 + 套餐版本</span>'
1461
- + (withPlan.length ? '<span class="lbl">可用额度</span><span class="val">' + fmtNum(credits) + ' Credits(' + esc(withPlan.map((a) => state.quotas[a.id].plan).join(' / ')) + ')</span>' : '');
1825
+ rows = T('<span class="lbl">额度体系</span><span class="val dim">Credits 余额 + 套餐版本</span>')
1826
+ + (withPlan.length ? T('<span class="lbl">可用额度</span><span class="val">') + fmtNum(credits) + T(' Credits(') + esc(withPlan.map((a) => state.quotas[a.id].plan).join(' / ')) + T(')</span>') : '');
1462
1827
  } else {
1463
1828
  const planned = list.filter((a) => { const q = state.quotas[a.id]; return q && !q.error && !q.empty; }).length;
1464
1829
  // ZCode 没有每日签到:不展示签到语义的行
1465
- rows = '<span class="lbl">活动领取</span><span class="val dim">手动领取,或在标签页开自动轮询</span>'
1466
- + '<span class="lbl">有效套餐</span><span class="val">' + planned + ' / ' + list.length + '</span>';
1830
+ rows = T('<span class="lbl">活动领取</span><span class="val dim">手动领取,或在标签页开自动轮询</span>')
1831
+ + T('<span class="lbl">有效套餐</span><span class="val">') + planned + ' / ' + list.length + '</span>';
1467
1832
  }
1468
1833
  if (p.id === 'qoder' && list.some((a) => a.provider === 'qoder')) {
1469
- rows += '<span class="lbl">国际版风控身份</span><span class="val' + (s.riskIdentity ? '' : ' warn') + '">' + (s.riskIdentity ? (s.riskSource === 'cli' ? '已就绪(设备身份组件)' : '已就绪') : umidOffer(s) ? '需安装设备身份组件' : '未检测到客户端') + '</span>';
1834
+ rows += T('<span class="lbl">国际版风控身份</span><span class="val') + (s.riskIdentity ? '' : ' warn') + '">' + (s.riskIdentity ? (s.riskSource === 'cli' ? T('已就绪(设备身份组件)') : T('已就绪')) : umidOffer(s) ? T('需安装设备身份组件') : T('未检测到客户端')) + '</span>';
1470
1835
  }
1471
1836
  if (CLIENT_LABEL[p.id]) {
1472
1837
  const cur = list.find((a) => a.product === p.id && isClientCurrent(a));
1473
1838
  const zid = p.id === 'zcode' ? s.zcodeCurrentIdentity : null;
1474
1839
  const live = cur ? esc(cur.name || cur.email || cur.uid)
1475
- : zid && (zid.email || zid.uid || zid.username) ? '未导入的账号(' + esc(zid.email || zid.username || zid.uid) + ')'
1476
- : p.id === 'mirasim' && s.mirasimCurrentUid ? '未导入的账号'
1477
- : p.id === 'catpaw' && s.catpawClient && s.catpawClient.signedIn && !list.some((a) => isClientCurrent(a)) ? '未导入的账号'
1478
- : p.id === 'trae' && s.traeClient && s.traeClient.signedIn && !list.some((a) => isClientCurrent(a)) ? '未导入的账号'
1479
- : p.id === 'minimax' && s.minimaxClient && s.minimaxClient.signedIn && !list.some((a) => isClientCurrent(a)) ? '未导入的账号'
1480
- : (p.id === 'workbuddy' && s.workbuddyCurrentUid ? '未导入的账号' : '—');
1481
- rows += '<span class="lbl">客户端当前</span><span class="val">' + live + '</span>';
1840
+ : zid && (zid.email || zid.uid || zid.username) ? T('未导入的账号(') + esc(zid.email || zid.username || zid.uid) + T(')')
1841
+ : p.id === 'mirasim' && s.mirasimCurrentUid ? T('未导入的账号')
1842
+ : p.id === 'catpaw' && s.catpawClient && s.catpawClient.signedIn && !list.some((a) => isClientCurrent(a)) ? T('未导入的账号')
1843
+ : p.id === 'trae' && s.traeClient && s.traeClient.signedIn && !list.some((a) => isClientCurrent(a)) ? T('未导入的账号')
1844
+ : p.id === 'minimax' && s.minimaxClient && s.minimaxClient.signedIn && !list.some((a) => isClientCurrent(a)) ? T('未导入的账号')
1845
+ : (p.id === 'workbuddy' && s.workbuddyCurrentUid ? T('未导入的账号') : '—');
1846
+ rows += T('<span class="lbl">客户端当前</span><span class="val">') + live + '</span>';
1482
1847
  }
1483
1848
  return '<div class="prodcard" data-view="' + p.id + '"><div class="prod-head"><span class="prod-name"><span class="dot ' + p.id + '"></span>' + p.label
1484
- + '<span class="badge">' + list.length + ' 个账号</span></span><span class="prod-go">查看' + I.chevRight + '</span></div><div class="prod-rows">' + rows + '</div></div>';
1849
+ + '<span class="badge">' + list.length + T(' 个账号</span></span><span class="prod-go">查看') + I.chevRight + '</span></div><div class="prod-rows">' + rows + '</div></div>';
1485
1850
  };
1486
1851
  $('prods').innerHTML = orderedVisibleComps().map((id) => {
1487
1852
  if (id === 'tenrouter') return trDashCard();
@@ -1492,101 +1857,114 @@ function renderDash() {
1492
1857
  // 需要处理:token 失效 / 即将过期、领取失败、国际版缺风控身份、10Router 额度告急 / 查询失败
1493
1858
  const att = [];
1494
1859
  if (!s.riskIdentity && state.accounts.some((a) => a.provider === 'qoder')) {
1495
- att.push({ view: 'qoder', name: 'Qoder 国际版', badges: '', pill: 'limited', text: umidOffer(s) ? '安装设备身份组件后才能领取' : '未检测到 Qoder 客户端,国际版无法领取' });
1860
+ att.push({ view: 'qoder', name: T('Qoder 国际版'), badges: '', pill: 'limited', text: umidOffer(s) ? T('安装设备身份组件后才能领取') : T('未检测到 Qoder 客户端,国际版无法领取') });
1496
1861
  }
1497
1862
  for (const a of state.accounts) {
1498
1863
  const ex = expiryCell(a);
1499
1864
  const st = statusOf(a);
1500
1865
  const base = { view: a.product, name: a.name || a.email || a.id, badges: provBadges(a.provider) };
1501
- if (a.verified === false) att.push(Object.assign({ pill: 'fail', text: 'token 无效' }, base));
1502
- else if (ex.label === '已过期') att.push(Object.assign({ pill: 'fail', text: 'token 已过期' }, base));
1503
- else if (ex.warn) att.push(Object.assign({ pill: 'limited', text: ex.label.slice(5, 10) + ' 过期' }, base));
1504
- if (st.cls === 'fail') att.push(Object.assign({ pill: 'fail', text: a.provider === 'workbuddy-intl' ? '活跃失败' : '领取失败', title: st.msg }, base));
1866
+ if (a.verified === false) att.push(Object.assign({ pill: 'fail', text: T('token 无效') }, base));
1867
+ else if (ex.label === '已过期') att.push(Object.assign({ pill: 'fail', text: T('token 已过期') }, base));
1868
+ else if (ex.warn) att.push(Object.assign({ pill: 'limited', text: ex.label.slice(5, 10) + T(' 过期') }, base));
1869
+ if (st.cls === 'fail') att.push(Object.assign({ pill: 'fail', text: a.provider === 'workbuddy-intl' ? T('活跃失败') : T('领取失败'), title: st.msg }, base));
1505
1870
  }
1506
1871
  for (const c of (tr.quotas && tr.quotas.connections) || []) {
1507
1872
  if (!compVisible('tenrouter')) break;
1508
1873
  const base = { view: 'tenrouter', name: c.name || c.email || c.providerName, badges: '<span class="badge prod tenrouter">' + esc(c.providerName) + '</span>' };
1509
- if (c.error) att.push(Object.assign({ pill: 'fail', text: '额度查询失败', title: c.error }, base));
1510
- else if (c.isActive && quotaLow(c)) att.push(Object.assign({ pill: 'limited', text: '额度告急' }, base));
1874
+ if (c.error) att.push(Object.assign({ pill: 'fail', text: T('额度查询失败'), title: c.error }, base));
1875
+ else if (c.isActive && quotaLow(c)) att.push(Object.assign({ pill: 'limited', text: T('额度告急') }, base));
1511
1876
  }
1512
1877
  // 隐藏掉的组件不进「需要处理」(如收起 10Router 后不再展示其额度告急)
1513
1878
  const attVisible = att.filter((x) => compVisible(x.view));
1514
- $('att-count').textContent = attVisible.length ? attVisible.length + ' 项' : '';
1879
+ $('att-count').textContent = attVisible.length ? attVisible.length + T(' 项') : '';
1515
1880
  $('attention').innerHTML = attVisible.length
1516
1881
  ? attVisible.slice(0, 8).map((x) => '<div class="item" data-view="' + x.view + '" title="' + esc(x.title || '') + '"><span class="item-main"><span class="item-name">' + esc(x.name) + '</span>' + x.badges + '</span>'
1517
1882
  + '<span class="item-side"><span class="pill ' + x.pill + '">' + esc(x.text) + '</span></span></div>').join('')
1518
- + (attVisible.length > 8 ? '<div class="muted-empty">还有 ' + (attVisible.length - 8) + ' 项</div>' : '')
1519
- : '<div class="muted-empty">' + (state.accounts.length ? '<span class="ok-glyph">' + I.check + '</span>一切正常' : '还没有账号') + '</div>';
1883
+ + (attVisible.length > 8 ? T('<div class="muted-empty">还有 ') + (attVisible.length - 8) + T(' 项</div>') : '')
1884
+ : '<div class="muted-empty">' + (state.accounts.length ? '<span class="ok-glyph">' + I.check + T('</span>一切正常') : T('还没有账号')) + '</div>';
1520
1885
 
1521
1886
  // 最近领取记录
1522
- const RES = { 'checked-in': ['ok', '领取成功'], already: ['ok', '已领过'], limited: ['limited', '本机已领'], 'no-activity': ['pending', '无可领活动'], failed: ['fail', '失败'] };
1887
+ const RES = { 'checked-in': ['ok', T('领取成功')], already: ['ok', T('已领过')], limited: ['limited', T('本机已领')], 'no-activity': ['pending', T('无可领活动')], failed: ['fail', T('失败')] };
1523
1888
  const recent = state.accounts.filter((a) => a.lastResult && a.lastResult.at && compVisible(a.product))
1524
1889
  .sort((x, y) => String(y.lastResult.at).localeCompare(String(x.lastResult.at))).slice(0, 8);
1525
- $('last-tick').textContent = sch.lastTick ? '上一轮 ' + fmtTime(sch.lastTick.at).slice(5) : '';
1890
+ $('last-tick').textContent = sch.lastTick ? T('上一轮 ') + fmtTime(sch.lastTick.at).slice(5) : '';
1526
1891
  $('recent').innerHTML = recent.length
1527
1892
  ? recent.map((a) => {
1528
1893
  const r = a.lastResult;
1529
1894
  let [cls, txt] = RES[r.status] || ['pending', r.status];
1530
1895
  // 国际版 WorkBuddy 是保持活跃(不保证奖励),不叫「领取」
1531
- if (a.provider === 'workbuddy-intl' && r.status === 'checked-in') txt = '活跃达成';
1896
+ if (a.provider === 'workbuddy-intl' && r.status === 'checked-in') txt = T('活跃达成');
1532
1897
  const label = r.status === 'checked-in' && r.amount ? txt + ' +' + fmtNum(r.amount) : txt;
1533
1898
  return '<div class="item" data-view="' + a.product + '" title="' + esc(r.message || '') + '"><span class="item-main"><span class="item-name">' + esc(a.name || a.email || a.id) + '</span>' + provBadges(a.provider) + '</span>'
1534
1899
  + '<span class="item-side"><span class="pill ' + cls + '">' + esc(label) + '</span><span class="item-time">' + fmtTime(r.at).slice(5) + '</span></span></div>';
1535
1900
  }).join('')
1536
- : '<div class="muted-empty">还没有领取记录</div>';
1901
+ : '<div class="muted-empty">' + T('还没有领取记录') + '</div>';
1537
1902
  }
1538
1903
 
1539
1904
  function renderStatus() {
1540
1905
  const s = state.status;
1541
1906
  if (!s) return;
1542
- $('ver').textContent = 'v' + s.version + ' · 数据目录 ' + s.dataDir;
1907
+ const buildTag = s.build && s.build.commit ? ' (' + s.build.commit + ')' : '';
1908
+ $('ver').textContent = 'v' + s.version + buildTag + T(' · 数据目录 ') + s.dataDir;
1909
+ if (s.build) $('ver').title = (s.build.commit || '') + (s.build.builtAt ? ' · ' + s.build.builtAt : '') + (s.build.channel && s.build.channel !== 'release' ? ' · ' + s.build.channel + (s.build.dirty ? '-dirty' : '') : '');
1543
1910
  document.title = 'CreditDaddy v' + s.version;
1544
- if (s.homepage) $('btn-home').href = s.homepage;
1911
+ if (s.homepage) $('hm-home').href = s.homepage;
1912
+ // 菜单脚注版本:面板版本 + 数据目录(与品牌区同一个口径,换行折行由 CSS 收敛)
1913
+ $('hmenu-version').textContent = 'CreditDaddy v' + s.version;
1914
+ $('hmenu-version').title = s.dataDir ? T('数据目录 ') + s.dataDir : '';
1915
+ // 状态胶囊:领取中显示强调色文字,空闲显示「下次自动领取 HH:MM」(时间用 <b> 提权重)
1545
1916
  const sch = s.scheduler || {};
1546
- $('sched').textContent = sch.ticking ? '正在领取…'
1547
- : sch.nextTickAt ? '下次自动领取 ' + fmtTime(sch.nextTickAt).slice(11) : '';
1548
- $('sched').title = sch.lastTick ? '上一轮 ' + fmtTime(sch.lastTick.at) + ':' + sch.lastTick.summary : '';
1917
+ const chip = $('sched');
1918
+ chip.classList.toggle('busy', Boolean(sch.ticking));
1919
+ if (sch.ticking) {
1920
+ chip.innerHTML = I.refresh + T('正在领取…');
1921
+ } else if (sch.nextTickAt) {
1922
+ chip.innerHTML = I.clock + T('下次自动领取 <b class="num">') + fmtTime(sch.nextTickAt).slice(11) + '</b>';
1923
+ } else {
1924
+ chip.innerHTML = '';
1925
+ }
1926
+ chip.title = sch.lastTick ? T('上一轮 ') + fmtTime(sch.lastTick.at) + T(':') + sch.lastTick.summary : '';
1549
1927
  // 产品相关提示只在对应标签页出现
1550
1928
  const hasIntl = state.view === 'qoder' && state.region !== 'cn' && state.accounts.some((a) => a.provider === 'qoder');
1551
1929
  const n = $('notice');
1552
1930
  n.classList.toggle('info', state.view === 'zcode' || state.view === 'mirasim' || state.view === 'catpaw' || state.view === 'trae' || state.view === 'minimax' || (hasIntl && Boolean(s.riskIdentity)));
1553
1931
  if (state.view === 'minimax') {
1554
1932
  n.style.display = '';
1555
- n.innerHTML = '<div class="z-row"><span class="z-label">每日签到</span><span class="z-text">每日签到领 800~2000 积分 + 400 连签加成。支持自动检测本机 MiniMax Code 凭据与 Bearer Token 导入,自带标准 Anthropic 本地网关。</span></div>';
1933
+ n.innerHTML = T('<div class="z-row"><span class="z-label">每日签到</span><span class="z-text">每日签到领 800~2000 积分 + 400 连签加成。支持自动检测本机 MiniMax Code 凭据与 Bearer Token 导入,自带标准 Anthropic 本地网关。</span></div>');
1556
1934
  } else if (state.view === 'trae') {
1557
1935
  n.style.display = '';
1558
- n.innerHTML = '<div class="z-row"><span class="z-label">每日签到</span><span class="z-text">每日签到领约 150 通用积分(7 天有效,多资源包自动归并)。凭据由本机 Trae 客户端解密导入,支持多账号快照一键切换。</span></div>';
1936
+ n.innerHTML = T('<div class="z-row"><span class="z-label">每日签到</span><span class="z-text">每日签到领约 150 通用积分(7 天有效,多资源包自动归并)。凭据由本机 Trae 客户端解密导入,支持多账号快照一键切换。</span></div>');
1559
1937
  } else if (state.view === 'catpaw') {
1560
1938
  n.style.display = '';
1561
- n.innerHTML = '妙手(美团 CatPaw)按套餐发放 Credits(体验版 / 专业版…),没有每日签到。凭据由妙手客户端登录管理:读取额度用本机解密出的登录 token 直连桌面网关,token 失效时到妙手客户端重新登录后再「本机导入」即可。'
1562
- + '多账号切换:写入本机凭据后重启妙手客户端即生效(切换会自动保存当前登录,不丢号)。'
1939
+ n.innerHTML = T('妙手(美团 CatPaw)按套餐发放 Credits(体验版 / 专业版…),没有每日签到。凭据由妙手客户端登录管理:读取额度用本机解密出的登录 token 直连桌面网关,token 失效时到妙手客户端重新登录后再「本机导入」即可。')
1940
+ + T('多账号切换:写入本机凭据后重启妙手客户端即生效(切换会自动保存当前登录,不丢号)。')
1563
1941
  + catpawClientChip(s);
1564
1942
  } else if (state.view === 'mirasim') {
1565
1943
  n.style.display = '';
1566
- n.innerHTML = 'mirasim(原生 AI 编程开发环境)额度按「平台额度」(5h 滚动窗口、7d 全局上限与分模型上限)分配。Token 约 1 小时过期,读取额度时会自动用 refreshToken 静默续期并同步回客户端,无需手动处理。'
1567
- + '切换账号需要至少两个已导入的账号(当前登录 + 目标账号):先在「添加账号 → 本机导入」把各账号分别导入,切换时当前登录会自动保存、目标账号写入客户端,桌面版切完自动重启 mirasim。'
1944
+ n.innerHTML = T('mirasim(原生 AI 编程开发环境)额度按「平台额度」(5h 滚动窗口、7d 全局上限与分模型上限)分配。Token 约 1 小时过期,读取额度时会自动用 refreshToken 静默续期并同步回客户端,无需手动处理。')
1945
+ + T('切换账号需要至少两个已导入的账号(当前登录 + 目标账号):先在「添加账号 → 本机导入」把各账号分别导入,切换时当前登录会自动保存、目标账号写入客户端,桌面版切完自动重启 mirasim。')
1568
1946
  + mirasimClientChip(s);
1569
1947
  } else if (state.view === 'zcode') {
1570
1948
  n.style.display = '';
1571
1949
  const ac = state.zAutoClaim === true;
1572
1950
  const ziv = state.zClaimIntervalMin || 2;
1573
1951
  const zwin = state.zClaimWindowMin === 0 ? 0 : (state.zClaimWindowMin || 60);
1574
- n.innerHTML = '<div class="z-row"><span class="z-label">活动领取</span><span class="z-text">活动包积分,每日 0 点左右刷新(无每日签到)。自动领取'
1575
- + (ac ? '每 ' + ziv + ' 分钟检查资格' + (zwin > 0 ? ',本次运行 ' + zwin + ' 分钟' : ',持续运行') : '未开启(右上角设置轮询计划)')
1576
- + ';礼物图标 = 手动临时领。</span></div>';
1952
+ n.innerHTML = T('<div class="z-row"><span class="z-label">活动领取</span><span class="z-text">活动包积分,每日 0 点左右刷新(无每日签到)。自动领取')
1953
+ + (ac ? T('每 ') + ziv + T(' 分钟检查资格') + (zwin > 0 ? T(',本次运行 ') + zwin + T(' 分钟') : T(',持续运行')) : T('未开启(右上角设置轮询计划)'))
1954
+ + T(';礼物图标 = 手动临时领。</span></div>');
1577
1955
  } else if (hasIntl && !s.riskIdentity && umidOffer(s)) {
1578
1956
  n.style.display = '';
1579
1957
  const busy = s.umid.installing || umidBusy;
1580
- n.innerHTML = 'Qoder 国际版只向带设备风控身份的请求下发「每天领 100 Credits」。这台 Linux / NAS 上没有 Qoder 客户端,'
1581
- + '可以安装「设备身份组件」:从 npm 官方包 @qoder-ai/qodercli 提取其内置的 UMID 程序(约 30MB 下载,校验 npm 完整性),装好后国际版即可领取(本机每天限领一个账号)。'
1582
- + ' <button class="btn primary" id="btn-umid" style="margin-left:6px"' + (busy ? ' disabled' : '') + '>' + (busy ? '安装中…' : '安装设备身份组件') + '</button>';
1958
+ n.innerHTML = T('Qoder 国际版只向带设备风控身份的请求下发「每天领 100 Credits」。这台 Linux / NAS 上没有 Qoder 客户端,')
1959
+ + T('可以安装「设备身份组件」:从 npm 官方包 @qoder-ai/qodercli 提取其内置的 UMID 程序(约 30MB 下载,校验 npm 完整性),装好后国际版即可领取(本机每天限领一个账号)。')
1960
+ + ' <button class="btn primary" id="btn-umid" style="margin-left:6px"' + (busy ? ' disabled' : '') + '>' + (busy ? T('安装中…') : T('安装设备身份组件')) + '</button>';
1583
1961
  } else if (hasIntl && !s.riskIdentity) {
1584
1962
  n.style.display = '';
1585
- n.textContent = 'Qoder 国际版领取需要本机安装 Qoder 客户端:Qoder 只向带设备风控身份(由客户端生成)的请求下发「每天领 100 Credits」。当前未检测到客户端,国际版账号将无法领取。';
1963
+ n.textContent = T('Qoder 国际版领取需要本机安装 Qoder 客户端:Qoder 只向带设备风控身份(由客户端生成)的请求下发「每天领 100 Credits」。当前未检测到客户端,国际版账号将无法领取。');
1586
1964
  } else if (hasIntl) {
1587
1965
  n.style.display = '';
1588
- n.textContent = '提示:Qoder 国际版每台设备每天只能有一个账号领取每日 100 Credits(服务端按设备限领),其余国际版账号会显示「本机已领」。'
1589
- + (s.riskSource === 'cli' && s.umid && s.umid.installed ? '(风控身份来自设备身份组件 qodercli ' + s.umid.installed.version + ')' : '');
1966
+ n.textContent = T('提示:Qoder 国际版每台设备每天只能有一个账号领取每日 100 Credits(服务端按设备限领),其余国际版账号会显示「本机已领」。')
1967
+ + (s.riskSource === 'cli' && s.umid && s.umid.installed ? T('(风控身份来自设备身份组件 qodercli ') + s.umid.installed.version + T(')') : '');
1590
1968
  } else {
1591
1969
  n.style.display = 'none';
1592
1970
  }
@@ -1595,20 +1973,20 @@ function renderStatus() {
1595
1973
  // mirasim 客户端状态芯片 + 「打开客户端」按钮
1596
1974
  function mirasimClientChip(s) {
1597
1975
  const c = s.mirasimClient || {};
1598
- if (!c.installed) return ' <span class="dim">客户端:未安装</span>';
1976
+ if (!c.installed) return T(' <span class="dim">客户端:未安装</span>');
1599
1977
  const canOpen = window.creditdaddy && typeof window.creditdaddy.openMirasimClient === 'function';
1600
- return ' <span class="dim">客户端:' + (c.running ? '运行中' : '未运行') + '</span>'
1601
- + (canOpen ? ' <button class="btn" id="btn-miraopen" style="margin-left:4px" title="打开本机 Mirasim 客户端">打开客户端</button>' : '');
1978
+ return T(' <span class="dim">客户端:') + (c.running ? T('运行中') : T('未运行')) + '</span>'
1979
+ + (canOpen ? T(' <button class="btn" id="btn-miraopen" style="margin-left:4px" title="打开本机 Mirasim 客户端">打开客户端</button>') : '');
1602
1980
  }
1603
1981
 
1604
1982
 
1605
1983
  // 妙手客户端状态芯片 + 「打开客户端」按钮
1606
1984
  function catpawClientChip(s) {
1607
1985
  const c = s.catpawClient || {};
1608
- if (!c.installed) return ' <span class="dim">客户端:未安装(本机导入 / 切换需要 Windows 妙手客户端)</span>';
1986
+ if (!c.installed) return T(' <span class="dim">客户端:未安装(本机导入 / 切换需要 Windows 妙手客户端)</span>');
1609
1987
  const canOpen = window.creditdaddy && typeof window.creditdaddy.openCatpawClient === 'function';
1610
- return ' <span class="dim">客户端:' + (c.running ? '运行中' : '未运行') + (c.signedIn ? '' : ' · 未登录') + '</span>'
1611
- + (canOpen ? ' <button class="btn" id="btn-cpopen" style="margin-left:4px" title="打开本机妙手客户端">打开客户端</button>' : '');
1988
+ return T(' <span class="dim">客户端:') + (c.running ? T('运行中') : T('未运行')) + (c.signedIn ? '' : T(' · 未登录')) + '</span>'
1989
+ + (canOpen ? T(' <button class="btn" id="btn-cpopen" style="margin-left:4px" title="打开本机妙手客户端">打开客户端</button>') : '');
1612
1990
  }
1613
1991
 
1614
1992
  // Qoder 设备身份组件(Linux / fnOS)
@@ -1618,7 +1996,7 @@ async function installUmid() {
1618
1996
  umidBusy = true; renderStatus();
1619
1997
  try {
1620
1998
  const r = await api('/api/qoder/umid/install', { method: 'POST' });
1621
- toast('设备身份组件已安装(qodercli ' + r.installed.version + '),Qoder 国际版可以领取了');
1999
+ toast(T('设备身份组件已安装(qodercli ') + r.installed.version + T('),Qoder 国际版可以领取了'));
1622
2000
  } catch (e) { toast(e.message, false); }
1623
2001
  umidBusy = false;
1624
2002
  await loadStatus(); render(); loadLogs();
@@ -1629,40 +2007,32 @@ document.addEventListener('click', async (e) => {
1629
2007
  if (e.target.closest('#zt-open')) {
1630
2008
  try {
1631
2009
  const r = await window.creditdaddy.openZcodeClient();
1632
- if (r && r.ok) toast('已启动 ZCode 客户端');
1633
- else toast((r && r.error) || '无法打开 ZCode 客户端', false);
2010
+ if (r && r.ok) toast(T('已启动 ZCode 客户端'));
2011
+ else toast((r && r.error) || T('无法打开 ZCode 客户端'), false);
1634
2012
  } catch (err) { toast(String(err && err.message || err), false); }
1635
2013
  return;
1636
2014
  }
1637
2015
  if (e.target.closest('#btn-miraopen')) {
1638
2016
  try {
1639
2017
  const r = await window.creditdaddy.openMirasimClient();
1640
- if (r && r.ok) toast('已启动 Mirasim 客户端');
1641
- else toast((r && r.error) || '无法打开 Mirasim 客户端', false);
2018
+ if (r && r.ok) toast(T('已启动 Mirasim 客户端'));
2019
+ else toast((r && r.error) || T('无法打开 Mirasim 客户端'), false);
1642
2020
  } catch (err) { toast(String(err && err.message || err), false); }
1643
2021
  return;
1644
2022
  }
1645
2023
  if (e.target.closest('#btn-cpopen')) {
1646
2024
  try {
1647
2025
  const r = await window.creditdaddy.openCatpawClient();
1648
- if (r && r.ok) toast('已启动妙手客户端');
1649
- else toast((r && r.error) || '无法打开妙手客户端', false);
2026
+ if (r && r.ok) toast(T('已启动妙手客户端'));
2027
+ else toast((r && r.error) || T('无法打开妙手客户端'), false);
1650
2028
  } catch (err) { toast(String(err && err.message || err), false); }
1651
2029
  return;
1652
2030
  }
1653
2031
  if (e.target.closest('#tt-open')) {
1654
2032
  try {
1655
2033
  const r = await window.creditdaddy.openTraeClient();
1656
- if (r && r.ok) toast('已启动 Trae 客户端');
1657
- else toast((r && r.error) || '无法打开 Trae 客户端', false);
1658
- } catch (err) { toast(String(err && err.message || err), false); }
1659
- return;
1660
- }
1661
- if (e.target.closest('#mt-open')) {
1662
- try {
1663
- const r = await window.creditdaddy.openMiniMaxClient();
1664
- if (r && r.ok) toast('已启动 MiniMax Code 客户端');
1665
- else toast((r && r.error) || '无法打开 MiniMax Code 客户端', false);
2034
+ if (r && r.ok) toast(T('已启动 Trae 客户端'));
2035
+ else toast((r && r.error) || T('无法打开 Trae 客户端'), false);
1666
2036
  } catch (err) { toast(String(err && err.message || err), false); }
1667
2037
  return;
1668
2038
  }
@@ -1673,14 +2043,22 @@ document.addEventListener('click', async (e) => {
1673
2043
  const allow = Array.isArray(state.tGatewayAllow) ? state.tGatewayAllow : [];
1674
2044
  $('tgw-allow').value = (allow.length ? allow : (state.tGatewaySuggest ? [state.tGatewaySuggest] : [])).join('\n');
1675
2045
  const st = state.tGatewayStats;
1676
- $('tgw-url').innerHTML = '接口地址 <code>POST http://127.0.0.1:' + location.port + '/gateway/trae/v1/messages</code>(局域网开放时把 127.0.0.1 换成本机 IP)'
2046
+ $('tgw-url').innerHTML = T('接口地址 <code>POST http://127.0.0.1:') + location.port + T('/gateway/trae/v1/messages</code>(局域网开放时把 127.0.0.1 换成本机 IP)')
1677
2047
  + (st && st.lastCallAt
1678
- ? '<br>最近调用 ' + esc(String(st.lastCallAt).slice(11, 19)) + ' · 共 ' + st.calls + ' 次' + (st.lastAccount ? ' · 最后用号 ' + esc(st.lastAccount) : '')
1679
- : '<br>尚未有调用');
2048
+ ? T('<br>最近调用 ') + esc(String(st.lastCallAt).slice(11, 19)) + T(' · 共 ') + st.calls + T(' 次') + (st.lastAccount ? T(' · 最后用号 ') + esc(st.lastAccount) : '')
2049
+ : T('<br>尚未有调用'));
1680
2050
  syncTgwLanFields();
1681
2051
  openModal('m-tgw');
1682
2052
  return;
1683
2053
  }
2054
+ if (e.target.closest('#mt-open')) {
2055
+ try {
2056
+ const r = await window.creditdaddy.openMiniMaxClient();
2057
+ if (r && r.ok) toast(T('已启动 MiniMax Code 客户端'));
2058
+ else toast((r && r.error) || T('无法打开 MiniMax Code 客户端'), false);
2059
+ } catch (err) { toast(String(err && err.message || err), false); }
2060
+ return;
2061
+ }
1684
2062
  if (e.target.closest('#mt-gw')) {
1685
2063
  // MiniMax 本地接口弹窗:开关 + 局域网 + 白名单设置
1686
2064
  $('mgw-on').checked = state.mGateway === true;
@@ -1688,10 +2066,10 @@ document.addEventListener('click', async (e) => {
1688
2066
  const allow = Array.isArray(state.mGatewayAllow) ? state.mGatewayAllow : [];
1689
2067
  $('mgw-allow').value = (allow.length ? allow : (state.mGatewaySuggest ? [state.mGatewaySuggest] : [])).join('\n');
1690
2068
  const st = state.mGatewayStats;
1691
- $('mgw-url').innerHTML = '接口地址 <code>POST http://127.0.0.1:' + location.port + '/gateway/minimax/v1/messages</code>(局域网开放时把 127.0.0.1 换成本机 IP)'
2069
+ $('mgw-url').innerHTML = T('接口地址 <code>POST http://127.0.0.1:') + location.port + T('/gateway/minimax/v1/messages</code>(局域网开放时把 127.0.0.1 换成本机 IP)')
1692
2070
  + (st && st.lastCallAt
1693
- ? '<br>最近调用 ' + esc(String(st.lastCallAt).slice(11, 19)) + ' · 共 ' + st.calls + ' 次' + (st.lastAccount ? ' · 最后用号 ' + esc(st.lastAccount) : '')
1694
- : '<br>尚未有调用');
2071
+ ? T('<br>最近调用 ') + esc(String(st.lastCallAt).slice(11, 19)) + T(' · 共 ') + st.calls + T(' 次') + (st.lastAccount ? T(' · 最后用号 ') + esc(st.lastAccount) : '')
2072
+ : T('<br>尚未有调用'));
1695
2073
  syncMgwLanFields();
1696
2074
  openModal('m-mgw');
1697
2075
  return;
@@ -1701,7 +2079,7 @@ document.addEventListener('click', async (e) => {
1701
2079
  $('zac-interval').value = state.zClaimIntervalMin || 2;
1702
2080
  $('zac-window').value = state.zClaimWindowMin === 0 ? 0 : (state.zClaimWindowMin || 60);
1703
2081
  $('zac-off').style.display = state.zAutoClaim === true ? '' : 'none';
1704
- $('zac-ok').textContent = state.zAutoClaim === true ? '保存' : '保存并启用';
2082
+ $('zac-ok').textContent = state.zAutoClaim === true ? T('保存') : T('保存并启用');
1705
2083
  openModal('m-zac');
1706
2084
  return;
1707
2085
  }
@@ -1712,22 +2090,14 @@ document.addEventListener('click', async (e) => {
1712
2090
  const allow = Array.isArray(state.zGatewayAllow) ? state.zGatewayAllow : [];
1713
2091
  $('zgw-allow').value = (allow.length ? allow : (state.zGatewaySuggest ? [state.zGatewaySuggest] : [])).join('\n');
1714
2092
  const st = state.zGatewayStats;
1715
- $('zgw-url').innerHTML = '接口地址 <code>POST http://127.0.0.1:' + location.port + '/gateway/zcode/v1/messages</code>(局域网开放时把 127.0.0.1 换成本机 IP)'
2093
+ $('zgw-url').innerHTML = T('接口地址 <code>POST http://127.0.0.1:') + location.port + T('/gateway/zcode/v1/messages</code>(局域网开放时把 127.0.0.1 换成本机 IP)')
1716
2094
  + (st && st.lastCallAt
1717
- ? '<br>最近调用 ' + esc(String(st.lastCallAt).slice(11, 19)) + ' · 共 ' + st.calls + ' 次' + (st.lastAccount ? ' · 最后用号 ' + esc(st.lastAccount) : '')
1718
- : '<br>尚未有调用');
2095
+ ? T('<br>最近调用 ') + esc(String(st.lastCallAt).slice(11, 19)) + T(' · 共 ') + st.calls + T(' 次') + (st.lastAccount ? T(' · 最后用号 ') + esc(st.lastAccount) : '')
2096
+ : T('<br>尚未有调用'));
1719
2097
  syncZgwLanFields();
1720
2098
  openModal('m-zgw');
1721
2099
  return;
1722
2100
  }
1723
- if (e.target.closest('#zt-net')) {
1724
- // 出口弹窗:顺序 + 代理地址一起保存;代理只回填到 placeholder(脱敏),留空即清除
1725
- (state.zProxyFirst === true ? $('znet-proxy') : $('znet-direct')).checked = true;
1726
- $('znet-url').value = '';
1727
- $('znet-url').placeholder = state.zProxyUrl || (state.zHasEnvProxy ? '跟随环境变量 HTTPS_PROXY' : 'http://192.168.31.101:7890');
1728
- openModal('m-znet');
1729
- return;
1730
- }
1731
2101
  });
1732
2102
  async function loadZNet() {
1733
2103
  try {
@@ -1809,11 +2179,11 @@ function quotaLow(c) {
1809
2179
 
1810
2180
  function quotaValue(q) {
1811
2181
  const pct = quotaPct(q);
1812
- if (q.percentScale || (!(q.total > 0) && pct !== null)) return { big: fmtNum(pct) + '%', small: '剩余', pct };
1813
- if (q.total > 0) return { big: fmtNum(quotaRemaining(q)), small: '/ ' + fmtNum(q.total) + (q.unit ? ' ' + q.unit : ''), pct };
1814
- return { big: fmtNum(q.used || 0), small: '已用', pct };
2182
+ if (q.percentScale || (!(q.total > 0) && pct !== null)) return { big: fmtNum(pct) + '%', small: T('剩余'), pct };
2183
+ if (q.total > 0) return { big: fmtNum(quotaRemaining(q)), small: '/ ' + fmtNum(q.total) + (q.unit ? ' ' + T(q.unit) : ''), pct };
2184
+ return { big: fmtNum(q.used || 0), small: T('已用'), pct };
1815
2185
  }
1816
- const resetLabel = (q) => (q.resetAt ? fmtTime(q.resetAt).slice(5) + (q.recurring === false ? ' 到期' : ' 重置') : '');
2186
+ const resetLabel = (q) => (q.resetAt ? fmtTime(q.resetAt).slice(5) + (q.recurring === false ? T(' 到期') : T(' 重置')) : '');
1817
2187
 
1818
2188
  // 明细整行:名称 + 数值/相对重置 + 水位条(CLIProxyAPI quotaRow 版式)
1819
2189
  function trRow(q) {
@@ -1822,11 +2192,12 @@ function trRow(q) {
1822
2192
 
1823
2193
  function lineVal(q) {
1824
2194
  const pct = quotaPct(q);
1825
- // 百分比口径(限时限额额度):一律显示 %,节约空间;积分类保持实际数字
1826
- const pctScale = q.percentScale || Number.isFinite(q.remainingPercentage);
1827
- const txt = (pctScale || !(q.total > 0))
1828
- ? '剩 ' + (pct !== null ? fmtNum(pct) + '%' : fmtNum(quotaRemaining(q)))
1829
- : '剩 ' + fmtNum(quotaRemaining(q)) + ' / ' + fmtNum(q.total || 0) + (q.unit ? ' ' + q.unit : '');
2195
+ // 数值优先:有 total 就显示「剩 x / y」——remainingPercentage 是上游四舍五入的
2196
+ // 粗略值(3.99/4 → 100%),只在没有绝对量时才退回百分比口径;
2197
+ // percentScale(Antigravity 这类限时窗口平等关系)保持百分比。
2198
+ const txt = (q.percentScale || !(q.total > 0))
2199
+ ? T('剩 ') + (pct !== null ? fmtNum(pct) + '%' : fmtNum(quotaRemaining(q)))
2200
+ : T('剩 ') + fmtNum(quotaRemaining(q)) + ' / ' + fmtNum(q.total || 0) + (q.unit ? ' ' + T(q.unit) : '');
1830
2201
  return '<span>' + esc(txt) + '</span> ' + relResetHtml(q);
1831
2202
  }
1832
2203
 
@@ -1835,7 +2206,7 @@ function lineVal(q) {
1835
2206
  function trPrimaryHtml(q) {
1836
2207
  if (q.percentScale) return trRow(q);
1837
2208
  const v = quotaValue(q);
1838
- return '<div class="tr-primary"><div class="tr-line tr-primary-line"><span class="tr-q-name" title="' + esc(q.name) + '">' + esc(q.name) + '</span>'
2209
+ return '<div class="tr-primary"><div class="tr-line tr-primary-line"><span class="tr-q-name" title="' + esc(T(q.name)) + '">' + esc(T(q.name)) + '</span>'
1839
2210
  + '<span class="tr-q-val"><b>' + esc(v.big) + '</b> <small class="dim">' + esc(v.small) + '</small> ' + relResetHtml(q) + '</span></div>'
1840
2211
  + (v.pct !== null ? trMeter(v.pct) : '')
1841
2212
  + '</div>';
@@ -1844,15 +2215,15 @@ function trPrimaryHtml(q) {
1844
2215
  function trQuotaHtml(q, { dimWhenEmpty = false } = {}) {
1845
2216
  const v = quotaValue(q);
1846
2217
  const used = v.pct !== null && v.pct <= 0;
1847
- const val = used && dimWhenEmpty ? '已用完' : (q.percentScale || !(q.total > 0) ? '剩余 ' + v.big : '剩余 ' + v.big + ' ' + v.small);
1848
- return '<div class="tr-q' + (used && dimWhenEmpty ? ' spent' : '') + '"><div class="tr-q-line"><span class="tr-q-name" title="' + esc(q.name) + '">' + esc(q.name) + '</span>'
2218
+ const val = used && dimWhenEmpty ? T('已用完') : (q.percentScale || !(q.total > 0) ? T('剩余 ') + v.big : T('剩余 ') + v.big + ' ' + v.small);
2219
+ return '<div class="tr-q' + (used && dimWhenEmpty ? ' spent' : '') + '"><div class="tr-q-line"><span class="tr-q-name" title="' + esc(T(q.name)) + '">' + esc(T(q.name)) + '</span>'
1849
2220
  + '<span class="tr-q-val">' + esc(val) + relResetHtml(q) + '</span></div>'
1850
2221
  + (!(used && dimWhenEmpty) ? trMeter(v.pct) : '')
1851
2222
  + '</div>';
1852
2223
  }
1853
2224
 
1854
2225
  // 积分包按名称系列聚合("Bonus Pack 18/19/20" → "Bonus Pack ×3"),去掉尾部序号作键
1855
- const packKey = (name) => String(name || '').replace(/[\s#(]*\d+\s*[)]?\s*$/, '').trim() || '积分包';
2226
+ const packKey = (name) => String(name || '').replace(/[\s#(]*\d+\s*[)]?\s*$/, '').trim() || T('积分包');
1856
2227
  function trPacksHtml(c, packs) {
1857
2228
  if (!packs.length) return '';
1858
2229
  const alive = packs.filter((q) => quotaRemaining(q) > 0);
@@ -1871,13 +2242,13 @@ function trPacksHtml(c, packs) {
1871
2242
  const gs = [...groups.values()].sort((a, b) => String(a.resetAt || '9999').localeCompare(String(b.resetAt || '9999')));
1872
2243
  const aliveSum = alive.reduce((n, q) => n + quotaRemaining(q), 0);
1873
2244
  const open = Boolean(state.trOpen && state.trOpen[c.id]);
1874
- const head = '<div class="tr-packs-head">积分包 <b>' + alive.length + '</b> 个可用'
1875
- + (alive.length ? ',共剩 ' + fmtNum(aliveSum) : '')
1876
- + (spent.length ? ' · ' + spent.length + ' 个已用完' : '') + '</div>';
2245
+ const head = T('<div class="tr-packs-head">积分包 <b>') + alive.length + T('</b> 个可用')
2246
+ + (alive.length ? T(',共剩 ') + fmtNum(aliveSum) : '')
2247
+ + (spent.length ? ' · ' + spent.length + T(' 个已用完') : '') + '</div>';
1877
2248
  const lines = gs.map((g) => trLine(
1878
2249
  g.name + (g.count > 1 ? ' ×' + g.count : ''),
1879
- esc('剩 ' + fmtNum(g.rem) + (g.tot > 0 ? ' / ' + fmtNum(g.tot) : ''))
1880
- + (g.resetAt ? '<span class="tr-q-when"> · 最早 ' + esc(fmtTime(g.resetAt).slice(5, 10)) + ' 到期</span>' : ''),
2250
+ esc(T('剩 ') + fmtNum(g.rem) + (g.tot > 0 ? ' / ' + fmtNum(g.tot) : ''))
2251
+ + (g.resetAt ? T('<span class="tr-q-when"> · 最早 ') + esc(fmtTime(g.resetAt).slice(5, 10)) + T(' 到期</span>') : ''),
1881
2252
  )).join('');
1882
2253
  const detail = open
1883
2254
  ? [...alive.sort((a, b) => String(a.resetAt || '9999').localeCompare(String(b.resetAt || '9999'))), ...spent]
@@ -1885,7 +2256,7 @@ function trPacksHtml(c, packs) {
1885
2256
  : '';
1886
2257
  const collapsible = alive.length > gs.length || spent.length > 0;
1887
2258
  return '<div class="tr-packs">' + head + (open ? detail : lines)
1888
- + (collapsible ? '<button class="tr-more" data-tr="toggle" data-id="' + esc(c.id) + '">' + (open ? '收起' : '逐包明细') + '</button>' : '')
2259
+ + (collapsible ? '<button class="tr-more" data-tr="toggle" data-id="' + esc(c.id) + '">' + (open ? T('收起') : T('逐包明细')) + '</button>' : '')
1889
2260
  + '</div>';
1890
2261
  }
1891
2262
 
@@ -1896,11 +2267,11 @@ function trCardHtml(c) {
1896
2267
  const showPlan = c.plan && !norm(c.plan).includes(norm(c.providerName)) && !norm(c.providerName).includes(norm(c.plan));
1897
2268
  let body;
1898
2269
  if (c.error) {
1899
- body = '<div class="msg err" title="' + esc(c.error) + '">查询失败:' + esc(c.error.slice(0, 80)) + '</div>';
2270
+ body = '<div class="msg err" title="' + esc(T(c.error)) + '">' + T('查询失败:') + esc(c.error.slice(0, 80)) + '</div>';
1900
2271
  } else {
1901
2272
  const { primary, windows, packs } = splitQuotas(c);
1902
2273
  if (!(primary || windows.length || packs.length)) {
1903
- body = '<div class="msg">' + esc(/no separate quota|billed at standard/i.test(c.message || '') ? '按量计费,无额度可显示' : (c.message || '该供应商未返回额度数据')) + '</div>';
2274
+ body = '<div class="msg">' + esc(/no separate quota|billed at standard/i.test(c.message || '') ? T('按量计费,无额度可显示') : (c.message || T('该供应商未返回额度数据'))) + '</div>';
1904
2275
  } else {
1905
2276
  // 与汇总卡同款默认视图:主额度 + 每个周 / 月窗口额度一条(带进度条);积分包走聚合行,「逐包明细」展开单包
1906
2277
  body = (primary ? trPrimaryHtml(primary) : '')
@@ -1910,12 +2281,12 @@ function trCardHtml(c) {
1910
2281
  }
1911
2282
  return '<div class="tr-card' + (c.isActive ? '' : ' off') + (body && body.startsWith('<div class="msg') ? ' flat' : '') + '" draggable="true" data-id="' + esc(c.id) + '" data-vendor="' + esc(c.providerName || c.provider || '') + '"><div class="row1"><div class="name-group">'
1912
2283
  + '<span class="badge prod tenrouter">' + esc(c.providerName) + '</span>'
1913
- + (TR_REGION[c.provider] ? '<span class="badge ' + TR_REGION[c.provider][1] + '">' + TR_REGION[c.provider][0] + '</span>' : '')
2284
+ + (TR_REGION[c.provider] ? '<span class="badge ' + TR_REGION[c.provider][1] + '">' + T(TR_REGION[c.provider][0]) + '</span>' : '')
1914
2285
  + '<span class="tr-name" title="' + esc([c.name, c.email].filter(Boolean).join(' · ')) + '">' + esc(who) + '</span>'
1915
2286
  + (showPlan ? '<span class="badge">' + esc(c.plan) + '</span>' : '')
1916
- + (c.isActive ? '' : '<span class="pill pending">已停用</span>')
1917
- + (c.limitReached ? '<span class="pill fail">已达上限</span>' : '')
1918
- + (c.isActive && !c.error && quotaLow(c) ? '<span class="pill limited">告急</span>' : '')
2287
+ + (c.isActive ? '' : T('<span class="pill pending">已停用</span>'))
2288
+ + (c.limitReached ? T('<span class="pill fail">已达上限</span>') : '')
2289
+ + (c.isActive && !c.error && quotaLow(c) ? T('<span class="pill limited">告急</span>') : '')
1919
2290
  + '</div></div>' + body + '</div>';
1920
2291
  }
1921
2292
 
@@ -1927,7 +2298,7 @@ const trSorted = (list) => list.slice().sort((a, b) => (b.isActive - a.isActive)
1927
2298
  function trGroups(list) {
1928
2299
  const map = new Map();
1929
2300
  for (const c of trSorted(list)) {
1930
- const k = c.providerName || c.provider || '其他';
2301
+ const k = c.providerName || c.provider || T('其他');
1931
2302
  if (!map.has(k)) map.set(k, []);
1932
2303
  map.get(k).push(c);
1933
2304
  }
@@ -1970,7 +2341,9 @@ function trVendorCardHtml(g) {
1970
2341
  if (!q || isAgg(q) || isDetail(q)) continue;
1971
2342
  if (!rowMap.has(q.name)) rowMap.set(q.name, { name: q.name, total: 0, rem: 0, anyTotal: false, pctScale: false, resetAt: null, pcts: [], unit: '' });
1972
2343
  const w = rowMap.get(q.name);
1973
- const isPctRow = q.percentScale || Number.isFinite(q.remainingPercentage);
2344
+ // 有绝对量(total>0)就不算百分比行:remainingPercentage 是上游四舍五入值,
2345
+ // 拿它当口径会把 3.99/4 显示成「剩 100%」,与 10Router 自己的 3.99/4 对不齐
2346
+ const isPctRow = q.percentScale || !(q.total > 0);
1974
2347
  if (isPctRow) w.pctScale = true;
1975
2348
  const pct = quotaPct(q);
1976
2349
  if (pct !== null) w.pcts.push(pct);
@@ -1995,14 +2368,14 @@ function trVendorCardHtml(g) {
1995
2368
  const alivePacks = packs.filter((q) => quotaRemaining(q) > 0);
1996
2369
  const packRem = alivePacks.reduce((n, q) => n + quotaRemaining(q), 0);
1997
2370
  const briefs = [];
1998
- if (alivePacks.length) briefs.push('积分包×' + alivePacks.length + ' 剩 ' + fmtNum(packRem));
1999
- else if (packs.length) briefs.push('积分包已用完');
2371
+ if (alivePacks.length) briefs.push(T('积分包×') + alivePacks.length + T(' 剩 ') + fmtNum(packRem));
2372
+ else if (packs.length) briefs.push(T('积分包已用完'));
2000
2373
  const hasQuotas = Boolean(primary || others.length || packs.length);
2001
2374
  let quotaHtml;
2002
2375
  if (errs === items.length) {
2003
- quotaHtml = '<div class="msg err">全部连接查询失败</div>';
2376
+ quotaHtml = T('<div class="msg err">全部连接查询失败</div>');
2004
2377
  } else if (!hasQuotas) {
2005
- quotaHtml = '<div class="msg">该供应商没有可汇总的额度</div>';
2378
+ quotaHtml = T('<div class="msg">该供应商没有可汇总的额度</div>');
2006
2379
  } else {
2007
2380
  // 「全部」视角不显示「最早到期」:每行自带相对重置时间,足够判断
2008
2381
  quotaHtml = (primary ? trPrimaryHtml(primary) : '')
@@ -2013,30 +2386,30 @@ function trVendorCardHtml(g) {
2013
2386
  }
2014
2387
  return '<div class="tr-card' + (quotaHtml && quotaHtml.startsWith('<div class="msg') ? ' flat' : '') + '" draggable="true" data-vendor="' + esc(g.name) + '"><div class="row1"><div class="name-group">'
2015
2388
  + '<span class="badge prod tenrouter">' + esc(g.name) + '</span>'
2016
- + (TR_REGION[prov] ? '<span class="badge ' + TR_REGION[prov][1] + '">' + TR_REGION[prov][0] + '</span>' : '')
2017
- + '<span class="badge">' + items.length + ' 个账号</span>'
2018
- + (lows ? '<span class="pill limited">告急 ×' + lows + '</span>' : '')
2019
- + (errs ? '<span class="pill fail" title="部分连接查询失败">失败 ×' + errs + '</span>' : '')
2389
+ + (TR_REGION[prov] ? '<span class="badge ' + TR_REGION[prov][1] + '">' + T(TR_REGION[prov][0]) + '</span>' : '')
2390
+ + '<span class="badge">' + items.length + T(' 个账号</span>')
2391
+ + (lows ? T('<span class="pill limited">告急 ×') + lows + '</span>' : '')
2392
+ + (errs ? T('<span class="pill fail" title="部分连接查询失败">失败 ×') + errs + '</span>' : '')
2020
2393
  + '</div></div>' + quotaHtml + '</div>';
2021
2394
  }
2022
2395
 
2023
2396
  function trSetupHtml() {
2024
- return '<div class="setup"><h3>接入 10Router</h3>'
2025
- + '<div class="hint">填 10Router 的地址和一个<b>虚拟 key</b>(10Router 仪表盘 → API Keys 创建的 sk-…),就能在这里:</div>'
2026
- + '<div class="hint" style="margin:6px 0 12px">· 查看 10Router 里其他供应商(Claude / Codex / Kiro / GLM …)的额度卡片(需要 10Router 1.2.1+)<br>'
2027
- + '· 把本机 ZCode / OpenCode / mirasim / 小米 MiMo / 10Router 的用量同步进 10Router 的用量统计(同 10router-sync 插件,自动每小时一次)<br>'
2028
- + '· 「10Router 本机」= 本机 10Router 实例自己记的账(如 zcode-free 经它中转的行)同步到聚合端;地址指向本机时自动跳过,避免自己导自己</div>'
2029
- + '<button class="btn primary" data-tr="settings">' + I.plus + '配置 10Router</button></div>';
2397
+ return T('<div class="setup"><h3>接入 10Router</h3>')
2398
+ + T('<div class="hint">填 10Router 的地址和一个<b>虚拟 key</b>(10Router 仪表盘 → API Keys 创建的 sk-…),就能在这里:</div>')
2399
+ + T('<div class="hint" style="margin:6px 0 12px">· 查看 10Router 里其他供应商(Claude / Codex / Kiro / GLM …)的额度卡片(需要 10Router 1.2.1+)<br>')
2400
+ + T('· 把本机 ZCode / OpenCode / mirasim / 小米 MiMo / 10Router 的用量同步进 10Router 的用量统计(同 10router-sync 插件,自动每小时一次)<br>')
2401
+ + T('· 「10Router 本机」= 本机 10Router 实例自己记的账(如 zcode-free 经它中转的行)同步到聚合端;地址指向本机时自动跳过,避免自己导自己</div>')
2402
+ + '<button class="btn primary" data-tr="settings">' + I.plus + T('配置 10Router</button></div>');
2030
2403
  }
2031
2404
 
2032
2405
  function renderTenrouter() {
2033
2406
  const box = $('tr-panel');
2034
2407
  const c = tr.config;
2035
- if (!c) { box.innerHTML = '<div class="empty">加载中…</div>'; return; }
2408
+ if (!c) { box.innerHTML = '<div class="empty">' + T('加载中…') + '</div>'; return; }
2036
2409
  const gwLine = state.zGateway === true
2037
- ? '<div class="gw-line"><span class="dot on pulse"></span> ZCode 体验包接口运行中'
2038
- + (state.zGatewayStats && state.zGatewayStats.lastCallAt ? ' · 最近调用 ' + esc(String(state.zGatewayStats.lastCallAt).slice(11, 19)) + ' · ' + state.zGatewayStats.calls + ' 次 · 最后用号 ' + esc(state.zGatewayStats.lastAccount || '-') : ' · 尚未有调用')
2039
- + ' · 模型 <code>zcode-free/glm-5.3-flash</code></div>'
2410
+ ? T('<div class="gw-line"><span class="dot on pulse"></span> ZCode 体验包接口运行中')
2411
+ + (state.zGatewayStats && state.zGatewayStats.lastCallAt ? T(' · 最近调用 ') + esc(String(state.zGatewayStats.lastCallAt).slice(11, 19)) + ' · ' + state.zGatewayStats.calls + T(' 次 · 最后用号 ') + esc(state.zGatewayStats.lastAccount || '-') : T(' · 尚未有调用'))
2412
+ + T(' · 模型 <code>zcode-free/glm-5.3-flash</code></div>')
2040
2413
  : '';
2041
2414
  if (!c.configured) { box.innerHTML = gwLine + trSetupHtml(); return; }
2042
2415
  const list = (tr.quotas && tr.quotas.connections) || [];
@@ -2063,36 +2436,36 @@ function renderTenrouter() {
2063
2436
  const activeVendor = vendor && shown.some((g) => g.name === vendor) ? vendor : '';
2064
2437
  if (groups.length) {
2065
2438
  tabStrip = '<div class="tr-tabs">'
2066
- + '<button class="btn' + (!activeVendor ? ' primary' : '') + '" data-tr="vendor" data-v="">全部</button>'
2439
+ + '<button class="btn' + (!activeVendor ? ' primary' : '') + T('" data-tr="vendor" data-v="">全部</button>')
2067
2440
  + shown.map((g) => '<button class="btn' + (activeVendor === g.name ? ' primary' : '') + '" data-tr="vendor" data-v="' + esc(g.name) + '">' + esc(g.name) + ' ' + g.items.length + '</button>').join('')
2068
2441
  + '</div>';
2069
2442
  }
2070
- if (tr.error) grid = '<div class="empty">' + esc(tr.error) + '</div>';
2071
- else if (!tr.quotas) grid = '<div class="empty">正在读取 10Router 供应商额度…</div>';
2072
- else if (!list.length) grid = '<div class="empty">10Router 里还没有可查额度的供应商连接(OAuth 或支持额度查询的 API Key)。</div>';
2443
+ if (tr.error) grid = '<div class="empty">' + esc(T(tr.error)) + '</div>';
2444
+ else if (!tr.quotas) grid = T('<div class="empty">正在读取 10Router 供应商额度…</div>');
2445
+ else if (!list.length) grid = T('<div class="empty">10Router 里还没有可查额度的供应商连接(OAuth 或支持额度查询的 API Key)。</div>');
2073
2446
  else if (activeVendor) {
2074
2447
  const g = groups.find((x) => x.name === activeVendor) || { items: [] };
2075
2448
  grid = '<div class="tr-vcards">' + sortedConnections(activeVendor, g.items).map(trCardHtml).join('') + '</div>';
2076
2449
  } else {
2077
2450
  grid = shown.length
2078
2451
  ? '<div class="tr-vcards">' + sortedVendorGroups(shown).map(trVendorCardHtml).join('') + '</div>'
2079
- : '<div class="empty">没有可显示的供应商(可在「设置」里关掉隐藏)</div>';
2452
+ : T('<div class="empty">没有可显示的供应商(可在「设置」里关掉隐藏)</div>');
2080
2453
  }
2081
2454
  const h = tr.health;
2082
- const healthStrong = !h ? '…' : h.ok ? '正常' : '异常';
2455
+ const healthStrong = !h ? '…' : h.ok ? T('正常') : T('异常');
2083
2456
  const healthDetail = !h ? '' : h.ok
2084
- ? (h.driver ? '存储驱动 ' + h.driver : '') + (h.lastDriverError ? '(驱动降级:' + h.lastDriverError + ')' : '')
2457
+ ? (h.driver ? T('存储驱动 ') + h.driver : '') + (h.lastDriverError ? T('(驱动降级:') + h.lastDriverError + T(')') : '')
2085
2458
  : (h.error || '');
2086
2459
  box.innerHTML = '<div class="toolbar"><div class="summary">'
2087
- + '<div class="stat" title="' + esc(healthDetail) + '"><span>10Router 健康</span><strong' + (h && !h.ok ? ' style="color:var(--err)"' : (h && h.lastDriverError ? ' style="color:var(--warn,#b45309)"' : '')) + '>' + healthStrong + '</strong></div><div class="divider"></div>'
2088
- + '<div class="stat"><span>供应商连接</span><strong>' + (tr.quotas ? list.length : '-') + '</strong></div><div class="divider"></div>'
2089
- + '<div class="stat"><span>额度告急</span><strong>' + (tr.quotas ? low : '-') + '</strong></div><div class="divider"></div>'
2090
- + '<div class="stat"><span>查询失败</span><strong>' + (tr.quotas ? failed : '-') + '</strong></div>'
2460
+ + '<div class="stat" title="' + esc(healthDetail) + T('"><span>10Router 健康</span><strong') + (h && !h.ok ? ' style="color:var(--err)"' : (h && h.lastDriverError ? ' style="color:var(--warn,#b45309)"' : '')) + '>' + healthStrong + '</strong></div><div class="divider"></div>'
2461
+ + T('<div class="stat"><span>供应商连接</span><strong>') + (tr.quotas ? list.length : '-') + '</strong></div><div class="divider"></div>'
2462
+ + T('<div class="stat"><span>额度告急</span><strong>') + (tr.quotas ? low : '-') + '</strong></div><div class="divider"></div>'
2463
+ + T('<div class="stat"><span>查询失败</span><strong>') + (tr.quotas ? failed : '-') + '</strong></div>'
2091
2464
  + '</div><div class="actions">'
2092
- + '<button class="btn" data-tr="refresh"' + (tr.loading ? ' disabled' : '') + '>' + (tr.loading ? '刷新中…' : '刷新额度') + '</button>'
2093
- + '<button class="btn" data-tr="sync" title="用量同步:自动开关、同步来源与手动同步(点我打开)"' + (tr.syncing || c.syncing ? ' disabled' : '') + '>'
2094
- + '<span class="dot' + (c.sync.enabled ? ' on' : '') + '"></span>' + (tr.syncing || c.syncing ? '同步中…' : '用量同步') + '</button>'
2095
- + '<button class="btn" data-tr="settings">设置</button>'
2465
+ + '<button class="btn" data-tr="refresh"' + (tr.loading ? ' disabled' : '') + '>' + (tr.loading ? T('刷新中…') : T('刷新额度')) + '</button>'
2466
+ + T('<button class="btn" data-tr="sync" title="用量同步:自动开关、同步来源与手动同步(点我打开)"') + (tr.syncing || c.syncing ? ' disabled' : '') + '>'
2467
+ + '<span class="dot' + (c.sync.enabled ? ' on' : '') + '"></span>' + (tr.syncing || c.syncing ? T('同步中…') : T('用量同步')) + '</button>'
2468
+ + T('<button class="btn" data-tr="settings">设置</button>')
2096
2469
  + '</div></div>'
2097
2470
  + tabStrip
2098
2471
  + '<div class="tr-grid">' + grid + '</div>';
@@ -2104,11 +2477,11 @@ function trDashCard() {
2104
2477
  const list = (tr.quotas && tr.quotas.connections) || [];
2105
2478
  const low = list.filter((x) => x.isActive && !x.error && quotaLow(x)).length;
2106
2479
  const ls = c.lastSync;
2107
- const rows = '<span class="lbl">供应商连接</span><span class="val">' + (tr.quotas ? list.length : tr.error ? '读取失败' : '…') + '</span>'
2108
- + '<span class="lbl">额度告急</span><span class="val' + (low ? ' warn' : '') + '">' + (tr.quotas ? low : '-') + '</span>'
2109
- + '<span class="lbl">用量同步</span><span class="val">' + (ls ? esc(fmtTime(ls.at).slice(5)) : c.sync.enabled ? '等待首次同步' : '手动') + '</span>';
2480
+ const rows = T('<span class="lbl">供应商连接</span><span class="val">') + (tr.quotas ? list.length : tr.error ? T('读取失败') : '…') + '</span>'
2481
+ + T('<span class="lbl">额度告急</span><span class="val') + (low ? ' warn' : '') + '">' + (tr.quotas ? low : '-') + '</span>'
2482
+ + T('<span class="lbl">用量同步</span><span class="val">') + (ls ? esc(fmtTime(ls.at).slice(5)) : c.sync.enabled ? T('等待首次同步') : T('手动')) + '</span>';
2110
2483
  return '<div class="prodcard" data-view="tenrouter"><div class="prod-head"><span class="prod-name"><span class="dot tenrouter"></span>10Router</span>'
2111
- + '<span class="prod-go">查看' + I.chevRight + '</span></div><div class="prod-rows">' + rows + '</div></div>';
2484
+ + T('<span class="prod-go">查看') + I.chevRight + '</span></div><div class="prod-rows">' + rows + '</div></div>';
2112
2485
  }
2113
2486
 
2114
2487
  async function loadTrConfig() {
@@ -2140,9 +2513,9 @@ function openTrSettings() {
2140
2513
  const c = tr.config || { sync: { enabled: true, sources: [] }, sources: [] };
2141
2514
  $('tr-endpoint').value = c.endpoint || '';
2142
2515
  $('tr-key').value = '';
2143
- $('tr-key').placeholder = c.keyMasked ? '已保存 ' + c.keyMasked + '(留空保持不变)' : 'sk-…';
2516
+ $('tr-key').placeholder = c.keyMasked ? T('已保存 ') + c.keyMasked + T('(留空保持不变)') : 'sk-…';
2144
2517
  $('tr-admin-pw').value = '';
2145
- $('tr-admin-pw').placeholder = c.hasAdminPassword ? '已保存(留空保持不变)' : '可选:10Router 面板密码';
2518
+ $('tr-admin-pw').placeholder = c.hasAdminPassword ? T('已保存(留空保持不变)') : T('可选:10Router 面板密码');
2146
2519
  $('tr-hide-empty').checked = state.trHideEmpty === true;
2147
2520
  $('tr-disconnect').style.display = c.configured ? '' : 'none';
2148
2521
  $('tr-test-msg').textContent = '';
@@ -2155,16 +2528,16 @@ function openTrSync() {
2155
2528
  $('tr-auto').checked = c.configured ? c.sync.enabled : true;
2156
2529
  $('tr-sources').innerHTML = (c.sources || []).map((x) => '<label class="check"><input type="checkbox" data-src="' + x.id + '"'
2157
2530
  + ((c.configured ? c.sync.sources.includes(x.id) : true) ? ' checked' : '') + (x.supported ? '' : ' disabled') + '> ' + esc(x.label)
2158
- + '<span class="dim">' + (x.found ? '' : '(本机未检测到)') + (x.supported ? '' : '(需要 Node 22.5+)') + '</span></label>').join('');
2531
+ + '<span class="dim">' + (x.found ? '' : T('(本机未检测到)')) + (x.supported ? '' : T('(需要 Node 22.5+)')) + '</span></label>').join('');
2159
2532
  const ls = c.lastSync;
2160
- $('tr-sync-status').textContent = ls ? '上次同步 ' + fmtTime(ls.at).slice(5) + ':' + ls.summary : '还没有同步过';
2533
+ $('tr-sync-status').textContent = ls ? T('上次同步 ') + fmtTime(ls.at).slice(5) + T(':') + ls.summary : T('还没有同步过');
2161
2534
  $('tr-sync-now').disabled = tr.syncing || c.syncing === true;
2162
2535
  openModal('m-trsync');
2163
2536
  }
2164
2537
  async function trSyncSave(close) {
2165
2538
  try {
2166
2539
  tr.config = await api('/api/tenrouter', { method: 'PUT', json: { syncEnabled: $('tr-auto').checked, sources: [...document.querySelectorAll('#tr-sources input[data-src]:checked')].map((x) => x.dataset.src) } });
2167
- if (close) { closeModal('m-trsync'); toast('用量同步设置已保存'); render(); }
2540
+ if (close) { closeModal('m-trsync'); toast(T('用量同步设置已保存')); render(); }
2168
2541
  } catch (e) { toast(e.message, false); throw e; }
2169
2542
  }
2170
2543
  function trForm() {
@@ -2178,36 +2551,36 @@ function trForm() {
2178
2551
  async function trTest() {
2179
2552
  const f = trForm();
2180
2553
  $('tr-test-msg').style.color = '';
2181
- $('tr-test-msg').textContent = '测试中…';
2554
+ $('tr-test-msg').textContent = T('测试中…');
2182
2555
  try {
2183
2556
  const r = await api('/api/tenrouter/test', { json: { endpoint: f.endpoint, key: f.key } });
2184
2557
  $('tr-test-msg').style.color = r.ok ? 'var(--ok)' : 'var(--err)';
2185
2558
  $('tr-test-msg').textContent = !r.ok ? '✗ ' + r.error
2186
- : r.quotas ? '✓ 连接成功:' + r.connections + ' 个可查额度的供应商连接' : '✓ 已连接,但' + r.warning + '(用量同步可以正常使用)';
2187
- } catch (e) { $('tr-test-msg').style.color = 'var(--err)'; $('tr-test-msg').textContent = '✗ ' + e.message; }
2559
+ : r.quotas ? T('✓ 连接成功:') + r.connections + T(' 个可查额度的供应商连接') : T('✓ 已连接,但') + r.warning + T('(用量同步可以正常使用)');
2560
+ } catch (e) { $('tr-test-msg').style.color = 'var(--err)'; $('tr-test-msg').textContent = '✗ ' + T(e.message); }
2188
2561
  }
2189
2562
  async function trSave() {
2190
2563
  const f = trForm();
2191
- if (!f.endpoint) return toast('请填写 10Router 地址', false);
2192
- if (!f.key && !(tr.config && tr.config.keyMasked)) return toast('请填写虚拟 key', false);
2564
+ if (!f.endpoint) return toast(T('请填写 10Router 地址'), false);
2565
+ if (!f.key && !(tr.config && tr.config.keyMasked)) return toast(T('请填写虚拟 key'), false);
2193
2566
  // 显示偏好只存在面板本地(localStorage),不上服务端
2194
2567
  state.trHideEmpty = $('tr-hide-empty').checked;
2195
2568
  try { localStorage.setItem('qd_tr_hideempty', state.trHideEmpty ? '1' : '0'); } catch (e) {}
2196
2569
  try {
2197
2570
  tr.config = await api('/api/tenrouter', { method: 'PUT', json: f });
2198
2571
  closeModal('m-10r');
2199
- toast('已保存 10Router 设置');
2572
+ toast(T('已保存 10Router 设置'));
2200
2573
  tr.quotas = null; tr.error = null;
2201
2574
  loadTrQuotas(true);
2202
2575
  } catch (e) { toast(e.message, false); }
2203
2576
  }
2204
2577
  async function trDisconnect() {
2205
- if (!(await uiConfirm('断开后会删除保存的 10Router 地址、key 与同步进度(10Router 里已同步的数据不受影响)。', '断开 10Router', '断开'))) return;
2578
+ if (!(await uiConfirm(T('断开后会删除保存的 10Router 地址、key 与同步进度(10Router 里已同步的数据不受影响)。'), T('断开 10Router'), T('断开')))) return;
2206
2579
  try { tr.config = await api('/api/tenrouter', { method: 'DELETE' }); tr.quotas = null; tr.error = null; closeModal('m-10r'); render(); }
2207
2580
  catch (e) { toast(e.message, false); }
2208
2581
  }
2209
2582
  async function trSyncAccounts() {
2210
- if (!(tr.config && tr.config.configured)) return toast('请先保存 10Router 设置', false);
2583
+ if (!(tr.config && tr.config.configured)) return toast(T('请先保存 10Router 设置'), false);
2211
2584
  const btn = $('tr-sync-accounts');
2212
2585
  const pw = $('tr-admin-pw').value;
2213
2586
  btn.disabled = true;
@@ -2269,6 +2642,12 @@ async function loadAccounts() {
2269
2642
  renderStatus();
2270
2643
  loadQuotas();
2271
2644
  }
2645
+ // 轻量刷新账号列表(只重拉 /api/accounts + 重绘,不重查每账号额度):
2646
+ // 「缺网页会话」徽章的数据源 qoderWebCapturedAt 在账号 meta 里,网页会话是在登录窗
2647
+ // 关闭时才收割绑定的(晚于登录成功提示数秒),登录成功后与周期轮询里都要按这个口径补拉。
2648
+ async function refreshAccountList() {
2649
+ try { state.accounts = (await api('/api/accounts')).accounts; render(); } catch (e) { /* 下一轮再试 */ }
2650
+ }
2272
2651
  let quotaRun = 0;
2273
2652
  let quotaLoadedAt = 0;
2274
2653
  async function loadQuotas(ids) {
@@ -2313,6 +2692,25 @@ function applyLogTabs() {
2313
2692
  b.classList.toggle('active', b.dataset.logLevel === state.logLevel);
2314
2693
  });
2315
2694
  }
2695
+ // 加载今日整段历史(结构化归档,按当前语言实时重渲染,突破环形缓冲 300 条上限)。
2696
+ // 常规 10 秒轮询会把视图刷回最近 200 条——历史视图点「切标签/级别」即回到常规轮询。
2697
+ $('btn-logs-history').addEventListener('click', async () => {
2698
+ const el = $('logs');
2699
+ if (!el) return;
2700
+ const qs = state.logTab === 'run' ? '' : ('&tag=' + encodeURIComponent(GW_LOG_TAG[state.logTab]));
2701
+ try {
2702
+ const data = await api('/api/logs?history=1' + qs);
2703
+ let logs = data.logs || [];
2704
+ if (state.logLevel !== 'all') logs = logs.filter((l) => l.level === 'warn' || l.level === 'error');
2705
+ const withTag = state.logTab === 'run';
2706
+ const ico = (lvl) => lvl === 'error' ? '✗' : lvl === 'warn' ? '⚠' : lvl === 'info' ? '·' : '';
2707
+ el.innerHTML = logs.length
2708
+ ? logs.map((l) => '<div class="' + esc(l.level) + '">[' + esc(l.at) + '] <span class="lvl-ico">' + ico(l.level) + '</span>' + (withTag ? esc(l.tag) + ' · ' : '') + esc(l.msg) + '</div>').join('')
2709
+ : T('(暂无历史记录)');
2710
+ el.scrollTop = el.scrollHeight;
2711
+ toast(T('已加载今日历史 {n} 条', { n: logs.length }), true);
2712
+ } catch (e) { toast(e.message, false); }
2713
+ });
2316
2714
  async function loadCurrentLogs() {
2317
2715
  const el = $('logs');
2318
2716
  if (!el) return;
@@ -2333,7 +2731,7 @@ async function loadCurrentLogs() {
2333
2731
  const ico = (lvl) => lvl === 'error' ? '✗' : lvl === 'warn' ? '⚠' : lvl === 'info' ? '·' : '';
2334
2732
  el.innerHTML = logs.length
2335
2733
  ? logs.map((l) => '<div class="' + esc(l.level) + '">[' + esc(l.at) + '] <span class="lvl-ico">' + ico(l.level) + '</span>' + (withTag ? esc(l.tag) + ' · ' : '') + esc(l.msg) + '</div>').join('')
2336
- : (state.logLevel === 'all' ? '(暂无)' : '(暂无警告或错误)');
2734
+ : (state.logLevel === 'all' ? T('(暂无)') : T('(暂无警告或错误)'));
2337
2735
  if (atBottom) el.scrollTop = el.scrollHeight;
2338
2736
  } catch (e) {}
2339
2737
  }
@@ -2345,15 +2743,15 @@ async function checkinOne(id) {
2345
2743
  const d = await api('/api/accounts/' + encodeURIComponent(id) + '/checkin', { method: 'POST' });
2346
2744
  const r = d.results && d.results[0];
2347
2745
  const accObj = state.accounts.find((x) => x.id === id);
2348
- const accLabel = (r && r.account) || (accObj && (accObj.name || accObj.id)) || '该账号';
2349
- toast(r ? (accLabel + ':' + (r.status === 'checked-in' ? '领取成功 +' + r.claimedAmount + ' Credits' : (r.error || r.message || r.status))) : d.summary, !(r && r.status === 'failed'));
2746
+ const accLabel = (r && r.account) || (accObj && (accObj.name || accObj.id)) || T('该账号');
2747
+ toast(r ? (accLabel + T(':') + (r.status === 'checked-in' ? T('领取成功 +') + r.claimedAmount + ' Credits' : (r.error || r.message || r.status))) : d.summary, !(r && r.status === 'failed'));
2350
2748
  } catch (e) { toast(e.message, false); }
2351
2749
  delete state.busy[id];
2352
2750
  await loadStatus(); await loadAccounts(); loadLogs();
2353
2751
  }
2354
2752
  async function checkinAll(btn, product) {
2355
2753
  const label = btn.innerHTML;
2356
- btn.disabled = true; btn.innerHTML = I.refresh + '领取中…';
2754
+ btn.disabled = true; btn.innerHTML = I.refresh + T('领取中…');
2357
2755
  try {
2358
2756
  const d = await api('/api/checkin', { json: { skipIfCheckedToday: false, product } });
2359
2757
  toast(d.summary);
@@ -2371,30 +2769,30 @@ async function onCardClick(e) {
2371
2769
  if (act === 'claim') return openClaim(id);
2372
2770
  if (act === 'switch') {
2373
2771
  const a = state.accounts.find((x) => x.id === id);
2374
- const client = CLIENT_LABEL[a && a.product] || (a && prodLabel(a.product)) || '客户端';
2772
+ const client = (CLIENT_LABEL[a && a.product] ? T(CLIENT_LABEL[a && a.product]) : '') || (a && prodLabel(a.product)) || T('客户端');
2375
2773
  const tip = a && a.product === 'zcode'
2376
- ? '\n需先退出 ZCode;切换后重新打开 ZCode 即为新账号。当前登录会先保存到 CreditDaddy,不会丢号。'
2774
+ ? T('\n需先退出 ZCode;切换后重新打开 ZCode 即为新账号。当前登录会先保存到 CreditDaddy,不会丢号。')
2377
2775
  : a && a.product === 'mirasim'
2378
- ? '\n需先退出 Mirasim;切换后重新打开即为新账号。当前登录会先保存到 CreditDaddy,不会丢号。'
2776
+ ? T('\n需先退出 Mirasim;切换后重新打开即为新账号。当前登录会先保存到 CreditDaddy,不会丢号。')
2379
2777
  : a && a.product === 'catpaw'
2380
- ? '\n需先退出妙手;切换后重新打开妙手即为新账号。当前登录会先保存到 CreditDaddy,不会丢号。'
2778
+ ? T('\n需先退出妙手;切换后重新打开妙手即为新账号。当前登录会先保存到 CreditDaddy,不会丢号。')
2381
2779
  : a && a.product === 'trae'
2382
- ? '\n需先退出 Trae;切换后重新打开 Trae 即为新账号。当前登录会先自动快照保存到 CreditDaddy,不会丢号。'
2780
+ ? T('\n需先退出 Trae;切换后重新打开 Trae 即为新账号。当前登录会先自动快照保存到 CreditDaddy,不会丢号。')
2383
2781
  : a && a.product === 'qoder'
2384
- ? '\n需先退出 Qoder;切换后重新打开 Qoder 即为新账号。运行中的 Qoder 退出时会把旧登录写回去,可选「关闭并强制切换」代为关进程。'
2385
- : '\n客户端会自动应用新会话;当前登录账号的最新 token 会先保存到 CreditDaddy。';
2386
- if (!(await uiConfirm('把 ' + client + ' 切换到「' + (a && a.name || id) + '」?' + tip, '切换账号', '切换'))) return;
2782
+ ? T('\n需先退出 Qoder;切换后重新打开 Qoder 即为新账号。运行中的 Qoder 退出时会把旧登录写回去,可选「关闭并强制切换」代为关进程。')
2783
+ : T('\n客户端会自动应用新会话;当前登录账号的最新 token 会先保存到 CreditDaddy。');
2784
+ if (!(await uiConfirm(T('把 {client} 切换到「{name}」?', { client, name: (a && a.name || id) }) + tip, T('切换账号'), T('切换')))) return;
2387
2785
  const doSwitch = (force) => api('/api/accounts/' + encodeURIComponent(id) + '/switch', { json: { force } });
2388
2786
  try {
2389
2787
  let r;
2390
2788
  try { r = await doSwitch(false); }
2391
2789
  catch (err) {
2392
2790
  if (err.code !== 'ZCODE_RUNNING' && err.code !== 'MIRASIM_RUNNING' && err.code !== 'CATPAW_RUNNING' && err.code !== 'TRAE_RUNNING' && err.code !== 'QODER_RUNNING') throw err;
2393
- if (!(await uiConfirm(err.message + '\n\n仍要强制切换吗?强制切换会先关闭客户端再写入登录,重新打开客户端即为新账号。', client + ' 正在运行', '关闭并强制切换'))) return;
2791
+ if (!(await uiConfirm(err.message + T('\n\n仍要强制切换吗?强制切换会先关闭客户端再写入登录,重新打开客户端即为新账号。'), client + T(' 正在运行'), T('关闭并强制切换')))) return;
2394
2792
  r = await doSwitch(true);
2395
2793
  }
2396
- const msg = r && r.alreadyActive ? client + ' 当前已是该账号'
2397
- : (r && r.closedClient ? '已关闭并切换 ' : '') + client + ' 已切换到 ' + (a && a.name || id);
2794
+ const msg = r && r.alreadyActive ? client + T(' 当前已是该账号')
2795
+ : (r && r.closedClient ? T('已关闭并切换 ') : '') + client + T(' 已切换到 ') + (a && a.name || id);
2398
2796
  toast(msg);
2399
2797
  await loadStatus(); await loadAccounts();
2400
2798
  // 桌面版切号后自动唤醒打开客户端,免去手动查找客户端快捷方式
@@ -2402,7 +2800,7 @@ async function onCardClick(e) {
2402
2800
  setTimeout(async () => {
2403
2801
  try {
2404
2802
  const re = await window.creditdaddy.openZcodeClient();
2405
- if (re && re.ok) toast('已重新启动 ZCode 客户端');
2803
+ if (re && re.ok) toast(T('已重新启动 ZCode 客户端'));
2406
2804
  } catch (e) {}
2407
2805
  }, 800);
2408
2806
  }
@@ -2410,7 +2808,7 @@ async function onCardClick(e) {
2410
2808
  setTimeout(async () => {
2411
2809
  try {
2412
2810
  const re = await window.creditdaddy.openMirasimClient();
2413
- if (re && re.ok) toast('已重新启动 Mirasim 客户端');
2811
+ if (re && re.ok) toast(T('已重新启动 Mirasim 客户端'));
2414
2812
  } catch (e) {}
2415
2813
  }, 800);
2416
2814
  }
@@ -2418,7 +2816,7 @@ async function onCardClick(e) {
2418
2816
  setTimeout(async () => {
2419
2817
  try {
2420
2818
  const re = await window.creditdaddy.openCatpawClient();
2421
- if (re && re.ok) toast('已重新启动妙手客户端');
2819
+ if (re && re.ok) toast(T('已重新启动妙手客户端'));
2422
2820
  } catch (e) {}
2423
2821
  }, 800);
2424
2822
  }
@@ -2426,8 +2824,8 @@ async function onCardClick(e) {
2426
2824
  setTimeout(async () => {
2427
2825
  try {
2428
2826
  const re = await window.creditdaddy.openTraeClient();
2429
- if (re && re.ok) toast('已重新启动 Trae 客户端');
2430
- else toast((re && re.error) || '无法打开 Trae 客户端(桌面版才能自动拉起)', false);
2827
+ if (re && re.ok) toast(T('已重新启动 Trae 客户端'));
2828
+ else toast((re && re.error) || T('无法打开 Trae 客户端(桌面版才能自动拉起)'), false);
2431
2829
  } catch (e) {}
2432
2830
  }, 800);
2433
2831
  }
@@ -2436,7 +2834,7 @@ async function onCardClick(e) {
2436
2834
  }
2437
2835
  if (act === 'rename') {
2438
2836
  const a = state.accounts.find((x) => x.id === id);
2439
- const name = await uiPrompt('修改备注名', a && a.name || '', { okText: '保存' });
2837
+ const name = await uiPrompt(T('修改备注名'), a && a.name || '', { okText: T('保存') });
2440
2838
  if (name === null) return;
2441
2839
  try { await api('/api/accounts/' + encodeURIComponent(id), { method: 'PATCH', json: { name } }); await loadAccounts(); }
2442
2840
  catch (err) { toast(err.message, false); }
@@ -2446,11 +2844,11 @@ async function onCardClick(e) {
2446
2844
  if (!armed[id]) {
2447
2845
  armed[id] = setTimeout(() => { delete armed[id]; btn.classList.remove('armed'); }, 3000);
2448
2846
  btn.classList.add('armed');
2449
- toast('再点一次确认删除');
2847
+ toast(T('再点一次确认删除'));
2450
2848
  return;
2451
2849
  }
2452
2850
  clearTimeout(armed[id]); delete armed[id];
2453
- try { await api('/api/accounts/' + encodeURIComponent(id), { method: 'DELETE' }); toast('已删除'); delete state.quotas[id]; await loadAccounts(); }
2851
+ try { await api('/api/accounts/' + encodeURIComponent(id), { method: 'DELETE' }); toast(T('已删除')); delete state.quotas[id]; await loadAccounts(); }
2454
2852
  catch (err) { toast(err.message, false); }
2455
2853
  }
2456
2854
  }
@@ -2467,7 +2865,7 @@ function loadCaptchaSdk() {
2467
2865
  const s = document.createElement('script');
2468
2866
  s.src = CAPTCHA_SDK_URL;
2469
2867
  s.onload = () => resolve();
2470
- s.onerror = () => reject(new Error('验证码组件加载失败(网络问题?)'));
2868
+ s.onerror = () => reject(new Error(T('验证码组件加载失败(网络问题?)')));
2471
2869
  document.head.appendChild(s);
2472
2870
  });
2473
2871
  }
@@ -2480,7 +2878,7 @@ async function openClaim(id) {
2480
2878
  claimState.plans = [];
2481
2879
  claimState.captcha = null;
2482
2880
  claimState.busy = false;
2483
- $('claim-body').innerHTML = '<div class="dim">读取可领取的活动…</div>';
2881
+ $('claim-body').innerHTML = T('<div class="dim">读取可领取的活动…</div>');
2484
2882
  openModal('m-claim');
2485
2883
  try {
2486
2884
  const [plansResp, cap] = await Promise.all([
@@ -2498,17 +2896,17 @@ async function openClaim(id) {
2498
2896
 
2499
2897
  function renderClaimBody() {
2500
2898
  if (!claimState.plans.length) {
2501
- $('claim-body').innerHTML = '<div class="muted-empty">当前没有可领取的活动</div>';
2899
+ $('claim-body').innerHTML = T('<div class="muted-empty">当前没有可领取的活动</div>');
2502
2900
  return;
2503
2901
  }
2504
2902
  $('claim-body').innerHTML = claimState.plans.map((pl, i) =>
2505
2903
  '<div class="claim-plan"><div><b>' + esc(pl.name || pl.planId) + '</b>'
2506
2904
  + (pl.description ? '<div class="dim grants">' + esc(pl.description) + '</div>' : '')
2507
- + (pl.grants && pl.grants.length ? '<div class="dim grants">' + pl.grants.map(esc).join(';') + '</div>' : '')
2508
- + '</div><button class="btn primary" data-claim="' + i + '"' + (claimState.busy ? ' disabled' : '') + '>领取</button></div>'
2905
+ + (pl.grants && pl.grants.length ? '<div class="dim grants">' + pl.grants.map(esc).join(T(';')) + '</div>' : '')
2906
+ + '</div><button class="btn primary" data-claim="' + i + '"' + (claimState.busy ? ' disabled' : '') + T('>领取</button></div>')
2509
2907
  ).join('')
2510
2908
  + '<div id="claim-status" class="dim" style="margin-top:8px"></div>'
2511
- + '<div id="cap-holder"></div><button class="btn" id="cap-btn" hidden>打开验证码</button>';
2909
+ + T('<div id="cap-holder"></div><button class="btn" id="cap-btn" hidden>打开验证码</button>');
2512
2910
  }
2513
2911
 
2514
2912
  // 阿里云验证码:先静默验证(8s 超时),不行就让用户点按钮弹验证码。移植自 zcode-switch captcha.js。
@@ -2522,13 +2920,13 @@ function runCaptcha(cfg) {
2522
2920
  if (done || !param) return;
2523
2921
  done = true;
2524
2922
  clearTimeout(timer);
2525
- st.textContent = '人机验证已通过';
2923
+ st.textContent = T('人机验证已通过');
2526
2924
  resolve(param);
2527
2925
  };
2528
2926
  const interactive = (why) => {
2529
2927
  if (done) return;
2530
2928
  clearTimeout(timer);
2531
- st.textContent = '需要完成人机验证:点下方按钮,按提示完成验证码';
2929
+ st.textContent = T('需要完成人机验证:点下方按钮,按提示完成验证码');
2532
2930
  if (btn) { btn.hidden = false; btn.focus(); }
2533
2931
  if (why && typeof why !== 'string') st.title = JSON.stringify(why).slice(0, 160);
2534
2932
  };
@@ -2542,7 +2940,7 @@ function runCaptcha(cfg) {
2542
2940
  button: '#cap-btn',
2543
2941
  getInstance: (instance) => {
2544
2942
  if (instance && typeof instance.startTracelessVerification === 'function') {
2545
- st.textContent = '人机验证中…';
2943
+ st.textContent = T('人机验证中…');
2546
2944
  instance.startTracelessVerification();
2547
2945
  timer = setTimeout(() => interactive(), 8000);
2548
2946
  } else interactive();
@@ -2561,7 +2959,7 @@ async function doClaim(i) {
2561
2959
  const st = $('claim-status');
2562
2960
  claimState.busy = true;
2563
2961
  renderClaimBody();
2564
- $('claim-status').textContent = '准备领取…';
2962
+ $('claim-status').textContent = T('准备领取…');
2565
2963
  let captchaParam = '';
2566
2964
  let region = '';
2567
2965
  try {
@@ -2571,27 +2969,27 @@ async function doClaim(i) {
2571
2969
  region = cfg.region || '';
2572
2970
  captchaParam = await runCaptcha(cfg);
2573
2971
  } else {
2574
- $('claim-status').textContent = '验证码配置不可用,按无验证码直接尝试…';
2972
+ $('claim-status').textContent = T('验证码配置不可用,按无验证码直接尝试…');
2575
2973
  }
2576
2974
  } catch (e) {
2577
- $('claim-status').innerHTML = '<span class="msg err">' + esc('验证码失败:' + e.message) + '</span>';
2975
+ $('claim-status').innerHTML = '<span class="msg err">' + esc(T('验证码失败:') + e.message) + '</span>';
2578
2976
  claimState.busy = false;
2579
2977
  renderClaimBody();
2580
2978
  return;
2581
2979
  }
2582
- $('claim-status').textContent = '领取中…';
2980
+ $('claim-status').textContent = T('领取中…');
2583
2981
  try {
2584
2982
  const r = await api('/api/accounts/' + encodeURIComponent(claimState.accountId) + '/zcode/claim', {
2585
2983
  json: { planId: pl.planId, captchaParam, region },
2586
2984
  });
2587
- toast('领取成功:' + (r.planName || pl.name || pl.planId));
2985
+ toast(T('领取成功:') + (r.planName || pl.name || pl.planId));
2588
2986
  closeModal('m-claim');
2589
2987
  loadQuotas([claimState.accountId]);
2590
2988
  loadLogs();
2591
2989
  } catch (e) {
2592
2990
  const errBox = $('claim-status');
2593
2991
  if (errBox) {
2594
- errBox.innerHTML = '<span class="msg err">' + esc(e.message) + (e.nextAt ? '(' + fmtTime(e.nextAt).slice(5, 16) + ' 后可再试)' : '') + '</span>';
2992
+ errBox.innerHTML = '<span class="msg err">' + esc(e.message) + (e.nextAt ? T('(') + fmtTime(e.nextAt).slice(5, 16) + T(' 后可再试)') : '') + '</span>';
2595
2993
  }
2596
2994
  }
2597
2995
  claimState.busy = false;
@@ -2604,7 +3002,11 @@ $('claim-body').addEventListener('click', (e) => {
2604
3002
  });
2605
3003
 
2606
3004
  // ── 弹窗 ──
2607
- function openModal(id) { $(id).classList.add('show'); }
3005
+ function openModal(id) {
3006
+ // 设置弹出层不是遮罩弹窗:开其它弹窗前先收掉,避免浮在遮罩弹窗之上(层级错乱来源)
3007
+ if (id !== 'm-settings') { const st = $('m-settings'); if (st) st.classList.remove('show'); }
3008
+ $(id).classList.add('show');
3009
+ }
2608
3010
  function closeModal(id) { $(id).classList.remove('show'); if (id === 'm-add') deviceStop(); if (id === 'm-dialog') settleDialog(null); }
2609
3011
 
2610
3012
  // 页面内对话框(替代原生 confirm / prompt:面板嵌在飞牛桌面窗口里是跨域 iframe,原生弹框可能被浏览器拦截)
@@ -2618,12 +3020,12 @@ function uiDialog({ title, message, input, okText }) {
2618
3020
  $('dlg-msg').style.display = message ? '' : 'none';
2619
3021
  $('dlg-field').style.display = input ? '' : 'none';
2620
3022
  if (input) { $('dlg-input').type = input.type || 'text'; $('dlg-input').value = input.value || ''; $('dlg-input').placeholder = input.placeholder || ''; }
2621
- $('dlg-ok').textContent = okText || '确定';
3023
+ $('dlg-ok').textContent = okText || T('确定');
2622
3024
  openModal('m-dialog');
2623
3025
  setTimeout(() => (input ? $('dlg-input') : $('dlg-ok')).focus(), 30);
2624
3026
  return new Promise((resolve) => { dialogResolve = (v) => resolve(input ? v : v !== null); });
2625
3027
  }
2626
- const uiConfirm = (message, title, okText) => uiDialog({ title: title || '请确认', message, okText });
3028
+ const uiConfirm = (message, title, okText) => uiDialog({ title: title || T('请确认'), message, okText });
2627
3029
  const uiPrompt = (title, value, opts = {}) => uiDialog({ title, message: opts.message, input: { value, type: opts.type, placeholder: opts.placeholder }, okText: opts.okText });
2628
3030
  function okDialog() { const v = $('dlg-field').style.display === 'none' ? true : $('dlg-input').value; $('m-dialog').classList.remove('show'); settleDialog(v); }
2629
3031
  $('dlg-ok').addEventListener('click', okDialog);
@@ -2633,7 +3035,7 @@ $('dlg-input').addEventListener('keydown', (e) => { if (e.key === 'Enter') okDia
2633
3035
  async function restartCreditDaddy() {
2634
3036
  const mask = document.createElement('div');
2635
3037
  mask.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.75);z-index:99999;display:flex;flex-direction:column;align-items:center;justify-content:center;color:#fff;font-size:15px;gap:12px;backdrop-filter:blur(4px);user-select:none;';
2636
- mask.innerHTML = '<div style="font-size:28px">⏳</div><div style="font-weight:600">正在重启 CreditDaddy,请稍候…</div><div style="font-size:12px;color:rgba(255,255,255,.6)">服务恢复后将自动刷新页面</div>';
3038
+ mask.innerHTML = T('<div style="font-size:28px">⏳</div><div style="font-weight:600">正在重启 CreditDaddy,请稍候…</div><div style="font-size:12px;color:rgba(255,255,255,.6)">服务恢复后将自动刷新页面</div>');
2637
3039
  document.body.appendChild(mask);
2638
3040
 
2639
3041
  try {
@@ -2656,14 +3058,14 @@ async function restartCreditDaddy() {
2656
3058
  } catch {}
2657
3059
  if (attempts > 30) {
2658
3060
  clearInterval(poll);
2659
- mask.innerHTML = '<div>重启超时或端口已改变</div><button class="btn primary" onclick="location.reload()" style="margin-top:10px">手动刷新</button>';
3061
+ mask.innerHTML = T('<div>重启超时或端口已改变</div><button class="btn primary" onclick="location.reload()" style="margin-top:10px">手动刷新</button>');
2660
3062
  }
2661
3063
  }, 1000);
2662
3064
  }
2663
3065
 
2664
3066
  async function maybePromptRestartForLan(oldLan, newLan) {
2665
3067
  if (oldLan !== newLan) {
2666
- const ok = await uiConfirm('局域网开放绑定改动需重启服务生效,是否立即重启 CreditDaddy?', '配置已保存', '立即重启');
3068
+ const ok = await uiConfirm(T('局域网开放绑定改动需重启服务生效,是否立即重启 CreditDaddy?'), T('配置已保存'), T('立即重启'));
2667
3069
  if (ok) {
2668
3070
  await restartCreditDaddy();
2669
3071
  return true;
@@ -2672,13 +3074,6 @@ async function maybePromptRestartForLan(oldLan, newLan) {
2672
3074
  return false;
2673
3075
  }
2674
3076
 
2675
- $('btn-restart-app').addEventListener('click', async () => {
2676
- const ok = await uiConfirm('确定要重启 CreditDaddy 服务吗?正在进行的任务可能会中断。', '重启服务', '重启');
2677
- if (ok) {
2678
- closeModal('m-settings');
2679
- await restartCreditDaddy();
2680
- }
2681
- });
2682
3077
 
2683
3078
  // ── 检查更新(桌面版):主进程推状态(update:state),面板只管渲染 ──
2684
3079
  const updBridge = window.creditdaddy && window.creditdaddy.update ? window.creditdaddy.update : null;
@@ -2690,55 +3085,66 @@ function renderUpdateModal(s) {
2690
3085
  updState = s;
2691
3086
  const body = $('upd-body');
2692
3087
  const acts = $('upd-actions');
2693
- const cur = '当前 v' + (s.current || '?');
2694
- const rel = '<button class="btn" id="upd-rel">打开 Releases</button>';
2695
- const close = '<button class="btn" data-close-uu>稍后</button>';
3088
+ const cur = T('当前 v') + (s.current || '?');
3089
+ const rel = T('<button class="btn" id="upd-rel">打开 Releases</button>');
3090
+ const close = T('<button class="btn" data-close-uu>稍后</button>');
2696
3091
  switch (s.phase) {
3092
+ case 'unsupported-browser':
3093
+ body.innerHTML = T('<div class="hint">在浏览器里打开的面板没有本机更新通道——请从 GitHub Releases 下载最新安装包安装。</div>');
3094
+ acts.innerHTML = rel + T('<button class="btn" data-close-uu>取消</button>');
3095
+ break;
2697
3096
  case 'unsupported':
2698
- body.innerHTML = '<div class="hint">当前环境不支持应用内更新(开发模式 / Portable / 未签名的 macOS 版),请从 GitHub Releases 手动下载。</div>';
2699
- acts.innerHTML = rel + '<button class="btn" data-close-uu>取消</button>';
3097
+ body.innerHTML = T('<div class="hint">当前环境不支持应用内更新(开发模式 / Portable / 未签名的 macOS 版),请从 GitHub Releases 手动下载。</div>');
3098
+ acts.innerHTML = rel + T('<button class="btn" data-close-uu>取消</button>');
2700
3099
  break;
2701
3100
  case 'checking':
2702
- body.innerHTML = '<div class="hint">正在检查更新…</div>';
3101
+ body.innerHTML = T('<div class="hint">正在检查更新…</div>');
2703
3102
  acts.innerHTML = close;
2704
3103
  break;
2705
3104
  case 'latest':
2706
- body.innerHTML = '<div class="hint"><span class="dot on"></span> 已是最新版本(' + cur + ')</div>';
2707
- acts.innerHTML = '<button class="btn primary" data-close-uu>好的</button>';
3105
+ body.innerHTML = T('<div class="hint"><span class="dot on"></span> 已是最新版本(') + cur + ')</div>';
3106
+ acts.innerHTML = T('<button class="btn primary" data-close-uu>好的</button>');
2708
3107
  break;
2709
3108
  case 'available':
2710
- body.innerHTML = s.via === 'updater'
2711
- ? '<div class="hint">发现新版本 <b>v' + esc(s.version) + '</b>(' + cur + ')——正在后台下载,完成后退出 CreditDaddy 时自动安装。</div>'
2712
- : '<div class="hint">发现新版本 <b>v' + esc(s.version) + '</b>(' + cur + (s.sizeMB ? ',约 ' + s.sizeMB + ' MB' : '') + '),可由本机直接下载安装。</div>';
2713
- acts.innerHTML = (s.via === 'github' ? updActBtn('upd-go', '立即下载安装', 'primary') : '') + rel + close;
3109
+ body.innerHTML = s.rebuild
3110
+ ? T('<div class="hint">检测到同版本新构建 <b>v') + esc(s.version) + (s.commit ? ' · ' + esc(s.commit) : '') + T('</b>——可能包含修复,可直接下载安装覆盖当前构建。</div>')
3111
+ : s.via === 'updater'
3112
+ ? T('<div class="hint">发现新版本 <b>v') + esc(s.version) + T('</b>(') + cur + T(')——正在后台下载,完成后退出 CreditDaddy 时自动安装。</div>')
3113
+ : T('<div class="hint">发现新版本 <b>v') + esc(s.version) + T('</b>(') + cur + (s.sizeMB ? T(',约 ') + s.sizeMB + ' MB' : '') + T('),可由本机直接下载安装。</div>');
3114
+ acts.innerHTML = (s.via === 'github' ? updActBtn('upd-go', T('立即下载安装'), 'primary') : '') + rel + close;
2714
3115
  break;
2715
3116
  case 'downloading':
2716
- body.innerHTML = '<div class="hint">正在下载 v' + esc(s.version) + '</div>'
3117
+ body.innerHTML = T('<div class="hint">正在下载 v') + esc(s.version) + '</div>'
2717
3118
  + '<div class="meter" style="margin:8px 0 4px"><span style="width:' + (s.percent || 0) + '%"></span></div>'
2718
- + '<div class="hint">' + (s.percent != null ? s.percent + '%' : '') + '(' + (s.mb || '…') + ' MB)' + (s.percent == null ? ' 获取大小中…' : '') + '</div>';
2719
- acts.innerHTML = '<button class="btn" data-close-uu>后台继续</button>';
3119
+ + '<div class="hint">' + (s.percent != null ? s.percent + '%' : '') + T('(') + (s.mb || '…') + T(' MB)') + (s.percent == null ? T(' 获取大小中…') : '') + '</div>';
3120
+ acts.innerHTML = T('<button class="btn" data-close-uu>后台继续</button>');
2720
3121
  break;
2721
3122
  case 'downloaded':
2722
3123
  body.innerHTML = '<div class="hint"><span class="dot on pulse"></span> <b>v' + esc(s.version) + '</b> '
2723
- + (s.via === 'updater' ? '已下载完成,重启即完成更新(退出时也会自动安装)。' : '安装包已下载完成,安装时 CreditDaddy 会先退出。') + '</div>';
2724
- acts.innerHTML = updActBtn('upd-go', s.via === 'updater' ? '立即重启更新' : '立即安装', 'primary') + close;
3124
+ + (s.via === 'updater' ? T('已下载完成,重启即完成更新(退出时也会自动安装)。') : T('安装包已下载完成,安装时 CreditDaddy 会先退出。')) + '</div>';
3125
+ acts.innerHTML = updActBtn('upd-go', s.via === 'updater' ? T('立即重启更新') : T('立即安装'), 'primary') + close;
2725
3126
  break;
2726
3127
  case 'error':
2727
3128
  default:
2728
- body.innerHTML = '<div class="hint" style="color:var(--err)">检查更新失败:' + esc(s.error || '未知错误') + '</div>';
2729
- acts.innerHTML = updActBtn('upd-retry', '重试', 'primary') + rel + '<button class="btn" data-close-uu>取消</button>';
3129
+ body.innerHTML = T('<div class="hint" style="color:var(--err)">检查更新失败:') + esc(T(s.error || '') || T('未知错误')) + '</div>';
3130
+ acts.innerHTML = updActBtn('upd-retry', T('重试'), 'primary') + rel + T('<button class="btn" data-close-uu>取消</button>');
2730
3131
  break;
2731
3132
  }
2732
3133
  const relBtn = $('upd-rel');
2733
- if (relBtn) relBtn.onclick = () => updBridge.openReleases();
3134
+ if (relBtn) relBtn.onclick = () => { if (updBridge) updBridge.openReleases(); else openFullChangelog(); };
2734
3135
  const goBtn = $('upd-go');
2735
- if (goBtn) { goBtn.disabled = true; goBtn.textContent = '处理中…'; updBridge.install().then(renderUpdateModal).catch((e) => { goBtn.disabled = false; goBtn.textContent = '重试'; }); }
3136
+ if (goBtn) { goBtn.disabled = true; goBtn.textContent = T('处理中…'); updBridge.install().then(renderUpdateModal).catch((e) => { goBtn.disabled = false; goBtn.textContent = T('重试'); }); }
2736
3137
  const retryBtn = $('upd-retry');
2737
3138
  if (retryBtn) retryBtn.onclick = () => updBridge.check().then(renderUpdateModal).catch(() => {});
2738
3139
  acts.querySelectorAll('[data-close-uu]').forEach((b) => { b.onclick = () => closeModal('m-update'); });
2739
3140
  }
2740
3141
  async function openUpdateModal(autoCheck) {
2741
- if (!updBridge) return;
3142
+ if (!updBridge) {
3143
+ // 纯浏览器:没有本机 updater 通道,保留入口并给出 Releases 下载承接
3144
+ openModal('m-update');
3145
+ renderUpdateModal({ phase: 'unsupported-browser', current: (state.status && state.status.version) || '' });
3146
+ return;
3147
+ }
2742
3148
  openModal('m-update');
2743
3149
  try {
2744
3150
  updState = await updBridge.state();
@@ -2749,10 +3155,36 @@ async function openUpdateModal(autoCheck) {
2749
3155
  }
2750
3156
  }
2751
3157
  if (updBridge) {
2752
- updBridge.onState((s) => { updState = s; $('upd-ver') && ($('upd-ver').textContent = '当前 v' + (s.current || '?')); if (document.getElementById('m-update').classList.contains('show')) renderUpdateModal(s); });
3158
+ updBridge.onState((s) => { updState = s; $('upd-ver') && ($('upd-ver').textContent = T('当前 v') + (s.current || '?')); if (document.getElementById('m-update').classList.contains('show')) renderUpdateModal(s); });
2753
3159
  updBridge.onOpen(() => openUpdateModal(true));
2754
3160
  }
2755
- $('btn-update').onclick = () => openUpdateModal(true);
3161
+ // 更新日志小窗:桌面版读随包 CHANGELOG.md 的简要摘要;网页面板直接开 GitHub Releases
3162
+ function openFullChangelog() {
3163
+ if (updBridge && typeof updBridge.openReleases === 'function') updBridge.openReleases();
3164
+ else window.open('https://github.com/techysy/CreditDaddy/releases', '_blank', 'noopener');
3165
+ }
3166
+ function openChangelogModal() {
3167
+ const openReleases = openFullChangelog;
3168
+ if (!(window.creditdaddy && typeof window.creditdaddy.changelogBrief === 'function')) { openReleases(); return; }
3169
+ window.creditdaddy.changelogBrief().then((r) => {
3170
+ const box = $('changelog-body');
3171
+ const strip = (s) => esc(String(s || '').replace(/\*\*/g, ''));
3172
+ box.innerHTML = (r && r.ok && r.sections && r.sections.length)
3173
+ ? r.sections.map((s) => '<div class="changelog-sec">'
3174
+ + '<div style="font-weight:700;margin:0 0 4px">v' + esc(s.version)
3175
+ + (s.date ? ' <span class="dim" style="font-weight:400;font-size:11px">' + esc(s.date) + '</span>' : '') + '</div>'
3176
+ + (s.theme ? '<div class="hint">' + strip(s.theme) + '</div>' : '')
3177
+ + (s.topics && s.topics.length ? '<div class="hint" style="margin-top:4px">' + s.topics.map(strip).join(' · ') + '</div>' : '')
3178
+ + '</div>').join('')
3179
+ : T('<div class="hint">暂无本地更新日志,请到 GitHub Releases 查看。</div>');
3180
+ openModal('m-changelog');
3181
+ }).catch(openReleases);
3182
+ }
3183
+ $('btn-changelog-full').onclick = openFullChangelog;
3184
+ // 托盘菜单触发(IPC)
3185
+ if (updBridge && typeof updBridge.onChangelogOpen === 'function') {
3186
+ updBridge.onChangelogOpen(openChangelogModal);
3187
+ }
2756
3188
 
2757
3189
  // ZCode 自动领取计划:保存(按新配置启用)/ 停用
2758
3190
  $('zac-ok').addEventListener('click', async () => {
@@ -2763,7 +3195,7 @@ $('zac-ok').addEventListener('click', async () => {
2763
3195
  state.zClaimWindowMin = r.claimWindowMin;
2764
3196
  renderStatus(); render();
2765
3197
  closeModal('m-zac');
2766
- toast('自动领取已开启:每 ' + r.claimIntervalMin + ' 分钟检查' + (r.claimWindowMin > 0 ? ',运行 ' + r.claimWindowMin + ' 分钟后自动关闭' : ',一直运行直到手动关闭'));
3198
+ toast(T('自动领取已开启:每 ') + r.claimIntervalMin + T(' 分钟检查') + (r.claimWindowMin > 0 ? T(',运行 ') + r.claimWindowMin + T(' 分钟后自动关闭') : T(',一直运行直到手动关闭')));
2767
3199
  } catch (err) { toast(err.message, false); }
2768
3200
  });
2769
3201
  $('zac-off').addEventListener('click', async () => {
@@ -2772,7 +3204,7 @@ $('zac-off').addEventListener('click', async () => {
2772
3204
  state.zAutoClaim = r.autoClaim === true;
2773
3205
  renderStatus(); render();
2774
3206
  closeModal('m-zac');
2775
- toast('ZCode 自动领取:已关闭');
3207
+ toast(T('ZCode 自动领取:已关闭'));
2776
3208
  } catch (err) { toast(err.message, false); }
2777
3209
  });
2778
3210
 
@@ -2794,8 +3226,8 @@ $('zgw-ok').addEventListener('click', async () => {
2794
3226
  renderStatus(); render();
2795
3227
  closeModal('m-zgw');
2796
3228
  if (await maybePromptRestartForLan(oldLan, state.zGatewayLan)) return;
2797
- toast('体验包接口:' + (r.enabled ? '已开启' : '已关闭')
2798
- + (r.enabled && r.lan ? ',局域网开放' + (r.allow && r.allow.length ? ',白名单 ' + r.allow.join('、') : ',白名单为空——白名单外需虚拟 key') + ';绑定改动重启后生效' : ''));
3229
+ toast(T('体验包接口:') + (r.enabled ? T('已开启') : T('已关闭'))
3230
+ + (r.enabled && r.lan ? T(',局域网开放') + (r.allow && r.allow.length ? T(',白名单 ') + r.allow.join(T('、')) : T(',白名单为空——白名单外需虚拟 key')) + T(';绑定改动重启后生效') : ''));
2799
3231
  } catch (err) { toast(err.message, false); }
2800
3232
  });
2801
3233
 
@@ -2817,8 +3249,8 @@ $('mgw-ok').addEventListener('click', async () => {
2817
3249
  renderStatus(); render();
2818
3250
  closeModal('m-mgw');
2819
3251
  if (await maybePromptRestartForLan(oldLan, state.mGatewayLan)) return;
2820
- toast('MiniMax 接口:' + (r.enabled ? '已开启' : '已关闭')
2821
- + (r.enabled && r.lan ? ',局域网开放' + (r.allow && r.allow.length ? ',白名单 ' + r.allow.join('、') : '') + ';绑定改动重启后生效' : ''));
3252
+ toast(T('MiniMax 接口:') + (r.enabled ? T('已开启') : T('已关闭'))
3253
+ + (r.enabled && r.lan ? T(',局域网开放') + (r.allow && r.allow.length ? T(',白名单 ') + r.allow.join(T('、')) : '') + T(';绑定改动重启后生效') : ''));
2822
3254
  } catch (err) { toast(err.message, false); }
2823
3255
  });
2824
3256
 
@@ -2840,27 +3272,35 @@ $('tgw-ok').addEventListener('click', async () => {
2840
3272
  renderStatus(); render();
2841
3273
  closeModal('m-tgw');
2842
3274
  if (await maybePromptRestartForLan(oldLan, state.tGatewayLan)) return;
2843
- toast('Trae 接口:' + (r.enabled ? '已开启' : '已关闭')
2844
- + (r.enabled && r.lan ? ',局域网开放' + (r.allow && r.allow.length ? ',白名单 ' + r.allow.join('、') : '') + ';绑定改动重启后生效' : ''));
3275
+ toast(T('Trae 接口:') + (r.enabled ? T('已开启') : T('已关闭'))
3276
+ + (r.enabled && r.lan ? T(',局域网开放') + (r.allow && r.allow.length ? T(',白名单 ') + r.allow.join(T('、')) : '') + T(';绑定改动重启后生效') : ''));
2845
3277
  } catch (err) { toast(err.message, false); }
2846
3278
  });
2847
3279
 
2848
- // ZCode 网络出口:顺序 + 代理地址保存
3280
+ // 网络出口(全局,面板设置里):顺序 + 代理地址保存;代理只回填到 placeholder(脱敏),留空即清除
2849
3281
  $('znet-ok').addEventListener('click', async () => {
2850
3282
  try {
2851
3283
  const body = { proxyFirst: $('znet-proxy').checked, proxyUrl: $('znet-url').value.trim() };
2852
3284
  const r = await api('/api/zcode/net', { method: 'PUT', json: body });
3285
+ state.zProxyFirst = r.proxyFirst === true;
2853
3286
  await loadZNet();
2854
- renderStatus(); render();
2855
- closeModal('m-znet');
2856
- toast('ZCode 出口:' + (r.proxyFirst ? '代理优先(直连兜底)' : '直连优先(代理兜底)') + (body.proxyUrl === '' ? ',代理已清除' : ''));
3287
+ $('znet-url').value = '';
3288
+ $('znet-url').placeholder = state.zProxyUrl || (state.zHasEnvProxy ? T('跟随环境变量 HTTPS_PROXY') : 'http://192.168.31.101:7890');
3289
+ $('znet-state').textContent = T('已保存:') + (state.zProxyFirst === true ? T('代理优先') : T('直连优先'))
3290
+ + (state.zProxyUrl ? T(' · 代理 ') + state.zProxyUrl : '');
3291
+ toast(T('网络出口:') + (r.proxyFirst ? T('代理优先(直连兜底)') : T('直连优先(代理兜底)')) + (body.proxyUrl === '' ? T(',代理已清除') : T(',代理已保存')));
2857
3292
  } catch (err) { toast(err.message, false); }
2858
3293
  });
2859
- document.querySelectorAll('.mask').forEach((m) => {
3294
+ document.querySelectorAll('.mask, .setpop').forEach((m) => {
2860
3295
  m.addEventListener('mousedown', (e) => { if (e.target === m) closeModal(m.id); });
2861
3296
  m.querySelectorAll('[data-close]').forEach((b) => b.addEventListener('click', () => closeModal(m.id)));
2862
3297
  });
2863
- document.addEventListener('keydown', (e) => { if (e.key === 'Escape') document.querySelectorAll('.mask.show').forEach((m) => closeModal(m.id)); });
3298
+ document.addEventListener('keydown', (e) => {
3299
+ if (e.key !== 'Escape') return;
3300
+ const pop = document.getElementById('hmenu-pop');
3301
+ if (pop && pop.classList.contains('show')) { pop.classList.remove('show'); return; } // ESC 先收下拉
3302
+ document.querySelectorAll('.mask.show, .setpop.show').forEach((m) => closeModal(m.id));
3303
+ });
2864
3304
  document.querySelectorAll('#m-add .tab').forEach((t) => t.addEventListener('click', () => switchTab(t.dataset.tab)));
2865
3305
  function switchTab(name) {
2866
3306
  document.querySelectorAll('#m-add .tab').forEach((t) => t.classList.toggle('active', t.dataset.tab === name));
@@ -2883,7 +3323,7 @@ function openAdd(product) {
2883
3323
  // 各产品支持的添加方式:MiniMax 三种都支持(浏览器登录 = 官方设备码授权,独立凭据链不与本机客户端互斥)
2884
3324
  const tabShown = { local: true, device: product !== 'mirasim' && product !== 'catpaw' && product !== 'trae', token: product !== 'zcode' && product !== 'mirasim' && product !== 'catpaw' && product !== 'trae' };
2885
3325
  document.querySelectorAll('#m-add .tab').forEach((t) => { t.style.display = tabShown[t.dataset.tab] ? '' : 'none'; });
2886
- $('local-hint').textContent = LOCAL_HINT[state.addProduct];
3326
+ $('local-hint').textContent = T(LOCAL_HINT[state.addProduct]);
2887
3327
  let first = null;
2888
3328
  for (const o of $('f-provider').options) {
2889
3329
  const ok = !product || provOf(o.value).prod === product;
@@ -2899,8 +3339,8 @@ function openAdd(product) {
2899
3339
  if (ok && !firstDev) firstDev = o.value;
2900
3340
  }
2901
3341
  if (firstDev) $('dev-provider').value = firstDev;
2902
- $('dev-hint').textContent = (hasIncognito() ? '在内置隐私窗口里登录(不带出系统浏览器已登录的账号),' : '在浏览器里登录并授权,')
2903
- + '完成后账号自动添加。' + (product === 'qoder' ? '' : 'WorkBuddy / ZCode 用浏览器登录的账号同样可以一键切换客户端。');
3342
+ $('dev-hint').textContent = (hasIncognito() ? T('在内置隐私窗口里登录(不带出系统浏览器已登录的账号),') : T('在浏览器里登录并授权,'))
3343
+ + T('完成后账号自动添加。') + (product === 'qoder' ? '' : T('WorkBuddy / ZCode 用浏览器登录的账号同样可以一键切换客户端。'));
2904
3344
  openModal('m-add');
2905
3345
  switchTab('local');
2906
3346
  localScan();
@@ -2909,46 +3349,46 @@ function openAdd(product) {
2909
3349
  // 本机导入
2910
3350
  async function localScan() {
2911
3351
  const box = $('scan-result');
2912
- $('scan-status').textContent = '扫描中(最多约 10 秒)…';
2913
- box.innerHTML = '<div class="hint">正在读取本机客户端凭据(Qoder / WorkBuddy / ZCode / mirasim / 妙手 / Trae / MiniMax)…</div>';
3352
+ $('scan-status').textContent = T('扫描中(最多约 10 秒)…');
3353
+ box.innerHTML = T('<div class="hint">正在读取本机客户端凭据(Qoder / WorkBuddy / ZCode / mirasim / 妙手 / Trae / MiniMax)…</div>');
2914
3354
  try {
2915
3355
  const r = await api('/api/local/scan', { method: 'POST' });
2916
3356
  $('scan-status').textContent = '';
2917
3357
  let html = '';
2918
3358
  const cands = r.candidates.filter((c) => !state.addProduct || (c.provider ? provOf(c.provider).prod : 'qoder') === state.addProduct);
2919
3359
  for (const c of cands) {
2920
- const title = c.kind === 'app' ? esc(c.name || c.email || c.phone || '(未命名)') : '<span style="font-family:monospace">' + esc(c.tokenMasked) + '</span>';
2921
- const prov = (c.provider ? provBadges(c.provider) : '<span class="badge">' + esc(c.kind) + '</span>') + (c.current ? '<span class="pill cur">当前登录</span>' : '');
3360
+ const title = c.kind === 'app' ? esc(c.name || c.email || c.phone || T('(未命名)')) : '<span style="font-family:monospace">' + esc(c.tokenMasked) + '</span>';
3361
+ const prov = (c.provider ? provBadges(c.provider) : '<span class="badge">' + esc(c.kind) + '</span>') + (c.current ? T('<span class="pill cur">当前登录</span>') : '');
2922
3362
  const sub = c.kind === 'app'
2923
- ? esc(c.source) + (c.email ? ' · ' + esc(mask(c.email)) : c.phone ? ' · ' + esc(c.phone) : '') + (c.expiresAt ? ' · 有效期至 ' + esc(fmtTime(c.expiresAt)) : '')
2924
- : esc(c.source) + ' · ' + esc(String(c.file || '').split(/[\\/]/).pop()) + '(' + c.hits + ' 处)';
2925
- const btns = c.imported ? '<span class="pill ok">已导入</span>' + (c.provider ? '<button class="btn" data-cid="' + esc(c.id) + '" title="用本机凭据更新 token / 有效期 / 邮箱">同步信息</button>' : '')
2926
- : c.provider ? '<button class="btn primary" data-cid="' + esc(c.id) + '">导入</button>'
2927
- : '<button class="btn" data-cid="' + esc(c.id) + '" data-prov="qoder">导入·国际</button><button class="btn" data-cid="' + esc(c.id) + '" data-prov="qoder-cn">导入·国内</button>';
3363
+ ? esc(c.source) + (c.email ? ' · ' + esc(mask(c.email)) : c.phone ? ' · ' + esc(c.phone) : '') + (c.expiresAt ? T(' · 有效期至 ') + esc(fmtTime(c.expiresAt)) : '')
3364
+ : esc(c.source) + ' · ' + esc(String(c.file || '').split(/[\\/]/).pop()) + T('(') + c.hits + T(' 处)');
3365
+ const btns = c.imported ? T('<span class="pill ok">已导入</span>') + (c.provider ? '<button class="btn" data-cid="' + esc(c.id) + T('" title="用本机凭据更新 token / 有效期 / 邮箱">同步信息</button>') : '')
3366
+ : c.provider ? '<button class="btn primary" data-cid="' + esc(c.id) + T('">导入</button>')
3367
+ : '<button class="btn" data-cid="' + esc(c.id) + T('" data-prov="qoder">导入·国际</button><button class="btn" data-cid="') + esc(c.id) + T('" data-prov="qoder-cn">导入·国内</button>');
2928
3368
  html += '<div class="cand"><div class="cand-main"><div class="cand-name">' + title + prov + '</div><div class="cand-sub">' + sub + '</div></div><div class="row">' + btns + '</div></div>';
2929
3369
  }
2930
3370
  for (const e of r.errors || []) {
2931
- html += '<div class="hint" style="color:var(--err)">读取失败:' + esc(e.file) + ':' + esc(e.error) + '</div>';
3371
+ html += T('<div class="hint" style="color:var(--err)">读取失败:') + esc(e.file) + T(':') + esc(e.error) + '</div>';
2932
3372
  }
2933
3373
  if (!cands.length) {
2934
- const where = state.addProduct ? prodLabel(state.addProduct) : 'Qoder / WorkBuddy / ZCode / mirasim / 妙手 / Trae / MiniMax';
2935
- const alt = state.addProduct === 'zcode' || state.addProduct === 'mirasim' || state.addProduct === 'catpaw' || state.addProduct === 'trae' ? '' : ',或改用「浏览器登录」「粘贴 Token」';
2936
- html += '<div class="hint">未发现已登录的 ' + where + ' 账号。请先在客户端登录' + alt + '。<br>已检查:Qoder / Qoder CN 客户端数据目录、WorkBuddy 凭据目录、ZCode 数据目录、mirasim 数据目录、妙手凭据目录、Trae 数据目录、MiniMax 数据目录'
2937
- + (r.scannedDirs && r.scannedDirs.length ? '、' + r.scannedDirs.map(esc).join('、') : '') + '</div>';
3374
+ const where = state.addProduct ? prodLabel(state.addProduct) : T('Qoder / WorkBuddy / ZCode / mirasim / 妙手 / Trae / MiniMax');
3375
+ const alt = state.addProduct === 'zcode' || state.addProduct === 'mirasim' || state.addProduct === 'catpaw' || state.addProduct === 'trae' ? '' : T(',或改用「浏览器登录」「粘贴 Token」');
3376
+ html += T('<div class="hint">未发现已登录的 ') + where + T(' 账号。请先在客户端登录') + alt + T('。<br>已检查:Qoder / Qoder CN 客户端数据目录、WorkBuddy 凭据目录、ZCode 数据目录、mirasim 数据目录、妙手凭据目录、Trae 数据目录、MiniMax 数据目录')
3377
+ + (r.scannedDirs && r.scannedDirs.length ? T('、') + r.scannedDirs.map(esc).join(T('、')) : '') + '</div>';
2938
3378
  }
2939
3379
  box.innerHTML = html;
2940
3380
  box.querySelectorAll('button[data-cid]').forEach((b) => b.addEventListener('click', async () => {
2941
3381
  b.disabled = true;
2942
3382
  try {
2943
3383
  const d = await api('/api/local/import', { json: { candidateId: b.dataset.cid, provider: b.dataset.prov } });
2944
- toast((d.updated ? '已更新:' : '已导入:') + (d.account.name || d.account.id));
3384
+ toast((d.updated ? T('已更新:') : T('已导入:')) + (d.account.name || d.account.id));
2945
3385
  await loadAccounts();
2946
3386
  localScan();
2947
3387
  } catch (e) { toast(e.message, false); b.disabled = false; }
2948
3388
  }));
2949
3389
  } catch (e) {
2950
3390
  $('scan-status').textContent = '';
2951
- box.innerHTML = '<div class="hint" style="color:var(--err)">扫描失败:' + esc(e.message) + '</div>';
3391
+ box.innerHTML = T('<div class="hint" style="color:var(--err)">扫描失败:') + esc(e.message) + '</div>';
2952
3392
  }
2953
3393
  }
2954
3394
 
@@ -2972,13 +3412,13 @@ async function deviceLogin() {
2972
3412
  const flow = await api('/api/auth/device/start', { json: { provider } });
2973
3413
  $('device-box').style.display = '';
2974
3414
  $('dev-cancel').style.display = '';
2975
- const where = hasIncognito() ? '已在隐私窗口打开登录页' : '请在浏览器打开下方链接';
2976
- $('dev-status').textContent = where + ',登录并授权(' + Math.floor(flow.expiresIn / 60) + ' 分钟内有效)';
3415
+ const where = hasIncognito() ? T('已在隐私窗口打开登录页') : T('请在浏览器打开下方链接');
3416
+ $('dev-status').textContent = where + T(',登录并授权(') + Math.floor(flow.expiresIn / 60) + T(' 分钟内有效)');
2977
3417
  $('dev-link').href = flow.url;
2978
3418
  $('dev-link').textContent = flow.url;
2979
3419
  openAuth(flow.url);
2980
3420
  pollDevice(flow.sessionId, 0, ++devGen);
2981
- } catch (e) { toast('发起授权失败:' + e.message, false); }
3421
+ } catch (e) { toast(T('发起授权失败:') + e.message, false); }
2982
3422
  }
2983
3423
  async function pollDevice(sessionId, n, gen) {
2984
3424
  if (gen !== devGen) return;
@@ -2986,20 +3426,24 @@ async function pollDevice(sessionId, n, gen) {
2986
3426
  const r = await api('/api/auth/device/poll', { json: { sessionId } });
2987
3427
  if (gen !== devGen) return;
2988
3428
  if (r.status === 'pending') {
2989
- if (n > 450) return deviceStop('授权超时,请重试');
2990
- $('dev-status').textContent = '等待授权中…(' + n + ')';
3429
+ if (n > 450) return deviceStop(T('授权超时,请重试'));
3430
+ $('dev-status').textContent = T('等待授权中…(') + n + T(')');
2991
3431
  devTimer = setTimeout(() => pollDevice(sessionId, n + 1, gen), 2000);
2992
3432
  } else if (r.status === 'ok') {
2993
- deviceStop('✓ 登录成功:' + (r.account.name || r.account.id) + (r.already ? '(账号已存在,已更新登录凭据)' : ''));
2994
- toast(r.already ? '已更新「' + (r.account.name || r.account.id) + '」的登录凭据' : '已添加:' + (r.account.name || r.account.id));
3433
+ deviceStop(T('✓ 登录成功:') + (r.account.name || r.account.id) + (r.already ? T('(账号已存在,已更新登录凭据)') : ''));
3434
+ toast(r.already ? T('已更新「') + (r.account.name || r.account.id) + T('」的登录凭据') : T('已添加:') + (r.account.name || r.account.id));
2995
3435
  if (r.account.id) delete state.quotas[r.account.id]; // 旧 token 的积分查询结果(可能是失败)作废,马上重查
2996
3436
  await loadStatus(); await loadAccounts();
3437
+ // 网页会话 Cookie 是登录窗「关闭」时才收割绑定的(比这里的成功提示晚好几秒)——
3438
+ // 补两轮轻量刷新,「缺网页会话」徽章随绑定落地即时消失,不必等 30s 周期轮。
3439
+ setTimeout(refreshAccountList, 12000);
3440
+ setTimeout(refreshAccountList, 30000);
2997
3441
  } else if (r.status === 'expired') {
2998
- deviceStop('会话已过期,请重新发起');
3442
+ deviceStop(T('会话已过期,请重新发起'));
2999
3443
  } else {
3000
- deviceStop('授权失败:' + (r.error || r.status));
3444
+ deviceStop(T('授权失败:') + (r.error || r.status));
3001
3445
  }
3002
- } catch (e) { if (gen === devGen) deviceStop('轮询异常:' + e.message); }
3446
+ } catch (e) { if (gen === devGen) deviceStop(T('轮询异常:') + e.message); }
3003
3447
  }
3004
3448
  function deviceStop(msg) {
3005
3449
  devGen++;
@@ -3011,11 +3455,11 @@ function deviceStop(msg) {
3011
3455
  // 粘贴 token
3012
3456
  async function addToken() {
3013
3457
  const token = $('f-token').value.trim();
3014
- if (!token) return toast('请粘贴 token', false);
3458
+ if (!token) return toast(T('请粘贴 token'), false);
3015
3459
  const b = $('btn-add-token'); b.disabled = true;
3016
3460
  try {
3017
3461
  const d = await api('/api/accounts', { json: { provider: $('f-provider').value, name: $('f-name').value.trim(), token } });
3018
- toast('已添加:' + (d.account.name || d.account.id) + (d.account.verified === false ? '(token 校验失败,请检查)' : ''), d.account.verified !== false);
3462
+ toast(T('已添加:') + (d.account.name || d.account.id) + (d.account.verified === false ? T('(token 校验失败,请检查)') : ''), d.account.verified !== false);
3019
3463
  $('f-token').value = ''; $('f-name').value = '';
3020
3464
  closeModal('m-add');
3021
3465
  await loadAccounts();
@@ -3031,10 +3475,11 @@ function qoderWebBadge(a) {
3031
3475
  const at = a.qoderWebCapturedAt;
3032
3476
  if (at && Date.now() - new Date(at).getTime() <= 7 * 86400000) return '';
3033
3477
  const stale = Boolean(at);
3478
+ const proxyHint = a.provider === 'qoder' ? T('(国际版按 IP 识别地区:需先在「面板设置 → 网络出口」配置出口代理)') : '';
3034
3479
  const tip = stale
3035
- ? '网页会话已超过 7 天(' + fmtTime(at).slice(0, 10) + ' 抓取),可能已过期:导出到 10Router 后「套餐内 Credits」可能缺失,用「浏览器登录」重新登录一次 Qoder 即可补上'
3036
- : '缺少 Qoder 网页会话(Cookie):导出到 10Router 后「套餐内 Credits」可能缺失,用「浏览器登录」登录一次 Qoder 即可自动抓取';
3037
- return '<span class="badge web web-warn" title="' + esc(tip) + '">' + (stale ? '网页会话过期' : '缺网页会话') + '</span>';
3480
+ ? T('网页会话已超过 7 天(') + fmtTime(at).slice(0, 10) + T(' 抓取),可能已过期:导出到 10Router 后「套餐内 Credits」可能缺失,用「浏览器登录」重新登录一次 Qoder 即可补上') + proxyHint
3481
+ : T('缺少 Qoder 网页会话(Cookie):导出到 10Router 后「套餐内 Credits」可能缺失,用「浏览器登录」登录一次 Qoder 即可自动抓取') + proxyHint;
3482
+ return '<span class="badge web web-warn" title="' + esc(tip) + '">' + (stale ? T('网页会话过期') : T('缺网页会话')) + '</span>';
3038
3483
  }
3039
3484
 
3040
3485
  // 导出弹窗里的网页会话提示(10Router issue #44):
@@ -3056,12 +3501,12 @@ function qoderWebSessionNote() {
3056
3501
  else if (Date.now() - new Date(at).getTime() > 7 * 86400000) stale.push(a);
3057
3502
  }
3058
3503
  if (!missing.length && !stale.length) return '';
3059
- const names = (arr) => arr.map((x) => x.name || x.email || x.uid || x.id).join('、');
3504
+ const names = (arr) => arr.map((x) => x.name || x.email || x.uid || x.id).join(T('、'));
3060
3505
  const bits = [];
3061
- if (missing.length) bits.push('没有网页会话:' + names(missing));
3062
- if (stale.length) bits.push('会话已超过 7 天:' + names(stale));
3063
- return '10router 的「套餐内 Credits」逐资源包明细依赖 Qoder 网页会话(' + bits.join(';')
3064
- + ')。这些账号过去后套餐积分可能显示缺失——在「浏览器登录」重新登录一次 Qoder 再导出即可补上。';
3506
+ if (missing.length) bits.push(T('没有网页会话:') + names(missing));
3507
+ if (stale.length) bits.push(T('会话已超过 7 天:') + names(stale));
3508
+ return T('10router 的「套餐内 Credits」逐资源包明细依赖 Qoder 网页会话(') + bits.join(T(';'))
3509
+ + T(')。这些账号过去后套餐积分可能显示缺失——在「浏览器登录」重新登录一次 Qoder 再导出即可补上。');
3065
3510
  }
3066
3511
  function refreshExportNote() {
3067
3512
  const msg = qoderWebSessionNote();
@@ -3073,8 +3518,8 @@ function refreshExportNote() {
3073
3518
  // 导出
3074
3519
  async function doExport() {
3075
3520
  const password = $('ex-password').value;
3076
- if (!password.trim()) return toast('导出必须设置加密口令', false);
3077
- if (password.length < 4) return toast('口令至少 4 位', false);
3521
+ if (!password.trim()) return toast(T('导出必须设置加密口令'), false);
3522
+ if (password.length < 4) return toast(T('口令至少 4 位'), false);
3078
3523
  try {
3079
3524
  const sel = $('ex-provider').value;
3080
3525
  const scope = sel.startsWith('p:') ? { product: sel.slice(2) } : { provider: sel || undefined };
@@ -3087,7 +3532,7 @@ async function doExport() {
3087
3532
  URL.revokeObjectURL(a.href);
3088
3533
  $('ex-password').value = '';
3089
3534
  closeModal('m-export');
3090
- toast('已导出加密文件(可在 10router 或本面板导入)');
3535
+ toast(T('已导出加密文件(可在 10router 或本面板导入)'));
3091
3536
  } catch (e) { toast(e.message, false); }
3092
3537
  }
3093
3538
 
@@ -3098,10 +3543,10 @@ $('file-input').addEventListener('change', async () => {
3098
3543
  const f = $('file-input').files[0];
3099
3544
  if (!f) return;
3100
3545
  let data;
3101
- try { data = JSON.parse(await f.text()); } catch (e) { return toast('文件不是合法 JSON', false); }
3546
+ try { data = JSON.parse(await f.text()); } catch (e) { return toast(T('文件不是合法 JSON'), false); }
3102
3547
  pendingImport = data;
3103
3548
  if (data && data.format === '10router-oauth-secure-v1') {
3104
- $('pass-hint').textContent = '「' + f.name + '」已加密(CreditDaddy / 10router 迁移格式),请输入导出时设置的口令。';
3549
+ $('pass-hint').textContent = T('「') + f.name + T('」已加密(CreditDaddy / 10router 迁移格式),请输入导出时设置的口令。');
3105
3550
  $('pass-input').value = '';
3106
3551
  openModal('m-pass');
3107
3552
  setTimeout(() => $('pass-input').focus(), 50);
@@ -3113,7 +3558,7 @@ async function submitImport(password) {
3113
3558
  try {
3114
3559
  const d = await api('/api/import', { json: { data: pendingImport, password } });
3115
3560
  closeModal('m-pass');
3116
- toast('导入完成(' + (d.source === '10router' ? '10router' : 'CreditDaddy') + '):新增 ' + d.added + ',续期 ' + d.updated + ',跳过 ' + d.skipped);
3561
+ toast(T('导入完成(') + (d.source === '10router' ? '10router' : 'CreditDaddy') + T('):新增 ') + d.added + T(',续期 ') + d.updated + T(',跳过 ') + d.skipped);
3117
3562
  pendingImport = null;
3118
3563
  await loadAccounts();
3119
3564
  } catch (e) {
@@ -3124,7 +3569,7 @@ async function submitImport(password) {
3124
3569
 
3125
3570
  // ── 绑定 ──
3126
3571
  // 全局组件开关:收起 Qoder / WorkBuddy / ZCode / 10Router 的标签页与仪表盘对应卡片
3127
- $('btn-comp').innerHTML = I.tune;
3572
+ $('btn-comp').innerHTML = I.settings;
3128
3573
 
3129
3574
  // ── 卡片拖拽排序(产品面板 + 10Router):手动顺序优先级最高,落库 localStorage ──
3130
3575
  const dragState = { kind: null, id: null, vendor: null };
@@ -3241,13 +3686,13 @@ function applyComponents() {
3241
3686
  // 右上角面板设置:访问密码 + 组件显隐与排序 + 每产品账号排序(默认积分多者靠前,可手动固定)
3242
3687
  function secStatusHtml() {
3243
3688
  if (!state.sec.loaded) return '<span class="sec-status">…</span>';
3244
- return state.sec.enabled ? '<span class="sec-status on">已开启</span>' : '<span class="sec-status">未开启</span>';
3689
+ return state.sec.enabled ? T('<span class="sec-status on">已开启</span>') : T('<span class="sec-status">未开启</span>');
3245
3690
  }
3246
3691
  function renderSecActions() {
3247
3692
  if (!state.sec.loaded) return '';
3248
3693
  return state.sec.enabled
3249
- ? '<button class="btn" data-sec-action="change">修改密码</button><button class="btn" data-sec-action="disable">关闭</button>'
3250
- : '<button class="btn primary" data-sec-action="enable">开启面板密码</button>';
3694
+ ? T('<button class="btn" data-sec-action="change">修改密码</button><button class="btn" data-sec-action="disable">关闭</button>')
3695
+ : T('<button class="btn primary" data-sec-action="enable">开启面板密码</button>');
3251
3696
  }
3252
3697
  async function applyPanelKey(next) {
3253
3698
  setKey(next);
@@ -3255,86 +3700,87 @@ async function applyPanelKey(next) {
3255
3700
  }
3256
3701
  async function doSecAction(action) {
3257
3702
  if (action === 'enable' || action === 'change') {
3258
- const pw = await uiPrompt(action === 'enable' ? '设置面板访问密码' : '输入新密码', '', {
3259
- type: 'password', okText: action === 'enable' ? '开启' : '保存',
3260
- placeholder: '至少 4 位,不能包含空格',
3261
- message: '设置后每次打开面板都需要输入此密码。导出账号的加密口令是独立设置,始终必填。',
3703
+ const pw = await uiPrompt(action === 'enable' ? T('设置面板访问密码') : T('输入新密码'), '', {
3704
+ type: 'password', okText: action === 'enable' ? T('开启') : T('保存'),
3705
+ placeholder: T('至少 4 位,不能包含空格'),
3706
+ message: T('设置后每次打开面板都需要输入此密码。导出账号的加密口令是独立设置,始终必填。'),
3262
3707
  });
3263
3708
  if (!pw) return;
3264
3709
  const val = String(pw).trim();
3265
- if (val.length < 4) return toast('面板访问密码至少 4 位', false);
3266
- if (/\s/.test(val)) return toast('面板访问密码不能包含空格', false);
3710
+ if (val.length < 4) return toast(T('面板访问密码至少 4 位'), false);
3711
+ if (/\s/.test(val)) return toast(T('面板访问密码不能包含空格'), false);
3267
3712
  try {
3268
3713
  const r = await api('/api/settings', { method: 'PUT', json: { panelKey: val } });
3269
3714
  await applyPanelKey(val);
3270
3715
  renderCompMenu();
3271
- toast(r.panelKeyEnabled ? '已设置面板访问密码,下次打开面板需要输入' : '已保存', true);
3716
+ toast(r.panelKeyEnabled ? T('已设置面板访问密码,下次打开面板需要输入') : T('已保存'), true);
3272
3717
  } catch (e) { toast(e.message, false); }
3273
3718
  return;
3274
3719
  }
3275
3720
  if (action === 'disable') {
3276
3721
  const ok = await uiConfirm(
3277
- '关闭后打开面板不再需要密码(局域网 / 公网访问将无保护,仅建议本机使用)。账号导出仍必须设置加密口令。\n\n确定关闭面板访问密码吗?',
3278
- '关闭面板访问密码', '关闭');
3722
+ T('关闭后打开面板不再需要密码(局域网 / 公网访问将无保护,仅建议本机使用)。账号导出仍必须设置加密口令。\n\n确定关闭面板访问密码吗?'),
3723
+ T('关闭面板访问密码'), T('关闭'));
3279
3724
  if (!ok) return;
3280
3725
  try {
3281
3726
  const r = await api('/api/settings', { method: 'PUT', json: { disable: true } });
3282
3727
  await loadSettings();
3283
3728
  await applyPanelKey(r.panelKeyEnabled ? getKey() : '');
3284
3729
  renderCompMenu();
3285
- toast(r.panelKeyEnabled ? '该部署环境(fnOS / 环境变量)强制启用访问密码,无法在面板内关闭' : '已关闭面板访问密码', true);
3730
+ toast(r.panelKeyEnabled ? T('该部署环境(fnOS / 环境变量)强制启用访问密码,无法在面板内关闭') : T('已关闭面板访问密码'), true);
3286
3731
  } catch (e) { toast(e.message, false); }
3287
3732
  }
3288
3733
  }
3289
3734
  function renderCompMenu() {
3290
- const compRows = state.compOrder.map((id) => {
3735
+ // 组件顺序横向 chips:一行放下全部产品,勾选 = 显隐,拖动 = 前后移(顺序与标签栏同步)
3736
+ const compChips = state.compOrder.map((id) => {
3291
3737
  const c = COMPONENTS.find((x) => x.id === id);
3292
3738
  if (!c) return '';
3293
3739
  const vis = compVisible(id);
3294
- return '<div class="comp-row' + (vis ? '' : ' off') + '">'
3295
- + '<label class="check"><input type="checkbox" data-comp="' + id + '"' + (vis ? ' checked' : '') + '>' + esc(c.label) + '</label>'
3296
- + '<span class="comp-ops">'
3297
- + '<button class="icon-btn mini" data-move="up" data-comp="' + id + '" title="上移(仪表盘与标签栏同步)">' + I.chevUp + '</button>'
3298
- + '<button class="icon-btn mini" data-move="down" data-comp="' + id + '" title="下移(仪表盘与标签栏同步)">' + I.chevDown + '</button>'
3299
- + '</span></div>';
3740
+ return '<span class="comp-chip' + (vis ? '' : ' off') + '" draggable="true" data-drag-comp="' + id + '" data-comp-toggle="' + id + '"'
3741
+ + ' title="' + T('点击切换显隐') + ' · ' + T('按住拖动调整顺序') + '">'
3742
+ + T('<span class="drag-grip">') + I.grip + '</span>'
3743
+ + '<span class="sort-name">' + esc(T(c.label)) + '</span>'
3744
+ + '</span>';
3300
3745
  }).join('');
3301
3746
  const sortSections = PRODUCTS.filter((p) => compVisible(p.id) && ofProduct(p.id).length).map((p) => {
3302
3747
  const manual = state.cardOrder[p.id];
3303
- const rows = sortedAccounts(p.id, ofProduct(p.id)).map((a) => '<div class="sort-row" data-aid="' + esc(a.id) + '">'
3304
- + '<span class="sort-name" title="' + esc(a.name || a.email || a.uid || a.id) + '">' + esc(a.name || a.email || ('#' + a.id.slice(0, 6))) + '</span>'
3305
- + '<span class="comp-ops">'
3306
- + '<button class="icon-btn mini" data-acc-move="up" data-prod="' + p.id + '" data-aid="' + esc(a.id) + '" title="上移">' + I.chevUp + '</button>'
3307
- + '<button class="icon-btn mini" data-acc-move="down" data-prod="' + p.id + '" data-aid="' + esc(a.id) + '" title="下移">' + I.chevDown + '</button>'
3308
- + '</span></div>').join('');
3309
- return '<details class="sort-sec"' + (manual && manual.length ? ' open' : '') + '><summary>' + esc(p.label)
3310
- + (manual && manual.length ? ' <span class="badge">手动排序</span>' : '')
3748
+ const rows = sortedAccounts(p.id, ofProduct(p.id)).map((a) => '<span class="comp-chip" draggable="true" data-drag-acc="' + esc(a.id) + '" data-prod="' + p.id + '" title="' + esc(a.name || a.email || a.uid || a.id) + '">'
3749
+ + T('<span class="drag-grip" title="按住拖动调整顺序">') + I.grip + '</span>'
3750
+ + '<span class="sort-name">' + esc(a.name || a.email || ('#' + a.id.slice(0, 6))) + '</span>'
3751
+ + '</span>').join('');
3752
+ return '<details class="sort-sec"' + (manual && manual.length ? ' open' : '') + '><summary>' + esc(T(p.label))
3753
+ + (manual && manual.length ? T(' <span class="badge">手动排序</span>') : '')
3311
3754
  + '<span class="dim" style="font-size:11px">' + ofProduct(p.id).length + '</span></summary>'
3312
- + '<div class="sort-hint">默认按剩余积分从多到少;拖拽卡片或用上下按钮固定顺序。</div>'
3313
- + rows
3314
- + (manual && manual.length ? '<button class="btn" data-acc-reset="' + p.id + '" style="margin-top:6px;width:100%">恢复默认排序</button>' : '')
3755
+ + T('<div class="sort-hint">默认按剩余积分从多到少;直接按住拖动卡片调整顺序。</div>')
3756
+ + '<div class="comp-chips">' + rows + '</div>'
3757
+ + (manual && manual.length ? '<button class="btn" data-acc-reset="' + p.id + T('" style="margin-top:6px">恢复默认排序</button>') : '')
3315
3758
  + '</details>';
3316
3759
  }).join('');
3760
+ $('sec-block').innerHTML =
3761
+ secStatusHtml()
3762
+ + T('<div class="hint">开启后每次打开面板都需要输入密码;密码可随时在此修改或关闭。')
3763
+ + (state.sec.enabled ? '' : T('账号导出不受影响(导出口令始终必填)。')) + '</div>'
3764
+ + '<div class="row">' + renderSecActions() + '</div>';
3317
3765
  $('comp-menu').innerHTML =
3318
- '<div class="comp-menu-title">面板访问密码 ' + secStatusHtml() + '</div>'
3319
- + '<div class="hint">开启后每次打开面板都需要输入密码;密码可随时在此修改或关闭。'
3320
- + (state.sec.enabled ? '' : '账号导出不受影响(导出口令始终必填)。') + '</div>'
3321
- + '<div class="row">' + renderSecActions() + '</div>'
3322
- + '<div class="comp-menu-title">自动签到</div>'
3766
+ T('<div class="comp-menu-title">自动签到</div>')
3323
3767
  + '<div class="hint">' + autoCheckinHint() + '</div>'
3324
- + '<div class="comp-menu-title">组件顺序(仪表盘与标签栏同步)</div>' + compRows
3325
- + '<div class="comp-menu-title">账号卡片排序</div>'
3326
- + (sortSections || '<div class="sort-hint">暂无账号</div>');
3768
+ + T('<div class="comp-menu-title">组件顺序(仪表盘与标签栏同步)</div>')
3769
+ + T('<div class="sort-hint">点按切换显隐(置灰 = 隐藏);按住拖动调整顺序。</div>')
3770
+ + '<div class="comp-chips">' + compChips + '</div>'
3771
+ + T('<div class="comp-menu-title">账号卡片排序</div>')
3772
+ + (sortSections || T('<div class="sort-hint">暂无账号</div>'));
3327
3773
  }
3328
3774
 
3329
3775
  /** 自动签到口径说明:调度周期 + 各产品业务日刷新时点 */
3330
3776
  function autoCheckinHint() {
3331
3777
  const sch = state.status && state.status.scheduler;
3332
- const next = sch && sch.nextTickAt ? ',下次执行 ' + esc(String(sch.nextTickAt).slice(11, 16)) : '';
3778
+ const next = sch && sch.nextTickAt ? T(',下次执行 ') + esc(String(sch.nextTickAt).slice(11, 16)) : '';
3333
3779
  const zc = sch && sch.zcode;
3334
3780
  const zcText = zc && zc.enabled
3335
- ? 'ZCode 自动领取运行中(每 ' + (zc.intervalMin || 2) + ' 分钟' + (zc.autoOffAt ? ',约 ' + Math.max(1, Math.ceil((new Date(zc.autoOffAt).getTime() - Date.now()) / 60000)) + ' 分钟后自动关闭' : ',一直运行') + '),周期可在 ZCode 页「自动领取计划」里改'
3336
- : 'ZCode 活动领取默认关闭,可在 ZCode 页「自动领取计划」里开启并配置轮询周期';
3337
- return '守护进程每 ~2 小时自动扫描执行一轮全部账号' + next + '。各产品「业务日」刷新时点(UTC+8):<b>Qoder 每日 10:00</b>(每台设备每日限领一个国际版账号)、<b>WorkBuddy 每日 00:00</b>,其余产品按 00:00;已领成功的账号按对应业务日记忆跳过,跨过刷新点后当天照常重试。' + esc(zcText) + '。';
3781
+ ? T('ZCode 自动领取运行中(每 ') + (zc.intervalMin || 2) + T(' 分钟') + (zc.autoOffAt ? T(',约 ') + Math.max(1, Math.ceil((new Date(zc.autoOffAt).getTime() - Date.now()) / 60000)) + T(' 分钟后自动关闭') : T(',一直运行')) + T('),周期可在 ZCode 页「自动领取计划」里改')
3782
+ : T('ZCode 活动领取默认关闭,可在 ZCode 页「自动领取计划」里开启并配置轮询周期');
3783
+ return T('守护进程每 ~2 小时自动扫描执行一轮全部账号') + next + T('。各产品「业务日」刷新时点(UTC+8):<b>Qoder 每日 10:00</b>(每台设备每日限领一个国际版账号)、<b>WorkBuddy 每日 00:00</b>,其余产品按 00:00;已领成功的账号按对应业务日记忆跳过,跨过刷新点后当天照常重试。') + esc(zcText) + T('。');
3338
3784
  }
3339
3785
 
3340
3786
  /** 在手动排序列表里移动一个 id;列表不存在时先按当前生效顺序物化一份 */
@@ -3350,49 +3796,45 @@ function moveAccountId(prod, aid, dir) {
3350
3796
  saveCardOrder();
3351
3797
  }
3352
3798
  // 右上角齿轮 → 面板设置弹窗(访问密码 + 组件显隐/排序 + 账号卡片排序)
3799
+ // 面板设置分页签:通用 / 网络 / 组件与排序(内容越来越长的解法——分 pane 而不是一条流水账)
3800
+ $('set-tabs').addEventListener('click', (e) => {
3801
+ const b = e.target.closest('button[data-set-tab]');
3802
+ if (!b) return;
3803
+ document.querySelectorAll('#set-tabs .tab').forEach((x) => x.classList.toggle('active', x === b));
3804
+ document.querySelectorAll('#m-settings .pane[data-set-pane]').forEach((p) => p.classList.toggle('active', p.dataset.setPane === b.dataset.setTab));
3805
+ });
3353
3806
  $('btn-comp').addEventListener('click', async () => {
3807
+ // 已打开再点齿轮 = 关闭(普通弹出层的开合直觉)
3808
+ if ($('m-settings').classList.contains('show')) { closeModal('m-settings'); return; }
3354
3809
  renderCompMenu();
3355
3810
  openModal('m-settings');
3811
+ // 每次打开都回到「通用」页签
3812
+ document.querySelectorAll('#set-tabs .tab').forEach((x) => x.classList.toggle('active', x.dataset.setTab === 'net'));
3813
+ document.querySelectorAll('#m-settings .pane[data-set-pane]').forEach((p) => p.classList.toggle('active', p.dataset.setPane === 'net'));
3356
3814
  await loadSettings();
3357
3815
  renderCompMenu();
3358
- // 桌面版才有检查更新入口(走 preload 桥到主进程的更新状态机)
3359
- if (updBridge) {
3360
- $('upd-entry').style.display = '';
3361
- try { const s = await updBridge.state(); updState = s; $('upd-ver').textContent = '当前 v' + (s.current || '?'); } catch {}
3362
- }
3363
- });
3364
- $('comp-menu').addEventListener('change', (e) => {
3365
- const c = e.target.dataset && e.target.dataset.comp;
3366
- if (!c) return;
3367
- if (e.target.checked) state.hidden.delete(c); else state.hidden.add(c);
3368
- saveHiddenComps();
3369
- applyComponents();
3370
- renderCompMenu();
3816
+ fillAutoCheckin();
3817
+ $('cfg-logs-display').value = logsDisplayDefault();
3818
+ $('cfg-wb-activity').checked = wbActOn();
3819
+ // 网络出口(全局):预填当前顺序与脱敏代理地址
3820
+ await loadZNet();
3821
+ (state.zProxyFirst === true ? $('znet-proxy') : $('znet-direct')).checked = true;
3822
+ $('znet-url').value = '';
3823
+ $('znet-url').placeholder = state.zProxyUrl || (state.zHasEnvProxy ? T('跟随环境变量 HTTPS_PROXY') : 'http://192.168.31.101:7890');
3824
+ $('znet-state').textContent = '';
3371
3825
  });
3372
3826
  $('comp-menu').addEventListener('click', (e) => {
3373
- const secBtn = e.target.closest && e.target.closest('[data-sec-action]');
3374
- if (secBtn) { doSecAction(secBtn.dataset.secAction); return; }
3375
- const move = e.target.closest && e.target.closest('[data-move]');
3376
- if (move) {
3377
- e.stopPropagation();
3378
- const id = move.dataset.comp;
3379
- const i = state.compOrder.indexOf(id);
3380
- const j = move.dataset.move === 'up' ? i - 1 : i + 1;
3381
- if (i < 0 || j < 0 || j >= state.compOrder.length) return;
3382
- [state.compOrder[i], state.compOrder[j]] = [state.compOrder[j], state.compOrder[i]];
3383
- saveCompOrder();
3827
+ const toggle = e.target.closest && e.target.closest('[data-comp-toggle]');
3828
+ if (toggle && Date.now() >= suppressToggleUntil) {
3829
+ const c = toggle.dataset.compToggle;
3830
+ if (state.hidden.has(c)) state.hidden.delete(c); else state.hidden.add(c);
3831
+ saveHiddenComps();
3384
3832
  applyComponents();
3385
3833
  renderCompMenu();
3386
3834
  return;
3387
3835
  }
3388
- const accMove = e.target.closest && e.target.closest('[data-acc-move]');
3389
- if (accMove) {
3390
- e.stopPropagation();
3391
- moveAccountId(accMove.dataset.prod, accMove.dataset.aid, accMove.dataset.accMove);
3392
- render();
3393
- renderCompMenu();
3394
- return;
3395
- }
3836
+ const secBtn = e.target.closest && e.target.closest('[data-sec-action]');
3837
+ if (secBtn) { doSecAction(secBtn.dataset.secAction); return; }
3396
3838
  const reset = e.target.closest && e.target.closest('[data-acc-reset]');
3397
3839
  if (reset) {
3398
3840
  e.stopPropagation();
@@ -3402,13 +3844,81 @@ $('comp-menu').addEventListener('click', (e) => {
3402
3844
  renderCompMenu();
3403
3845
  }
3404
3846
  });
3405
- applyComponents();
3847
+
3848
+ // ── 拖拽重排:组件与账号卡片共用(HTML5 DnD,横向 chip 按中线决定插前/插后)──
3849
+ let dragCtx = null; // {type:'comp'|'acc', id, prod}
3850
+ let suppressToggleUntil = 0; // 拖放结束瞬-ms 内不触发显隐切换(防止拖着拖着被当成点击)
3851
+ const clearDropMarks = () => document.querySelectorAll('.comp-chip.drop-before, .comp-chip.drop-after, .comp-chip.dragging')
3852
+ .forEach((x) => x.classList.remove('drop-before', 'drop-after', 'dragging'));
3853
+ $('comp-menu').addEventListener('dragstart', (e) => {
3854
+ const compEl = e.target.closest('[data-drag-comp]');
3855
+ const accEl = e.target.closest('[data-drag-acc]');
3856
+ dragCtx = compEl ? { type: 'comp', id: compEl.dataset.dragComp }
3857
+ : accEl ? { type: 'acc', prod: accEl.dataset.prod, id: accEl.dataset.dragAcc } : null;
3858
+ if (!dragCtx) return;
3859
+ e.dataTransfer.effectAllowed = 'move';
3860
+ try { e.dataTransfer.setData('text/plain', dragCtx.id); } catch {}
3861
+ (compEl || accEl).classList.add('dragging');
3862
+ });
3863
+ $('comp-menu').addEventListener('dragend', () => { dragCtx = null; suppressToggleUntil = Date.now() + 150; clearDropMarks(); });
3864
+ $('comp-menu').addEventListener('dragover', (e) => {
3865
+ if (!dragCtx) return;
3866
+ const sel = dragCtx.type === 'comp' ? '[data-drag-comp]' : '[data-drag-acc]';
3867
+ const t = e.target.closest(sel);
3868
+ if (!t) { e.dataTransfer.dropEffect = 'none'; return; }
3869
+ if (dragCtx.type === 'acc' && t.dataset.prod !== dragCtx.prod) { e.dataTransfer.dropEffect = 'none'; return; }
3870
+ e.preventDefault();
3871
+ e.dataTransfer.dropEffect = 'move';
3872
+ const r = t.getBoundingClientRect();
3873
+ const before = e.clientX < r.left + r.width / 2;
3874
+ t.classList.toggle('drop-before', before);
3875
+ t.classList.toggle('drop-after', !before);
3876
+ });
3877
+ $('comp-menu').addEventListener('dragleave', (e) => {
3878
+ const t = e.target.closest('.comp-chip');
3879
+ if (t) t.classList.remove('drop-before', 'drop-after');
3880
+ });
3881
+ $('comp-menu').addEventListener('drop', (e) => {
3882
+ if (!dragCtx) return;
3883
+ const sel = dragCtx.type === 'comp' ? '[data-drag-comp]' : '[data-drag-acc]';
3884
+ const t = e.target.closest(sel);
3885
+ clearDropMarks();
3886
+ if (!t) { dragCtx = null; return; }
3887
+ e.preventDefault();
3888
+ const r = t.getBoundingClientRect();
3889
+ const after = e.clientX >= r.left + r.width / 2;
3890
+ if (dragCtx.type === 'comp') {
3891
+ const arr = state.compOrder.filter((id) => id !== dragCtx.id);
3892
+ let idx = arr.indexOf(t.dataset.dragComp);
3893
+ if (idx < 0) { dragCtx = null; return; }
3894
+ if (after) idx += 1;
3895
+ arr.splice(idx, 0, dragCtx.id);
3896
+ state.compOrder = arr;
3897
+ saveCompOrder();
3898
+ applyComponents();
3899
+ } else {
3900
+ const baseIds = sortedAccounts(dragCtx.prod, ofProduct(dragCtx.prod)).map((a) => a.id);
3901
+ const arr = (state.cardOrder[dragCtx.prod] && state.cardOrder[dragCtx.prod].length ? state.cardOrder[dragCtx.prod].slice() : baseIds)
3902
+ .filter((id) => id !== dragCtx.id);
3903
+ let idx = arr.indexOf(t.dataset.dragAcc);
3904
+ if (idx < 0) { dragCtx = null; return; }
3905
+ if (after) idx += 1;
3906
+ arr.splice(idx, 0, dragCtx.id);
3907
+ state.cardOrder[dragCtx.prod] = arr;
3908
+ saveCardOrder();
3909
+ }
3910
+ suppressToggleUntil = Date.now() + 150;
3911
+ dragCtx = null;
3912
+ renderCompMenu();
3913
+ });
3914
+
3915
+ applyComponents();
3406
3916
  renderCompMenu();
3407
3917
 
3408
- $('btn-checkin-all').innerHTML = I.play + '全部领取';
3409
- for (const id of ['btn-add', 'btn-add-dash']) $(id).innerHTML = I.plus + '添加账号';
3410
- for (const id of ['btn-import', 'btn-import-dash']) { $(id).innerHTML = I.upload + '导入'; $(id).onclick = pickImport; }
3411
- for (const id of ['btn-export', 'btn-export-dash']) $(id).innerHTML = I.download + '导出';
3918
+ $('btn-checkin-all').innerHTML = I.play + T('全部领取');
3919
+ for (const id of ['btn-add', 'btn-add-dash']) $(id).innerHTML = I.plus + T('添加账号');
3920
+ for (const id of ['btn-import', 'btn-import-dash']) { $(id).innerHTML = I.upload + T('导入'); $(id).onclick = pickImport; }
3921
+ for (const id of ['btn-export', 'btn-export-dash']) $(id).innerHTML = I.download + T('导出');
3412
3922
  $('btn-checkin-all').onclick = () => checkinAll($('btn-checkin-all'));
3413
3923
  $('btn-checkin').onclick = () => checkinAll($('btn-checkin'), state.view);
3414
3924
  $('btn-add-dash').onclick = () => openAdd('');
@@ -3427,7 +3937,7 @@ $('btn-pass-ok').onclick = () => submitImport($('pass-input').value);
3427
3937
  $('pass-input').addEventListener('keydown', (e) => { if (e.key === 'Enter') submitImport($('pass-input').value); });
3428
3938
  $('btn-scan').onclick = localScan;
3429
3939
  $('btn-device').onclick = deviceLogin;
3430
- $('dev-cancel').onclick = () => deviceStop('已取消');
3940
+ $('dev-cancel').onclick = () => deviceStop(T('已取消'));
3431
3941
  $('dev-link').addEventListener('click', (e) => { e.preventDefault(); openAuth($('dev-link').href); });
3432
3942
  $('btn-add-token').onclick = addToken;
3433
3943
  $('f-token').addEventListener('keydown', (e) => { if (e.key === 'Enter') addToken(); });
@@ -3476,15 +3986,16 @@ $('region').addEventListener('click', (e) => {
3476
3986
  renderStatus();
3477
3987
  });
3478
3988
  $('f-provider').addEventListener('change', () => {
3479
- $('f-token').placeholder = $('f-provider').value === 'minimax'
3480
- ? 'MiniMax OAuth Bearer Token(mmoat_…)'
3989
+ $('f-token').placeholder = $('f-provider').value.startsWith('minimax')
3990
+ ? T('MiniMax OAuth Bearer Token(mmoat_…)')
3481
3991
  : $('f-provider').value.startsWith('workbuddy')
3482
- ? 'WorkBuddy / CodeBuddy 登录 token(eyJ…,可在 CodeBuddyExtension 的 auth/*.info 中找到)'
3483
- : 'dt-… / jt-… 或 PAT(pt-…,自动兑换)';
3992
+ ? T('WorkBuddy / CodeBuddy 登录 token(eyJ…,可在 CodeBuddyExtension 的 auth/*.info 中找到)')
3993
+ : T('dt-… / jt-… 或 PAT(pt-…,自动兑换)');
3484
3994
  });
3485
3995
  $('btn-logs').onclick = () => {
3486
3996
  const c = $('log-card').classList.toggle('collapsed');
3487
- $('btn-logs').textContent = c ? '展开' : '收起';
3997
+ try { localStorage.setItem('qd_logs_expanded', c ? '0' : '1'); } catch {}
3998
+ $('btn-logs').textContent = c ? T('展开') : T('收起');
3488
3999
  if (!c) { loadCurrentLogs(); $('logs').scrollTop = $('logs').scrollHeight; }
3489
4000
  };
3490
4001
  $('log-tabs').addEventListener('click', (e) => {
@@ -3516,7 +4027,7 @@ function applyDashFold() {
3516
4027
  });
3517
4028
  }
3518
4029
  document.querySelectorAll('.dash-cols .fold-head').forEach((h) => {
3519
- h.title = '展开 / 收起';
4030
+ h.title = T('展开 / 收起');
3520
4031
  h.addEventListener('click', () => {
3521
4032
  const k = h.closest('.pcard').dataset.fold;
3522
4033
  state.dashFold[k] = state.dashFold[k] !== true;
@@ -3528,11 +4039,15 @@ applyDashFold();
3528
4039
 
3529
4040
  // ── 启动:先直接请求(本机模式无需密钥);只有服务端返回 401 时才询问密钥 ──
3530
4041
  initTheme();
3531
- $('btn-home').innerHTML = I.github;
3532
4042
  applyView();
4043
+ function hideBootBusy() { const b = $('boot-busy'); if (!b) return; clearTimeout(hideBootBusy._t); b.classList.add('fade'); setTimeout(() => b.remove(), 300); }
3533
4044
  (function boot() {
3534
4045
  function start() {
3535
- loadStatus().then(loadAccounts).catch((e) => toast(e.message, false));
4046
+ // 兜底:接口长时间不返回也不能让加载态永久盖着
4047
+ hideBootBusy._t = setTimeout(hideBootBusy, 9000);
4048
+ applyDictFromServer().then(() => {
4049
+ window.__syncTheme?.(); // 头部主题按钮的图标/标题在字典到达后重渲染(initTheme 时字典还没到,T() 当时只能回落中文)
4050
+ loadStatus().then(loadAccounts).then(hideBootBusy, (e) => { toast(e.message, false); hideBootBusy(); });
3536
4051
  loadSettings().then(renderCompMenu);
3537
4052
  loadZNet();
3538
4053
  loadZGateway();
@@ -3547,12 +4062,15 @@ applyView();
3547
4062
  });
3548
4063
  loadLogs();
3549
4064
  setInterval(loadLogs, 10000);
3550
- setInterval(async () => { await loadStatus(); await loadZNet(); await loadZGateway(); await loadMGateway(); await loadTGateway(); render(); }, 30000);
4065
+ setInterval(async () => { await loadStatus(); await loadZNet(); await loadZGateway(); await loadMGateway(); await loadTGateway(); await refreshAccountList(); render(); }, 30000);
4066
+ });
3551
4067
  }
3552
4068
  // 先直接请求(本机模式无需密码);需要密码时 api() 会弹框并重试
3553
- api('/api/status').then(start).catch((e) => toast(e.code === 'PANEL_KEY'
3554
- ? '未输入面板访问密码,刷新页面可重试' : '无法连接守护进程:' + e.message, false));
4069
+ api('/api/status').then(start).catch((e) => { hideBootBusy(); toast(e.code === 'PANEL_KEY'
4070
+ ? T('未输入面板访问密码,刷新页面可重试') : T('无法连接守护进程:') + e.message, false); });
3555
4071
  })();
3556
4072
  </script>
4073
+ <!-- 语言切换加载态 -->
4074
+ <div id="locale-busy" hidden><div class="lb-card"><span class="lb-spin"></span><span data-i18n="正在切换语言…">正在切换语言…</span></div></div>
3557
4075
  </body>
3558
4076
  </html>