creditdaddy 0.9.5 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +56 -8
- package/bin/creditdaddy.js +209 -191
- package/package.json +4 -3
- package/src/accounts.js +161 -143
- package/src/authDevice.js +205 -205
- package/src/catpawClient.js +103 -0
- package/src/catpawLocal.js +229 -0
- package/src/checkin.js +357 -230
- package/src/constants.js +97 -81
- package/src/daemon.js +935 -658
- package/src/localDetect.js +147 -147
- package/src/logger.js +39 -1
- package/src/mirasimClient.js +189 -189
- package/src/mirasimLocal.js +386 -307
- package/src/panel.html +765 -214
- package/src/providers.js +190 -100
- package/src/qoderApp.js +524 -332
- package/src/qoderClient.js +237 -199
- package/src/qoderUmid.js +198 -198
- package/src/store.js +238 -222
- package/src/tenrouter.js +263 -221
- package/src/tenrouterAccounts.js +163 -0
- package/src/traeClient.js +258 -0
- package/src/traeLocal.js +337 -0
- package/src/transfer.js +160 -141
- package/src/usageSync.js +505 -319
- package/src/workbuddyAuth.js +127 -126
- package/src/workbuddyClient.js +302 -302
- package/src/workbuddyLocal.js +133 -133
- package/src/zcodeAuth.js +131 -131
- package/src/zcodeAutoClaim.js +97 -96
- package/src/zcodeClient.js +580 -529
- package/src/zcodeGateway.js +570 -0
- package/src/zcodeLocal.js +334 -319
- package/src/zcodePlanShape.json +29 -0
- package/src/zcrypto.js +160 -160
package/src/panel.html
CHANGED
|
@@ -43,13 +43,23 @@
|
|
|
43
43
|
.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; }
|
|
44
44
|
.ver { color: var(--text-3); font-size: 11px; line-height: 1.3; font-variant-numeric: tabular-nums; }
|
|
45
45
|
.head-right { display: flex; align-items: center; gap: 10px; color: var(--text-3); font-size: 12px; }
|
|
46
|
-
/*
|
|
47
|
-
.comp-
|
|
48
|
-
.comp-menu {
|
|
49
|
-
.comp-menu
|
|
50
|
-
.comp-menu-title { font-size: 11px; color: var(--text-3); }
|
|
46
|
+
/* 面板设置(右上角齿轮 → 弹窗):访问密码 + 组件显隐与排序 + 账号卡片排序 */
|
|
47
|
+
.comp-menu { display: flex; flex-direction: column; gap: 8px; }
|
|
48
|
+
.comp-menu-title { font-size: 11px; color: var(--text-3); margin: 10px 0 4px; padding-top: 8px; border-top: 1px dashed var(--border-subtle); }
|
|
49
|
+
.comp-menu-title:first-child { margin-top: 0; padding-top: 0; border-top: 0; }
|
|
51
50
|
.comp-menu .check { margin-right: 0; font-size: 12.5px; color: var(--text); }
|
|
52
|
-
|
|
51
|
+
/* 产品页 notice 的功能分区行 */
|
|
52
|
+
.z-row { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
|
53
|
+
.dot { width: 7px; height: 7px; border-radius: 50%; background: var(--text-3); opacity: .45; flex: none; }
|
|
54
|
+
.dot.on { background: var(--ok); opacity: 1; box-shadow: 0 0 5px var(--ok); }
|
|
55
|
+
.dot.pulse { animation: dot-pulse 2.2s ease-out infinite; }
|
|
56
|
+
@keyframes dot-pulse { 0% { box-shadow: 0 0 0 0 rgba(46, 229, 157, .45); } 70% { box-shadow: 0 0 0 5px transparent; } 100% { box-shadow: 0 0 0 0 transparent; } }
|
|
57
|
+
.z-row + .z-row { margin-top: 7px; padding-top: 7px; border-top: 1px dashed var(--border-subtle); }
|
|
58
|
+
.z-label { font-size: 11px; color: var(--text-3); flex: none; width: 68px; }
|
|
59
|
+
.z-text { flex: 1 1 320px; min-width: 0; }
|
|
60
|
+
/* 10Router 状态条分两行 */
|
|
61
|
+
.sec-status { font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--border); color: var(--text-3); }
|
|
62
|
+
.sec-status.on { color: var(--ok); border-color: rgba(var(--accent-rgb), .45); }
|
|
53
63
|
.comp-row { display: flex; align-items: center; justify-content: space-between; gap: 6px; }
|
|
54
64
|
.comp-row.off .check { opacity: .45; }
|
|
55
65
|
.comp-ops { display: inline-flex; gap: 2px; flex: none; }
|
|
@@ -95,9 +105,9 @@
|
|
|
95
105
|
.notice { margin: 12px 16px 0; padding: 9px 12px; border-radius: 10px; background: var(--warn-soft); color: var(--warn); font-size: 12px; line-height: 1.6; }
|
|
96
106
|
|
|
97
107
|
/* cards */
|
|
98
|
-
.list { display: grid; grid-template-columns: repeat(auto-fill, minmax(440px, 1fr)); gap:
|
|
99
|
-
.card { position: relative; padding: 11px 12px 12px; border-radius: 12px; transition: background .12s; min-width: 0; }
|
|
100
|
-
.card:hover { background: var(--hover); }
|
|
108
|
+
.list { display: grid; grid-template-columns: repeat(auto-fill, minmax(440px, 1fr)); gap: 10px; padding: 10px; }
|
|
109
|
+
.card { position: relative; padding: 11px 12px 12px; border-radius: 12px; background: var(--surface-2); border: 1px solid var(--border-subtle); transition: background .12s, border-color .12s; min-width: 0; }
|
|
110
|
+
.card:hover { background: var(--hover); border-color: var(--border-strong); }
|
|
101
111
|
.row1 { display: flex; align-items: center; justify-content: space-between; gap: 10px; min-height: 28px; margin-bottom: 7px; }
|
|
102
112
|
.name-group { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; min-width: 0; }
|
|
103
113
|
.name { font-size: 14px; font-weight: 600; cursor: text; border-radius: 5px; padding: 0 2px; margin: 0 -2px; max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
@@ -112,6 +122,10 @@
|
|
|
112
122
|
html[data-theme="dark"] .badge.prod.workbuddy { color: #5eead4; }
|
|
113
123
|
.badge.prod.zcode { color: #1d4ed8; background: rgba(37,99,235,.1); }
|
|
114
124
|
html[data-theme="dark"] .badge.prod.zcode { color: #93c5fd; }
|
|
125
|
+
.badge.prod.catpaw { color: #c2410c; background: rgba(249,115,22,.14); }
|
|
126
|
+
html[data-theme="dark"] .badge.prod.catpaw { color: #fdba74; }
|
|
127
|
+
.badge.prod.trae { color: #047857; background: rgba(16,185,129,.14); }
|
|
128
|
+
html[data-theme="dark"] .badge.prod.trae { color: #6ee7b7; }
|
|
115
129
|
.seg-ctl { display: inline-flex; gap: 2px; padding: 2px; border-radius: 9px; background: var(--hover); }
|
|
116
130
|
.seg-ctl button { border: none; background: transparent; color: var(--text-2); font-size: 12px; font-weight: 600; padding: 4px 10px; border-radius: 7px; cursor: pointer; }
|
|
117
131
|
.seg-ctl button.active { background: var(--surface); color: var(--text); box-shadow: 0 1px 3px rgba(0,0,0,.08); }
|
|
@@ -124,6 +138,8 @@
|
|
|
124
138
|
.pill.fail { color: var(--err); border-color: var(--err-soft); }
|
|
125
139
|
.pill.cur { color: #0f766e; border-color: rgba(20,184,166,.5); }
|
|
126
140
|
html[data-theme="dark"] .pill.cur { color: #5eead4; }
|
|
141
|
+
.pill.gw { color: #4338ca; border-color: rgba(99,102,241,.45); }
|
|
142
|
+
html[data-theme="dark"] .pill.gw { color: #a5b4fc; }
|
|
127
143
|
.ops { display: flex; gap: 6px; flex-shrink: 0; }
|
|
128
144
|
|
|
129
145
|
.meta { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 8px; font-size: 12px; }
|
|
@@ -144,16 +160,24 @@
|
|
|
144
160
|
#cap-holder { margin-top: 10px; min-height: 0; }
|
|
145
161
|
#cap-btn { margin-top: 10px; }
|
|
146
162
|
.bar { display: flex; gap: 2px; width: 100%; height: 5px; background: color-mix(in srgb, var(--tertiary) 80%, transparent); border-radius: 3px; overflow: hidden; }
|
|
147
|
-
.seg { display: block; height: 5px; min-width: 3px; background:
|
|
163
|
+
.seg { display: block; height: 5px; min-width: 3px; background: var(--ok); opacity: var(--a, 1); transition: filter .15s; }
|
|
148
164
|
.seg.used { background: color-mix(in srgb, var(--text-3) 35%, transparent); }
|
|
149
165
|
.seg:hover { filter: brightness(1.12); }
|
|
150
166
|
.msg { margin-top: 7px; font-size: 11px; color: var(--text-3); line-height: 1.5; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
151
167
|
.msg.err { color: var(--err); }
|
|
152
168
|
.empty { grid-column: 1 / -1; text-align: center; color: var(--text-3); padding: 42px 12px; font-size: 13px; line-height: 1.9; }
|
|
169
|
+
.gw-line { text-align: center; color: var(--text-3); padding: 14px 16px 4px; font-size: 13px; line-height: 1.9; }
|
|
170
|
+
.kpi-label[title], .stat span[title] { cursor: help; }
|
|
153
171
|
|
|
154
172
|
/* section card */
|
|
155
173
|
.pcard { margin-top: 14px; background: var(--surface); border: 1px solid var(--border-subtle); border-radius: 14px; padding: 12px 14px; }
|
|
156
174
|
.pcard-title { display: flex; align-items: center; justify-content: space-between; gap: 8px; font-size: 13px; font-weight: 700; }
|
|
175
|
+
.fold-head { cursor: pointer; user-select: none; }
|
|
176
|
+
.fold-side { display: inline-flex; align-items: center; gap: 6px; color: var(--text-3); }
|
|
177
|
+
.fold-ico { display: inline-flex; }
|
|
178
|
+
.fold-ico svg { width: 14px; height: 14px; }
|
|
179
|
+
.pcard .fold-body { display: none; margin-top: 4px; }
|
|
180
|
+
.pcard.open .fold-body { display: block; }
|
|
157
181
|
.logs { margin-top: 8px; font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 11px; line-height: 1.65; max-height: 240px; overflow-y: auto; background: var(--tertiary); border-radius: 9px; padding: 8px 10px; }
|
|
158
182
|
.logs .info { color: var(--text-2); } .logs .warn { color: var(--warn); } .logs .error { color: var(--err); } .logs .debug { color: var(--text-3); }
|
|
159
183
|
.collapsed .logs { display: none; }
|
|
@@ -181,8 +205,9 @@
|
|
|
181
205
|
.hint { font-size: 12px; color: var(--text-3); line-height: 1.7; }
|
|
182
206
|
.hint a { color: #4f86ff; word-break: break-all; }
|
|
183
207
|
.box { border: 1px solid var(--border-subtle); border-radius: 10px; padding: 10px 12px; margin-top: 10px; background: var(--surface-2); }
|
|
184
|
-
.cand { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding:
|
|
185
|
-
.cand
|
|
208
|
+
.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); }
|
|
209
|
+
.cand + .cand { margin-top: 8px; }
|
|
210
|
+
.cand .row { flex: none; flex-wrap: nowrap; gap: 6px; }
|
|
186
211
|
.cand-main { min-width: 0; }
|
|
187
212
|
.cand-name { font-weight: 600; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
|
188
213
|
.cand-sub { font-size: 11px; color: var(--text-3); margin-top: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
@@ -200,8 +225,15 @@
|
|
|
200
225
|
.view { display: none; } .view.active { display: block; }
|
|
201
226
|
|
|
202
227
|
/* 仪表盘 */
|
|
203
|
-
.dash-
|
|
204
|
-
.dash-title { font-size:
|
|
228
|
+
.dash-panel { margin-bottom: 12px; }
|
|
229
|
+
.dash-title { font-size: 13px; font-weight: 700; }
|
|
230
|
+
.dash-panel .toolbar { padding: 8px 16px; }
|
|
231
|
+
.dash-panel .kpis { display: flex; gap: 0; margin: 0; padding: 0 10px 8px; }
|
|
232
|
+
.dash-panel .kpi { background: none; border: none; border-radius: 0; padding: 6px 14px; flex: 1 1 0; min-width: 0; }
|
|
233
|
+
.dash-panel .kpi-value { font-size: 22px; margin-top: 2px; }
|
|
234
|
+
.dash-panel .kpi-sub { margin-top: 2px; }
|
|
235
|
+
.dash-panel .kpi + .kpi { border-left: 1px dashed var(--border-subtle); }
|
|
236
|
+
@media (max-width: 900px) { .dash-panel .kpis { flex-wrap: wrap; } .dash-panel .kpi { flex: 1 1 40%; } .dash-panel .kpi + .kpi { border-left: none; } }
|
|
205
237
|
.kpis { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; margin-bottom: 10px; }
|
|
206
238
|
.kpi { background: var(--surface); border: 1px solid var(--border-subtle); border-radius: 14px; padding: 14px 16px; min-width: 0; }
|
|
207
239
|
.kpi-label { color: var(--text-3); font-size: 12px; font-weight: 500; }
|
|
@@ -215,7 +247,7 @@
|
|
|
215
247
|
.prod-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
216
248
|
.prod-name { display: flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 700; }
|
|
217
249
|
.dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
|
218
|
-
.dot.qoder { background: #7c3aed; } .dot.workbuddy { background: #14b8a6; } .dot.zcode { background: #2563eb; }
|
|
250
|
+
.dot.qoder { background: #7c3aed; } .dot.workbuddy { background: #14b8a6; } .dot.zcode { background: #2563eb; } .dot.mirasim { background: #64748b; } .dot.catpaw { background: #f97316; } .dot.trae { background: #10b981; }
|
|
219
251
|
.prod-go { color: var(--text-3); font-size: 12px; white-space: nowrap; display: inline-flex; align-items: center; gap: 2px; }
|
|
220
252
|
.prod-go svg { width: 12px; height: 12px; transition: transform .15s; }
|
|
221
253
|
.prodcard:hover .prod-go { color: var(--text); }
|
|
@@ -223,7 +255,7 @@
|
|
|
223
255
|
.prod-rows { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 7px 12px; font-size: 12px; align-items: center; }
|
|
224
256
|
.prod-rows .val { text-align: right; }
|
|
225
257
|
.meter { height: 5px; background: color-mix(in srgb, var(--tertiary) 80%, transparent); border-radius: 3px; overflow: hidden; }
|
|
226
|
-
.meter span { display: block; height: 100%; background:
|
|
258
|
+
.meter span { display: block; height: 100%; background: var(--ok); border-radius: 3px; }
|
|
227
259
|
.dash-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
|
228
260
|
.dash-cols .pcard { margin-top: 0; min-width: 0; }
|
|
229
261
|
.item { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 8px 4px; margin: 0 -4px; border-bottom: 1px solid var(--border-subtle); font-size: 12px; cursor: pointer; border-radius: 6px; }
|
|
@@ -247,10 +279,11 @@
|
|
|
247
279
|
.tr-vrow { }
|
|
248
280
|
.tr-vhead { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; font-size: 13px; }
|
|
249
281
|
.tr-vhead .dim { font-size: 12px; }
|
|
250
|
-
.tr-tabs { display: flex; gap: 6px; flex-wrap: wrap; margin:
|
|
282
|
+
.tr-tabs { display: flex; gap: 6px; flex-wrap: wrap; margin: 14px 12px 4px; }
|
|
251
283
|
.tr-vcards { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; align-items: stretch; }
|
|
252
284
|
@media (max-width: 1180px) { .tr-vcards { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
|
253
285
|
@media (max-width: 820px) { .tr-vcards { grid-template-columns: 1fr; } }
|
|
286
|
+
.tr-card.flat { align-self: start; }
|
|
254
287
|
.tr-card { border: 1px solid var(--border-subtle); border-radius: 12px; padding: 11px 12px 12px; background: var(--surface); min-width: 0; }
|
|
255
288
|
.tr-card.off { opacity: .6; }
|
|
256
289
|
.tr-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 8px; }
|
|
@@ -262,7 +295,7 @@
|
|
|
262
295
|
.tr-q-val { font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
263
296
|
.tr-q-val.low { color: var(--err); font-weight: 600; }
|
|
264
297
|
.tr-meter { height: 5px; border-radius: 3px; background: color-mix(in srgb, var(--tertiary) 80%, transparent); overflow: hidden; }
|
|
265
|
-
.tr-meter span { display: block; height: 100%; border-radius: 3px; background:
|
|
298
|
+
.tr-meter span { display: block; height: 100%; border-radius: 3px; background: var(--ok); }
|
|
266
299
|
.tr-meter span.low { background: var(--err); }
|
|
267
300
|
.tr-meter span.mid { background: #d97706; }
|
|
268
301
|
.tr-reset { color: var(--text-3); font-weight: 400; }
|
|
@@ -290,7 +323,6 @@
|
|
|
290
323
|
.tr-packs-head { font-size: 11px; color: var(--text-3); margin-bottom: 2px; }
|
|
291
324
|
.tr-packs-head b { color: var(--text-2); }
|
|
292
325
|
.tr-more { margin-top: 7px; border: none; background: none; color: #4f86ff; font-size: 12px; cursor: pointer; padding: 0; }
|
|
293
|
-
.tr-bar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; padding: 10px 16px; border-bottom: 1px solid var(--border-subtle); font-size: 12px; color: var(--text-2); }
|
|
294
326
|
.chip { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: 999px; font-size: 11px; background: var(--tertiary); color: var(--text-3); }
|
|
295
327
|
.chip.on { background: rgba(var(--accent-rgb), .14); color: var(--text); }
|
|
296
328
|
.setup { padding: 22px; max-width: 620px; }
|
|
@@ -317,10 +349,7 @@
|
|
|
317
349
|
</div>
|
|
318
350
|
<div class="head-right">
|
|
319
351
|
<span id="sched" class="num"></span>
|
|
320
|
-
<
|
|
321
|
-
<button class="icon-btn" id="btn-comp" title="面板设置:组件显隐与排序、账号卡片排序"></button>
|
|
322
|
-
<div class="comp-menu" id="comp-menu" hidden></div>
|
|
323
|
-
</div>
|
|
352
|
+
<button class="icon-btn" id="btn-comp" title="面板设置:访问密码、组件显隐与排序、账号卡片排序"></button>
|
|
324
353
|
<a class="icon-btn" id="btn-home" href="https://github.com/techysy/CreditDaddy" target="_blank" rel="noopener" title="项目主页(GitHub)"></a>
|
|
325
354
|
<button class="icon-btn" id="btn-theme" title="切换亮色 / 暗色"></button>
|
|
326
355
|
</div>
|
|
@@ -332,25 +361,29 @@
|
|
|
332
361
|
<button data-view="workbuddy">WorkBuddy<span class="cnt" id="cnt-workbuddy">0</span></button>
|
|
333
362
|
<button data-view="zcode">ZCode<span class="cnt" id="cnt-zcode">0</span></button>
|
|
334
363
|
<button data-view="mirasim">mirasim<span class="cnt" id="cnt-mirasim">0</span></button>
|
|
364
|
+
<button data-view="catpaw">妙手<span class="cnt" id="cnt-catpaw">0</span></button>
|
|
365
|
+
<button data-view="trae">Trae<span class="cnt" id="cnt-trae">0</span></button>
|
|
335
366
|
<button data-view="tenrouter">10Router<span class="cnt" id="cnt-tenrouter">–</span></button>
|
|
336
367
|
</nav>
|
|
337
368
|
|
|
338
369
|
<!-- 仪表盘 -->
|
|
339
370
|
<section class="view active" id="view-dash">
|
|
340
|
-
<div class="dash-
|
|
341
|
-
<div class="
|
|
342
|
-
|
|
343
|
-
<
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
371
|
+
<div class="panel dash-panel">
|
|
372
|
+
<div class="toolbar">
|
|
373
|
+
<div class="summary"><span class="dash-title">总览</span></div>
|
|
374
|
+
<div class="actions">
|
|
375
|
+
<button class="btn primary" id="btn-checkin-all"></button>
|
|
376
|
+
<button class="btn" id="btn-add-dash"></button>
|
|
377
|
+
<button class="btn" id="btn-import-dash"></button>
|
|
378
|
+
<button class="btn" id="btn-export-dash"></button>
|
|
379
|
+
</div>
|
|
347
380
|
</div>
|
|
381
|
+
<div class="kpis" id="kpis"></div>
|
|
348
382
|
</div>
|
|
349
|
-
<div class="kpis" id="kpis"></div>
|
|
350
383
|
<div class="prods" id="prods"></div>
|
|
351
384
|
<div class="dash-cols">
|
|
352
|
-
<section class="pcard"><div class="pcard-title"><span>最近记录</span><span class="hint" id="last-tick"></span></div><div id="recent"></div></section>
|
|
353
|
-
<section class="pcard"><div class="pcard-title"><span>需要处理</span><span class="hint" id="att-count"></span></div><div id="attention"></div></section>
|
|
385
|
+
<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>
|
|
386
|
+
<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>
|
|
354
387
|
</div>
|
|
355
388
|
</section>
|
|
356
389
|
|
|
@@ -373,6 +406,15 @@
|
|
|
373
406
|
</div>
|
|
374
407
|
<div class="actions">
|
|
375
408
|
<button class="btn primary" id="btn-checkin"></button>
|
|
409
|
+
<span id="z-actions" style="display:none">
|
|
410
|
+
<button class="btn" id="zt-ac"></button>
|
|
411
|
+
<button class="btn" id="zt-net"></button>
|
|
412
|
+
<button class="btn" id="zt-gw"></button>
|
|
413
|
+
<button class="btn" id="zt-open" style="display:none"></button>
|
|
414
|
+
</span>
|
|
415
|
+
<span id="t-actions" style="display:none">
|
|
416
|
+
<button class="btn" id="tt-open" style="display:none"></button>
|
|
417
|
+
</span>
|
|
376
418
|
<button class="btn" id="btn-add"></button>
|
|
377
419
|
<button class="btn" id="btn-import"></button>
|
|
378
420
|
<button class="btn" id="btn-export"></button>
|
|
@@ -448,13 +490,13 @@
|
|
|
448
490
|
<div class="modal-title"><span>导出账号</span><button class="close" data-close>✕</button></div>
|
|
449
491
|
<div class="field">
|
|
450
492
|
<label>范围</label>
|
|
451
|
-
<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></select>
|
|
493
|
+
<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></select>
|
|
452
494
|
</div>
|
|
453
495
|
<div class="field">
|
|
454
|
-
<label
|
|
455
|
-
<input class="input" id="ex-password" type="password" placeholder="至少 4
|
|
496
|
+
<label>加密口令(必填)</label>
|
|
497
|
+
<input class="input" id="ex-password" type="password" placeholder="至少 4 位" autocomplete="new-password">
|
|
456
498
|
</div>
|
|
457
|
-
<div class="hint"
|
|
499
|
+
<div class="hint">导出文件一律加密(10router 迁移格式,scrypt + AES-256-GCM),可直接在 10router 对应提供商页导入(WorkBuddy 对应 10router 的 CodeBuddy)。10router 按提供商导入,建议按单个版本导出。<b>口令不会被保存</b>,请牢记——遗失后文件无法解开、无法导入。</div>
|
|
458
500
|
<div class="modal-actions"><button class="btn" data-close>取消</button><button class="btn primary" id="btn-do-export">导出</button></div>
|
|
459
501
|
</div>
|
|
460
502
|
</div>
|
|
@@ -469,17 +511,38 @@
|
|
|
469
511
|
</div>
|
|
470
512
|
</div>
|
|
471
513
|
|
|
514
|
+
<!-- 面板设置(右上角齿轮) -->
|
|
515
|
+
<div class="mask" id="m-settings">
|
|
516
|
+
<div class="modal">
|
|
517
|
+
<div class="modal-title"><span>面板设置</span><button class="close" data-close>✕</button></div>
|
|
518
|
+
<div class="comp-menu" id="comp-menu"></div>
|
|
519
|
+
<div class="field" id="upd-entry" style="display:none;margin-top:12px">
|
|
520
|
+
<label>版本与更新</label>
|
|
521
|
+
<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>
|
|
522
|
+
</div>
|
|
523
|
+
<div class="modal-actions"><button class="btn" data-close>关闭</button></div>
|
|
524
|
+
</div>
|
|
525
|
+
</div>
|
|
526
|
+
|
|
527
|
+
<!-- 检查更新(桌面版):状态由主进程 update:state 推送 -->
|
|
528
|
+
<div class="mask" id="m-update">
|
|
529
|
+
<div class="modal sm">
|
|
530
|
+
<div class="modal-title"><span>检查更新</span><button class="close" data-close>✕</button></div>
|
|
531
|
+
<div id="upd-body"></div>
|
|
532
|
+
<div class="modal-actions" id="upd-actions"></div>
|
|
533
|
+
</div>
|
|
534
|
+
</div>
|
|
535
|
+
|
|
472
536
|
<!-- 10Router 设置 -->
|
|
473
537
|
<div class="mask" id="m-10r">
|
|
474
538
|
<div class="modal">
|
|
475
539
|
<div class="modal-title"><span>10Router 连接设置</span><button class="close" data-close>✕</button></div>
|
|
476
540
|
<div class="hint">填 10Router 的访问地址和仪表盘里创建的<b>虚拟 key(sk-…)</b>。key 只保存在 CreditDaddy 守护进程的数据目录(0600),面板里只显示脱敏值。</div>
|
|
477
|
-
<div class="field" style="margin-top:12px"><label>10Router 地址</label><input class="input mono" id="tr-endpoint" placeholder="http://127.0.0.1:20128
|
|
541
|
+
<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>
|
|
478
542
|
<div class="field"><label>虚拟 key</label><input class="input mono" id="tr-key" placeholder="sk-…" autocomplete="off"></div>
|
|
479
|
-
<div class="field"><label
|
|
480
|
-
|
|
481
|
-
<div id="tr-
|
|
482
|
-
<div class="hint" style="margin-top:4px">ZCode 只同步官方渠道;经 10Router 中转的 mirasim 调用会跳过,避免重复计数。10Router 按行签名去重,重复同步不会产生重复数据。</div>
|
|
543
|
+
<div class="field"><label>10Router 面板密码(可选)</label><input class="input mono" id="tr-admin-pw" type="password" placeholder="配置后「同步账号」走 OAuth 通道(带 refreshToken 自动续期);留空保持不变" autocomplete="new-password"></div>
|
|
544
|
+
<div class="field"><label>账号同步</label>
|
|
545
|
+
<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>
|
|
483
546
|
</div>
|
|
484
547
|
<div class="field"><label>显示</label>
|
|
485
548
|
<div><label class="check"><input type="checkbox" id="tr-hide-empty"> 「全部」视图隐藏没有额度 / 余额为 0 的供应商汇总卡</label></div>
|
|
@@ -493,6 +556,18 @@
|
|
|
493
556
|
</div>
|
|
494
557
|
</div>
|
|
495
558
|
|
|
559
|
+
<!-- 10Router 用量同步 -->
|
|
560
|
+
<div class="mask" id="m-trsync">
|
|
561
|
+
<div class="modal sm">
|
|
562
|
+
<div class="modal-title"><span>用量同步(同 10router-sync 插件)</span><button class="close" data-close>✕</button></div>
|
|
563
|
+
<div class="field"><label class="check"><input type="checkbox" id="tr-auto"> 每小时自动同步本机用量到 10Router</label></div>
|
|
564
|
+
<div class="field"><label>同步来源</label><div id="tr-sources"></div></div>
|
|
565
|
+
<div class="hint">ZCode 只同步官方渠道;经 10Router 中转的 mirasim 调用会跳过,避免重复计数。「10Router 本机」只把实例原生产生的行标记为网关同步。10Router 按行签名去重,重复同步不会产生重复数据。</div>
|
|
566
|
+
<div class="hint" id="tr-sync-status" style="margin-top:8px"></div>
|
|
567
|
+
<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>
|
|
568
|
+
</div>
|
|
569
|
+
</div>
|
|
570
|
+
|
|
496
571
|
<!-- ZCode 活动领取 -->
|
|
497
572
|
<div class="mask" id="m-claim">
|
|
498
573
|
<div class="modal">
|
|
@@ -512,6 +587,59 @@
|
|
|
512
587
|
</div>
|
|
513
588
|
</div>
|
|
514
589
|
|
|
590
|
+
<!-- ZCode 自动领取计划 -->
|
|
591
|
+
<div class="mask" id="m-zac">
|
|
592
|
+
<div class="modal sm">
|
|
593
|
+
<div class="modal-title"><span>ZCode 自动领取计划</span><button class="close" data-close>✕</button></div>
|
|
594
|
+
<div class="field">
|
|
595
|
+
<label>轮询周期(分钟,1–720)</label>
|
|
596
|
+
<input class="input" id="zac-interval" type="number" min="1" max="720" step="1">
|
|
597
|
+
</div>
|
|
598
|
+
<div class="field">
|
|
599
|
+
<label>单次运行时长(分钟,0 = 一直运行直到手动关闭)</label>
|
|
600
|
+
<input class="input" id="zac-window" type="number" min="0" max="1440" step="5">
|
|
601
|
+
</div>
|
|
602
|
+
<div class="hint">开启后按此周期检查活动资格并自动领取(首次检查在开启 2 分钟后开始)。活动包由服务端在每日 0 点左右刷新放出——若运行时段没有覆盖 0 点,会错过当次领取,建议把运行时长设为 0,或让时段跨过 0 点。需要验证码时无法自动通过,卡片上会提示手动领取。</div>
|
|
603
|
+
<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>
|
|
604
|
+
</div>
|
|
605
|
+
</div>
|
|
606
|
+
|
|
607
|
+
<!-- ZCode 网络出口 -->
|
|
608
|
+
<div class="mask" id="m-znet">
|
|
609
|
+
<div class="modal sm">
|
|
610
|
+
<div class="modal-title"><span>ZCode 网络出口</span><button class="close" data-close>✕</button></div>
|
|
611
|
+
<div class="field">
|
|
612
|
+
<label>上游接口访问顺序(失败 / 被拦时自动回退另一条)</label>
|
|
613
|
+
<label class="check"><input type="radio" name="znet-order" id="znet-direct"> 直连优先(代理兜底)</label>
|
|
614
|
+
<label class="check"><input type="radio" name="znet-order" id="znet-proxy"> 代理优先(直连兜底)</label>
|
|
615
|
+
</div>
|
|
616
|
+
<div class="field">
|
|
617
|
+
<label>出口代理(http://host:port,支持口令 http://user:pass@host:port)</label>
|
|
618
|
+
<input class="input" id="znet-url" placeholder="http://192.168.31.101:7890">
|
|
619
|
+
</div>
|
|
620
|
+
<div class="hint">留空 = 清除已保存的代理,退回 HTTPS_PROXY 环境变量;取消 = 不改动。当前代理只显示脱敏后的地址。</div>
|
|
621
|
+
<div class="modal-actions"><button class="btn" data-close>取消</button><button class="btn primary" id="znet-ok">保存</button></div>
|
|
622
|
+
</div>
|
|
623
|
+
</div>
|
|
624
|
+
|
|
625
|
+
<!-- ZCode 体验包接口 -->
|
|
626
|
+
<div class="mask" id="m-zgw">
|
|
627
|
+
<div class="modal sm">
|
|
628
|
+
<div class="modal-title"><span>ZCode 体验包接口</span><button class="close" data-close>✕</button></div>
|
|
629
|
+
<div class="hint" id="zgw-url" style="margin-bottom:10px"></div>
|
|
630
|
+
<div class="field">
|
|
631
|
+
<label class="check"><input type="checkbox" id="zgw-on"> 启用体验包接口(/gateway/v1/messages)</label>
|
|
632
|
+
</div>
|
|
633
|
+
<div class="field" id="zgw-lan-fields">
|
|
634
|
+
<label class="check"><input type="checkbox" id="zgw-lan"> 对局域网开放(重启后绑定 0.0.0.0)</label>
|
|
635
|
+
<label>IP 白名单(每行一个,支持 192.168.31.* 通配)</label>
|
|
636
|
+
<textarea class="input" id="zgw-allow" rows="4" placeholder="192.168.31.101"></textarea>
|
|
637
|
+
</div>
|
|
638
|
+
<div class="hint">开启后,领到的体验包额度变成本机的一个补全接口——10Router 建一个 anthropic-compatible 节点指向本机 /gateway 即可调用(需要桌面版,隐藏窗口过验证码)。白名单内的机器<b>免密直连</b>:10Router 的 zcode-free 本就是免授权供应商,把 10Router 服务地址的 IP 加白即可;白名单外的请求一律拒绝。本机回环(127.0.0.1)始终放行。</div>
|
|
639
|
+
<div class="modal-actions"><button class="btn" data-close>取消</button><button class="btn primary" id="zgw-ok">保存</button></div>
|
|
640
|
+
</div>
|
|
641
|
+
</div>
|
|
642
|
+
|
|
515
643
|
<input type="file" id="file-input" accept=".json,application/json" style="display:none">
|
|
516
644
|
<div id="toast"></div>
|
|
517
645
|
|
|
@@ -542,7 +670,7 @@ const I = {
|
|
|
542
670
|
|
|
543
671
|
const $ = (id) => document.getElementById(id);
|
|
544
672
|
const state = { accounts: [], quotas: {}, status: null, busy: {}, view: 'dash', region: '', addProduct: '' };
|
|
545
|
-
const VIEWS = ['dash', 'qoder', 'workbuddy', 'zcode', 'mirasim', 'tenrouter'];
|
|
673
|
+
const VIEWS = ['dash', 'qoder', 'workbuddy', 'zcode', 'mirasim', 'catpaw', 'trae', 'tenrouter'];
|
|
546
674
|
// 10Router 集成:config 来自 /api/tenrouter,quotas 来自 /api/tenrouter/quotas
|
|
547
675
|
const tr = { config: null, quotas: null, error: null, loading: false, syncing: false };
|
|
548
676
|
const PRODUCTS = [
|
|
@@ -550,6 +678,8 @@ const PRODUCTS = [
|
|
|
550
678
|
{ id: 'workbuddy', label: 'WorkBuddy' },
|
|
551
679
|
{ id: 'zcode', label: 'ZCode' },
|
|
552
680
|
{ id: 'mirasim', label: 'mirasim' },
|
|
681
|
+
{ id: 'catpaw', label: '妙手' },
|
|
682
|
+
{ id: 'trae', label: 'Trae' },
|
|
553
683
|
];
|
|
554
684
|
const prodLabel = (id) => (PRODUCTS.find((x) => x.id === id) || { label: id }).label;
|
|
555
685
|
|
|
@@ -559,16 +689,25 @@ const COMPONENTS = [
|
|
|
559
689
|
{ id: 'workbuddy', label: 'WorkBuddy' },
|
|
560
690
|
{ id: 'zcode', label: 'ZCode' },
|
|
561
691
|
{ id: 'mirasim', label: 'mirasim' },
|
|
692
|
+
{ id: 'catpaw', label: '妙手' },
|
|
693
|
+
{ id: 'trae', label: 'Trae' },
|
|
562
694
|
{ id: 'tenrouter', label: '10Router' },
|
|
563
695
|
];
|
|
564
696
|
const HIDDEN_COMP_KEY = 'qd_hidden_components';
|
|
697
|
+
const DEFAULT_HIDDEN_COMP = ['catpaw']; // 妙手默认收起,右上角设置里可打开
|
|
565
698
|
state.hidden = (() => {
|
|
699
|
+
let list = DEFAULT_HIDDEN_COMP;
|
|
700
|
+
const raw = localStorage.getItem(HIDDEN_COMP_KEY);
|
|
701
|
+
if (raw !== null) { try { list = JSON.parse(raw) || []; } catch { list = []; } }
|
|
702
|
+
// 一次性迁移:旧偏好是在「妙手默认隐藏」之前存的,补上默认值;此后完全尊重手动选择
|
|
703
|
+
if (raw !== null && localStorage.getItem('qd_hidden_comp_migrated') !== '1') {
|
|
704
|
+
list = [...new Set([...list, ...DEFAULT_HIDDEN_COMP])];
|
|
705
|
+
try { localStorage.setItem('qd_hidden_comp_migrated', '1'); } catch {}
|
|
706
|
+
}
|
|
566
707
|
const set = new Set();
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
}
|
|
571
|
-
} catch {}
|
|
708
|
+
for (const v of list) {
|
|
709
|
+
if (COMPONENTS.some((c) => c.id === v)) set.add(v);
|
|
710
|
+
}
|
|
572
711
|
return set;
|
|
573
712
|
})();
|
|
574
713
|
const compVisible = (id) => !state.hidden.has(id);
|
|
@@ -671,10 +810,14 @@ const PROV = {
|
|
|
671
810
|
'workbuddy-intl': { prod: 'workbuddy', prodLabel: 'WorkBuddy', region: '国际版', cls: 'intl' },
|
|
672
811
|
zcode: { prod: 'zcode', prodLabel: 'ZCode', region: '', cls: '' },
|
|
673
812
|
mirasim: { prod: 'mirasim', prodLabel: 'mirasim', region: '', cls: '' },
|
|
813
|
+
catpaw: { prod: 'catpaw', prodLabel: '妙手', region: '', cls: '' },
|
|
814
|
+
trae: { prod: 'trae', prodLabel: 'Trae', region: '', cls: '' },
|
|
674
815
|
};
|
|
675
|
-
// 支持每日签到的产品(ZCode / mirasim
|
|
676
|
-
const CHECKIN_PRODUCTS = new Set(['qoder', 'workbuddy']);
|
|
677
|
-
const CLIENT_LABEL = { workbuddy: 'WorkBuddy', zcode: 'ZCode', mirasim: 'mirasim' };
|
|
816
|
+
// 支持每日签到的产品(ZCode / mirasim / 妙手 额度靠套餐或滚动窗口发放,不做签到)
|
|
817
|
+
const CHECKIN_PRODUCTS = new Set(['qoder', 'workbuddy', 'trae']);
|
|
818
|
+
const CLIENT_LABEL = { qoder: 'Qoder', workbuddy: 'WorkBuddy', zcode: 'ZCode', mirasim: 'mirasim', catpaw: '妙手', trae: 'Trae' };
|
|
819
|
+
// 额度单位为 Credits/时长而非「积分」口径的产品(不计入剩余积分合计)
|
|
820
|
+
const CREDIT_VIEWS = new Set(['zcode', 'mirasim', 'catpaw']);
|
|
678
821
|
const provOf = (p) => PROV[p] || { prod: 'qoder', prodLabel: p, region: '', cls: '' };
|
|
679
822
|
const provBadges = (p, regionOnly, flavor) => {
|
|
680
823
|
const v = provOf(p);
|
|
@@ -720,8 +863,8 @@ function toast(msg, ok) {
|
|
|
720
863
|
}
|
|
721
864
|
|
|
722
865
|
// ── API ──
|
|
723
|
-
//
|
|
724
|
-
//
|
|
866
|
+
// 面板访问密码(在「设置」里开启,或 fnOS / CREDITDADDY_PASSWORD 环境变量注入时需要)。内存里的值优先:
|
|
867
|
+
// 面板嵌在飞牛桌面窗口里是跨域 iframe,浏览器拦截第三方存储时 localStorage 不可用,只靠它会每个请求都 401
|
|
725
868
|
let memKey = null;
|
|
726
869
|
function getKey() {
|
|
727
870
|
if (memKey !== null) return memKey;
|
|
@@ -738,7 +881,8 @@ function promptKey(invalid) {
|
|
|
738
881
|
keyPrompt = uiPrompt('输入面板访问密码', '', {
|
|
739
882
|
type: 'password', okText: '进入',
|
|
740
883
|
message: (invalid ? '密码不正确,请重新输入。\n' : '')
|
|
741
|
-
+ '
|
|
884
|
+
+ '面板访问密码在右上角「设置」里管理。\n'
|
|
885
|
+
+ 'fnOS:安装向导设置的密码(忘了可在 fnOS「应用设置」里重设)。\n命令行部署:环境变量 CREDITDADDY_PASSWORD 的值。',
|
|
742
886
|
}).then((k) => { keyPrompt = null; if (k === null || !k.trim()) return false; setKey(k.trim()); return true; });
|
|
743
887
|
}
|
|
744
888
|
return keyPrompt;
|
|
@@ -766,6 +910,16 @@ async function api(p, opts = {}, attempt = 0) {
|
|
|
766
910
|
// 10Router 汇总卡「隐藏无额度」偏好(面板本地)
|
|
767
911
|
try { state.trHideEmpty = localStorage.getItem('qd_tr_hideempty') === '1'; } catch (e) {}
|
|
768
912
|
|
|
913
|
+
// 面板访问密码状态(/api/settings)。boot 后异步拉取,供设置弹窗显示
|
|
914
|
+
state.sec = { enabled: false, loaded: false };
|
|
915
|
+
async function loadSettings() {
|
|
916
|
+
try {
|
|
917
|
+
const d = await api('/api/settings');
|
|
918
|
+
state.sec = { enabled: Boolean(d.panelKeyEnabled), loaded: true };
|
|
919
|
+
if (!d.panelKeyEnabled) setKey(''); // 关闭后清掉本地缓存的旧密钥
|
|
920
|
+
} catch (e) { /* 401(密码开启但未输入)等状态由 promptKey 流程处理 */ }
|
|
921
|
+
}
|
|
922
|
+
|
|
769
923
|
// ── 主题 ──
|
|
770
924
|
function applyTheme(t) {
|
|
771
925
|
document.documentElement.dataset.theme = t;
|
|
@@ -870,6 +1024,15 @@ function creditHtml(a) {
|
|
|
870
1024
|
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>';
|
|
871
1025
|
if (q.empty) return '<div class="credit-line"><span class="dim">无有效套餐(仅免费额度)</span></div><div class="bar"></div>';
|
|
872
1026
|
|
|
1027
|
+
// 妙手:Credits 钱包余额 + 当前版本(体验版 / 专业版),无「总量」口径,不画水位条
|
|
1028
|
+
if (a.product === 'catpaw') {
|
|
1029
|
+
const planBadge = q.plan ? '<span class="badge">' + esc(q.plan) + '</span>' : '';
|
|
1030
|
+
const expHtml = q.planExpiresAt ? '<span class="dim">' + fmtTime(q.planExpiresAt).slice(0, 10) + ' 到期</span>'
|
|
1031
|
+
: q.nextRefreshAt ? '<span class="dim">' + fmtTime(q.nextRefreshAt).slice(0, 10) + ' 下次刷新</span>' : '';
|
|
1032
|
+
return '<div class="credit-line"><span class="credit"><span class="lbl">可用额度</span><b>' + fmtNum(q.remaining) + '</b>Credits</span>'
|
|
1033
|
+
+ '<span class="credit dim">' + planBadge + (planBadge && expHtml ? ' · ' : '') + expHtml + '</span></div>';
|
|
1034
|
+
}
|
|
1035
|
+
|
|
873
1036
|
// 多窗口配额体系(mirasim 等):每个窗口平等等权,独立一行带分散对齐的名称、数值与三档细水位条
|
|
874
1037
|
if (a.product === 'mirasim' && q.parts && q.parts.length) {
|
|
875
1038
|
const isExhausted = q.exceeded || (q.remaining <= 0);
|
|
@@ -940,13 +1103,14 @@ function cardHtml(a) {
|
|
|
940
1103
|
+ '<span class="name" data-act="rename" title="点击修改备注名">' + esc(a.name || '(未命名)') + '</span>'
|
|
941
1104
|
+ provBadges(a.provider, true, a.flavor)
|
|
942
1105
|
+ (isCurrent ? '<span class="pill cur" title="' + esc(CLIENT_LABEL[a.product] || '') + ' 客户端当前登录的账号">客户端当前</span>' : '')
|
|
1106
|
+
+ (a.product === 'zcode' && isGatewayCurrent(a) ? '<span class="pill gw" title="体验包接口最近一次补全用的账号(多账号时按轮换自动选择,与客户端登录无关)">接口使用中</span>' : '')
|
|
943
1107
|
+ (a.isPat ? '<span class="badge">PAT</span>' : '')
|
|
944
1108
|
+ (a.verified === false ? '<span class="pill fail">token 无效</span>' : '')
|
|
945
1109
|
+ (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>' : '')
|
|
946
1110
|
+ '</div><div class="ops">'
|
|
947
1111
|
+ (a.product === 'zcode' ? '<button class="icon-btn" data-act="claim" title="活动领取(billing/preview → claim,可能需过一次验证码)">' + I.gift + '</button>' : '')
|
|
948
1112
|
+ (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>' : '')
|
|
949
|
-
+ (
|
|
1113
|
+
+ (a.canSwitch && !isCurrent && st.cls !== 'fail' ? '<button class="icon-btn" data-act="switch" title="切换 ' + (CLIENT_LABEL[a.product] || prodLabel(a.product)) + ' 客户端到此账号">' + I.swap + '</button>' : '')
|
|
950
1114
|
+ '<button class="icon-btn" data-act="quota" title="刷新积分">' + I.refresh + '</button>'
|
|
951
1115
|
+ '<button class="icon-btn del" data-act="del" title="删除(点两次确认)">' + I.trash + '</button>'
|
|
952
1116
|
+ '</div></div>'
|
|
@@ -963,23 +1127,36 @@ function cardHtml(a) {
|
|
|
963
1127
|
|
|
964
1128
|
const isDone = (a) => Boolean(state.status && state.status.todayDone && state.status.todayDone[a.id] === state.status.today);
|
|
965
1129
|
const ofProduct = (prod) => state.accounts.filter((a) => a.product === prod);
|
|
966
|
-
const currentUidOf = (prod) => state.status && (prod === 'workbuddy' ? state.status.workbuddyCurrentUid : prod === 'zcode' ? state.status.zcodeCurrentUid : null);
|
|
967
|
-
|
|
1130
|
+
const currentUidOf = (prod) => state.status && (prod === 'workbuddy' ? state.status.workbuddyCurrentUid : prod === 'zcode' ? state.status.zcodeCurrentUid : prod === 'catpaw' ? state.status.catpawCurrentUid : prod === 'trae' ? state.status.traeCurrentUid : null);
|
|
1131
|
+
/** 接口使用中判定:体验包接口最近一次补全用的账号(lastAccount 记的是 name || uid || id) */
|
|
1132
|
+
function isGatewayCurrent(a) {
|
|
1133
|
+
const last = state.zGatewayStats && state.zGatewayStats.lastAccount;
|
|
1134
|
+
return Boolean(last) && (a.name === last || a.uid === last || a.id === last);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// 客户端当前登录判定:WorkBuddy / mirasim / 妙手 按 uid;ZCode 按 uid 或 email(早期导入的账号可能只存了其中一个)
|
|
968
1138
|
function isClientCurrent(a) {
|
|
969
1139
|
const s = state.status;
|
|
970
1140
|
if (!s) return false;
|
|
971
1141
|
if (a.product === 'workbuddy') return Boolean(a.uid && s.workbuddyCurrentUid && s.workbuddyCurrentUid === a.uid);
|
|
1142
|
+
if (a.product === 'qoder') {
|
|
1143
|
+
const cur = s.qoderCurrent || {};
|
|
1144
|
+
return Boolean(a.uid && cur[a.provider] && String(cur[a.provider]) === String(a.uid));
|
|
1145
|
+
}
|
|
972
1146
|
if (a.product === 'mirasim') return Boolean(a.uid && s.mirasimCurrentUid && s.mirasimCurrentUid === a.uid);
|
|
1147
|
+
if (a.product === 'catpaw') return Boolean(a.uid && s.catpawCurrentUid && s.catpawCurrentUid === a.uid);
|
|
1148
|
+
if (a.product === 'trae') return Boolean(a.uid && s.traeCurrentUid && s.traeCurrentUid === a.uid);
|
|
973
1149
|
if (a.product === 'zcode') {
|
|
974
1150
|
const id = s.zcodeCurrentIdentity;
|
|
975
1151
|
return Boolean(id && ((a.uid && id.uid != null && String(id.uid) === String(a.uid)) || (a.email && id.email && id.email === a.email)));
|
|
976
1152
|
}
|
|
977
1153
|
return false;
|
|
978
1154
|
}
|
|
979
|
-
// 积分合计:只统计「积分」口径(ZCode
|
|
1155
|
+
// 积分合计:只统计「积分」口径(ZCode / 妙手是 token 额度或 Credits 余额,单位不同,不相加)
|
|
980
1156
|
function creditSum(list) {
|
|
981
1157
|
let sum = 0, known = 0, loading = 0;
|
|
982
1158
|
for (const a of list) {
|
|
1159
|
+
if (CREDIT_VIEWS.has(a.product)) continue;
|
|
983
1160
|
const q = state.quotas[a.id];
|
|
984
1161
|
if (!q) { loading++; continue; }
|
|
985
1162
|
if (q.error) continue;
|
|
@@ -990,6 +1167,8 @@ function creditSum(list) {
|
|
|
990
1167
|
}
|
|
991
1168
|
|
|
992
1169
|
function render() {
|
|
1170
|
+
$('z-actions').style.display = 'none'; // 兜底:切到仪表盘 / 10Router 时 renderProd 不跑,先藏起来
|
|
1171
|
+
$('t-actions').style.display = 'none';
|
|
993
1172
|
for (const p of PRODUCTS) $('cnt-' + p.id).textContent = ofProduct(p.id).length;
|
|
994
1173
|
$('cnt-tenrouter').textContent = tr.quotas ? tr.quotas.connections.length : tr.config && tr.config.configured ? '…' : '–';
|
|
995
1174
|
if (state.view === 'dash') renderDash();
|
|
@@ -1002,6 +1181,8 @@ const EMPTY_TIP = {
|
|
|
1002
1181
|
workbuddy: '还没有 WorkBuddy 账号。<br>点「添加账号」可读取本机 WorkBuddy 客户端(当前 + 历史会话),或用浏览器登录 WorkBuddy / CodeBuddy 账号。',
|
|
1003
1182
|
zcode: '还没有 ZCode 账号。<br>点「添加账号」可读取本机 ZCode 客户端已登录的账号,或用浏览器登录(BigModel / Z.ai),多个账号可一键切换。',
|
|
1004
1183
|
mirasim: '还没有 mirasim 账号。<br>点「添加账号」可读取本机 mirasim 客户端当前登录的账号(本地解密,token 不出本机)。',
|
|
1184
|
+
catpaw: '还没有妙手账号。<br>点「添加账号」可读取本机妙手(美团 CatPaw)客户端当前登录的账号(本地解密凭据,token 不出本机)。',
|
|
1185
|
+
trae: '还没有 Trae 账号。<br>点「添加账号 → 本机导入」可读取本机 Trae 客户端当前登录的账号(本地解密 storage.json 凭据,token 不出本机)。多个账号需先在 Trae 里分别登录并逐个导入。',
|
|
1005
1186
|
};
|
|
1006
1187
|
|
|
1007
1188
|
function renderProd() {
|
|
@@ -1018,19 +1199,19 @@ function renderProd() {
|
|
|
1018
1199
|
// 国内版 = 每日签到送积分;国际版 = 保持活跃(活跃不等于必有奖励)
|
|
1019
1200
|
const cnAcc = acc.filter((a) => a.provider === 'workbuddy');
|
|
1020
1201
|
const intlAcc = acc.filter((a) => a.provider === 'workbuddy-intl');
|
|
1021
|
-
$('s-done-wrap').innerHTML = (cnAcc.length ? '<div class="stat"><span>领取(国内)</span><strong>' + cnAcc.filter(isDone).length + ' / ' + cnAcc.length + '</strong></div><div class="divider"></div>' : '')
|
|
1022
|
-
+ (intlAcc.length ? '<div class="stat"><span>活跃(国际)</span><strong>' + intlAcc.filter(isDone).length + ' / ' + intlAcc.length + '</strong></div><div class="divider"></div>' : '');
|
|
1202
|
+
$('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>' : '')
|
|
1203
|
+
+ (intlAcc.length ? '<div class="stat"><span title="国际版定期发送活跃请求保持资格;活跃不等于必有奖励">活跃(国际)</span><strong>' + intlAcc.filter(isDone).length + ' / ' + intlAcc.length + '</strong></div><div class="divider"></div>' : '');
|
|
1023
1204
|
} else {
|
|
1024
1205
|
// 从 WorkBuddy 页切回来时 s-done 可能被 innerHTML 换没了,先重建结构再写值
|
|
1025
1206
|
if (!$('s-done')) $('s-done-wrap').innerHTML = '<div class="stat"><span>今日已领</span><strong id="s-done"></strong></div><div class="divider"></div>';
|
|
1026
1207
|
$('s-done').textContent = acc.filter(isDone).length + ' / ' + acc.length;
|
|
1027
1208
|
}
|
|
1028
1209
|
const c = creditSum(acc);
|
|
1029
|
-
$('s-credits-label').textContent = prod
|
|
1030
|
-
$('s-credits').textContent = (prod
|
|
1210
|
+
$('s-credits-label').textContent = CREDIT_VIEWS.has(prod) ? '有效套餐' : '剩余积分';
|
|
1211
|
+
$('s-credits').textContent = CREDIT_VIEWS.has(prod)
|
|
1031
1212
|
? acc.filter((a) => { const q = state.quotas[a.id]; return q && !q.error && !q.empty; }).length + ' / ' + acc.length
|
|
1032
|
-
: c.known ? fmtNum(c.sum) : '-';
|
|
1033
|
-
const hasRegions = prod
|
|
1213
|
+
: c.loading ? '…' : (c.known ? fmtNum(c.sum) : '-');
|
|
1214
|
+
const hasRegions = !CREDIT_VIEWS.has(prod) && prod !== 'trae'; // Trae 不分国际 / 国内版
|
|
1034
1215
|
$('region').style.display = hasRegions ? '' : 'none';
|
|
1035
1216
|
$('region-div').style.display = hasRegions ? '' : 'none';
|
|
1036
1217
|
document.querySelectorAll('#region button').forEach((x) => x.classList.toggle('active', x.dataset.r === state.region));
|
|
@@ -1039,10 +1220,40 @@ function renderProd() {
|
|
|
1039
1220
|
const hasIntl = acc.some((a) => a.provider === 'workbuddy-intl');
|
|
1040
1221
|
$('btn-checkin').innerHTML = I.play + (prod === 'workbuddy' && hasIntl ? '领取 / 活跃全部' : '领取全部 ' + prodLabel(prod));
|
|
1041
1222
|
}
|
|
1223
|
+
// 顶栏产品专属入口:ZCode 设置组、Trae 打开客户端,带状态灯(运行中绿点呼吸)
|
|
1224
|
+
const dot = (on, pulse) => '<span class="dot' + (on ? ' on' : '') + (on && pulse ? ' pulse' : '') + '"></span>';
|
|
1225
|
+
$('z-actions').style.display = prod === 'zcode' ? 'contents' : 'none';
|
|
1226
|
+
if (prod === 'zcode') {
|
|
1227
|
+
const acOn = state.zAutoClaim === true;
|
|
1228
|
+
const gwOn = state.zGateway === true;
|
|
1229
|
+
$('zt-ac').innerHTML = dot(acOn) + '自动领取';
|
|
1230
|
+
$('zt-ac').title = acOn ? '自动领取运行中——点我调整轮询周期 / 运行时长' : '自动领取已关闭——点我设置轮询周期与运行时长并开启';
|
|
1231
|
+
$('zt-net').innerHTML = '出口:<b style="font-weight:700;color:var(--text)">' + (state.zProxyFirst === true ? '代理优先' : '直连优先') + '</b>';
|
|
1232
|
+
$('zt-net').title = '网络出口:直连 / 代理顺序与代理地址';
|
|
1233
|
+
$('zt-gw').innerHTML = dot(gwOn) + '接口' + (gwOn && state.zGatewayLan === true ? '·局域网' : '');
|
|
1234
|
+
$('zt-gw').title = '体验包接口:开关、局域网开放与 IP 白名单';
|
|
1235
|
+
const zc = (state.status && state.status.zcodeClient) || {};
|
|
1236
|
+
const canOpen = zc.installed && window.creditdaddy && typeof window.creditdaddy.openZcodeClient === 'function';
|
|
1237
|
+
$('zt-open').style.display = canOpen ? '' : 'none';
|
|
1238
|
+
if (canOpen) {
|
|
1239
|
+
$('zt-open').innerHTML = dot(zc.running === true, true) + '打开客户端';
|
|
1240
|
+
$('zt-open').title = zc.running ? 'ZCode 客户端运行中,点我唤到前台' : 'ZCode 客户端未运行,点我启动';
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
$('t-actions').style.display = prod === 'trae' ? 'contents' : 'none';
|
|
1244
|
+
if (prod === 'trae') {
|
|
1245
|
+
const tc = (state.status && state.status.traeClient) || {};
|
|
1246
|
+
const canOpen = tc.installed && window.creditdaddy && typeof window.creditdaddy.openTraeClient === 'function';
|
|
1247
|
+
$('tt-open').style.display = canOpen ? '' : 'none';
|
|
1248
|
+
if (canOpen) {
|
|
1249
|
+
$('tt-open').innerHTML = dot(tc.running === true, true) + '打开客户端';
|
|
1250
|
+
$('tt-open').title = tc.running ? 'Trae 客户端运行中,点我唤到前台' : 'Trae 客户端未运行,点我启动';
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1042
1253
|
}
|
|
1043
1254
|
|
|
1044
|
-
function kpi(label, value, sub) {
|
|
1045
|
-
return '<div class="kpi"><div class="kpi-label">' + label + '</div><div class="kpi-value">' + value + '</div><div class="kpi-sub" title="' + esc(sub.replace(/<[^>]+>/g, '')) + '">' + sub + '</div></div>';
|
|
1255
|
+
function kpi(label, value, sub, labelTitle) {
|
|
1256
|
+
return '<div class="kpi"><div class="kpi-label"' + (labelTitle ? ' title="' + esc(labelTitle) + '"' : '') + '>' + label + '</div><div class="kpi-value">' + value + '</div><div class="kpi-sub" title="' + esc(sub.replace(/<[^>]+>/g, '')) + '">' + sub + '</div></div>';
|
|
1046
1257
|
}
|
|
1047
1258
|
const meter = (done, total) => '<div class="meter"><span style="width:' + (total ? Math.round(done / total * 100) : 0) + '%"></span></div>';
|
|
1048
1259
|
|
|
@@ -1068,8 +1279,9 @@ function renderDash() {
|
|
|
1068
1279
|
: done + ' <small>/ ' + signable.length + '</small>',
|
|
1069
1280
|
!signable.length ? '暂无可领取的账号'
|
|
1070
1281
|
: (doneDom === domAcc.length && doneIntl === intlAcc.length ? (intlAcc.length ? '今天全部完成(国际为活跃请求,不保证奖励)' : '今天全部签完')
|
|
1071
|
-
: '待办 ' + (signable.length - done) + (failed ? ' · 失败 ' + failed : ''))
|
|
1072
|
-
|
|
1282
|
+
: '待办 ' + (signable.length - done) + (failed ? ' · 失败 ' + failed : '')),
|
|
1283
|
+
'领取 = 国内版每日签到;活跃 = 国际版定期活跃请求(保持资格,不保证奖励)')
|
|
1284
|
+
+ kpi('剩余积分', credit.loading ? '…' : (credit.known ? fmtNum(credit.sum) : '-'), credit.loading ? '正在读取各账号额度…' : creditByProd)
|
|
1073
1285
|
+ kpi('下次自动领取', sch.ticking ? '领取中…' : sch.nextTickAt ? fmtTime(sch.nextTickAt).slice(11) : '-',
|
|
1074
1286
|
sch.lastTick ? '上一轮 ' + fmtTime(sch.lastTick.at).slice(5) + ' · ' + esc(sch.lastTick.summary || '') : '每 ~2 小时一轮');
|
|
1075
1287
|
|
|
@@ -1103,6 +1315,11 @@ function renderDash() {
|
|
|
1103
1315
|
const planned = list.filter((a) => { const q = state.quotas[a.id]; return q && !q.error && !q.empty; }).length;
|
|
1104
1316
|
rows = '<span class="lbl">额度体系</span><span class="val dim">5h / 7d 滚动窗口</span>'
|
|
1105
1317
|
+ '<span class="lbl">有效套餐</span><span class="val">' + planned + ' / ' + list.length + '</span>';
|
|
1318
|
+
} else if (p.id === 'catpaw') {
|
|
1319
|
+
const withPlan = list.filter((a) => { const q = state.quotas[a.id]; return q && !q.error && q.plan; });
|
|
1320
|
+
const credits = withPlan.reduce((s, a) => s + (Number(state.quotas[a.id].remaining) || 0), 0);
|
|
1321
|
+
rows = '<span class="lbl">额度体系</span><span class="val dim">Credits 余额 + 套餐版本</span>'
|
|
1322
|
+
+ (withPlan.length ? '<span class="lbl">可用额度</span><span class="val">' + fmtNum(credits) + ' Credits(' + esc(withPlan.map((a) => state.quotas[a.id].plan).join(' / ')) + ')</span>' : '');
|
|
1106
1323
|
} else {
|
|
1107
1324
|
const planned = list.filter((a) => { const q = state.quotas[a.id]; return q && !q.error && !q.empty; }).length;
|
|
1108
1325
|
// ZCode 没有每日签到:不展示签到语义的行
|
|
@@ -1118,6 +1335,8 @@ function renderDash() {
|
|
|
1118
1335
|
const live = cur ? esc(cur.name || cur.email || cur.uid)
|
|
1119
1336
|
: zid && (zid.email || zid.uid || zid.username) ? '未导入的账号(' + esc(zid.email || zid.username || zid.uid) + ')'
|
|
1120
1337
|
: p.id === 'mirasim' && s.mirasimCurrentUid ? '未导入的账号'
|
|
1338
|
+
: p.id === 'catpaw' && s.catpawClient && s.catpawClient.signedIn && !list.some((a) => isClientCurrent(a)) ? '未导入的账号'
|
|
1339
|
+
: p.id === 'trae' && s.traeClient && s.traeClient.signedIn && !list.some((a) => isClientCurrent(a)) ? '未导入的账号'
|
|
1121
1340
|
: (p.id === 'workbuddy' && s.workbuddyCurrentUid ? '未导入的账号' : '—');
|
|
1122
1341
|
rows += '<span class="lbl">客户端当前</span><span class="val">' + live + '</span>';
|
|
1123
1342
|
}
|
|
@@ -1190,22 +1409,28 @@ function renderStatus() {
|
|
|
1190
1409
|
// 产品相关提示只在对应标签页出现
|
|
1191
1410
|
const hasIntl = state.view === 'qoder' && state.region !== 'cn' && state.accounts.some((a) => a.provider === 'qoder');
|
|
1192
1411
|
const n = $('notice');
|
|
1193
|
-
n.classList.toggle('info', state.view === 'zcode' || state.view === 'mirasim' || (hasIntl && Boolean(s.riskIdentity)));
|
|
1194
|
-
if (state.view === '
|
|
1412
|
+
n.classList.toggle('info', state.view === 'zcode' || state.view === 'mirasim' || state.view === 'catpaw' || state.view === 'trae' || (hasIntl && Boolean(s.riskIdentity)));
|
|
1413
|
+
if (state.view === 'trae') {
|
|
1414
|
+
n.style.display = '';
|
|
1415
|
+
n.innerHTML = '<div class="z-row"><span class="z-label">每日签到</span><span class="z-text">每日签到领约 150 通用积分(7 天有效,多资源包自动归并)。凭据由本机 Trae 客户端解密导入,支持多账号快照一键切换。</span></div>';
|
|
1416
|
+
} else if (state.view === 'catpaw') {
|
|
1417
|
+
n.style.display = '';
|
|
1418
|
+
n.innerHTML = '妙手(美团 CatPaw)按套餐发放 Credits(体验版 / 专业版…),没有每日签到。凭据由妙手客户端登录管理:读取额度用本机解密出的登录 token 直连桌面网关,token 失效时到妙手客户端重新登录后再「本机导入」即可。'
|
|
1419
|
+
+ '多账号切换:写入本机凭据后重启妙手客户端即生效(切换会自动保存当前登录,不丢号)。'
|
|
1420
|
+
+ catpawClientChip(s);
|
|
1421
|
+
} else if (state.view === 'mirasim') {
|
|
1195
1422
|
n.style.display = '';
|
|
1196
1423
|
n.innerHTML = 'mirasim(原生 AI 编程开发环境)额度按「平台额度」(5h 滚动窗口、7d 全局上限与分模型上限)分配。Token 约 1 小时过期,读取额度时会自动用 refreshToken 静默续期并同步回客户端,无需手动处理。'
|
|
1197
1424
|
+ '切换账号需要至少两个已导入的账号(当前登录 + 目标账号):先在「添加账号 → 本机导入」把各账号分别导入,切换时当前登录会自动保存、目标账号写入客户端,桌面版切完自动重启 mirasim。'
|
|
1198
1425
|
+ mirasimClientChip(s);
|
|
1199
1426
|
} else if (state.view === 'zcode') {
|
|
1200
1427
|
n.style.display = '';
|
|
1201
|
-
const pf = state.zProxyFirst === true;
|
|
1202
|
-
const pu = state.zProxyUrl;
|
|
1203
1428
|
const ac = state.zAutoClaim === true;
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
+
|
|
1208
|
-
+
|
|
1429
|
+
const ziv = state.zClaimIntervalMin || 2;
|
|
1430
|
+
const zwin = state.zClaimWindowMin === 0 ? 0 : (state.zClaimWindowMin || 60);
|
|
1431
|
+
n.innerHTML = '<div class="z-row"><span class="z-label">活动领取</span><span class="z-text">活动包积分,每日 0 点左右刷新(无每日签到)。自动领取'
|
|
1432
|
+
+ (ac ? '每 ' + ziv + ' 分钟检查资格' + (zwin > 0 ? ',本次运行 ' + zwin + ' 分钟' : ',持续运行') : '未开启(右上角设置轮询计划)')
|
|
1433
|
+
+ ';礼物图标 = 手动临时领。</span></div>';
|
|
1209
1434
|
} else if (hasIntl && !s.riskIdentity && umidOffer(s)) {
|
|
1210
1435
|
n.style.display = '';
|
|
1211
1436
|
const busy = s.umid.installing || umidBusy;
|
|
@@ -1224,15 +1449,6 @@ function renderStatus() {
|
|
|
1224
1449
|
}
|
|
1225
1450
|
}
|
|
1226
1451
|
|
|
1227
|
-
// ZCode 客户端状态芯片 + 「打开客户端」按钮(仅桌面版:面板里有 Electron 桥才显示按钮)
|
|
1228
|
-
function zcodeClientChip(s) {
|
|
1229
|
-
const c = s.zcodeClient || {};
|
|
1230
|
-
if (!c.installed) return ' <span class="dim">客户端:未安装</span>';
|
|
1231
|
-
const canOpen = window.creditdaddy && typeof window.creditdaddy.openZcodeClient === 'function';
|
|
1232
|
-
return ' <span class="dim">客户端:' + (c.running ? '运行中' : '未运行') + '</span>'
|
|
1233
|
-
+ (canOpen ? ' <button class="btn" id="btn-zopen" style="margin-left:4px" title="打开本机 ZCode 客户端">打开客户端</button>' : '');
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
1452
|
// mirasim 客户端状态芯片 + 「打开客户端」按钮
|
|
1237
1453
|
function mirasimClientChip(s) {
|
|
1238
1454
|
const c = s.mirasimClient || {};
|
|
@@ -1242,6 +1458,16 @@ function mirasimClientChip(s) {
|
|
|
1242
1458
|
+ (canOpen ? ' <button class="btn" id="btn-miraopen" style="margin-left:4px" title="打开本机 Mirasim 客户端">打开客户端</button>' : '');
|
|
1243
1459
|
}
|
|
1244
1460
|
|
|
1461
|
+
|
|
1462
|
+
// 妙手客户端状态芯片 + 「打开客户端」按钮
|
|
1463
|
+
function catpawClientChip(s) {
|
|
1464
|
+
const c = s.catpawClient || {};
|
|
1465
|
+
if (!c.installed) return ' <span class="dim">客户端:未安装(本机导入 / 切换需要 Windows 妙手客户端)</span>';
|
|
1466
|
+
const canOpen = window.creditdaddy && typeof window.creditdaddy.openCatpawClient === 'function';
|
|
1467
|
+
return ' <span class="dim">客户端:' + (c.running ? '运行中' : '未运行') + (c.signedIn ? '' : ' · 未登录') + '</span>'
|
|
1468
|
+
+ (canOpen ? ' <button class="btn" id="btn-cpopen" style="margin-left:4px" title="打开本机妙手客户端">打开客户端</button>' : '');
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1245
1471
|
// Qoder 设备身份组件(Linux / fnOS)
|
|
1246
1472
|
const umidOffer = (s) => Boolean(s && s.umid && s.umid.supported && !s.umid.installed);
|
|
1247
1473
|
let umidBusy = false;
|
|
@@ -1255,8 +1481,9 @@ async function installUmid() {
|
|
|
1255
1481
|
await loadStatus(); render(); loadLogs();
|
|
1256
1482
|
}
|
|
1257
1483
|
$('notice').addEventListener('click', (e) => { if (e.target.closest('#btn-umid')) installUmid(); });
|
|
1258
|
-
|
|
1259
|
-
|
|
1484
|
+
// 覆盖 #notice(各产品横幅)与 #z-actions(ZCode 工具栏按钮组)的点击
|
|
1485
|
+
document.addEventListener('click', async (e) => {
|
|
1486
|
+
if (e.target.closest('#zt-open')) {
|
|
1260
1487
|
try {
|
|
1261
1488
|
const r = await window.creditdaddy.openZcodeClient();
|
|
1262
1489
|
if (r && r.ok) toast('已启动 ZCode 客户端');
|
|
@@ -1272,33 +1499,53 @@ $('notice').addEventListener('click', async (e) => {
|
|
|
1272
1499
|
} catch (err) { toast(String(err && err.message || err), false); }
|
|
1273
1500
|
return;
|
|
1274
1501
|
}
|
|
1275
|
-
if (e.target.closest('#btn-
|
|
1502
|
+
if (e.target.closest('#btn-cpopen')) {
|
|
1276
1503
|
try {
|
|
1277
|
-
const r = await
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
} catch (err) { toast(err.message, false); }
|
|
1504
|
+
const r = await window.creditdaddy.openCatpawClient();
|
|
1505
|
+
if (r && r.ok) toast('已启动妙手客户端');
|
|
1506
|
+
else toast((r && r.error) || '无法打开妙手客户端', false);
|
|
1507
|
+
} catch (err) { toast(String(err && err.message || err), false); }
|
|
1282
1508
|
return;
|
|
1283
1509
|
}
|
|
1284
|
-
if (e.target.closest('#
|
|
1510
|
+
if (e.target.closest('#tt-open')) {
|
|
1285
1511
|
try {
|
|
1286
|
-
const r = await
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
} catch (err) { toast(err.message, false); }
|
|
1512
|
+
const r = await window.creditdaddy.openTraeClient();
|
|
1513
|
+
if (r && r.ok) toast('已启动 Trae 客户端');
|
|
1514
|
+
else toast((r && r.error) || '无法打开 Trae 客户端', false);
|
|
1515
|
+
} catch (err) { toast(String(err && err.message || err), false); }
|
|
1291
1516
|
return;
|
|
1292
1517
|
}
|
|
1293
|
-
if (e.target.closest('#
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1518
|
+
if (e.target.closest('#zt-ac')) {
|
|
1519
|
+
// 自动领取计划弹窗:周期 / 运行时长由用户决定
|
|
1520
|
+
$('zac-interval').value = state.zClaimIntervalMin || 2;
|
|
1521
|
+
$('zac-window').value = state.zClaimWindowMin === 0 ? 0 : (state.zClaimWindowMin || 60);
|
|
1522
|
+
$('zac-off').style.display = state.zAutoClaim === true ? '' : 'none';
|
|
1523
|
+
$('zac-ok').textContent = state.zAutoClaim === true ? '保存' : '保存并启用';
|
|
1524
|
+
openModal('m-zac');
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
if (e.target.closest('#zt-gw')) {
|
|
1528
|
+
// 体验包接口弹窗:开关 + 局域网 + 白名单一次设置;白名单默认预填 10Router 服务地址的主机
|
|
1529
|
+
$('zgw-on').checked = state.zGateway === true;
|
|
1530
|
+
$('zgw-lan').checked = state.zGatewayLan === true;
|
|
1531
|
+
const allow = Array.isArray(state.zGatewayAllow) ? state.zGatewayAllow : [];
|
|
1532
|
+
$('zgw-allow').value = (allow.length ? allow : (state.zGatewaySuggest ? [state.zGatewaySuggest] : [])).join('\n');
|
|
1533
|
+
const st = state.zGatewayStats;
|
|
1534
|
+
$('zgw-url').innerHTML = '接口地址 <code>POST http://127.0.0.1:' + location.port + '/gateway/v1/messages</code>(局域网开放时把 127.0.0.1 换成本机 IP)'
|
|
1535
|
+
+ (st && st.lastCallAt
|
|
1536
|
+
? '<br>最近调用 ' + esc(String(st.lastCallAt).slice(11, 19)) + ' · 共 ' + st.calls + ' 次' + (st.lastAccount ? ' · 最后用号 ' + esc(st.lastAccount) : '')
|
|
1537
|
+
: '<br>尚未有调用');
|
|
1538
|
+
syncZgwLanFields();
|
|
1539
|
+
openModal('m-zgw');
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1542
|
+
if (e.target.closest('#zt-net')) {
|
|
1543
|
+
// 出口弹窗:顺序 + 代理地址一起保存;代理只回填到 placeholder(脱敏),留空即清除
|
|
1544
|
+
(state.zProxyFirst === true ? $('znet-proxy') : $('znet-direct')).checked = true;
|
|
1545
|
+
$('znet-url').value = '';
|
|
1546
|
+
$('znet-url').placeholder = state.zProxyUrl || (state.zHasEnvProxy ? '跟随环境变量 HTTPS_PROXY' : 'http://192.168.31.101:7890');
|
|
1547
|
+
openModal('m-znet');
|
|
1548
|
+
return;
|
|
1302
1549
|
}
|
|
1303
1550
|
});
|
|
1304
1551
|
async function loadZNet() {
|
|
@@ -1307,8 +1554,22 @@ async function loadZNet() {
|
|
|
1307
1554
|
state.zProxyFirst = d.proxyFirst === true;
|
|
1308
1555
|
state.zProxyUrl = d.proxyUrlMasked || null;
|
|
1309
1556
|
state.zAutoClaim = d.autoClaim === true;
|
|
1557
|
+
state.zClaimIntervalMin = d.claimIntervalMin;
|
|
1558
|
+
state.zClaimWindowMin = d.claimWindowMin;
|
|
1310
1559
|
state.zHasEnvProxy = d.hasEnvProxy === true;
|
|
1311
|
-
if (state.view === 'zcode') renderStatus();
|
|
1560
|
+
if (state.view === 'zcode') { renderStatus(); render(); }
|
|
1561
|
+
} catch (e) {}
|
|
1562
|
+
}
|
|
1563
|
+
async function loadZGateway() {
|
|
1564
|
+
try {
|
|
1565
|
+
const d = await api('/api/zcode-gateway');
|
|
1566
|
+
state.zGateway = d.enabled === true;
|
|
1567
|
+
state.zGatewayLan = d.lan === true;
|
|
1568
|
+
state.zGatewayAllow = d.allow || [];
|
|
1569
|
+
state.zGatewaySuggest = d.suggestAllow || '';
|
|
1570
|
+
state.zGatewayHasCaptcha = d.hasCaptcha === true;
|
|
1571
|
+
state.zGatewayStats = d.stats || null;
|
|
1572
|
+
if (state.view === 'zcode') { renderStatus(); render(); }
|
|
1312
1573
|
} catch (e) {}
|
|
1313
1574
|
}
|
|
1314
1575
|
|
|
@@ -1441,7 +1702,7 @@ function trCardHtml(c) {
|
|
|
1441
1702
|
+ trPacksHtml(c, packs);
|
|
1442
1703
|
}
|
|
1443
1704
|
}
|
|
1444
|
-
return '<div class="tr-card' + (c.isActive ? '' : ' off') + '" draggable="true" data-id="' + esc(c.id) + '" data-vendor="' + esc(c.providerName || c.provider || '') + '"><div class="row1"><div class="name-group">'
|
|
1705
|
+
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">'
|
|
1445
1706
|
+ '<span class="badge prod tenrouter">' + esc(c.providerName) + '</span>'
|
|
1446
1707
|
+ (TR_REGION[c.provider] ? '<span class="badge ' + TR_REGION[c.provider][1] + '">' + TR_REGION[c.provider][0] + '</span>' : '')
|
|
1447
1708
|
+ '<span class="tr-name" title="' + esc([c.name, c.email].filter(Boolean).join(' · ')) + '">' + esc(who) + '</span>'
|
|
@@ -1486,74 +1747,51 @@ function trVendorCardHtml(g) {
|
|
|
1486
1747
|
const splitOf = items.map((x) => ({ x, s: splitQuotas(x) }));
|
|
1487
1748
|
// Total Points / 含 Cycle|Capacity 的包是全包汇总:连接级聚合跳过它们,避免与子包重复计数
|
|
1488
1749
|
const isAgg = (q) => isAggregatePack(q && q.name);
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
if (
|
|
1498
|
-
// 限时百分比窗口(如 Claude 5h):跨连接取平均百分比,而不是加总
|
|
1499
|
-
const pcts = same.map((p) => quotaPct(p)).filter((x) => x !== null);
|
|
1500
|
-
primary = {
|
|
1501
|
-
name,
|
|
1502
|
-
total: 0,
|
|
1503
|
-
remaining: 0,
|
|
1504
|
-
used: 0,
|
|
1505
|
-
unit: '',
|
|
1506
|
-
percentScale: true,
|
|
1507
|
-
remainingPercentage: pcts.length ? pcts.reduce((a, b) => a + b, 0) / pcts.length : null,
|
|
1508
|
-
recurring: same[0].recurring,
|
|
1509
|
-
resetAt: same.map((p) => p.resetAt).filter(Boolean).sort()[0] || null,
|
|
1510
|
-
};
|
|
1511
|
-
} else {
|
|
1512
|
-
primary = {
|
|
1513
|
-
name,
|
|
1514
|
-
total,
|
|
1515
|
-
remaining: same.reduce((n, p) => n + quotaRemaining(p), 0),
|
|
1516
|
-
used: same.reduce((n, p) => n + (p.used || 0), 0),
|
|
1517
|
-
unit: same[0].unit || '',
|
|
1518
|
-
percentScale: same[0].percentScale === true,
|
|
1519
|
-
recurring: same[0].recurring,
|
|
1520
|
-
resetAt: same.map((p) => p.resetAt).filter(Boolean).sort()[0] || null,
|
|
1521
|
-
};
|
|
1522
|
-
}
|
|
1750
|
+
// 非包行按「名称」归并求和,不论它在该连接卡里扮演主额度还是窗口:各连接主行
|
|
1751
|
+
// 名称不一致时(Qoder CN 有的账号有 Plan Credits 行、有的没有),只按第一行的
|
|
1752
|
+
// 名字取主行会把其余连接的主行静默丢掉。
|
|
1753
|
+
// detailOnly / summarizesDetail:10Router 新版把汇总行拆出的逐包明细行(每资源包
|
|
1754
|
+
// 一条)——父行已含其数值,供应商级聚合跳过,避免重复计数。
|
|
1755
|
+
const isDetail = (q) => q && q.detailOnly === true;
|
|
1756
|
+
let headlineName = null;
|
|
1757
|
+
for (const { s } of splitOf) {
|
|
1758
|
+
if (s.primary && !isAgg(s.primary) && !isDetail(s.primary)) { headlineName = s.primary.name; break; }
|
|
1523
1759
|
}
|
|
1524
|
-
|
|
1525
|
-
// 周期窗口:同名加总;本身是百分比口径的行(percentScale / remainingPercentage)按 % 均值聚合
|
|
1526
|
-
const winMap = new Map();
|
|
1760
|
+
const rowMap = new Map();
|
|
1527
1761
|
const packs = [];
|
|
1528
1762
|
for (const { s } of splitOf) {
|
|
1529
|
-
for (const q of s.windows
|
|
1530
|
-
if (!
|
|
1531
|
-
|
|
1763
|
+
for (const q of [s.primary, ...s.windows]) {
|
|
1764
|
+
if (!q || isAgg(q) || isDetail(q)) continue;
|
|
1765
|
+
if (!rowMap.has(q.name)) rowMap.set(q.name, { name: q.name, total: 0, rem: 0, anyTotal: false, pctScale: false, resetAt: null, pcts: [], unit: '' });
|
|
1766
|
+
const w = rowMap.get(q.name);
|
|
1532
1767
|
const isPctRow = q.percentScale || Number.isFinite(q.remainingPercentage);
|
|
1533
1768
|
if (isPctRow) w.pctScale = true;
|
|
1534
1769
|
const pct = quotaPct(q);
|
|
1535
1770
|
if (pct !== null) w.pcts.push(pct);
|
|
1536
|
-
if (!isPctRow && q.total > 0) { w.anyTotal = true; w.total += q.total; w.rem += quotaRemaining(q); }
|
|
1771
|
+
if (!isPctRow && q.total > 0) { w.anyTotal = true; w.total += q.total; w.rem += quotaRemaining(q); if (!w.unit) w.unit = q.unit || ''; }
|
|
1537
1772
|
if (q.resetAt && (!w.resetAt || String(q.resetAt) < String(w.resetAt))) w.resetAt = q.resetAt;
|
|
1538
1773
|
}
|
|
1539
|
-
packs.push(...s.packs.filter((x) => !isAgg(x)));
|
|
1774
|
+
packs.push(...s.packs.filter((x) => !isAgg(x))); // 明细行照常进「积分包×N」简讯:只作计数与剩余提示,不参与行求和
|
|
1540
1775
|
}
|
|
1541
|
-
|
|
1542
|
-
const alivePacks = packs.filter((q) => quotaRemaining(q) > 0);
|
|
1543
|
-
const packRem = alivePacks.reduce((n, q) => n + quotaRemaining(q), 0);
|
|
1544
|
-
|
|
1545
|
-
// 周 / 月度等窗口额度也给进度条:百分比口径显示 % 均值;固定额度加总数字
|
|
1776
|
+
// 百分比口径(限时窗口)→ % 均值;固定额度 → 数字加总;无总量 → % 兜底
|
|
1546
1777
|
const avgPct = (arr) => (arr.length ? arr.reduce((a, b) => a + b, 0) / arr.length : null);
|
|
1547
|
-
const
|
|
1778
|
+
const toRowObj = (w) => (w.pctScale
|
|
1548
1779
|
? { name: w.name, total: 0, remainingPercentage: avgPct(w.pcts), percentScale: true, resetAt: w.resetAt, recurring: true }
|
|
1549
1780
|
: (w.anyTotal
|
|
1550
|
-
? { name: w.name, total: w.total, remaining: w.rem, used: w.total - w.rem, unit:
|
|
1551
|
-
: { name: w.name, total: 0, remainingPercentage: avgPct(w.pcts), percentScale: true, resetAt: w.resetAt, recurring: true }))
|
|
1781
|
+
? { name: w.name, total: w.total, remaining: w.rem, used: w.total - w.rem, unit: w.unit, percentScale: false, resetAt: w.resetAt, recurring: true }
|
|
1782
|
+
: { name: w.name, total: 0, remainingPercentage: avgPct(w.pcts), percentScale: true, resetAt: w.resetAt, recurring: true }));
|
|
1783
|
+
const headline = headlineName ? rowMap.get(headlineName) : null;
|
|
1784
|
+
if (headline) rowMap.delete(headlineName);
|
|
1785
|
+
const primary = headline ? toRowObj(headline) : null;
|
|
1786
|
+
const others = [...rowMap.values()];
|
|
1787
|
+
const winRows = others.map((w) => trRow(toRowObj(w))).join('');
|
|
1552
1788
|
|
|
1789
|
+
const alivePacks = packs.filter((q) => quotaRemaining(q) > 0);
|
|
1790
|
+
const packRem = alivePacks.reduce((n, q) => n + quotaRemaining(q), 0);
|
|
1553
1791
|
const briefs = [];
|
|
1554
1792
|
if (alivePacks.length) briefs.push('积分包×' + alivePacks.length + ' 剩 ' + fmtNum(packRem));
|
|
1555
1793
|
else if (packs.length) briefs.push('积分包已用完');
|
|
1556
|
-
const hasQuotas = Boolean(primary ||
|
|
1794
|
+
const hasQuotas = Boolean(primary || others.length || packs.length);
|
|
1557
1795
|
let quotaHtml;
|
|
1558
1796
|
if (errs === items.length) {
|
|
1559
1797
|
quotaHtml = '<div class="msg err">全部连接查询失败</div>';
|
|
@@ -1567,7 +1805,7 @@ function trVendorCardHtml(g) {
|
|
|
1567
1805
|
? '<div class="tr-summary dim"><span class="tr-sum-txt">' + briefs.join(' · ') + '</span></div>'
|
|
1568
1806
|
: '');
|
|
1569
1807
|
}
|
|
1570
|
-
return '<div class="tr-card" draggable="true" data-vendor="' + esc(g.name) + '"><div class="row1"><div class="name-group">'
|
|
1808
|
+
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">'
|
|
1571
1809
|
+ '<span class="badge prod tenrouter">' + esc(g.name) + '</span>'
|
|
1572
1810
|
+ (TR_REGION[prov] ? '<span class="badge ' + TR_REGION[prov][1] + '">' + TR_REGION[prov][0] + '</span>' : '')
|
|
1573
1811
|
+ '<span class="badge">' + items.length + ' 个账号</span>'
|
|
@@ -1580,7 +1818,8 @@ function trSetupHtml() {
|
|
|
1580
1818
|
return '<div class="setup"><h3>接入 10Router</h3>'
|
|
1581
1819
|
+ '<div class="hint">填 10Router 的地址和一个<b>虚拟 key</b>(10Router 仪表盘 → API Keys 创建的 sk-…),就能在这里:</div>'
|
|
1582
1820
|
+ '<div class="hint" style="margin:6px 0 12px">· 查看 10Router 里其他供应商(Claude / Codex / Kiro / GLM …)的额度卡片(需要 10Router 1.2.1+)<br>'
|
|
1583
|
-
+ '· 把本机 ZCode / OpenCode / mirasim / 小米 MiMo 的用量同步进 10Router 的用量统计(同 10router-sync
|
|
1821
|
+
+ '· 把本机 ZCode / OpenCode / mirasim / 小米 MiMo / 10Router 的用量同步进 10Router 的用量统计(同 10router-sync 插件,自动每小时一次)<br>'
|
|
1822
|
+
+ '· 「10Router 本机」= 本机 10Router 实例自己记的账(如 zcode-free 经它中转的行)同步到聚合端;地址指向本机时自动跳过,避免自己导自己</div>'
|
|
1584
1823
|
+ '<button class="btn primary" data-tr="settings">' + I.plus + '配置 10Router</button></div>';
|
|
1585
1824
|
}
|
|
1586
1825
|
|
|
@@ -1588,15 +1827,15 @@ function renderTenrouter() {
|
|
|
1588
1827
|
const box = $('tr-panel');
|
|
1589
1828
|
const c = tr.config;
|
|
1590
1829
|
if (!c) { box.innerHTML = '<div class="empty">加载中…</div>'; return; }
|
|
1591
|
-
|
|
1830
|
+
const gwLine = state.zGateway === true
|
|
1831
|
+
? '<div class="gw-line"><span class="dot on pulse"></span> ZCode 体验包接口运行中'
|
|
1832
|
+
+ (state.zGatewayStats && state.zGatewayStats.lastCallAt ? ' · 最近调用 ' + esc(String(state.zGatewayStats.lastCallAt).slice(11, 19)) + ' · ' + state.zGatewayStats.calls + ' 次 · 最后用号 ' + esc(state.zGatewayStats.lastAccount || '-') : ' · 尚未有调用')
|
|
1833
|
+
+ ' · 模型 <code>zcode-free/glm-5.3-flash</code></div>'
|
|
1834
|
+
: '';
|
|
1835
|
+
if (!c.configured) { box.innerHTML = gwLine + trSetupHtml(); return; }
|
|
1592
1836
|
const list = (tr.quotas && tr.quotas.connections) || [];
|
|
1593
1837
|
const low = list.filter((x) => x.isActive && !x.error && quotaLow(x)).length;
|
|
1594
1838
|
const failed = list.filter((x) => x.error).length;
|
|
1595
|
-
const ls = c.lastSync;
|
|
1596
|
-
const srcChips = (c.sources || []).map((x) => {
|
|
1597
|
-
const on = x.found && c.sync.sources.includes(x.id);
|
|
1598
|
-
return '<span class="chip' + (on ? ' on' : '') + '" title="' + (x.found ? (x.supported ? '已检测到' : '需要 Node 22.5+') : '本机未检测到') + '">' + esc(x.label) + (x.found ? '' : ' ·无') + '</span>';
|
|
1599
|
-
}).join('');
|
|
1600
1839
|
let grid;
|
|
1601
1840
|
let tabStrip = '';
|
|
1602
1841
|
const groups = list.length ? trGroups(list) : [];
|
|
@@ -1644,15 +1883,11 @@ function renderTenrouter() {
|
|
|
1644
1883
|
+ '<div class="stat"><span>额度告急</span><strong>' + (tr.quotas ? low : '-') + '</strong></div><div class="divider"></div>'
|
|
1645
1884
|
+ '<div class="stat"><span>查询失败</span><strong>' + (tr.quotas ? failed : '-') + '</strong></div>'
|
|
1646
1885
|
+ '</div><div class="actions">'
|
|
1647
|
-
+ '<button class="btn" data-tr="refresh"' + (tr.loading ? ' disabled' : '') + '>' +
|
|
1648
|
-
+ '<button class="btn" data-tr="sync"' + (tr.syncing || c.syncing ? ' disabled' : '') + '>'
|
|
1886
|
+
+ '<button class="btn" data-tr="refresh"' + (tr.loading ? ' disabled' : '') + '>' + (tr.loading ? '刷新中…' : '刷新额度') + '</button>'
|
|
1887
|
+
+ '<button class="btn" data-tr="sync" title="用量同步:自动开关、同步来源与手动同步(点我打开)"' + (tr.syncing || c.syncing ? ' disabled' : '') + '>'
|
|
1888
|
+
+ '<span class="dot' + (c.sync.enabled ? ' on' : '') + '"></span>' + (tr.syncing || c.syncing ? '同步中…' : '用量同步') + '</button>'
|
|
1649
1889
|
+ '<button class="btn" data-tr="settings">设置</button>'
|
|
1650
1890
|
+ '</div></div>'
|
|
1651
|
-
+ '<div class="tr-bar"><span title="' + esc(c.endpoint) + '">' + esc(c.endpoint) + ' · ' + esc(c.keyMasked) + '</span>'
|
|
1652
|
-
+ '<span>用量同步:' + (c.sync.enabled ? '每小时自动' : '手动') + '</span>' + srcChips
|
|
1653
|
-
+ '<span style="margin-left:auto" title="' + esc(ls ? ls.summary : '') + '">' + (ls ? '上次同步 ' + esc(fmtTime(ls.at).slice(5)) + ' · ' + esc(ls.summary.slice(0, 40)) : '还没有同步过') + '</span>'
|
|
1654
|
-
+ (tr.quotas ? '<span>额度更新于 ' + esc(fmtTime(tr.quotas.generatedAt).slice(11)) + '</span>' : '')
|
|
1655
|
-
+ '</div>'
|
|
1656
1891
|
+ tabStrip
|
|
1657
1892
|
+ '<div class="tr-grid">' + grid + '</div>';
|
|
1658
1893
|
}
|
|
@@ -1700,22 +1935,38 @@ function openTrSettings() {
|
|
|
1700
1935
|
$('tr-endpoint').value = c.endpoint || '';
|
|
1701
1936
|
$('tr-key').value = '';
|
|
1702
1937
|
$('tr-key').placeholder = c.keyMasked ? '已保存 ' + c.keyMasked + '(留空保持不变)' : 'sk-…';
|
|
1703
|
-
$('tr-
|
|
1938
|
+
$('tr-admin-pw').value = '';
|
|
1939
|
+
$('tr-admin-pw').placeholder = c.hasAdminPassword ? '已保存(留空保持不变)' : '可选:10Router 面板密码';
|
|
1704
1940
|
$('tr-hide-empty').checked = state.trHideEmpty === true;
|
|
1705
|
-
$('tr-sources').innerHTML = (c.sources || []).map((x) => '<label class="check"><input type="checkbox" data-src="' + x.id + '"'
|
|
1706
|
-
+ ((c.configured ? c.sync.sources.includes(x.id) : true) ? ' checked' : '') + (x.supported ? '' : ' disabled') + '> ' + esc(x.label)
|
|
1707
|
-
+ '<span class="dim">' + (x.found ? '' : '(本机未检测到)') + (x.supported ? '' : '(需要 Node 22.5+)') + '</span></label>').join('');
|
|
1708
1941
|
$('tr-disconnect').style.display = c.configured ? '' : 'none';
|
|
1709
1942
|
$('tr-test-msg').textContent = '';
|
|
1710
1943
|
openModal('m-10r');
|
|
1711
1944
|
setTimeout(() => $(c.configured ? 'tr-key' : 'tr-endpoint').focus(), 30);
|
|
1712
1945
|
}
|
|
1946
|
+
// 用量同步弹窗:自动开关 + 来源勾选 + 手动同步
|
|
1947
|
+
function openTrSync() {
|
|
1948
|
+
const c = tr.config || { sync: { enabled: true, sources: [] }, sources: [] };
|
|
1949
|
+
$('tr-auto').checked = c.configured ? c.sync.enabled : true;
|
|
1950
|
+
$('tr-sources').innerHTML = (c.sources || []).map((x) => '<label class="check"><input type="checkbox" data-src="' + x.id + '"'
|
|
1951
|
+
+ ((c.configured ? c.sync.sources.includes(x.id) : true) ? ' checked' : '') + (x.supported ? '' : ' disabled') + '> ' + esc(x.label)
|
|
1952
|
+
+ '<span class="dim">' + (x.found ? '' : '(本机未检测到)') + (x.supported ? '' : '(需要 Node 22.5+)') + '</span></label>').join('');
|
|
1953
|
+
const ls = c.lastSync;
|
|
1954
|
+
$('tr-sync-status').textContent = ls ? '上次同步 ' + fmtTime(ls.at).slice(5) + ':' + ls.summary : '还没有同步过';
|
|
1955
|
+
$('tr-sync-now').disabled = tr.syncing || c.syncing === true;
|
|
1956
|
+
openModal('m-trsync');
|
|
1957
|
+
}
|
|
1958
|
+
async function trSyncSave(close) {
|
|
1959
|
+
try {
|
|
1960
|
+
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) } });
|
|
1961
|
+
if (close) { closeModal('m-trsync'); toast('用量同步设置已保存'); render(); }
|
|
1962
|
+
} catch (e) { toast(e.message, false); throw e; }
|
|
1963
|
+
}
|
|
1713
1964
|
function trForm() {
|
|
1714
1965
|
return {
|
|
1715
|
-
|
|
1966
|
+
// 从 LLM 客户端复制的 base URL 常带 /v1 后缀——10Router 只认站点根地址,带 /v1 会让所有 /api/* 调用 404、误报「需要 1.2.1+」
|
|
1967
|
+
endpoint: $('tr-endpoint').value.trim().replace(/\/+$/, '').replace(/\/v1$/, ''),
|
|
1716
1968
|
key: $('tr-key').value.trim(),
|
|
1717
|
-
|
|
1718
|
-
sources: [...document.querySelectorAll('#tr-sources input[data-src]:checked')].map((x) => x.dataset.src),
|
|
1969
|
+
adminPassword: $('tr-admin-pw').value, // 留空 = 服务端保持原值
|
|
1719
1970
|
};
|
|
1720
1971
|
}
|
|
1721
1972
|
async function trTest() {
|
|
@@ -1749,13 +2000,26 @@ async function trDisconnect() {
|
|
|
1749
2000
|
try { tr.config = await api('/api/tenrouter', { method: 'DELETE' }); tr.quotas = null; tr.error = null; closeModal('m-10r'); render(); }
|
|
1750
2001
|
catch (e) { toast(e.message, false); }
|
|
1751
2002
|
}
|
|
2003
|
+
async function trSyncAccounts() {
|
|
2004
|
+
if (!(tr.config && tr.config.configured)) return toast('请先保存 10Router 设置', false);
|
|
2005
|
+
const btn = $('tr-sync-accounts');
|
|
2006
|
+
const pw = $('tr-admin-pw').value;
|
|
2007
|
+
btn.disabled = true;
|
|
2008
|
+
try {
|
|
2009
|
+
const r = await api('/api/tenrouter/sync-accounts', { method: 'POST', json: pw ? { adminPassword: pw } : {} });
|
|
2010
|
+
toast(r.summary, !/失败/.test(r.summary));
|
|
2011
|
+
if (pw) await loadTrConfig();
|
|
2012
|
+
loadLogs();
|
|
2013
|
+
} catch (e) { toast(e.message, false); }
|
|
2014
|
+
btn.disabled = false;
|
|
2015
|
+
}
|
|
1752
2016
|
$('tr-panel').addEventListener('click', (e) => {
|
|
1753
2017
|
const b = e.target.closest('[data-tr]');
|
|
1754
2018
|
if (!b) return;
|
|
1755
2019
|
if (b.dataset.tr === 'toggle') { state.trOpen = Object.assign({}, state.trOpen, { [b.dataset.id]: !(state.trOpen && state.trOpen[b.dataset.id]) }); render(); return; }
|
|
1756
2020
|
if (b.dataset.tr === 'settings') openTrSettings();
|
|
1757
2021
|
else if (b.dataset.tr === 'refresh') loadTrQuotas(true);
|
|
1758
|
-
else if (b.dataset.tr === 'sync')
|
|
2022
|
+
else if (b.dataset.tr === 'sync') openTrSync();
|
|
1759
2023
|
else if (b.dataset.tr === 'vendor') {
|
|
1760
2024
|
const v = b.dataset.v || '';
|
|
1761
2025
|
state.trVendor = v;
|
|
@@ -1764,12 +2028,33 @@ $('tr-panel').addEventListener('click', (e) => {
|
|
|
1764
2028
|
}
|
|
1765
2029
|
});
|
|
1766
2030
|
$('tr-test').onclick = trTest;
|
|
2031
|
+
$('tr-sync-save').onclick = () => trSyncSave(true);
|
|
2032
|
+
$('tr-sync-now').onclick = async () => {
|
|
2033
|
+
try { await trSyncSave(false); } catch (e) { return; }
|
|
2034
|
+
closeModal('m-trsync');
|
|
2035
|
+
trSync();
|
|
2036
|
+
};
|
|
2037
|
+
$('tr-sync-accounts').onclick = trSyncAccounts;
|
|
1767
2038
|
$('tr-save').onclick = trSave;
|
|
1768
2039
|
$('tr-disconnect').onclick = trDisconnect;
|
|
1769
2040
|
|
|
1770
2041
|
// ── 数据加载 ──
|
|
1771
2042
|
async function loadStatus() {
|
|
1772
2043
|
try { state.status = await api('/api/status'); renderStatus(); } catch (e) {}
|
|
2044
|
+
syncAfterZcodeClaims();
|
|
2045
|
+
}
|
|
2046
|
+
// 后台自动领取成功后,立即刷新这些账号的额度与卡片(不必等 2 分钟一轮的余额刷新)
|
|
2047
|
+
let seenZcodeTickAt = null;
|
|
2048
|
+
async function syncAfterZcodeClaims() {
|
|
2049
|
+
const z = state.status && state.status.scheduler && state.status.scheduler.zcode;
|
|
2050
|
+
const t = z && z.lastTick;
|
|
2051
|
+
if (!t || t.at === seenZcodeTickAt) return;
|
|
2052
|
+
const first = seenZcodeTickAt === null;
|
|
2053
|
+
seenZcodeTickAt = t.at;
|
|
2054
|
+
if (first || !(t.claimedIds && t.claimedIds.length)) return; // 首次加载已全量拉取
|
|
2055
|
+
try { state.accounts = (await api('/api/accounts')).accounts; } catch (e) {}
|
|
2056
|
+
loadQuotas(t.claimedIds);
|
|
2057
|
+
loadZGateway();
|
|
1773
2058
|
}
|
|
1774
2059
|
async function loadAccounts() {
|
|
1775
2060
|
const data = await api('/api/accounts');
|
|
@@ -1779,8 +2064,10 @@ async function loadAccounts() {
|
|
|
1779
2064
|
loadQuotas();
|
|
1780
2065
|
}
|
|
1781
2066
|
let quotaRun = 0;
|
|
2067
|
+
let quotaLoadedAt = 0;
|
|
1782
2068
|
async function loadQuotas(ids) {
|
|
1783
2069
|
const run = ++quotaRun;
|
|
2070
|
+
quotaLoadedAt = Date.now();
|
|
1784
2071
|
const queue = (ids || state.accounts.map((a) => a.id)).slice();
|
|
1785
2072
|
if (ids) for (const id of ids) delete state.quotas[id];
|
|
1786
2073
|
const worker = async () => {
|
|
@@ -1809,7 +2096,9 @@ async function checkinOne(id) {
|
|
|
1809
2096
|
try {
|
|
1810
2097
|
const d = await api('/api/accounts/' + encodeURIComponent(id) + '/checkin', { method: 'POST' });
|
|
1811
2098
|
const r = d.results && d.results[0];
|
|
1812
|
-
|
|
2099
|
+
const accObj = state.accounts.find((x) => x.id === id);
|
|
2100
|
+
const accLabel = (r && r.account) || (accObj && (accObj.name || accObj.id)) || '该账号';
|
|
2101
|
+
toast(r ? (accLabel + ':' + (r.status === 'checked-in' ? '领取成功 +' + r.claimedAmount + ' Credits' : (r.error || r.message || r.status))) : d.summary, !(r && r.status === 'failed'));
|
|
1813
2102
|
} catch (e) { toast(e.message, false); }
|
|
1814
2103
|
delete state.busy[id];
|
|
1815
2104
|
await loadStatus(); await loadAccounts(); loadLogs();
|
|
@@ -1834,19 +2123,25 @@ async function onCardClick(e) {
|
|
|
1834
2123
|
if (act === 'claim') return openClaim(id);
|
|
1835
2124
|
if (act === 'switch') {
|
|
1836
2125
|
const a = state.accounts.find((x) => x.id === id);
|
|
1837
|
-
const client = CLIENT_LABEL[a && a.product] || '客户端';
|
|
2126
|
+
const client = CLIENT_LABEL[a && a.product] || (a && prodLabel(a.product)) || '客户端';
|
|
1838
2127
|
const tip = a && a.product === 'zcode'
|
|
1839
2128
|
? '\n需先退出 ZCode;切换后重新打开 ZCode 即为新账号。当前登录会先保存到 CreditDaddy,不会丢号。'
|
|
1840
2129
|
: a && a.product === 'mirasim'
|
|
1841
2130
|
? '\n需先退出 Mirasim;切换后重新打开即为新账号。当前登录会先保存到 CreditDaddy,不会丢号。'
|
|
1842
|
-
:
|
|
2131
|
+
: a && a.product === 'catpaw'
|
|
2132
|
+
? '\n需先退出妙手;切换后重新打开妙手即为新账号。当前登录会先保存到 CreditDaddy,不会丢号。'
|
|
2133
|
+
: a && a.product === 'trae'
|
|
2134
|
+
? '\n需先退出 Trae;切换后重新打开 Trae 即为新账号。当前登录会先自动快照保存到 CreditDaddy,不会丢号。'
|
|
2135
|
+
: a && a.product === 'qoder'
|
|
2136
|
+
? '\n需先退出 Qoder;切换后重新打开 Qoder 即为新账号。运行中的 Qoder 退出时会把旧登录写回去,可选「关闭并强制切换」代为关进程。'
|
|
2137
|
+
: '\n客户端会自动应用新会话;当前登录账号的最新 token 会先保存到 CreditDaddy。';
|
|
1843
2138
|
if (!(await uiConfirm('把 ' + client + ' 切换到「' + (a && a.name || id) + '」?' + tip, '切换账号', '切换'))) return;
|
|
1844
2139
|
const doSwitch = (force) => api('/api/accounts/' + encodeURIComponent(id) + '/switch', { json: { force } });
|
|
1845
2140
|
try {
|
|
1846
2141
|
let r;
|
|
1847
2142
|
try { r = await doSwitch(false); }
|
|
1848
2143
|
catch (err) {
|
|
1849
|
-
if (err.code !== 'ZCODE_RUNNING' && err.code !== 'MIRASIM_RUNNING') throw err;
|
|
2144
|
+
if (err.code !== 'ZCODE_RUNNING' && err.code !== 'MIRASIM_RUNNING' && err.code !== 'CATPAW_RUNNING' && err.code !== 'TRAE_RUNNING' && err.code !== 'QODER_RUNNING') throw err;
|
|
1850
2145
|
if (!(await uiConfirm(err.message + '\n\n仍要强制切换吗?强制切换会先关闭客户端再写入登录,重新打开客户端即为新账号。', client + ' 正在运行', '关闭并强制切换'))) return;
|
|
1851
2146
|
r = await doSwitch(true);
|
|
1852
2147
|
}
|
|
@@ -1871,6 +2166,23 @@ async function onCardClick(e) {
|
|
|
1871
2166
|
} catch (e) {}
|
|
1872
2167
|
}, 800);
|
|
1873
2168
|
}
|
|
2169
|
+
if (a && a.product === 'catpaw' && r && !r.alreadyActive && window.creditdaddy && typeof window.creditdaddy.openCatpawClient === 'function') {
|
|
2170
|
+
setTimeout(async () => {
|
|
2171
|
+
try {
|
|
2172
|
+
const re = await window.creditdaddy.openCatpawClient();
|
|
2173
|
+
if (re && re.ok) toast('已重新启动妙手客户端');
|
|
2174
|
+
} catch (e) {}
|
|
2175
|
+
}, 800);
|
|
2176
|
+
}
|
|
2177
|
+
if (a && a.product === 'trae' && r && !r.alreadyActive && window.creditdaddy && typeof window.creditdaddy.openTraeClient === 'function') {
|
|
2178
|
+
setTimeout(async () => {
|
|
2179
|
+
try {
|
|
2180
|
+
const re = await window.creditdaddy.openTraeClient();
|
|
2181
|
+
if (re && re.ok) toast('已重新启动 Trae 客户端');
|
|
2182
|
+
else toast((re && re.error) || '无法打开 Trae 客户端(桌面版才能自动拉起)', false);
|
|
2183
|
+
} catch (e) {}
|
|
2184
|
+
}, 800);
|
|
2185
|
+
}
|
|
1874
2186
|
} catch (err) { toast(err.message, false); }
|
|
1875
2187
|
return;
|
|
1876
2188
|
}
|
|
@@ -2068,6 +2380,135 @@ const uiPrompt = (title, value, opts = {}) => uiDialog({ title, message: opts.me
|
|
|
2068
2380
|
function okDialog() { const v = $('dlg-field').style.display === 'none' ? true : $('dlg-input').value; $('m-dialog').classList.remove('show'); settleDialog(v); }
|
|
2069
2381
|
$('dlg-ok').addEventListener('click', okDialog);
|
|
2070
2382
|
$('dlg-input').addEventListener('keydown', (e) => { if (e.key === 'Enter') okDialog(); });
|
|
2383
|
+
|
|
2384
|
+
// ── 检查更新(桌面版):主进程推状态(update:state),面板只管渲染 ──
|
|
2385
|
+
const updBridge = window.creditdaddy && window.creditdaddy.update ? window.creditdaddy.update : null;
|
|
2386
|
+
let updState = null;
|
|
2387
|
+
function updActBtn(id, text, cls, fn) {
|
|
2388
|
+
return '<button class="btn' + (cls ? ' ' + cls : '') + '" id="' + id + '">' + text + '</button>';
|
|
2389
|
+
}
|
|
2390
|
+
function renderUpdateModal(s) {
|
|
2391
|
+
updState = s;
|
|
2392
|
+
const body = $('upd-body');
|
|
2393
|
+
const acts = $('upd-actions');
|
|
2394
|
+
const cur = '当前 v' + (s.current || '?');
|
|
2395
|
+
const rel = '<button class="btn" id="upd-rel">打开 Releases</button>';
|
|
2396
|
+
const close = '<button class="btn" data-close-uu>稍后</button>';
|
|
2397
|
+
switch (s.phase) {
|
|
2398
|
+
case 'unsupported':
|
|
2399
|
+
body.innerHTML = '<div class="hint">当前环境不支持应用内更新(开发模式 / Portable / 未签名的 macOS 版),请从 GitHub Releases 手动下载。</div>';
|
|
2400
|
+
acts.innerHTML = rel + '<button class="btn" data-close-uu>取消</button>';
|
|
2401
|
+
break;
|
|
2402
|
+
case 'checking':
|
|
2403
|
+
body.innerHTML = '<div class="hint">正在检查更新…</div>';
|
|
2404
|
+
acts.innerHTML = close;
|
|
2405
|
+
break;
|
|
2406
|
+
case 'latest':
|
|
2407
|
+
body.innerHTML = '<div class="hint"><span class="dot on"></span> 已是最新版本(' + cur + ')</div>';
|
|
2408
|
+
acts.innerHTML = '<button class="btn primary" data-close-uu>好的</button>';
|
|
2409
|
+
break;
|
|
2410
|
+
case 'available':
|
|
2411
|
+
body.innerHTML = s.via === 'updater'
|
|
2412
|
+
? '<div class="hint">发现新版本 <b>v' + esc(s.version) + '</b>(' + cur + ')——正在后台下载,完成后退出 CreditDaddy 时自动安装。</div>'
|
|
2413
|
+
: '<div class="hint">发现新版本 <b>v' + esc(s.version) + '</b>(' + cur + (s.sizeMB ? ',约 ' + s.sizeMB + ' MB' : '') + '),可由本机直接下载安装。</div>';
|
|
2414
|
+
acts.innerHTML = (s.via === 'github' ? updActBtn('upd-go', '立即下载安装', 'primary') : '') + rel + close;
|
|
2415
|
+
break;
|
|
2416
|
+
case 'downloading':
|
|
2417
|
+
body.innerHTML = '<div class="hint">正在下载 v' + esc(s.version) + '</div>'
|
|
2418
|
+
+ '<div class="meter" style="margin:8px 0 4px"><span style="width:' + (s.percent || 0) + '%"></span></div>'
|
|
2419
|
+
+ '<div class="hint">' + (s.percent != null ? s.percent + '%' : '') + '(' + (s.mb || '…') + ' MB)' + (s.percent == null ? ' 获取大小中…' : '') + '</div>';
|
|
2420
|
+
acts.innerHTML = '<button class="btn" data-close-uu>后台继续</button>';
|
|
2421
|
+
break;
|
|
2422
|
+
case 'downloaded':
|
|
2423
|
+
body.innerHTML = '<div class="hint"><span class="dot on pulse"></span> <b>v' + esc(s.version) + '</b> '
|
|
2424
|
+
+ (s.via === 'updater' ? '已下载完成,重启即完成更新(退出时也会自动安装)。' : '安装包已下载完成,安装时 CreditDaddy 会先退出。') + '</div>';
|
|
2425
|
+
acts.innerHTML = updActBtn('upd-go', s.via === 'updater' ? '立即重启更新' : '立即安装', 'primary') + close;
|
|
2426
|
+
break;
|
|
2427
|
+
case 'error':
|
|
2428
|
+
default:
|
|
2429
|
+
body.innerHTML = '<div class="hint" style="color:var(--err)">检查更新失败:' + esc(s.error || '未知错误') + '</div>';
|
|
2430
|
+
acts.innerHTML = updActBtn('upd-retry', '重试', 'primary') + rel + '<button class="btn" data-close-uu>取消</button>';
|
|
2431
|
+
break;
|
|
2432
|
+
}
|
|
2433
|
+
const relBtn = $('upd-rel');
|
|
2434
|
+
if (relBtn) relBtn.onclick = () => updBridge.openReleases();
|
|
2435
|
+
const goBtn = $('upd-go');
|
|
2436
|
+
if (goBtn) { goBtn.disabled = true; goBtn.textContent = '处理中…'; updBridge.install().then(renderUpdateModal).catch((e) => { goBtn.disabled = false; goBtn.textContent = '重试'; }); }
|
|
2437
|
+
const retryBtn = $('upd-retry');
|
|
2438
|
+
if (retryBtn) retryBtn.onclick = () => updBridge.check().then(renderUpdateModal).catch(() => {});
|
|
2439
|
+
acts.querySelectorAll('[data-close-uu]').forEach((b) => { b.onclick = () => closeModal('m-update'); });
|
|
2440
|
+
}
|
|
2441
|
+
async function openUpdateModal(autoCheck) {
|
|
2442
|
+
if (!updBridge) return;
|
|
2443
|
+
openModal('m-update');
|
|
2444
|
+
try {
|
|
2445
|
+
updState = await updBridge.state();
|
|
2446
|
+
renderUpdateModal(updState);
|
|
2447
|
+
if (autoCheck) renderUpdateModal(await updBridge.check());
|
|
2448
|
+
} catch (e) {
|
|
2449
|
+
renderUpdateModal({ phase: 'error', error: String((e && e.message) || e), current: (updState && updState.current) || '' });
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
if (updBridge) {
|
|
2453
|
+
updBridge.onState((s) => { updState = s; $('upd-ver') && ($('upd-ver').textContent = '当前 v' + (s.current || '?')); if (document.getElementById('m-update').classList.contains('show')) renderUpdateModal(s); });
|
|
2454
|
+
updBridge.onOpen(() => openUpdateModal(true));
|
|
2455
|
+
}
|
|
2456
|
+
$('btn-update').onclick = () => openUpdateModal(true);
|
|
2457
|
+
|
|
2458
|
+
// ZCode 自动领取计划:保存(按新配置启用)/ 停用
|
|
2459
|
+
$('zac-ok').addEventListener('click', async () => {
|
|
2460
|
+
try {
|
|
2461
|
+
const r = await api('/api/zcode/net', { method: 'PUT', json: { autoClaim: true, claimIntervalMin: Number($('zac-interval').value), claimWindowMin: Number($('zac-window').value) } });
|
|
2462
|
+
state.zAutoClaim = r.autoClaim === true;
|
|
2463
|
+
state.zClaimIntervalMin = r.claimIntervalMin;
|
|
2464
|
+
state.zClaimWindowMin = r.claimWindowMin;
|
|
2465
|
+
renderStatus(); render();
|
|
2466
|
+
closeModal('m-zac');
|
|
2467
|
+
toast('自动领取已开启:每 ' + r.claimIntervalMin + ' 分钟检查' + (r.claimWindowMin > 0 ? ',运行 ' + r.claimWindowMin + ' 分钟后自动关闭' : ',一直运行直到手动关闭'));
|
|
2468
|
+
} catch (err) { toast(err.message, false); }
|
|
2469
|
+
});
|
|
2470
|
+
$('zac-off').addEventListener('click', async () => {
|
|
2471
|
+
try {
|
|
2472
|
+
const r = await api('/api/zcode/net', { method: 'PUT', json: { autoClaim: false } });
|
|
2473
|
+
state.zAutoClaim = r.autoClaim === true;
|
|
2474
|
+
renderStatus(); render();
|
|
2475
|
+
closeModal('m-zac');
|
|
2476
|
+
toast('ZCode 自动领取:已关闭');
|
|
2477
|
+
} catch (err) { toast(err.message, false); }
|
|
2478
|
+
});
|
|
2479
|
+
|
|
2480
|
+
// ZCode 体验包接口:开关 + 局域网开放 + 免密 IP 白名单一次保存
|
|
2481
|
+
function syncZgwLanFields() {
|
|
2482
|
+
const off = !$('zgw-on').checked;
|
|
2483
|
+
$('zgw-lan-fields').querySelectorAll('input, textarea').forEach((el) => { el.disabled = off; });
|
|
2484
|
+
$('zgw-lan-fields').style.opacity = off ? '.45' : '';
|
|
2485
|
+
}
|
|
2486
|
+
$('zgw-on').addEventListener('change', syncZgwLanFields);
|
|
2487
|
+
$('zgw-ok').addEventListener('click', async () => {
|
|
2488
|
+
const allow = $('zgw-allow').value.split('\n').map((x) => x.trim()).filter(Boolean);
|
|
2489
|
+
try {
|
|
2490
|
+
const r = await api('/api/zcode-gateway', { method: 'PUT', json: { enabled: $('zgw-on').checked, lan: $('zgw-lan').checked, allow } });
|
|
2491
|
+
state.zGateway = r.enabled === true;
|
|
2492
|
+
state.zGatewayLan = r.lan === true;
|
|
2493
|
+
state.zGatewayAllow = r.allow || [];
|
|
2494
|
+
renderStatus(); render();
|
|
2495
|
+
closeModal('m-zgw');
|
|
2496
|
+
toast('体验包接口:' + (r.enabled ? '已开启' : '已关闭')
|
|
2497
|
+
+ (r.enabled && r.lan ? ',局域网开放' + (r.allow && r.allow.length ? ',白名单 ' + r.allow.join('、') : ',白名单为空——白名单外需虚拟 key') + ';绑定改动重启后生效' : ''));
|
|
2498
|
+
} catch (err) { toast(err.message, false); }
|
|
2499
|
+
});
|
|
2500
|
+
|
|
2501
|
+
// ZCode 网络出口:顺序 + 代理地址保存
|
|
2502
|
+
$('znet-ok').addEventListener('click', async () => {
|
|
2503
|
+
try {
|
|
2504
|
+
const body = { proxyFirst: $('znet-proxy').checked, proxyUrl: $('znet-url').value.trim() };
|
|
2505
|
+
const r = await api('/api/zcode/net', { method: 'PUT', json: body });
|
|
2506
|
+
await loadZNet();
|
|
2507
|
+
renderStatus(); render();
|
|
2508
|
+
closeModal('m-znet');
|
|
2509
|
+
toast('ZCode 出口:' + (r.proxyFirst ? '代理优先(直连兜底)' : '直连优先(代理兜底)') + (body.proxyUrl === '' ? ',代理已清除' : ''));
|
|
2510
|
+
} catch (err) { toast(err.message, false); }
|
|
2511
|
+
});
|
|
2071
2512
|
document.querySelectorAll('.mask').forEach((m) => {
|
|
2072
2513
|
m.addEventListener('mousedown', (e) => { if (e.target === m) closeModal(m.id); });
|
|
2073
2514
|
m.querySelectorAll('[data-close]').forEach((b) => b.addEventListener('click', () => closeModal(m.id)));
|
|
@@ -2086,10 +2527,13 @@ const LOCAL_HINT = {
|
|
|
2086
2527
|
workbuddy: '读取本机 WorkBuddy 客户端的当前会话与历史会话(CodeBuddyExtension 凭据目录),token 不出本机。',
|
|
2087
2528
|
zcode: '读取本机 ZCode 客户端当前登录的账号(本地解密 ~/.zcode/v2/credentials.json),token 不出本机。',
|
|
2088
2529
|
mirasim: '读取本机 mirasim 客户端当前登录的账号(本地解密 ~/.mirasim/setting.json),token 不出本机。',
|
|
2530
|
+
catpaw: '读取本机妙手(美团 CatPaw)客户端当前登录的账号(本地解密 %APPDATA%\\catpaw-moon\\catx-credential.json),token 不出本机。',
|
|
2531
|
+
trae: '读取本机 Trae 客户端当前登录的账号(本地解密 <客户端数据目录>\\User\\globalStorage\\storage.json 的 tc 信封),token 不出本机。',
|
|
2089
2532
|
};
|
|
2090
2533
|
function openAdd(product) {
|
|
2091
2534
|
state.addProduct = product || '';
|
|
2092
|
-
|
|
2535
|
+
// Trae 凭据只能从本机客户端 storage.json 解密:不支持浏览器登录 / 粘贴 Token
|
|
2536
|
+
const tabShown = { local: true, device: product !== 'mirasim' && product !== 'catpaw' && product !== 'trae', token: product !== 'zcode' && product !== 'mirasim' && product !== 'catpaw' && product !== 'trae' };
|
|
2093
2537
|
document.querySelectorAll('#m-add .tab').forEach((t) => { t.style.display = tabShown[t.dataset.tab] ? '' : 'none'; });
|
|
2094
2538
|
$('local-hint').textContent = LOCAL_HINT[state.addProduct];
|
|
2095
2539
|
let first = null;
|
|
@@ -2118,7 +2562,7 @@ function openAdd(product) {
|
|
|
2118
2562
|
async function localScan() {
|
|
2119
2563
|
const box = $('scan-result');
|
|
2120
2564
|
$('scan-status').textContent = '扫描中(最多约 10 秒)…';
|
|
2121
|
-
box.innerHTML = '<div class="hint"
|
|
2565
|
+
box.innerHTML = '<div class="hint">正在读取本机客户端凭据(Qoder / WorkBuddy / ZCode / mirasim / 妙手 / Trae)…</div>';
|
|
2122
2566
|
try {
|
|
2123
2567
|
const r = await api('/api/local/scan', { method: 'POST' });
|
|
2124
2568
|
$('scan-status').textContent = '';
|
|
@@ -2135,11 +2579,13 @@ async function localScan() {
|
|
|
2135
2579
|
: '<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>';
|
|
2136
2580
|
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>';
|
|
2137
2581
|
}
|
|
2138
|
-
for (const e of r.errors || [])
|
|
2582
|
+
for (const e of r.errors || []) {
|
|
2583
|
+
html += '<div class="hint" style="color:var(--err)">读取失败:' + esc(e.file) + ':' + esc(e.error) + '</div>';
|
|
2584
|
+
}
|
|
2139
2585
|
if (!cands.length) {
|
|
2140
|
-
const where = state.addProduct ? prodLabel(state.addProduct) : 'Qoder / WorkBuddy / ZCode / mirasim';
|
|
2141
|
-
const alt = state.addProduct === 'zcode' || state.addProduct === 'mirasim' ? '' : ',或改用「浏览器登录」「粘贴 Token」';
|
|
2142
|
-
html += '<div class="hint">未发现已登录的 ' + where + ' 账号。请先在客户端登录' + alt + '。<br>已检查:Qoder / Qoder CN 客户端数据目录、WorkBuddy 凭据目录、ZCode 数据目录、mirasim 数据目录'
|
|
2586
|
+
const where = state.addProduct ? prodLabel(state.addProduct) : 'Qoder / WorkBuddy / ZCode / mirasim / 妙手 / Trae';
|
|
2587
|
+
const alt = state.addProduct === 'zcode' || state.addProduct === 'mirasim' || state.addProduct === 'catpaw' ? '' : ',或改用「浏览器登录」「粘贴 Token」';
|
|
2588
|
+
html += '<div class="hint">未发现已登录的 ' + where + ' 账号。请先在客户端登录' + alt + '。<br>已检查:Qoder / Qoder CN 客户端数据目录、WorkBuddy 凭据目录、ZCode 数据目录、mirasim 数据目录、妙手凭据目录、Trae 数据目录'
|
|
2143
2589
|
+ (r.scannedDirs && r.scannedDirs.length ? '、' + r.scannedDirs.map(esc).join('、') : '') + '</div>';
|
|
2144
2590
|
}
|
|
2145
2591
|
box.innerHTML = html;
|
|
@@ -2232,20 +2678,21 @@ async function addToken() {
|
|
|
2232
2678
|
// 导出
|
|
2233
2679
|
async function doExport() {
|
|
2234
2680
|
const password = $('ex-password').value;
|
|
2235
|
-
if (password
|
|
2681
|
+
if (!password.trim()) return toast('导出必须设置加密口令', false);
|
|
2682
|
+
if (password.length < 4) return toast('口令至少 4 位', false);
|
|
2236
2683
|
try {
|
|
2237
2684
|
const sel = $('ex-provider').value;
|
|
2238
2685
|
const scope = sel.startsWith('p:') ? { product: sel.slice(2) } : { provider: sel || undefined };
|
|
2239
|
-
const payload = await api('/api/export', { json: Object.assign({ password
|
|
2686
|
+
const payload = await api('/api/export', { json: Object.assign({ password }, scope) });
|
|
2240
2687
|
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
|
|
2241
2688
|
const a = document.createElement('a');
|
|
2242
2689
|
a.href = URL.createObjectURL(blob);
|
|
2243
|
-
a.download = 'creditdaddy-' + (sel.replace('p:', '') || 'all') + '-' + new Date().toISOString().slice(0, 10) +
|
|
2690
|
+
a.download = 'creditdaddy-' + (sel.replace('p:', '') || 'all') + '-' + new Date().toISOString().slice(0, 10) + '.secure.json';
|
|
2244
2691
|
a.click();
|
|
2245
2692
|
URL.revokeObjectURL(a.href);
|
|
2246
2693
|
$('ex-password').value = '';
|
|
2247
2694
|
closeModal('m-export');
|
|
2248
|
-
toast(
|
|
2695
|
+
toast('已导出加密文件(可在 10router 或本面板导入)');
|
|
2249
2696
|
} catch (e) { toast(e.message, false); }
|
|
2250
2697
|
}
|
|
2251
2698
|
|
|
@@ -2396,7 +2843,54 @@ function applyComponents() {
|
|
|
2396
2843
|
renderStatus();
|
|
2397
2844
|
}
|
|
2398
2845
|
|
|
2399
|
-
//
|
|
2846
|
+
// 右上角面板设置:访问密码 + 组件显隐与排序 + 每产品账号排序(默认积分多者靠前,可手动固定)
|
|
2847
|
+
function secStatusHtml() {
|
|
2848
|
+
if (!state.sec.loaded) return '<span class="sec-status">…</span>';
|
|
2849
|
+
return state.sec.enabled ? '<span class="sec-status on">已开启</span>' : '<span class="sec-status">未开启</span>';
|
|
2850
|
+
}
|
|
2851
|
+
function renderSecActions() {
|
|
2852
|
+
if (!state.sec.loaded) return '';
|
|
2853
|
+
return state.sec.enabled
|
|
2854
|
+
? '<button class="btn" data-sec-action="change">修改密码</button><button class="btn" data-sec-action="disable">关闭</button>'
|
|
2855
|
+
: '<button class="btn primary" data-sec-action="enable">开启面板密码</button>';
|
|
2856
|
+
}
|
|
2857
|
+
async function applyPanelKey(next) {
|
|
2858
|
+
setKey(next);
|
|
2859
|
+
state.sec = { enabled: Boolean(next), loaded: true };
|
|
2860
|
+
}
|
|
2861
|
+
async function doSecAction(action) {
|
|
2862
|
+
if (action === 'enable' || action === 'change') {
|
|
2863
|
+
const pw = await uiPrompt(action === 'enable' ? '设置面板访问密码' : '输入新密码', '', {
|
|
2864
|
+
type: 'password', okText: action === 'enable' ? '开启' : '保存',
|
|
2865
|
+
placeholder: '至少 4 位,不能包含空格',
|
|
2866
|
+
message: '设置后每次打开面板都需要输入此密码。导出账号的加密口令是独立设置,始终必填。',
|
|
2867
|
+
});
|
|
2868
|
+
if (!pw) return;
|
|
2869
|
+
const val = String(pw).trim();
|
|
2870
|
+
if (val.length < 4) return toast('面板访问密码至少 4 位', false);
|
|
2871
|
+
if (/\s/.test(val)) return toast('面板访问密码不能包含空格', false);
|
|
2872
|
+
try {
|
|
2873
|
+
const r = await api('/api/settings', { method: 'PUT', json: { panelKey: val } });
|
|
2874
|
+
await applyPanelKey(val);
|
|
2875
|
+
renderCompMenu();
|
|
2876
|
+
toast(r.panelKeyEnabled ? '已设置面板访问密码,下次打开面板需要输入' : '已保存', true);
|
|
2877
|
+
} catch (e) { toast(e.message, false); }
|
|
2878
|
+
return;
|
|
2879
|
+
}
|
|
2880
|
+
if (action === 'disable') {
|
|
2881
|
+
const ok = await uiConfirm(
|
|
2882
|
+
'关闭后打开面板不再需要密码(局域网 / 公网访问将无保护,仅建议本机使用)。账号导出仍必须设置加密口令。\n\n确定关闭面板访问密码吗?',
|
|
2883
|
+
'关闭面板访问密码', '关闭');
|
|
2884
|
+
if (!ok) return;
|
|
2885
|
+
try {
|
|
2886
|
+
const r = await api('/api/settings', { method: 'PUT', json: { disable: true } });
|
|
2887
|
+
await loadSettings();
|
|
2888
|
+
await applyPanelKey(r.panelKeyEnabled ? getKey() : '');
|
|
2889
|
+
renderCompMenu();
|
|
2890
|
+
toast(r.panelKeyEnabled ? '该部署环境(fnOS / 环境变量)强制启用访问密码,无法在面板内关闭' : '已关闭面板访问密码', true);
|
|
2891
|
+
} catch (e) { toast(e.message, false); }
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2400
2894
|
function renderCompMenu() {
|
|
2401
2895
|
const compRows = state.compOrder.map((id) => {
|
|
2402
2896
|
const c = COMPONENTS.find((x) => x.id === id);
|
|
@@ -2425,11 +2919,29 @@ function renderCompMenu() {
|
|
|
2425
2919
|
+ (manual && manual.length ? '<button class="btn" data-acc-reset="' + p.id + '" style="margin-top:6px;width:100%">恢复默认排序</button>' : '')
|
|
2426
2920
|
+ '</details>';
|
|
2427
2921
|
}).join('');
|
|
2428
|
-
$('comp-menu').innerHTML =
|
|
2429
|
-
|
|
2922
|
+
$('comp-menu').innerHTML =
|
|
2923
|
+
'<div class="comp-menu-title">面板访问密码 ' + secStatusHtml() + '</div>'
|
|
2924
|
+
+ '<div class="hint">开启后每次打开面板都需要输入密码;密码可随时在此修改或关闭。'
|
|
2925
|
+
+ (state.sec.enabled ? '' : '账号导出不受影响(导出口令始终必填)。') + '</div>'
|
|
2926
|
+
+ '<div class="row">' + renderSecActions() + '</div>'
|
|
2927
|
+
+ '<div class="comp-menu-title">自动签到</div>'
|
|
2928
|
+
+ '<div class="hint">' + autoCheckinHint() + '</div>'
|
|
2929
|
+
+ '<div class="comp-menu-title">组件顺序(仪表盘与标签栏同步)</div>' + compRows
|
|
2930
|
+
+ '<div class="comp-menu-title">账号卡片排序</div>'
|
|
2430
2931
|
+ (sortSections || '<div class="sort-hint">暂无账号</div>');
|
|
2431
2932
|
}
|
|
2432
2933
|
|
|
2934
|
+
/** 自动签到口径说明:调度周期 + 各产品业务日刷新时点 */
|
|
2935
|
+
function autoCheckinHint() {
|
|
2936
|
+
const sch = state.status && state.status.scheduler;
|
|
2937
|
+
const next = sch && sch.nextTickAt ? ',下次执行 ' + esc(String(sch.nextTickAt).slice(11, 16)) : '';
|
|
2938
|
+
const zc = sch && sch.zcode;
|
|
2939
|
+
const zcText = zc && zc.enabled
|
|
2940
|
+
? 'ZCode 自动领取运行中(每 ' + (zc.intervalMin || 2) + ' 分钟' + (zc.autoOffAt ? ',约 ' + Math.max(1, Math.ceil((new Date(zc.autoOffAt).getTime() - Date.now()) / 60000)) + ' 分钟后自动关闭' : ',一直运行') + '),周期可在 ZCode 页「自动领取计划」里改'
|
|
2941
|
+
: 'ZCode 活动领取默认关闭,可在 ZCode 页「自动领取计划」里开启并配置轮询周期';
|
|
2942
|
+
return '守护进程每 ~2 小时自动扫描执行一轮全部账号' + next + '。各产品「业务日」刷新时点(UTC+8):<b>Qoder 每日 10:00</b>(每台设备每日限领一个国际版账号)、<b>WorkBuddy 每日 00:00</b>,其余产品按 00:00;已领成功的账号按对应业务日记忆跳过,跨过刷新点后当天照常重试。' + esc(zcText) + '。';
|
|
2943
|
+
}
|
|
2944
|
+
|
|
2433
2945
|
/** 在手动排序列表里移动一个 id;列表不存在时先按当前生效顺序物化一份 */
|
|
2434
2946
|
function moveAccountId(prod, aid, dir) {
|
|
2435
2947
|
const baseIds = sortedAccounts(prod, ofProduct(prod)).map((a) => a.id);
|
|
@@ -2442,15 +2954,17 @@ function moveAccountId(prod, aid, dir) {
|
|
|
2442
2954
|
state.cardOrder[prod] = arr;
|
|
2443
2955
|
saveCardOrder();
|
|
2444
2956
|
}
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2957
|
+
// 右上角齿轮 → 面板设置弹窗(访问密码 + 组件显隐/排序 + 账号卡片排序)
|
|
2958
|
+
$('btn-comp').addEventListener('click', async () => {
|
|
2959
|
+
renderCompMenu();
|
|
2960
|
+
openModal('m-settings');
|
|
2961
|
+
await loadSettings();
|
|
2962
|
+
renderCompMenu();
|
|
2963
|
+
// 桌面版才有检查更新入口(走 preload 桥到主进程的更新状态机)
|
|
2964
|
+
if (updBridge) {
|
|
2965
|
+
$('upd-entry').style.display = '';
|
|
2966
|
+
try { const s = await updBridge.state(); updState = s; $('upd-ver').textContent = '当前 v' + (s.current || '?'); } catch {}
|
|
2967
|
+
}
|
|
2454
2968
|
});
|
|
2455
2969
|
$('comp-menu').addEventListener('change', (e) => {
|
|
2456
2970
|
const c = e.target.dataset && e.target.dataset.comp;
|
|
@@ -2461,6 +2975,8 @@ $('comp-menu').addEventListener('change', (e) => {
|
|
|
2461
2975
|
renderCompMenu();
|
|
2462
2976
|
});
|
|
2463
2977
|
$('comp-menu').addEventListener('click', (e) => {
|
|
2978
|
+
const secBtn = e.target.closest && e.target.closest('[data-sec-action]');
|
|
2979
|
+
if (secBtn) { doSecAction(secBtn.dataset.secAction); return; }
|
|
2464
2980
|
const move = e.target.closest && e.target.closest('[data-move]');
|
|
2465
2981
|
if (move) {
|
|
2466
2982
|
e.stopPropagation();
|
|
@@ -2503,7 +3019,12 @@ $('btn-checkin').onclick = () => checkinAll($('btn-checkin'), state.view);
|
|
|
2503
3019
|
$('btn-add-dash').onclick = () => openAdd('');
|
|
2504
3020
|
$('btn-add').onclick = () => openAdd(state.view);
|
|
2505
3021
|
$('btn-export-dash').onclick = () => { $('ex-provider').value = ''; openModal('m-export'); };
|
|
2506
|
-
$('btn-export').onclick = () => {
|
|
3022
|
+
$('btn-export').onclick = () => {
|
|
3023
|
+
const v = state.view;
|
|
3024
|
+
// ZCode 是单一 provider,直接按 provider 选;其余产品页按产品线(p:);仪表盘 / 10Router 导出全部
|
|
3025
|
+
$('ex-provider').value = v === 'zcode' ? 'zcode' : (v === 'qoder' || v === 'workbuddy' || v === 'mirasim' || v === 'catpaw' || v === 'trae') ? 'p:' + v : '';
|
|
3026
|
+
openModal('m-export');
|
|
3027
|
+
};
|
|
2507
3028
|
$('btn-do-export').onclick = doExport;
|
|
2508
3029
|
$('btn-pass-ok').onclick = () => submitImport($('pass-input').value);
|
|
2509
3030
|
$('pass-input').addEventListener('keydown', (e) => { if (e.key === 'Enter') submitImport($('pass-input').value); });
|
|
@@ -2528,6 +3049,7 @@ function applyView() {
|
|
|
2528
3049
|
$('view-dash').classList.toggle('active', state.view === 'dash');
|
|
2529
3050
|
$('view-prod').classList.toggle('active', state.view !== 'dash' && state.view !== 'tenrouter');
|
|
2530
3051
|
$('view-10r').classList.toggle('active', state.view === 'tenrouter');
|
|
3052
|
+
if (state.view === 'tenrouter') loadZGateway();
|
|
2531
3053
|
if (state.view === 'tenrouter' && tr.config && tr.config.configured && !tr.quotas && !tr.loading) loadTrQuotas();
|
|
2532
3054
|
render();
|
|
2533
3055
|
renderStatus();
|
|
@@ -2550,7 +3072,8 @@ $('region').addEventListener('click', (e) => {
|
|
|
2550
3072
|
state.region = b.dataset.r;
|
|
2551
3073
|
render();
|
|
2552
3074
|
renderStatus();
|
|
2553
|
-
});
|
|
3075
|
+
});
|
|
3076
|
+
$('f-provider').addEventListener('change', () => {
|
|
2554
3077
|
$('f-token').placeholder = $('f-provider').value.startsWith('workbuddy')
|
|
2555
3078
|
? 'WorkBuddy / CodeBuddy 登录 token(eyJ…,可在 CodeBuddyExtension 的 auth/*.info 中找到)'
|
|
2556
3079
|
: 'dt-… / jt-… 或 PAT(pt-…,自动兑换)';
|
|
@@ -2561,6 +3084,27 @@ $('btn-logs').onclick = () => {
|
|
|
2561
3084
|
if (!c) { loadLogs(); $('logs').scrollTop = $('logs').scrollHeight; }
|
|
2562
3085
|
};
|
|
2563
3086
|
|
|
3087
|
+
// ── 仪表盘底部两卡:默认收起,点标题展开,展开状态存本地 ──
|
|
3088
|
+
const DASH_FOLD_KEY = 'qd_dash_fold';
|
|
3089
|
+
state.dashFold = (() => { try { return JSON.parse(localStorage.getItem(DASH_FOLD_KEY) || '{}'); } catch { return {}; } })();
|
|
3090
|
+
function applyDashFold() {
|
|
3091
|
+
document.querySelectorAll('.dash-cols .pcard[data-fold]').forEach((el) => {
|
|
3092
|
+
const open = state.dashFold[el.dataset.fold] === true;
|
|
3093
|
+
el.classList.toggle('open', open);
|
|
3094
|
+
el.querySelector('.fold-ico').innerHTML = open ? I.chevUp : I.chevDown;
|
|
3095
|
+
});
|
|
3096
|
+
}
|
|
3097
|
+
document.querySelectorAll('.dash-cols .fold-head').forEach((h) => {
|
|
3098
|
+
h.title = '展开 / 收起';
|
|
3099
|
+
h.addEventListener('click', () => {
|
|
3100
|
+
const k = h.closest('.pcard').dataset.fold;
|
|
3101
|
+
state.dashFold[k] = state.dashFold[k] !== true;
|
|
3102
|
+
try { localStorage.setItem(DASH_FOLD_KEY, JSON.stringify(state.dashFold)); } catch {}
|
|
3103
|
+
applyDashFold();
|
|
3104
|
+
});
|
|
3105
|
+
});
|
|
3106
|
+
applyDashFold();
|
|
3107
|
+
|
|
2564
3108
|
// ── 启动:先直接请求(本机模式无需密钥);只有服务端返回 401 时才询问密钥 ──
|
|
2565
3109
|
initTheme();
|
|
2566
3110
|
$('btn-home').innerHTML = I.github;
|
|
@@ -2568,12 +3112,19 @@ applyView();
|
|
|
2568
3112
|
(function boot() {
|
|
2569
3113
|
function start() {
|
|
2570
3114
|
loadStatus().then(loadAccounts).catch((e) => toast(e.message, false));
|
|
3115
|
+
loadSettings().then(renderCompMenu);
|
|
2571
3116
|
loadZNet();
|
|
3117
|
+
loadZGateway();
|
|
2572
3118
|
loadTrConfig().then(() => loadTrQuotas());
|
|
2573
3119
|
setInterval(() => loadTrQuotas(), 5 * 60 * 1000);
|
|
3120
|
+
// 账号余额(ZCode 剩余 token 等)自动刷新:可见时每 2 分钟一轮;从托盘/后台切回时若超过 1 分钟没刷过立即补一轮
|
|
3121
|
+
setInterval(() => { if (!document.hidden) loadQuotas(); }, 2 * 60 * 1000);
|
|
3122
|
+
document.addEventListener('visibilitychange', () => {
|
|
3123
|
+
if (!document.hidden && Date.now() - quotaLoadedAt > 60 * 1000) loadQuotas();
|
|
3124
|
+
});
|
|
2574
3125
|
loadLogs();
|
|
2575
3126
|
setInterval(loadLogs, 10000);
|
|
2576
|
-
setInterval(async () => { await loadStatus(); render(); }, 30000);
|
|
3127
|
+
setInterval(async () => { await loadStatus(); await loadZNet(); await loadZGateway(); render(); }, 30000);
|
|
2577
3128
|
}
|
|
2578
3129
|
// 先直接请求(本机模式无需密码);需要密码时 api() 会弹框并重试
|
|
2579
3130
|
api('/api/status').then(start).catch((e) => toast(e.code === 'PANEL_KEY'
|