dsh-connect-workbuddy 2.3.1 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,986 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>账号池 · 演示设置界面 — dsh-connect-workbuddy</title>
7
+ <style>
8
+ /* ============================================================================
9
+ 沿用插件真实设计令牌(--dsw-alias-*)与类名前缀 dsm-workbuddy-*。
10
+ 这份演示稿是「国内版 tab 内新增账号池区块」的形态预览,不是最终实现。
11
+ ========================================================================== */
12
+ :root{
13
+ --dsw-alias-border-l2:#36373b; --dsw-alias-border-l1:#2c2d31;
14
+ --dsw-alias-bg-layer-1:#1b1c20; --dsw-alias-bg-layer-2:#25262b; --dsw-alias-bg-layer-3:#202126;
15
+ --dsw-alias-label-primary:#e6e6e6; --dsw-alias-label-secondary:#b8b8b8;
16
+ --dsw-alias-label-tertiary:#999; --dsw-alias-label-dimmed:#777;
17
+ --dsw-alias-brand-primary:#5686fe;
18
+ --dsw-alias-state-success-primary:#22c55e; --dsw-alias-state-warn-primary:#f59e0b;
19
+ --dsw-alias-state-error-primary:#ef4444;
20
+ }
21
+ @media (prefers-color-scheme: light){
22
+ :root{
23
+ --dsw-alias-border-l2:#e2e4e9; --dsw-alias-border-l1:#eceef2;
24
+ --dsw-alias-bg-layer-1:#f6f7f9; --dsw-alias-bg-layer-2:#fff; --dsw-alias-bg-layer-3:#fbfbfc;
25
+ --dsw-alias-label-primary:#1f2023; --dsw-alias-label-secondary:#4b4f56;
26
+ --dsw-alias-label-tertiary:#767a82; --dsw-alias-label-dimmed:#a5a9b0;
27
+ --dsw-alias-brand-primary:#3f6fe0;
28
+ --dsw-alias-state-success-primary:#16a34a; --dsw-alias-state-warn-primary:#d97706;
29
+ --dsw-alias-state-error-primary:#dc2626;
30
+ }
31
+ }
32
+ *{box-sizing:border-box}
33
+ body{
34
+ margin:0; padding:28px 20px 64px;
35
+ font:14px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Hiragino Sans GB","Microsoft YaHei",sans-serif;
36
+ color:var(--dsw-alias-label-primary); background:var(--dsw-alias-bg-layer-1);
37
+ -webkit-font-smoothing:antialiased;
38
+ }
39
+ .wrap{max-width:760px;margin:0 auto;display:flex;flex-direction:column;gap:18px}
40
+ .demo-note{
41
+ padding:12px 14px;border:1px dashed var(--dsw-alias-border-l2);border-radius:10px;
42
+ background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.7;
43
+ }
44
+ .demo-note b{color:var(--dsw-alias-label-secondary);font-weight:600}
45
+ .demo-note code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;
46
+ background:var(--dsw-alias-bg-layer-2);padding:1px 5px;border-radius:4px;border:1px solid var(--dsw-alias-border-l1)}
47
+
48
+ /* ---- 卡片外壳(与 dsm-plugin-card 一致) ---- */
49
+ .dsm-plugin-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);
50
+ border-radius:12px;list-style:none;transition:border-color .16s,background .16s}
51
+ .dsm-plugin-card-open{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}
52
+ .dsm-plugin-card-header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;
53
+ background:transparent;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}
54
+ .dsm-plugin-card-head{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}
55
+ .dsm-plugin-card-title{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}
56
+ .dsm-plugin-card-description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}
57
+ .dsm-plugin-card-chevron{color:var(--dsw-alias-label-tertiary);flex:none;width:16px;height:16px;position:relative;transition:transform .16s}
58
+ .dsm-plugin-card-chevron::before{content:"";display:block;position:absolute;left:4px;top:5px;width:7px;height:7px;
59
+ border-right:1.6px solid currentColor;border-bottom:1.6px solid currentColor;transform:rotate(45deg)}
60
+ .dsm-plugin-card-chevron-open{transform:rotate(180deg)}
61
+ .dsm-plugin-card-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding:0 0 12px}
62
+ .dsm-plugin-card-icon{width:32px;height:32px;flex:none;border-radius:7px;display:grid;place-items:center;
63
+ background:linear-gradient(140deg,#5686fe,#7b5cff);color:#fff;font-size:15px;font-weight:700}
64
+
65
+ /* ---- tab 栏 ---- */
66
+ .dsm-workbuddy-tabs{display:flex;gap:6px;padding:4px;border:1px solid var(--dsw-alias-border-l2);
67
+ border-radius:10px;background:var(--dsw-alias-bg-layer-3);margin:16px 0 4px}
68
+ .dsm-workbuddy-tab{appearance:none;font:inherit;cursor:pointer;flex:1;border:0;border-radius:7px;
69
+ padding:7px 10px;color:var(--dsw-alias-label-tertiary);font-size:13px;font-weight:500;line-height:18px;
70
+ background:transparent;transition:color .15s,background .15s;white-space:nowrap}
71
+ .dsm-workbuddy-tab:hover:not(.dsm-workbuddy-tab-active){color:var(--dsw-alias-label-primary)}
72
+ .dsm-workbuddy-tab-active{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);
73
+ box-shadow:inset 0 0 0 1px var(--dsw-alias-border-l2)}
74
+ .dsm-workbuddy-tab-dot{display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:6px;vertical-align:baseline}
75
+ .dsm-workbuddy-usage{display:flex;flex-direction:column;gap:16px;margin:0;padding:16px 0 4px}
76
+
77
+ /* ---- 【原有区块】账号状态行 / 选择 / 积分 / 模型管理 ---- */
78
+ .dsm-workbuddy-usage-account{display:flex;align-items:center;justify-content:space-between;gap:12px;
79
+ padding:12px 14px;border:1px solid var(--dsw-alias-border-l2);border-radius:14px;background:var(--dsw-alias-bg-layer-2)}
80
+ .dsm-workbuddy-usage-account-copy{display:flex;flex-direction:column;gap:3px;min-width:0}
81
+ .dsm-workbuddy-usage-status{display:flex;align-items:center;gap:10px;font-size:15px;font-weight:500;color:var(--dsw-alias-label-primary)}
82
+ .dsm-workbuddy-usage-dot{width:9px;height:9px;border-radius:50%;flex:0 0 auto}
83
+ .dsm-workbuddy-usage-text{margin:0;font-size:14px;line-height:22px;color:var(--dsw-alias-label-secondary)}
84
+ .dsm-workbuddy-account-picker{display:flex;flex-direction:column;gap:8px}
85
+ .dsm-workbuddy-account-state{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}
86
+ .dsm-workbuddy-usage-select-wrap{position:relative}
87
+ .dsm-workbuddy-usage-select{appearance:none;width:100%;font:inherit;padding:10px 34px 10px 12px;
88
+ border:1px solid var(--dsw-alias-border-l2);border-radius:10px;color:var(--dsw-alias-label-primary);
89
+ background:var(--dsw-alias-bg-layer-3);cursor:pointer}
90
+ .dsm-workbuddy-usage-select-wrap::after{content:"";position:absolute;top:50%;right:12px;width:7px;height:7px;
91
+ transform:translateY(-65%) rotate(45deg);border-right:1.6px solid var(--dsw-alias-label-secondary);
92
+ border-bottom:1.6px solid var(--dsw-alias-label-secondary);pointer-events:none}
93
+ .wb-credits-panels{display:grid;grid-template-columns:1fr 1fr;gap:12px}
94
+ .dsm-workbuddy-credit-panel{display:flex;flex-direction:column;min-width:0;gap:7px;padding:14px;
95
+ border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-2)}
96
+ .dsm-workbuddy-credit-panel-title{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}
97
+ .dsm-workbuddy-credit-panel-value{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
98
+ color:var(--dsw-alias-label-primary);font-size:15px;line-height:21px}
99
+ .dsm-workbuddy-credit-panel-meta{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}
100
+ .wb-credit-soon{display:flex;align-items:baseline;justify-content:space-between;gap:8px;
101
+ color:var(--dsw-alias-label-secondary);font-size:12px}
102
+ .wb-credit-soon strong{color:var(--dsw-alias-state-warn-primary)}
103
+ .dsm-workbuddy-credit-panel-total{position:relative;align-items:center;text-align:center;overflow:hidden}
104
+ .dsm-workbuddy-credit-panel-total::before{content:"";position:absolute;top:0;left:0;right:0;height:3px;opacity:.9;background:#4d9b6d}
105
+ .wb-credit-total-body{display:flex;flex-direction:column;align-items:center;gap:2px}
106
+ .wb-credit-total-value{color:var(--dsw-alias-label-primary);font-size:26px;line-height:32px;font-weight:600;
107
+ font-variant-numeric:tabular-nums}
108
+ .dsm-workbuddy-checkin{display:flex;flex-direction:column;align-items:center;width:100%;
109
+ padding-top:12px;border-top:1px solid var(--dsw-alias-border-l2)}
110
+ .dsm-workbuddy-checkin-button{width:100%;padding:4px 10px;font-size:12px}
111
+ .wb-models-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap}
112
+ .wb-models-title{margin:0;font-size:15px;font-weight:600;color:var(--dsw-alias-label-primary)}
113
+ .wb-models-summary{margin:2px 0 0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}
114
+ .wb-model-list{margin-top:10px;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;overflow:hidden;
115
+ background:var(--dsw-alias-bg-layer-2)}
116
+ .wb-model-row{display:grid;grid-template-columns:auto 1fr auto auto;gap:12px;align-items:center;
117
+ padding:10px 14px;border-top:1px solid var(--dsw-alias-border-l1)}
118
+ .wb-model-row:first-child{border-top:0}
119
+ .wb-model-check,.wb-model-img{display:inline-flex;align-items:center;gap:6px;cursor:pointer;font-size:12px;
120
+ color:var(--dsw-alias-label-tertiary)}
121
+ .wb-model-check input,.wb-model-img input{margin:0;cursor:pointer;accent-color:var(--dsw-alias-brand-primary)}
122
+ .wb-model-name{display:inline-flex;align-items:baseline;gap:7px;color:var(--dsw-alias-label-primary);
123
+ font-size:13px;font-weight:500;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
124
+ .wb-rate{color:var(--dsw-alias-label-tertiary);font-size:11px;font-weight:400;flex:none}
125
+ .wb-probe{padding:3px 10px;font-size:12px}
126
+ .wb-models-foot{margin:10px 0 0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}
127
+
128
+ /* ---- 【新增】手动 / 自动冲突提示 ---- */
129
+ .pool-conflict{display:flex;align-items:flex-start;gap:9px;padding:10px 12px;border-radius:10px;
130
+ border:1px solid var(--dsw-alias-state-warn-primary);background:rgba(245,158,11,.08)}
131
+ .pool-conflict[hidden]{display:none}
132
+ .pool-conflict-icon{flex:none;width:16px;height:16px;border-radius:50%;display:grid;place-items:center;
133
+ font-size:11px;font-weight:700;background:var(--dsw-alias-state-warn-primary);color:#fff;margin-top:1px}
134
+ .pool-conflict-main{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1}
135
+ .pool-conflict-main b{font-size:12px;font-weight:600;color:var(--dsw-alias-state-warn-primary)}
136
+ .pool-conflict-main span{font-size:12px;line-height:17px;color:var(--dsw-alias-label-secondary)}
137
+ .pool-conflict-btn{flex:none;padding:3px 10px;font-size:12px;align-self:center}
138
+ .dsm-workbuddy-usage-select:disabled{opacity:.55;cursor:not-allowed}
139
+
140
+ /* ---- 【新增】草稿 / 保存 ---- */
141
+ .pool-save-bar{display:flex;align-items:center;gap:9px;flex-wrap:wrap}
142
+ .pool-dirty{font-size:12px;line-height:18px;color:var(--dsw-alias-state-warn-primary);font-weight:500}
143
+ .pool-dirty[hidden]{display:none}
144
+ .pool-set.is-dirty{background:rgba(245,158,11,.055)}
145
+ .pool-set.is-dirty .pool-set-copy b::after{content:" *";color:var(--dsw-alias-state-warn-primary)}
146
+
147
+ /* ---- 【新增】区域独立标识 ---- */
148
+ .region-badge{display:inline-flex;align-items:center;gap:5px;padding:1px 8px;border-radius:6px;
149
+ font-size:11px;font-weight:600;border:1px solid var(--dsw-alias-border-l2);
150
+ background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-tertiary)}
151
+ .region-independent{margin:0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}
152
+
153
+ /* ---- 【新增区块】分隔标识 ---- */
154
+ .pool-new-marker{display:flex;align-items:center;gap:10px;margin:4px 0}
155
+ .pool-new-line{flex:1;height:1px;background:var(--dsw-alias-brand-primary);opacity:.34}
156
+ .pool-new-tag{flex:none;font-size:11px;font-weight:600;letter-spacing:.02em;color:var(--dsw-alias-brand-primary);
157
+ padding:3px 10px;border:1px dashed var(--dsw-alias-brand-primary);border-radius:999px;opacity:.9}
158
+
159
+ /* ---- 通用按钮 ---- */
160
+ .dsm-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid transparent;border-radius:8px;
161
+ padding:5px 14px;font-size:13px;line-height:1.5;transition:opacity .15s,background .15s,border-color .15s}
162
+ .dsm-btn:disabled{opacity:.42;cursor:default}
163
+ .dsm-btn-outline{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:transparent;font-weight:500}
164
+ .dsm-btn-outline:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed);background:rgba(127,127,127,.08)}
165
+ .dsm-btn-primary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);font-weight:500}
166
+ .dsm-btn-primary:hover:not(:disabled){opacity:.9}
167
+
168
+ /* ---- 区块标题 ---- */
169
+ .pool-section{display:flex;flex-direction:column;gap:10px}
170
+ .pool-head{display:flex;align-items:baseline;justify-content:space-between;gap:10px;flex-wrap:wrap}
171
+ .pool-title{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary);margin:0}
172
+ .pool-sub{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary);margin:0}
173
+ .pool-actions{display:flex;gap:8px;flex-wrap:wrap}
174
+
175
+ /* ---- 状态条 ---- */
176
+ .pool-bar{display:flex;align-items:center;gap:10px;padding:11px 14px;border-radius:12px;
177
+ border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);font-size:13px}
178
+ .pool-bar-dot{width:9px;height:9px;border-radius:50%;flex:0 0 auto}
179
+ .pool-bar-main{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
180
+ .pool-bar-name{color:var(--dsw-alias-label-primary);font-weight:500}
181
+ .pool-bar-meta{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}
182
+ .mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px}
183
+
184
+ /* ---- 账号表 ---- */
185
+ .pool-table{width:100%;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;overflow:hidden;background:var(--dsw-alias-bg-layer-2)}
186
+ .pool-row{display:grid;grid-template-columns:1.5fr .85fr 1fr 1.15fr;gap:10px;align-items:center;
187
+ padding:10px 14px;border-top:1px solid var(--dsw-alias-border-l1);font-size:13px}
188
+ .pool-row:first-child{border-top:0}
189
+ .pool-row-head{padding:8px 14px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-tertiary);
190
+ font-size:11px;font-weight:600;letter-spacing:.02em;text-transform:uppercase}
191
+ .pool-acc{display:flex;align-items:center;gap:8px;min-width:0}
192
+ .pool-acc-name{display:flex;flex-direction:column;gap:1px;min-width:0}
193
+ .pool-acc-name b{font-weight:500;color:var(--dsw-alias-label-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
194
+ .pool-acc-name span{font-size:11px;color:var(--dsw-alias-label-tertiary)}
195
+ .pool-credits{font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-secondary)}
196
+ .pool-credits b{color:var(--dsw-alias-label-primary);font-weight:600}
197
+ .pool-expiry{font-size:12px;color:var(--dsw-alias-label-tertiary);line-height:17px}
198
+ .pool-expiry-warn{color:var(--dsw-alias-state-warn-primary)}
199
+
200
+ /* ---- 徽章 ---- */
201
+ .badge{display:inline-flex;align-items:center;gap:5px;padding:2px 9px;border-radius:999px;
202
+ font-size:12px;line-height:18px;font-weight:500;white-space:nowrap;border:1px solid transparent}
203
+ .badge-dot{width:6px;height:6px;border-radius:50%;flex:none}
204
+ .badge-ok{color:var(--dsw-alias-state-success-primary);background:rgba(34,197,94,.12);border-color:rgba(34,197,94,.32)}
205
+ .badge-ok .badge-dot{background:var(--dsw-alias-state-success-primary)}
206
+ .badge-warn{color:var(--dsw-alias-state-warn-primary);background:rgba(245,158,11,.12);border-color:rgba(245,158,11,.32)}
207
+ .badge-warn .badge-dot{background:var(--dsw-alias-state-warn-primary)}
208
+ .badge-err{color:var(--dsw-alias-state-error-primary);background:rgba(239,68,68,.12);border-color:rgba(239,68,68,.32)}
209
+ .badge-err .badge-dot{background:var(--dsw-alias-state-error-primary)}
210
+ .badge-idle{color:var(--dsw-alias-label-tertiary);background:rgba(127,127,127,.1);border-color:var(--dsw-alias-border-l2)}
211
+ .badge-idle .badge-dot{background:var(--dsw-alias-label-dimmed)}
212
+ .badge-info{color:var(--dsw-alias-brand-primary);background:rgba(86,134,254,.12);border-color:rgba(86,134,254,.32)}
213
+ .badge-info .badge-dot{background:var(--dsw-alias-brand-primary)}
214
+
215
+ /* ---- 行内 spinner ---- */
216
+ .spin{width:12px;height:12px;flex:none;border-radius:50%;border:2px solid var(--dsw-alias-border-l2);
217
+ border-top-color:var(--dsw-alias-brand-primary);animation:spin .7s linear infinite;display:inline-block;vertical-align:-2px}
218
+ @keyframes spin{to{transform:rotate(360deg)}}
219
+
220
+ /* ---- 设置项 ---- */
221
+ .pool-settings{display:flex;flex-direction:column;gap:1px;border:1px solid var(--dsw-alias-border-l2);
222
+ border-radius:12px;overflow:hidden;background:var(--dsw-alias-bg-layer-2)}
223
+ .pool-set{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;padding:12px 14px;
224
+ border-top:1px solid var(--dsw-alias-border-l1)}
225
+ .pool-set:first-child{border-top:0}
226
+ .pool-set-copy{display:flex;flex-direction:column;gap:3px;min-width:0;flex:1}
227
+ .pool-set-copy b{font-weight:500;color:var(--dsw-alias-label-primary);font-size:13px}
228
+ .pool-set-copy span{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}
229
+ .pool-set-ctl{flex:none;display:flex;align-items:center;gap:8px}
230
+
231
+ /* switch */
232
+ .sw{position:relative;display:inline-block;width:38px;height:22px;flex:none;cursor:pointer}
233
+ .sw input{opacity:0;width:0;height:0;position:absolute}
234
+ .sw-track{position:absolute;inset:0;border-radius:999px;background:var(--dsw-alias-border-l2);transition:background .18s}
235
+ .sw-track::after{content:"";position:absolute;top:3px;left:3px;width:16px;height:16px;border-radius:50%;
236
+ background:#fff;transition:transform .18s;box-shadow:0 1px 3px rgba(0,0,0,.3)}
237
+ .sw input:checked + .sw-track{background:var(--dsw-alias-brand-primary)}
238
+ .sw input:checked + .sw-track::after{transform:translateX(16px)}
239
+ .sw input:disabled + .sw-track{opacity:.4;cursor:default}
240
+
241
+ /* select / number */
242
+ .pool-select,.pool-num{appearance:none;font:inherit;font-size:13px;padding:6px 10px;border-radius:8px;
243
+ border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);
244
+ background:var(--dsw-alias-bg-layer-3);cursor:pointer}
245
+ .pool-select{padding-right:28px;background-image:linear-gradient(45deg,transparent 50%,var(--dsw-alias-label-tertiary) 50%),
246
+ linear-gradient(135deg,var(--dsw-alias-label-tertiary) 50%,transparent 50%);
247
+ background-position:calc(100% - 15px) 51%,calc(100% - 10px) 51%;background-size:5px 5px,5px 5px;background-repeat:no-repeat}
248
+ .pool-num{width:74px;text-align:center;font-variant-numeric:tabular-nums}
249
+ .pool-unit{font-size:12px;color:var(--dsw-alias-label-tertiary)}
250
+
251
+ /* ---- 轮换规则说明 ---- */
252
+ .pool-rule{border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-2);
253
+ padding:12px 14px;display:flex;flex-direction:column;gap:9px}
254
+ .pool-rule-line{display:flex;gap:9px;align-items:flex-start;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}
255
+ .pool-rule-idx{flex:none;width:17px;height:17px;border-radius:50%;display:grid;place-items:center;font-size:10px;
256
+ font-weight:700;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);
257
+ color:var(--dsw-alias-label-tertiary);margin-top:1px}
258
+ .pool-rule-line b{color:var(--dsw-alias-label-primary);font-weight:600}
259
+
260
+ /* ---- 活动日志 ---- */
261
+ .pool-log{border:1px solid var(--dsw-alias-border-l2);border-radius:12px;overflow:hidden;background:var(--dsw-alias-bg-layer-2)}
262
+ .pool-log-head{display:flex;align-items:center;justify-content:space-between;padding:9px 14px;
263
+ background:var(--dsw-alias-bg-layer-3);border-bottom:1px solid var(--dsw-alias-border-l1);
264
+ font-size:12px;font-weight:600;color:var(--dsw-alias-label-secondary)}
265
+ .pool-log-body{max-height:186px;overflow-y:auto}
266
+ .pool-log-row{display:flex;gap:10px;padding:7px 14px;border-top:1px solid var(--dsw-alias-border-l1);
267
+ font-size:12px;line-height:18px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}
268
+ .pool-log-row:first-child{border-top:0}
269
+ .pool-log-time{color:var(--dsw-alias-label-tertiary);flex:none}
270
+ .pool-log-text{color:var(--dsw-alias-label-secondary);min-width:0;word-break:break-word}
271
+ .pool-log-empty{padding:14px;font-size:12px;color:var(--dsw-alias-label-tertiary)}
272
+ .log-ok{color:var(--dsw-alias-state-success-primary)}
273
+ .log-warn{color:var(--dsw-alias-state-warn-primary)}
274
+ .log-err{color:var(--dsw-alias-state-error-primary)}
275
+ .log-rot{color:var(--dsw-alias-brand-primary)}
276
+
277
+ /* ---- 结算/确认条 ---- */
278
+ .pool-confirm{border:1px solid var(--dsw-alias-state-warn-primary);border-radius:12px;
279
+ background:rgba(245,158,11,.07);padding:12px 14px;display:flex;flex-direction:column;gap:9px}
280
+ .pool-confirm-title{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary)}
281
+ .pool-confirm-text{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}
282
+ .pool-confirm-actions{display:flex;gap:8px;flex-wrap:wrap}
283
+ .btn-warn{background:var(--dsw-alias-state-warn-primary);color:#fff;font-weight:500;border-color:transparent}
284
+ .btn-warn:hover:not(:disabled){opacity:.9}
285
+ .pool-foot{margin:0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}
286
+ </style>
287
+ </head>
288
+ <body>
289
+ <div class="wrap">
290
+
291
+ <div class="demo-note">
292
+ <b>演示稿(Demo)</b> — 这是<b>国内版 tab 的完整卡片</b>,非最终实现。
293
+ 上半部分是插件<b>原有的设置</b>(账号状态 / 账号选择 / 积分与签到 / 模型管理),
294
+ 下半部分蓝色虚线以下是<b>本次新增的账号池</b>。数据均为示例值,按钮只驱动本地假状态,
295
+ 不发任何真实请求。视觉沿用插件真实的 <code>--dsw-alias-*</code> 令牌与 <code>dsm-*</code> 类名,
296
+ 配色随系统深浅色切换。<br />
297
+ 规划文档:<code>docs/ACCOUNT_POOL_PLAN.md</code>
298
+ </div>
299
+
300
+ <!-- ============ 插件卡片 ============ -->
301
+ <div class="dsm-plugin-card dsm-plugin-card-open">
302
+ <div class="dsm-plugin-card-header">
303
+ <span class="dsm-plugin-card-icon">W</span>
304
+ <span class="dsm-plugin-card-head">
305
+ <span class="dsm-plugin-card-title">DSH Connect WorkBuddy</span>
306
+ <span class="dsm-plugin-card-description">把本机登录的 WorkBuddy 模型接入 DeepSeek Harness</span>
307
+ </span>
308
+ <span class="dsm-plugin-card-chevron dsm-plugin-card-chevron-open"></span>
309
+ </div>
310
+
311
+ <div class="dsm-plugin-card-body">
312
+ <!-- tab 栏:国内版 / 国际版 -->
313
+ <div class="dsm-workbuddy-tabs">
314
+ <button class="dsm-workbuddy-tab dsm-workbuddy-tab-active" type="button">
315
+ <span class="dsm-workbuddy-tab-dot" style="background:var(--dsw-alias-state-success-primary)"></span>国内版
316
+ </button>
317
+ <button class="dsm-workbuddy-tab" type="button">
318
+ <span class="dsm-workbuddy-tab-dot" style="background:var(--dsw-alias-state-success-primary)"></span>国际版
319
+ </button>
320
+ </div>
321
+
322
+ <div class="dsm-workbuddy-usage">
323
+
324
+ <!-- ==================== [原有] 账号状态行 ==================== -->
325
+ <div class="dsm-workbuddy-usage-account">
326
+ <div class="dsm-workbuddy-usage-account-copy">
327
+ <span class="dsm-workbuddy-usage-status">
328
+ <span class="dsm-workbuddy-usage-dot" style="background:var(--dsw-alias-state-success-primary)"></span>
329
+ 已登录
330
+ </span>
331
+ <span class="dsm-workbuddy-usage-text" style="font-size:13px">账号A · 138****6621</span>
332
+ </div>
333
+ <button class="dsm-btn dsm-btn-outline" type="button">重新检测账号</button>
334
+ </div>
335
+
336
+ <!-- ==================== [原有] 账号选择 ==================== -->
337
+ <section class="dsm-workbuddy-account-picker">
338
+ <div class="dsm-workbuddy-usage-select-wrap">
339
+ <select class="dsm-workbuddy-usage-select" id="manualPicker">
340
+ <option selected>账号A · 138****6621(当前)</option>
341
+ <option>账号B · 189****3302</option>
342
+ <option>账号C · 152****7788</option>
343
+ </select>
344
+ </div>
345
+ <span class="dsm-workbuddy-account-state">勾选后即切换该账号;再选一次可清除,跟回桌面端当前登录。</span>
346
+
347
+ <!-- 【新增】冲突提示:自动轮换开启时手动选择被禁用 -->
348
+ <div class="pool-conflict" id="conflictNotice">
349
+ <span class="pool-conflict-icon">!</span>
350
+ <span class="pool-conflict-main">
351
+ <b>手动选择已停用:本区域已开启自动轮换</b>
352
+ <span>账号由插件按「可用性 → 积分 → 可用时间」自动挑选,因此不能再手动指定。
353
+ 上面的下拉已置灰,避免"显示 A 却计费 B"。</span>
354
+ </span>
355
+ <button class="dsm-btn dsm-btn-outline pool-conflict-btn" type="button" id="btnDisableRotate">
356
+ 关闭自动轮换
357
+ </button>
358
+ </div>
359
+ </section>
360
+
361
+ <!-- ==================== [原有] 积分 / 签到 ==================== -->
362
+ <div class="wb-credits-panels">
363
+ <section class="dsm-workbuddy-credit-panel">
364
+ <span class="dsm-workbuddy-credit-panel-title">积分包</span>
365
+ <span class="dsm-workbuddy-credit-panel-value">2 个有效</span>
366
+ <span class="dsm-workbuddy-credit-panel-meta">最近过期:21 天后</span>
367
+ <div class="wb-credit-soon"><span>3 天内过期</span><strong>0</strong></div>
368
+ </section>
369
+ <section class="dsm-workbuddy-credit-panel dsm-workbuddy-credit-panel-total">
370
+ <div class="wb-credit-total-body">
371
+ <span class="dsm-workbuddy-credit-panel-title">剩余总积分</span>
372
+ <strong class="wb-credit-total-value">1,240</strong>
373
+ </div>
374
+ <div class="dsm-workbuddy-checkin">
375
+ <button class="dsm-btn dsm-btn-primary dsm-workbuddy-checkin-button" type="button">签到领积分</button>
376
+ </div>
377
+ </section>
378
+ </div>
379
+
380
+ <!-- ==================== [原有] 模型管理 ==================== -->
381
+ <section class="dsm-workbuddy-models">
382
+ <div class="wb-models-head">
383
+ <div>
384
+ <h3 class="wb-models-title">模型</h3>
385
+ <p class="wb-models-summary">已启用 3 / 共 4 个(国内版目录)</p>
386
+ </div>
387
+ <button class="dsm-btn dsm-btn-outline" type="button">重新读取模型</button>
388
+ </div>
389
+ <div class="wb-model-list">
390
+ <div class="wb-model-row">
391
+ <label class="wb-model-check"><input type="checkbox" checked /><span></span></label>
392
+ <span class="wb-model-name">Deepseek-V4.1-Flash <span class="wb-rate">x0.00</span></span>
393
+ <label class="wb-model-img"><input type="checkbox" checked /><span>图片</span></label>
394
+ <button class="dsm-btn dsm-btn-outline wb-probe" type="button">测试</button>
395
+ </div>
396
+ <div class="wb-model-row">
397
+ <label class="wb-model-check"><input type="checkbox" checked /><span></span></label>
398
+ <span class="wb-model-name">GLM-5.3 <span class="wb-rate">x0.79</span></span>
399
+ <label class="wb-model-img"><input type="checkbox" /><span>图片</span></label>
400
+ <button class="dsm-btn dsm-btn-outline wb-probe" type="button">测试</button>
401
+ </div>
402
+ <div class="wb-model-row">
403
+ <label class="wb-model-check"><input type="checkbox" checked /><span></span></label>
404
+ <span class="wb-model-name">Kimi-K3 <span class="wb-rate">x1.20</span></span>
405
+ <label class="wb-model-img"><input type="checkbox" /><span>图片</span></label>
406
+ <button class="dsm-btn dsm-btn-outline wb-probe" type="button">测试</button>
407
+ </div>
408
+ <div class="wb-model-row">
409
+ <label class="wb-model-check"><input type="checkbox" /><span></span></label>
410
+ <span class="wb-model-name">Hy3 <span class="wb-rate">x0.50</span></span>
411
+ <label class="wb-model-img"><input type="checkbox" /><span>图片</span></label>
412
+ <button class="dsm-btn dsm-btn-outline wb-probe" type="button">测试</button>
413
+ </div>
414
+ </div>
415
+ <p class="wb-models-foot">每次测试会发一次真实体积请求(实测约 25k input tokens,credit ≈ 0.72),因此只支持逐个测试。</p>
416
+ </section>
417
+
418
+ <!-- ==================== 【新增】账号池区块 ==================== -->
419
+ <div class="pool-new-marker">
420
+ <span class="pool-new-line"></span>
421
+ <span class="pool-new-tag">以下为新增功能(演示重点)</span>
422
+ <span class="pool-new-line"></span>
423
+ </div>
424
+
425
+ <div class="pool-section">
426
+ <div class="pool-head">
427
+ <div>
428
+ <p class="pool-title">账号池 <span class="region-badge">国内版</span></p>
429
+ <p class="pool-sub">池内 3 个国内版账号 · 目标模型 <b class="mono">deepseek-v4.1-flash</b>(免费,x0.00 倍率)</p>
430
+ </div>
431
+ </div>
432
+ <p class="region-independent">
433
+ 国内版与国际版是<b>各自独立</b>的两个账号池:账号、开关、间隔、目标模型互不影响。
434
+ 这边开自动轮换,国际版那边仍可保持手动。切到「国际版」tab 可以看到另一套同样的控件。
435
+ </p>
436
+
437
+ <!-- 当前轮换结果 -->
438
+ <div class="pool-bar" id="currentBar">
439
+ <span class="pool-bar-dot" style="background:var(--dsw-alias-state-success-primary)"></span>
440
+ <span class="pool-bar-main">
441
+ <span class="pool-bar-name">当前计费账号:账号A · 138****6621</span>
442
+ <span class="pool-bar-meta" id="currentMeta">
443
+ 积分 1,240(最高)· deepseek-v4.1-flash 可用 · 上次测试 14:02
444
+ </span>
445
+ </span>
446
+ <span class="badge badge-info"><span class="badge-dot"></span>自动轮换中</span>
447
+ </div>
448
+
449
+ <!-- 两个功能按钮 -->
450
+ <div class="pool-actions">
451
+ <button class="dsm-btn dsm-btn-primary" type="button" id="btnCheckin">
452
+ 一键签到所有账号
453
+ </button>
454
+ <button class="dsm-btn dsm-btn-outline" type="button" id="btnTest">
455
+ 一键测试所有账号
456
+ </button>
457
+ <span class="pool-unit" id="actionHint" style="align-self:center"></span>
458
+ </div>
459
+
460
+ <!-- 账号表 -->
461
+ <div class="pool-table">
462
+ <div class="pool-row pool-row-head">
463
+ <span>账号</span><span>积分</span><span>v4.1-flash</span><span>签到 / 备注</span>
464
+ </div>
465
+ <div id="accountRows"><!-- 由脚本渲染 --></div>
466
+ </div>
467
+
468
+ <p class="pool-foot" id="poolFoot">
469
+ 每次测试会向 <b class="mono">deepseek-v4.1-flash</b> 发一次真实体积请求(实测约 25k input tokens)。
470
+ 该模型倍率为 0,但仍会消耗少量额度;测试严格串行,避免把账号打成限流。
471
+ </p>
472
+ </div>
473
+
474
+ <!-- ==================== 轮换规则 ==================== -->
475
+ <div class="pool-section">
476
+ <div class="pool-head">
477
+ <p class="pool-title">轮换规则</p>
478
+ <p class="pool-sub">按「可用性 → 积分 → 可用时间」三级排序</p>
479
+ </div>
480
+ <div class="pool-rule">
481
+ <div class="pool-rule-line">
482
+ <span class="pool-rule-idx">1</span>
483
+ <span><b>可用性优先</b> —— 只有最近一次测试为「可用」的账号才进入候选;
484
+ 「被限流」是暂时出池、按上游给出的重置时间回池,<b>不是永久剔除</b>。</span>
485
+ </div>
486
+ <div class="pool-rule-line">
487
+ <span class="pool-rule-idx">2</span>
488
+ <span><b>积分高者优先</b> —— 在可用的账号里选积分最多的那个承接请求,积分见底的账号先损耗。</span>
489
+ </div>
490
+ <div class="pool-rule-line">
491
+ <span class="pool-rule-idx">3</span>
492
+ <span><b>快过期的先用</b> —— 积分包 3 天内到期的账号提前使用,过期即作废。</span>
493
+ </div>
494
+ <div class="pool-rule-line">
495
+ <span class="pool-rule-idx">4</span>
496
+ <span><b>平手比凭据新鲜度</b> —— 前三项相同时取 token 过期时间更晚的账号。</span>
497
+ </div>
498
+ </div>
499
+ </div>
500
+
501
+ <!-- ==================== 设置项 ==================== -->
502
+ <div class="pool-section">
503
+ <div class="pool-head">
504
+ <p class="pool-title">账号池设置</p>
505
+ <span class="pool-dirty" id="dirtyBadge" hidden>● 有未保存的修改</span>
506
+ </div>
507
+ <p class="region-independent">
508
+ 下面四项是<b>偏好设置</b>:改动后需要点「保存」才落盘(与「模型管理」一致)。
509
+ 池里的<b>探测结果与轮换记录</b>由插件自动写入,不经过这个保存按钮,
510
+ 因此保存不会把你刚测出来的结果回退掉。
511
+ </p>
512
+ <div class="pool-settings">
513
+ <div class="pool-set">
514
+ <span class="pool-set-copy">
515
+ <b>启用账号池</b>
516
+ <span>关闭时完全保持插件现状:跟随 WorkBuddy 桌面端当前登录,不按积分重排任何账号。</span>
517
+ </span>
518
+ <span class="pool-set-ctl">
519
+ <label class="sw"><input type="checkbox" id="swEnabled" checked /><span class="sw-track"></span></label>
520
+ </span>
521
+ </div>
522
+
523
+ <div class="pool-set">
524
+ <span class="pool-set-copy">
525
+ <b>按积分自动轮换</b>
526
+ <span>允许插件按上面的规则为我切换计费账号。默认关闭 —— 打开即表示同意「账单账号可能被自动改变」,
527
+ 每次切换都会写入下方活动记录。</span>
528
+ </span>
529
+ <span class="pool-set-ctl">
530
+ <label class="sw"><input type="checkbox" id="swRotate" checked /><span class="sw-track"></span></label>
531
+ </span>
532
+ </div>
533
+
534
+ <div class="pool-set">
535
+ <span class="pool-set-copy">
536
+ <b>自动测试间隔</b>
537
+ <span>每隔一段时间自动测试池内账号的 <b class="mono">deepseek-v4.1-flash</b> 可用性。
538
+ 仅在「启用账号池」时运行,关闭即清除定时器。</span>
539
+ </span>
540
+ <span class="pool-set-ctl">
541
+ <input class="pool-num" type="number" id="numInterval" min="5" max="1440" step="5" value="30" />
542
+ <span class="pool-unit">分钟</span>
543
+ </span>
544
+ </div>
545
+
546
+ <div class="pool-set">
547
+ <span class="pool-set-copy">
548
+ <b>目标测试模型</b>
549
+ <span><b>自动</b>(默认):从本区域模型目录里挑一个<b>倍率为 0 的免费模型</b>,
550
+ 多个候选取上下文窗口最大的。<b>国内版与国际版挑出的模型可能不同</b>——
551
+ 国际版目前有 3 个免费模型,国内版要等模型目录刷新后才出现。
552
+ 也可以手动指定,手动指定的值优先于自动挑选。</span>
553
+ </span>
554
+ <span class="pool-set-ctl">
555
+ <select class="pool-select" id="selModel">
556
+ <option value="auto" selected>自动挑免费模型(推荐)</option>
557
+ <option value="deepseek-v4.1-flash">deepseek-v4.1-flash(x0.00)</option>
558
+ <option value="glm-5.3">GLM-5.3(x0.79,收费)</option>
559
+ <option value="kimi-k3">Kimi-K3(x1.20,收费)</option>
560
+ </select>
561
+ </span>
562
+ </div>
563
+ <div class="pool-set" id="autoPickRow">
564
+ <span class="pool-set-copy">
565
+ <span id="autoPickNote"></span>
566
+ </span>
567
+ </div>
568
+ </div>
569
+
570
+ <!-- 【新增】保存 / 放弃(与「模型管理」同一套草稿语义) -->
571
+ <div class="pool-save-bar">
572
+ <button class="dsm-btn dsm-btn-primary" type="button" id="btnSavePool" disabled>保存</button>
573
+ <button class="dsm-btn dsm-btn-outline" type="button" id="btnDiscardPool" disabled>放弃修改</button>
574
+ <span class="pool-unit" id="saveHint">修改任一设置后即可保存。</span>
575
+ </div>
576
+ <p class="region-independent" id="runtimeNote">
577
+ 提示:下方活动记录里的<b>测试结果与轮换</b>是运行时事实,由插件自动落盘 ——
578
+ 上面这个「保存」只写你的四项偏好,两者互不覆盖。
579
+ </p>
580
+ </div>
581
+
582
+ <!-- ==================== 活动记录 ==================== -->
583
+ <div class="pool-section">
584
+ <div class="pool-log">
585
+ <div class="pool-log-head">
586
+ <span>活动记录</span>
587
+ <button class="dsm-btn dsm-btn-outline" type="button" id="btnClearLog" style="padding:2px 10px;font-size:12px">清空</button>
588
+ </div>
589
+ <div class="pool-log-body" id="logBody">
590
+ <div class="pool-log-empty">尚无活动。点击上方按钮后,每次签到、测试与账号轮换都会记录在这里。</div>
591
+ </div>
592
+ </div>
593
+ </div>
594
+
595
+ </div><!-- /usage -->
596
+ </div><!-- /card-body -->
597
+ </div><!-- /card -->
598
+
599
+ </div>
600
+
601
+ <script>
602
+ /* ============================================================================
603
+ 演示逻辑:纯本地假状态,不发起任何网络请求。
604
+ ========================================================================== */
605
+ const HOUR = 3600 * 1000;
606
+ const now = Date.now();
607
+
608
+ /* 示例账号池。credits/expiry 对应真实的 WorkBuddyWebCredits 字段。 */
609
+ let accounts = [
610
+ { id:'a1', name:'账号A', masked:'138****6621', credits:1240, expiringSoon:0,
611
+ nearestExpiryMs: now + 21*24*HOUR, checkedIn:false, probe:'ok', probeAgo:38*60*1000, note:'' },
612
+ { id:'a2', name:'账号B', masked:'189****3302', credits:980, expiringSoon:320,
613
+ nearestExpiryMs: now + 2*24*HOUR + 5*HOUR, checkedIn:false, probe:'rate-limited', probeAgo:9*60*1000, note:'上游:02:30:30 重置' },
614
+ { id:'a3', name:'账号C(未命名)', masked:'152****7788', credits:0, expiringSoon:0,
615
+ nearestExpiryMs: null, checkedIn:false, probe:'out-of-credit', probeAgo:41*60*1000, note:'积分耗尽' },
616
+ ];
617
+
618
+ let currentId = 'a1';
619
+ let running = false;
620
+ let log = [];
621
+
622
+ const $ = id => document.getElementById(id);
623
+ const fmt = n => n.toLocaleString('zh-CN');
624
+ const clock = () => new Date().toLocaleTimeString('zh-CN', { hour12:false });
625
+ const agoText = ms => ms < 60000 ? '刚刚' : (ms < HOUR ? Math.round(ms/60000)+' 分钟前' : Math.round(ms/HOUR)+' 小时前');
626
+
627
+ function expiryText(a){
628
+ if (a.nearestExpiryMs === null) return { text:'无可用积分包', warn:false };
629
+ const days = (a.nearestExpiryMs - Date.now()) / (24*HOUR);
630
+ const d = Math.max(0, Math.round(days));
631
+ return { text:`最近积分包 ${d} 天后过期`, warn: d <= 3 };
632
+ }
633
+
634
+ /* ---- 轮换打分:可用性 → 积分 → 快过期优先 ---- */
635
+ function ranked(){
636
+ const usable = accounts.filter(a => a.probe === 'ok');
637
+ return usable.slice().sort((x, y) => {
638
+ if (y.credits !== x.credits) return y.credits - x.credits; // 积分高者优先
639
+ const ex = x.nearestExpiryMs ?? Infinity, ey = y.nearestExpiryMs ?? Infinity;
640
+ return ex - ey; // 快过期的先用
641
+ });
642
+ }
643
+
644
+ function badge(a){
645
+ switch (a.probe){
646
+ case 'ok': return '<span class="badge badge-ok"><span class="badge-dot"></span>可用</span>';
647
+ case 'rate-limited': return '<span class="badge badge-warn"><span class="badge-dot"></span>被限流</span>';
648
+ case 'out-of-credit': return '<span class="badge badge-err"><span class="badge-dot"></span>积分耗尽</span>';
649
+ case 'testing': return '<span class="badge badge-idle"><span class="spin"></span>测试中</span>';
650
+ default: return '<span class="badge badge-idle"><span class="badge-dot"></span>未测试</span>';
651
+ }
652
+ }
653
+
654
+ function render(){
655
+ const rows = $('accountRows');
656
+ rows.innerHTML = '';
657
+ for (const a of accounts){
658
+ const isCurrent = a.id === currentId;
659
+ const e = expiryText(a);
660
+ const checkinCell = a.checkedIn
661
+ ? '<span class="badge badge-ok"><span class="badge-dot"></span>已签到</span>'
662
+ + (a.note ? `<div class="pool-expiry">${a.note}</div>` : '')
663
+ : (a.note
664
+ ? `<div class="pool-expiry${a.probe==='rate-limited'?' pool-expiry-warn':''}">${a.note}</div>`
665
+ : '<span class="badge badge-idle"><span class="badge-dot"></span>未签到</span>');
666
+
667
+ const row = document.createElement('div');
668
+ row.className = 'pool-row';
669
+ row.innerHTML = `
670
+ <span class="pool-acc">
671
+ <span class="badge-dot" style="background:${isCurrent?'var(--dsw-alias-brand-primary)':'var(--dsw-alias-border-l2)'};width:8px;height:8px"></span>
672
+ <span class="pool-acc-name">
673
+ <b>${a.name}${isCurrent?' · 当前':''}</b>
674
+ <span class="mono">${a.masked}</span>
675
+ </span>
676
+ </span>
677
+ <span class="pool-credits">
678
+ <b>${fmt(a.credits)}</b>
679
+ <div class="pool-expiry${e.warn?' pool-expiry-warn':''}">${e.text}</div>
680
+ </span>
681
+ <span>${badge(a)}
682
+ <div class="pool-expiry" style="margin-top:3px">${a.probe==='testing'?'正在发送真实请求…':agoText(a.probeAgo)+'测试'}</div>
683
+ </span>
684
+ <span>${checkinCell}</span>`;
685
+ rows.appendChild(row);
686
+ }
687
+ renderCurrent();
688
+ renderLog();
689
+ }
690
+
691
+ function renderCurrent(){
692
+ const r = ranked();
693
+ const top = r[0];
694
+ const el = $('currentBar'), meta = $('currentMeta');
695
+ if (!top){
696
+ el.querySelector('.pool-bar-dot').style.background = 'var(--dsw-alias-state-error-primary)';
697
+ el.querySelector('.pool-bar-name').textContent = '当前无可用账号';
698
+ meta.textContent = '池内没有任何账号通过 deepseek-v4.1-flash 测试,请重新签到或检查账号。';
699
+ el.querySelector('.badge').outerHTML = '<span class="badge badge-err"><span class="badge-dot"></span>无候选</span>';
700
+ return;
701
+ }
702
+ const cur = accounts.find(a => a.id === currentId);
703
+ const rotated = !cur || cur.probe !== 'ok' || cur.id !== top.id;
704
+ const dot = el.querySelector('.pool-bar-dot');
705
+ dot.style.background = rotated ? 'var(--dsw-alias-brand-primary)' : 'var(--dsw-alias-state-success-primary)';
706
+ const nameEl = el.querySelector('.pool-bar-name');
707
+ nameEl.textContent = `当前计费账号:${top.name} · ${top.masked}`;
708
+ const bits = [`积分 ${fmt(top.credits)}(池内最高)`, 'deepseek-v4.1-flash 可用'];
709
+ if (top.expiringSoon > 0) bits.push(`${top.expiringSoon} 积分 3 天内过期,优先使用`);
710
+ meta.textContent = bits.join(' · ');
711
+ const badgeEl = el.querySelector('.badge');
712
+ badgeEl.className = 'badge badge-info';
713
+ badgeEl.innerHTML = '<span class="badge-dot"></span>自动轮换命中';
714
+ syncRotation(top.id);
715
+ }
716
+
717
+ /* 自动轮换:仅在「按积分自动轮换」打开时改变计费账号 */
718
+ function syncRotation(nextId){
719
+ if (!$('swRotate').checked || !$('swEnabled').checked) return;
720
+ if (nextId === currentId) return;
721
+ const from = accounts.find(a => a.id === currentId);
722
+ const to = accounts.find(a => a.id === nextId);
723
+ addLog(`轮换:${from?from.name:'—'} → ${to.name}(积分 ${fmt(to.credits)} 更高)`, 'log-rot');
724
+ currentId = nextId;
725
+ // 轮换事实立即落盘,但只写运行时记录,绝不写用户的 accounts 选择槽
726
+ persistRuntime(`当前计费账号切换为 ${to.name}(未改动你手动选的账号记录)`);
727
+ }
728
+
729
+ function addLog(text, cls){
730
+ log.unshift({ t: clock(), text, cls: cls || '' });
731
+ renderLog();
732
+ }
733
+
734
+ function renderLog(){
735
+ const body = $('logBody');
736
+ if (log.length === 0){
737
+ body.innerHTML = '<div class="pool-log-empty">尚无活动。点击上方按钮后,每次签到、测试与账号轮换都会记录在这里。</div>';
738
+ return;
739
+ }
740
+ body.innerHTML = log.map(e =>
741
+ `<div class="pool-log-row"><span class="pool-log-time">${e.t}</span><span class="pool-log-text ${e.cls}">${e.text}</span></div>`
742
+ ).join('');
743
+ }
744
+
745
+ const sleep = ms => new Promise(r => setTimeout(r, ms));
746
+
747
+ /* ---- 功能一:一键签到所有账号(串行) ---- */
748
+ async function runCheckin(){
749
+ if (running) return;
750
+ running = true; setBusy(true);
751
+ addLog(`开始一键签到,共 ${accounts.length} 个账号(串行)`, 'log-rot');
752
+ for (const a of accounts){
753
+ await sleep(520);
754
+ if (a.checkedIn){ addLog(`${a.name}:今日已签到,跳过(幂等,不重复领取)`); continue; }
755
+ if (a.probe === 'out-of-credit' && a.credits === 0){
756
+ a.checkedIn = true; a.note = '签到成功 +60(仍在耗尽状态)';
757
+ addLog(`${a.name}:签到成功 +60 积分`, 'log-ok');
758
+ } else {
759
+ a.checkedIn = true; a.note = '签到成功 +60 · 连续 7 天';
760
+ a.credits += 60;
761
+ addLog(`${a.name}:签到成功 +60 积分,当前 ${fmt(a.credits)}`, 'log-ok');
762
+ }
763
+ render();
764
+ }
765
+ addLog('一键签到完成', 'log-ok');
766
+ running = false; setBusy(false); render();
767
+ }
768
+
769
+ /* ---- 功能二:一键测试所有账号 × deepseek-v4.1-flash(串行) ---- */
770
+ async function runTest(){
771
+ if (running) return;
772
+ running = true; setBusy(true);
773
+ const model = $('selModel').value;
774
+ addLog(`开始测试 ${accounts.length} 个账号 × ${model}(串行,每次一个真实请求)`, 'log-rot');
775
+ for (const a of accounts){
776
+ a.probe = 'testing'; render();
777
+ await sleep(900);
778
+ if (a.credits === 0){
779
+ a.probe = 'out-of-credit'; a.note = '积分耗尽'; a.probeAgo = 0;
780
+ addLog(`${a.name}:out-of-credit —— 积分耗尽,按配额刷新时间回池`, 'log-err');
781
+ } else if (a.id === 'a2'){
782
+ a.probe = 'rate-limited'; a.note = '上游:将在 02:30:30 重置'; a.probeAgo = 0;
783
+ addLog(`${a.name}:rate-limited —— 模型本身可用,仅在限流,按上游重置时间回池`, 'log-warn');
784
+ } else {
785
+ a.probe = 'ok'; a.note = ''; a.probeAgo = 0;
786
+ addLog(`${a.name}:ok —— 可用(credit 0.00,耗时 1.8s)`, 'log-ok');
787
+ }
788
+ render();
789
+ }
790
+ addLog('测试完成,已按规则重算候选账号', 'log-ok');
791
+ persistRuntime('测试结果与每个账号的可用状态已写入运行时记录(不占草稿)');
792
+ renderCurrent();
793
+ running = false; setBusy(false); render();
794
+ }
795
+
796
+ function setBusy(on){
797
+ $('btnCheckin').disabled = on;
798
+ $('btnTest').disabled = on;
799
+ $('actionHint').innerHTML = on ? '<span class="spin"></span> 串行执行中…' : '';
800
+ }
801
+
802
+ /* ============================================================================
803
+ 两种状态,两套写入语义(与真实插件一致):
804
+ - savedPrefs:偏好设置,改动进草稿,点「保存」才落盘
805
+ - runtime :探测结果 / 轮换事实,插件自动落盘,不经过保存按钮
806
+ ========================================================================== */
807
+ const savedPrefs = { enabled:true, rotateByCredits:true, intervalMinutes:30, targetModel:'auto' };
808
+ let draftPrefs = null; // null = 无未保存修改
809
+ let runtimeSnap = {}; // 模拟 Host 侧自动落盘的运行时事实
810
+
811
+ const prefsOf = () => draftPrefs ?? savedPrefs;
812
+ const isDirty = () => draftPrefs !== null;
813
+
814
+ function editPref(key, value){
815
+ const base = draftPrefs ?? { ...savedPrefs };
816
+ draftPrefs = { ...base, [key]: value };
817
+ renderPrefs();
818
+ }
819
+
820
+ /* 把偏好渲染进控件(保存后 / 放弃后 / 首次加载) */
821
+ function renderPrefs(){
822
+ const p = prefsOf();
823
+ $('swEnabled').checked = p.enabled;
824
+ $('swRotate').checked = p.rotateByCredits;
825
+ $('numInterval').value = p.intervalMinutes;
826
+ $('selModel').value = p.targetModel;
827
+ $('swRotate').disabled = !p.enabled;
828
+ $('numInterval').disabled = !p.enabled;
829
+ $('selModel').disabled = !p.enabled;
830
+
831
+ const dirty = isDirty();
832
+ $('dirtyBadge').hidden = !dirty;
833
+ $('btnSavePool').disabled = !dirty;
834
+ $('btnDiscardPool').disabled = !dirty;
835
+ $('saveHint').textContent = dirty
836
+ ? '有未保存的修改 —— 保存后写入本区域配置。'
837
+ : '修改任一设置后即可保存。';
838
+ document.querySelectorAll('.pool-set').forEach(n => n.classList.remove('is-dirty'));
839
+ if (dirty){
840
+ // 标出被改过的行(演示用:整块提示)
841
+ $('autoPickRow').classList.add('is-dirty');
842
+ }
843
+ syncConflict();
844
+ renderAutoPick();
845
+ }
846
+
847
+ function savePrefs(){
848
+ if (!isDirty()) return;
849
+ const before = { ...savedPrefs }, after = { ...draftPrefs };
850
+ Object.assign(savedPrefs, after);
851
+ draftPrefs = null;
852
+ const changed = Object.keys(after).filter(k => before[k] !== after[k]);
853
+ addLog(`已保存账号池设置(${changed.join('、') || '无变化'})—— 写入本区域偏好,未触碰运行时结果`, 'log-ok');
854
+ renderPrefs(); render();
855
+ }
856
+
857
+ function discardPrefs(){
858
+ if (!isDirty()) return;
859
+ draftPrefs = null;
860
+ addLog('已放弃修改,回到上次保存的设置', 'log-warn');
861
+ renderPrefs(); render();
862
+ }
863
+
864
+ /* 运行时事实的"自动落盘":只记录,不参与草稿 */
865
+ function persistRuntime(note){
866
+ runtimeSnap.updatedAt = clock();
867
+ addLog(`(自动落盘)${note}`, 'log-rot');
868
+ }
869
+
870
+ /* ---- 目标测试模型:按区域 catalog 自动挑倍率为 0 的 ---- */
871
+ /* 演示用的两套区域目录(真实值取自 src/catalog.ts 的静态 fallback 与 live 形态)。
872
+ 注意国内版静态 fallback 不带 creditMultiplier —— 免费模型要刷新后才知道。 */
873
+ const REGION_CATALOG = {
874
+ cn: [
875
+ { id:'auto', name:'Auto', contextWindow:168000, creditMultiplier:undefined },
876
+ { id:'hy3', name:'Hy3', contextWindow:192000, creditMultiplier:undefined },
877
+ { id:'glm-5.3', name:'GLM-5.3', contextWindow:1000000, creditMultiplier:0.79 },
878
+ { id:'kimi-k3-1', name:'Kimi-K3', contextWindow:1000000, creditMultiplier:1.20 },
879
+ ],
880
+ global: [
881
+ { id:'deepseek-v4.1-flash', name:'deepseek-v4.1-flash', contextWindow:1000000, creditMultiplier:0 },
882
+ { id:'hy4-preview', name:'Hy4 preview', contextWindow:1000000, creditMultiplier:0 },
883
+ { id:'hy3', name:'Hy3', contextWindow:192000, creditMultiplier:0 },
884
+ { id:'glm-5.3', name:'GLM-5.3', contextWindow:1000000, creditMultiplier:0.79 },
885
+ ],
886
+ };
887
+
888
+ /** 严格判 === 0(缺失倍率不等于免费),多个候选取窗口最大的。 */
889
+ function pickFreeModel(region){
890
+ const candidates = REGION_CATALOG[region].filter(m => m.creditMultiplier === 0);
891
+ if (candidates.length === 0) return undefined;
892
+ return candidates.reduce((best, m) => m.contextWindow > best.contextWindow ? m : best);
893
+ }
894
+
895
+ function resolveTargetModel(){
896
+ const choice = prefsOf().targetModel;
897
+ if (choice !== 'auto') return { id: choice, manual: true };
898
+ const picked = pickFreeModel('cn');
899
+ return picked === undefined ? { id: undefined, manual: false } : { id: picked.id, manual: false, model: picked };
900
+ }
901
+
902
+ function renderAutoPick(){
903
+ const target = resolveTargetModel();
904
+ const note = $('autoPickNote');
905
+ const row = $('autoPickRow');
906
+ if (target.manual){
907
+ note.innerHTML = `已手动指定 <b class="mono">${target.id}</b> —— 自动挑选不再生效,注意该模型可能收费。`;
908
+ return;
909
+ }
910
+ if (target.id === undefined){
911
+ // 国内版静态目录里没有倍率为 0 的模型:不静默回退到收费模型
912
+ note.innerHTML = `<b style="color:var(--dsw-alias-state-warn-primary)">本区域暂无免费模型</b>
913
+ —— 国内版要等模型目录刷新后才会出现倍率为 0 的条目。
914
+ 为避免产生你没预期的开销,<b>不会</b>自动回退到收费模型;请先刷新模型目录,或手动指定一个目标模型。`;
915
+ return;
916
+ }
917
+ note.innerHTML = `自动选中 <b class="mono">${target.id}</b>
918
+ (x0.00 免费,上下文 ${(target.model.contextWindow/1000).toFixed(0)}k)
919
+ —— 从本区域 catalog 内倍率为 0 的候选中取窗口最大的。`;
920
+ }
921
+
922
+ /* ---- 手动 / 自动 互斥 ---- */
923
+ function syncConflict(){
924
+ const p = prefsOf();
925
+ const autoRotate = p.rotateByCredits && p.enabled;
926
+ const picker = $('manualPicker');
927
+ // 自动轮换开启 -> 手动下拉置灰 + 显示冲突说明(含就地关闭按钮)
928
+ picker.disabled = autoRotate;
929
+ $('conflictNotice').hidden = !autoRotate;
930
+ if (autoRotate){
931
+ picker.classList.add('is-locked');
932
+ picker.title = '已开启自动轮换,账号由插件自动选择;要手动指定请先关闭自动轮换。';
933
+ } else {
934
+ picker.classList.remove('is-locked');
935
+ picker.title = '';
936
+ }
937
+ }
938
+
939
+ function disableRotate(from){
940
+ // 走草稿:与其它偏好一致,保存后才真正生效
941
+ editPref('rotateByCredits', false);
942
+ addLog(from === 'notice'
943
+ ? '(草稿)已从冲突提示关闭自动轮换 —— 点「保存」后生效,手动选择随之恢复'
944
+ : '(草稿)已关闭自动轮换', 'log-warn');
945
+ renderCurrent(); render();
946
+ }
947
+
948
+ /* ---- 绑定 ---- */
949
+ $('btnCheckin').addEventListener('click', runCheckin);
950
+ $('btnTest').addEventListener('click', runTest);
951
+ $('btnClearLog').addEventListener('click', () => { log = []; renderLog(); });
952
+ $('btnDisableRotate').addEventListener('click', () => disableRotate('notice'));
953
+ $('manualPicker').addEventListener('change', e => {
954
+ addLog(`手动指定计费账号:${e.target.options[e.target.selectedIndex].text.split('(')[0]}`, 'log-rot');
955
+ });
956
+ $('swEnabled').addEventListener('change', e => {
957
+ editPref('enabled', e.target.checked);
958
+ addLog(e.target.checked ? '(草稿)账号池:启用' : '(草稿)账号池:关闭 —— 关闭后恢复跟随桌面端当前登录', e.target.checked ? 'log-ok' : 'log-warn');
959
+ });
960
+ $('swRotate').addEventListener('change', e => {
961
+ editPref('rotateByCredits', e.target.checked);
962
+ addLog(e.target.checked
963
+ ? '(草稿)开启自动轮换:手动选择将停用'
964
+ : '(草稿)关闭自动轮换:手动选择将恢复可用', e.target.checked ? 'log-rot' : 'log-warn');
965
+ });
966
+ $('numInterval').addEventListener('change', e => {
967
+ let v = Number(e.target.value);
968
+ if (!Number.isFinite(v) || v < 5){ v = 5; e.target.value = 5; addLog('间隔下限为 5 分钟(保护账号不被测成限流)', 'log-warn'); }
969
+ if (v > 1440){ v = 1440; e.target.value = 1440; addLog('间隔上限为 1440 分钟(24 小时)', 'log-warn'); }
970
+ editPref('intervalMinutes', v);
971
+ addLog(`(草稿)自动测试间隔:每 ${v} 分钟`);
972
+ });
973
+ $('btnSavePool').addEventListener('click', savePrefs);
974
+ $('btnDiscardPool').addEventListener('click', discardPrefs);
975
+ $('selModel').addEventListener('change', e => {
976
+ editPref('targetModel', e.target.value);
977
+ });
978
+
979
+ renderPrefs();
980
+ render();
981
+ addLog('账号池就绪:池内 3 个账号,目标模型由本区域 catalog 自动挑选');
982
+ addLog('本区域已保存「自动轮换=开」,因此上方手动选账号处于停用状态');
983
+ addLog('注意:本区域已开启自动轮换,上方手动选账号已相应停用');
984
+ </script>
985
+ </body>
986
+ </html>