dsh-connect-workbuddy 3.1.0 → 3.3.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.
@@ -81,33 +81,9 @@ body{
81
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
82
  .dsm-workbuddy-usage-dot{width:9px;height:9px;border-radius:50%;flex:0 0 auto}
83
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}
84
+ /* 账号选择器已整块删除:账号一律在账号池里管(成员勾选就是选择面),
85
+ 而池开启时由排序决定谁服务,一个「改了也不生效」的下拉比没有更糟。 */
85
86
  .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
87
  .wb-models-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap}
112
88
  .wb-models-title{margin:0;font-size:15px;font-weight:600;color:var(--dsw-alias-label-primary)}
113
89
  .wb-models-summary{margin:2px 0 0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}
@@ -125,17 +101,11 @@ body{
125
101
  .wb-probe{padding:3px 10px;font-size:12px}
126
102
  .wb-models-foot{margin:10px 0 0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}
127
103
 
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}
104
+ /* 注意:这里原本有一条「手动 / 自动冲突提示」样式块(含只读下拉的置灰样式)。
105
+ 新版删掉了「按积分自动轮换」这个设置,于是「手动还是自动」不再是二选一,那一整套冲突状态
106
+ (置灰下拉 + 告警块 + 解锁按钮)不复存在,故整块删除 —— 留着一套没人使用的警告样式,
107
+ 只会让人以为还有这个状态。
108
+ 目前的关系是:开启账号池时由池子的排序决定谁服务,你保存的选择不被改写;关闭后它立刻完全生效。 */
139
109
 
140
110
  /* ---- 【新增】草稿 / 保存 ---- */
141
111
  .pool-save-bar{display:flex;align-items:center;gap:9px;flex-wrap:wrap}
@@ -238,17 +208,16 @@ body{
238
208
  .sw input:checked + .sw-track::after{transform:translateX(16px)}
239
209
  .sw input:disabled + .sw-track{opacity:.4;cursor:default}
240
210
 
241
- /* select / number */
242
- .pool-select,.pool-num{appearance:none;font:inherit;font-size:13px;padding:6px 10px;border-radius:8px;
211
+ /* select(原「自动测试间隔」的 number 输入随该设置一起删除,故不再有 .pool-num 规则) */
212
+ .pool-select{appearance:none;font:inherit;font-size:13px;padding:6px 10px 6px 10px;border-radius:8px;
243
213
  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%),
214
+ background:var(--dsw-alias-bg-layer-3);cursor:pointer;
215
+ padding-right:28px;background-image:linear-gradient(45deg,transparent 50%,var(--dsw-alias-label-tertiary) 50%),
246
216
  linear-gradient(135deg,var(--dsw-alias-label-tertiary) 50%,transparent 50%);
247
217
  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
218
  .pool-unit{font-size:12px;color:var(--dsw-alias-label-tertiary)}
250
219
 
251
- /* ---- 轮换规则说明 ---- */
220
+ /* ---- 换号顺序说明(原「轮换规则」) ---- */
252
221
  .pool-rule{border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-2);
253
222
  padding:12px 14px;display:flex;flex-direction:column;gap:9px}
254
223
  .pool-rule-line{display:flex;gap:9px;align-items:flex-start;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}
@@ -272,7 +241,8 @@ body{
272
241
  .log-ok{color:var(--dsw-alias-state-success-primary)}
273
242
  .log-warn{color:var(--dsw-alias-state-warn-primary)}
274
243
  .log-err{color:var(--dsw-alias-state-error-primary)}
275
- .log-rot{color:var(--dsw-alias-brand-primary)}
244
+ /* 品牌蓝:既用于「换号重试」这条运行时事实,也用于插件的自动落盘行 */
245
+ .log-swap{color:var(--dsw-alias-brand-primary)}
276
246
 
277
247
  /* ---- 结算/确认条 ---- */
278
248
  .pool-confirm{border:1px solid var(--dsw-alias-state-warn-primary);border-radius:12px;
@@ -290,7 +260,7 @@ body{
290
260
 
291
261
  <div class="demo-note">
292
262
  <b>演示稿(Demo)</b> — 这是<b>国内版 tab 的完整卡片</b>,非最终实现。
293
- 上半部分是插件<b>原有的设置</b>(账号状态 / 账号选择 / 积分与签到 / 模型管理),
263
+ 上半部分是插件<b>原有的设置</b>(区域状态 / 积分与签到),
294
264
  下半部分蓝色虚线以下是<b>本次新增的账号池</b>。数据均为示例值,按钮只驱动本地假状态,
295
265
  不发任何真实请求。视觉沿用插件真实的 <code>--dsw-alias-*</code> 令牌与 <code>dsm-*</code> 类名,
296
266
  配色随系统深浅色切换。<br />
@@ -330,91 +300,11 @@ body{
330
300
  </span>
331
301
  <span class="dsm-workbuddy-usage-text" style="font-size:13px">账号A · 138****6621</span>
332
302
  </div>
333
- <button class="dsm-btn dsm-btn-outline" type="button">重新检测账号</button>
303
+ <!-- 「重新检测账号」已移到下面的账号池区块:那里才是管理账号的地方,
304
+ 新登录的账号会以「一行待勾选」的形式出现。(真机同样如此;未登录状态
305
+ 下卡片仍保留一个副本,因为那时账号池整块不渲染。) -->
334
306
  </div>
335
307
 
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
308
  <!-- ==================== 【新增】账号池区块 ==================== -->
419
309
  <div class="pool-new-marker">
420
310
  <span class="pool-new-line"></span>
@@ -428,22 +318,27 @@ body{
428
318
  <p class="pool-title">账号池 <span class="region-badge">国内版</span></p>
429
319
  <p class="pool-sub">池内 3 个国内版账号 · 目标模型 <b class="mono">deepseek-v4.1-flash</b>(免费,x0.00 倍率)</p>
430
320
  </div>
321
+ <button class="dsm-btn dsm-btn-outline" type="button">重新检测账号</button>
431
322
  </div>
432
323
  <p class="region-independent">
433
- 国内版与国际版是<b>各自独立</b>的两个账号池:账号、开关、间隔、目标模型互不影响。
434
- 这边开自动轮换,国际版那边仍可保持手动。切到「国际版」tab 可以看到另一套同样的控件。
324
+ 国内版与国际版是<b>各自独立</b>的两个账号池:账号与目标模型互不影响。
325
+ 切到「国际版」tab 可以看到另一套同样的控件。
435
326
  </p>
436
327
 
437
- <!-- 当前轮换结果 -->
328
+ <!-- 当前使用的账号:这是个显式标注,不是靠行底色的深浅去猜 -->
438
329
  <div class="pool-bar" id="currentBar">
439
330
  <span class="pool-bar-dot" style="background:var(--dsw-alias-state-success-primary)"></span>
440
331
  <span class="pool-bar-main">
441
- <span class="pool-bar-name">当前计费账号:账号A · 138****6621</span>
332
+ <span class="pool-bar-name">当前使用:账号A · 138****6621</span>
442
333
  <span class="pool-bar-meta" id="currentMeta">
443
- 积分 1,240(最高)· deepseek-v4.1-flash 可用 · 上次测试 14:02
334
+ 积分 1,240 · deepseek-v4.1-flash 可用 · 上次测试 14:02
335
+ </span>
336
+ <span class="pool-bar-meta">
337
+ 下一个请求从它开始。开启账号池后,请求在上游失败时可能改用池里另一个可用账号重试 ——
338
+ 那只是为这一次请求借用额度,不会改变这里的选定。
444
339
  </span>
445
340
  </span>
446
- <span class="badge badge-info"><span class="badge-dot"></span>自动轮换中</span>
341
+ <span class="badge badge-info" id="failoverBadge"><span class="badge-dot"></span>自动换号已启用</span>
447
342
  </div>
448
343
 
449
344
  <!-- 两个功能按钮 -->
@@ -454,6 +349,9 @@ body{
454
349
  <button class="dsm-btn dsm-btn-outline" type="button" id="btnTest">
455
350
  一键测试所有账号
456
351
  </button>
352
+ <button class="dsm-btn dsm-btn-outline" type="button" id="btnSimulate">
353
+ 模拟一次失败请求
354
+ </button>
457
355
  <span class="pool-unit" id="actionHint" style="align-self:center"></span>
458
356
  </div>
459
357
 
@@ -471,29 +369,72 @@ body{
471
369
  </p>
472
370
  </div>
473
371
 
474
- <!-- ==================== 轮换规则 ==================== -->
372
+ <!-- ==================== [原有] 模型管理 ==================== -->
373
+ <section class="dsm-workbuddy-models">
374
+ <div class="wb-models-head">
375
+ <div>
376
+ <h3 class="wb-models-title">模型</h3>
377
+ <p class="wb-models-summary">已启用 3 / 共 4 个(国内版目录)</p>
378
+ </div>
379
+ <button class="dsm-btn dsm-btn-outline" type="button">重新读取模型</button>
380
+ </div>
381
+ <div class="wb-model-list">
382
+ <div class="wb-model-row">
383
+ <label class="wb-model-check"><input type="checkbox" checked /><span></span></label>
384
+ <span class="wb-model-name">Deepseek-V4.1-Flash <span class="wb-rate">x0.00</span></span>
385
+ <label class="wb-model-img"><input type="checkbox" checked /><span>图片</span></label>
386
+ <button class="dsm-btn dsm-btn-outline wb-probe" type="button">测试</button>
387
+ </div>
388
+ <div class="wb-model-row">
389
+ <label class="wb-model-check"><input type="checkbox" checked /><span></span></label>
390
+ <span class="wb-model-name">GLM-5.3 <span class="wb-rate">x0.79</span></span>
391
+ <label class="wb-model-img"><input type="checkbox" /><span>图片</span></label>
392
+ <button class="dsm-btn dsm-btn-outline wb-probe" type="button">测试</button>
393
+ </div>
394
+ <div class="wb-model-row">
395
+ <label class="wb-model-check"><input type="checkbox" checked /><span></span></label>
396
+ <span class="wb-model-name">Kimi-K3 <span class="wb-rate">x1.20</span></span>
397
+ <label class="wb-model-img"><input type="checkbox" /><span>图片</span></label>
398
+ <button class="dsm-btn dsm-btn-outline wb-probe" type="button">测试</button>
399
+ </div>
400
+ <div class="wb-model-row">
401
+ <label class="wb-model-check"><input type="checkbox" /><span></span></label>
402
+ <span class="wb-model-name">Hy3 <span class="wb-rate">x0.50</span></span>
403
+ <label class="wb-model-img"><input type="checkbox" /><span>图片</span></label>
404
+ <button class="dsm-btn dsm-btn-outline wb-probe" type="button">测试</button>
405
+ </div>
406
+ </div>
407
+ <p class="wb-models-foot">每次测试会发一次真实体积请求(实测约 25k input tokens,credit ≈ 0.72),因此只支持逐个测试。</p>
408
+ </section>
409
+
410
+ <!-- ==================== 换号规则 ==================== -->
475
411
  <div class="pool-section">
476
412
  <div class="pool-head">
477
- <p class="pool-title">轮换规则</p>
478
- <p class="pool-sub">按「可用性 → 积分 → 可用时间」三级排序</p>
413
+ <p class="pool-title">自动换号规则</p>
414
+ <p class="pool-sub">开启「启用账号池」即生效:失败时依次用池内其他可用账号重试</p>
479
415
  </div>
480
416
  <div class="pool-rule">
481
417
  <div class="pool-rule-line">
482
418
  <span class="pool-rule-idx">1</span>
483
- <span><b>可用性优先</b> —— 只有最近一次测试为「可用」的账号才进入候选;
484
- 「被限流」是暂时出池、按上游给出的重置时间回池,<b>不是永久剔除</b>。</span>
419
+ <span><b>每一次请求都由池子的排序选出服务账号</b>(可用优先,其次积分高、快过期的积分包、凭据更新的),
420
+ 所以上面那行「当前使用」是排序的结果,不一定是你手动选的那个。该账号失败时,
421
+ 再按同一个排序取下一个<b>可用</b>成员重试,直到有人接手或全部不可用;全部失败才把错误报出来(并注明尝试过几个账号)。</span>
485
422
  </div>
486
423
  <div class="pool-rule-line">
487
424
  <span class="pool-rule-idx">2</span>
488
- <span><b>积分高者优先</b> —— 在可用的账号里选积分最多的那个承接请求,积分见底的账号先损耗。</span>
425
+ <span><b>只跳过「已知不可用」的成员</b>:仍在冷却期内的限流账号、凭据被上游拒绝的账号。
426
+ <b>从没测试过的账号默认视为可用</b>——刚发现的登录不该因为「还没测」就被排除在外。
427
+ 积分也参与排序,但只在有新鲜读数时(卡片轮询 / 批量测试 / 保存时读到的),请求路径不会为了排序去打上游。</span>
489
428
  </div>
490
429
  <div class="pool-rule-line">
491
430
  <span class="pool-rule-idx">3</span>
492
- <span><b>快过期的先用</b> —— 积分包 3 天内到期的账号提前使用,过期即作废。</span>
431
+ <span><b>限流、无积分、凭据失效、网关/网络错误都会换号</b>——这些都是某个账号的状态,换一个很可能就过了。
432
+ <b>请求本身不合法(HTTP 400)不换</b>:同一个请求体换谁都一样是 400,重试只是白等。</span>
493
433
  </div>
494
434
  <div class="pool-rule-line">
495
435
  <span class="pool-rule-idx">4</span>
496
- <span><b>平手比凭据新鲜度</b> —— 前三项相同时取 token 过期时间更晚的账号。</span>
436
+ <span><b>不写回你保存的选择</b>。排序只通过运行时覆盖决定「谁服务」,从不改写你选定的账号;
437
+ 关掉账号池,你选定的那个立刻完全恢复生效。上面那行「当前使用」就是当下真正在服务的账号。</span>
497
438
  </div>
498
439
  </div>
499
440
  </div>
@@ -505,44 +446,22 @@ body{
505
446
  <span class="pool-dirty" id="dirtyBadge" hidden>● 有未保存的修改</span>
506
447
  </div>
507
448
  <p class="region-independent">
508
- 下面四项是<b>偏好设置</b>:改动后需要点「保存」才落盘(与「模型管理」一致)。
509
- 池里的<b>探测结果与轮换记录</b>由插件自动写入,不经过这个保存按钮,
449
+ 下面是<b>偏好设置</b>:改动后需要点「保存」才落盘(与「模型管理」一致)。
450
+ 池里的<b>探测结果</b>由插件自动写入,不经过这个保存按钮,
510
451
  因此保存不会把你刚测出来的结果回退掉。
511
452
  </p>
512
453
  <div class="pool-settings">
513
454
  <div class="pool-set">
514
455
  <span class="pool-set-copy">
515
456
  <b>启用账号池</b>
516
- <span>关闭时完全保持插件现状:跟随 WorkBuddy 桌面端当前登录,不按积分重排任何账号。</span>
457
+ <span>开启后,请求在上游失败时会依次用池里其他可用账号重试,直到有人接住或全部不可用为止。
458
+ 你自己的账号永远排第一,下一个请求仍从它开始。关闭时跟随你选择的账号,失败就如实报错。</span>
517
459
  </span>
518
460
  <span class="pool-set-ctl">
519
461
  <label class="sw"><input type="checkbox" id="swEnabled" checked /><span class="sw-track"></span></label>
520
462
  </span>
521
463
  </div>
522
464
 
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
465
  <div class="pool-set">
547
466
  <span class="pool-set-copy">
548
467
  <b>目标测试模型</b>
@@ -574,8 +493,8 @@ body{
574
493
  <span class="pool-unit" id="saveHint">修改任一设置后即可保存。</span>
575
494
  </div>
576
495
  <p class="region-independent" id="runtimeNote">
577
- 提示:下方活动记录里的<b>测试结果与轮换</b>是运行时事实,由插件自动落盘 ——
578
- 上面这个「保存」只写你的四项偏好,两者互不覆盖。
496
+ 提示:下方活动记录里的<b>测试结果与换号</b>是运行时事实,由插件自动落盘 ——
497
+ 上面这个「保存」只写你的偏好,两者互不覆盖。
579
498
  </p>
580
499
  </div>
581
500
 
@@ -587,7 +506,7 @@ body{
587
506
  <button class="dsm-btn dsm-btn-outline" type="button" id="btnClearLog" style="padding:2px 10px;font-size:12px">清空</button>
588
507
  </div>
589
508
  <div class="pool-log-body" id="logBody">
590
- <div class="pool-log-empty">尚无活动。点击上方按钮后,每次签到、测试与账号轮换都会记录在这里。</div>
509
+ <div class="pool-log-empty">尚无活动。点击上方按钮后,每次签到、测试与自动换号都会记录在这里。</div>
591
510
  </div>
592
511
  </div>
593
512
  </div>
@@ -631,14 +550,20 @@ function expiryText(a){
631
550
  return { text:`最近积分包 ${d} 天后过期`, warn: d <= 3 };
632
551
  }
633
552
 
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
- });
553
+ /* ---- 换号候选:从未测试过的账号视为可用;限流/耗尽的先排除 ---- */
554
+ /*
555
+ * 这是真机里的判定规则,演示里照它实现,好让「点了测试之后会发生什么」看得见:
556
+ * - 最近一次测试为「可用」→ 候选;
557
+ * - **从未测试过** → 也是候选(刚发现的账号不该因为「还没测」就被排除);
558
+ * - 「被限流」/「积分耗尽」且未到恢复时间 → 不参与换号;
559
+ * - 「测试中」→ 结果还不知道,此刻不参与。
560
+ */
561
+ function availableForFailover(){
562
+ return accounts.filter(a =>
563
+ a.probe === 'ok' || a.probe === 'untested' || a.probe === undefined
564
+ || (a.probe === 'rate-limited' && a.probeRetryDue === true)
565
+ || (a.probe === 'out-of-credit' && a.probeRetryDue === true),
566
+ );
642
567
  }
643
568
 
644
569
  function badge(a){
@@ -665,12 +590,13 @@ function render(){
665
590
  : '<span class="badge badge-idle"><span class="badge-dot"></span>未签到</span>');
666
591
 
667
592
  const row = document.createElement('div');
668
- row.className = 'pool-row';
593
+ row.className = 'pool-row' + (isCurrent ? ' pool-row-current' : '');
669
594
  row.innerHTML = `
670
595
  <span class="pool-acc">
671
596
  <span class="badge-dot" style="background:${isCurrent?'var(--dsw-alias-brand-primary)':'var(--dsw-alias-border-l2)'};width:8px;height:8px"></span>
672
597
  <span class="pool-acc-name">
673
- <b>${a.name}${isCurrent?' · 当前':''}</b>
598
+ <b>${a.name}</b>
599
+ ${isCurrent ? '<span class="pool-current-tag">当前使用</span>' : ''}
674
600
  <span class="mono">${a.masked}</span>
675
601
  </span>
676
602
  </span>
@@ -689,42 +615,34 @@ function render(){
689
615
  }
690
616
 
691
617
  function renderCurrent(){
692
- const r = ranked();
693
- const top = r[0];
694
618
  const el = $('currentBar'), meta = $('currentMeta');
695
- if (!top){
619
+ const cur = accounts.find(a => a.id === currentId);
620
+ const badgeEl = $('failoverBadge');
621
+ if (!cur){
696
622
  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>';
623
+ el.querySelector('.pool-bar-name').textContent = '当前没有可用的账号';
624
+ meta.textContent = '本机没有可结算的登录,请先在 WorkBuddy 桌面端登录。';
625
+ badgeEl.className = 'badge badge-err';
626
+ badgeEl.innerHTML = '<span class="badge-dot"></span>无账号';
700
627
  return;
701
628
  }
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 天内过期,优先使用`);
629
+ el.querySelector('.pool-bar-dot').style.background = 'var(--dsw-alias-state-success-primary)';
630
+ el.querySelector('.pool-bar-name').textContent = `当前使用:${cur.name} · ${cur.masked}`;
631
+ // 这里说的是「下一个请求从谁开始」,而不是「上一次请求谁服务了」——
632
+ // 换号只为一次请求借用额度,不写回选择,所以这两者在换号发生后会不同。
633
+ // 演示里如实区分,免得把「当前使用」误解成「上一条请求的计费方」。
634
+ const bits = [`积分 ${fmt(cur.credits)}`, `${$('selModel').value === 'auto' ? '免费目标模型' : $('selModel').value} 可用性:${probeText(cur.probe)}`];
635
+ if (cur.expiringSoon > 0) bits.push(`${cur.expiringSoon} 积分 3 天内过期`);
710
636
  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);
637
+ const on = $('swEnabled').checked;
638
+ badgeEl.className = on ? 'badge badge-info' : 'badge badge-idle';
639
+ badgeEl.innerHTML = on
640
+ ? '<span class="badge-dot"></span>自动换号已启用'
641
+ : '<span class="badge-dot"></span>账号池已关闭';
715
642
  }
716
643
 
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
- }
644
+ const probeText = p => p === 'ok' ? '可用' : p === 'rate-limited' ? '被限流'
645
+ : p === 'out-of-credit' ? '积分耗尽' : p === 'testing' ? '测试中' : '未测试';
728
646
 
729
647
  function addLog(text, cls){
730
648
  log.unshift({ t: clock(), text, cls: cls || '' });
@@ -734,7 +652,7 @@ function addLog(text, cls){
734
652
  function renderLog(){
735
653
  const body = $('logBody');
736
654
  if (log.length === 0){
737
- body.innerHTML = '<div class="pool-log-empty">尚无活动。点击上方按钮后,每次签到、测试与账号轮换都会记录在这里。</div>';
655
+ body.innerHTML = '<div class="pool-log-empty">尚无活动。点击上方按钮后,每次签到、测试与自动换号都会记录在这里。</div>';
738
656
  return;
739
657
  }
740
658
  body.innerHTML = log.map(e =>
@@ -748,7 +666,7 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
748
666
  async function runCheckin(){
749
667
  if (running) return;
750
668
  running = true; setBusy(true);
751
- addLog(`开始一键签到,共 ${accounts.length} 个账号(串行)`, 'log-rot');
669
+ addLog(`开始一键签到,共 ${accounts.length} 个账号(串行)`, 'log-swap');
752
670
  for (const a of accounts){
753
671
  await sleep(520);
754
672
  if (a.checkedIn){ addLog(`${a.name}:今日已签到,跳过(幂等,不重复领取)`); continue; }
@@ -771,7 +689,7 @@ async function runTest(){
771
689
  if (running) return;
772
690
  running = true; setBusy(true);
773
691
  const model = $('selModel').value;
774
- addLog(`开始测试 ${accounts.length} 个账号 × ${model}(串行,每次一个真实请求)`, 'log-rot');
692
+ addLog(`开始测试 ${accounts.length} 个账号 × ${model}(串行,每次一个真实请求)`, 'log-swap');
775
693
  for (const a of accounts){
776
694
  a.probe = 'testing'; render();
777
695
  await sleep(900);
@@ -796,15 +714,16 @@ async function runTest(){
796
714
  function setBusy(on){
797
715
  $('btnCheckin').disabled = on;
798
716
  $('btnTest').disabled = on;
717
+ $('btnSimulate').disabled = on;
799
718
  $('actionHint').innerHTML = on ? '<span class="spin"></span> 串行执行中…' : '';
800
719
  }
801
720
 
802
721
  /* ============================================================================
803
722
  两种状态,两套写入语义(与真实插件一致):
804
723
  - savedPrefs:偏好设置,改动进草稿,点「保存」才落盘
805
- - runtime :探测结果 / 轮换事实,插件自动落盘,不经过保存按钮
724
+ - runtime :探测结果,插件自动落盘,不经过保存按钮
806
725
  ========================================================================== */
807
- const savedPrefs = { enabled:true, rotateByCredits:true, intervalMinutes:30, targetModel:'auto' };
726
+ const savedPrefs = { enabled:true, targetModel:'auto' };
808
727
  let draftPrefs = null; // null = 无未保存修改
809
728
  let runtimeSnap = {}; // 模拟 Host 侧自动落盘的运行时事实
810
729
 
@@ -821,11 +740,7 @@ function editPref(key, value){
821
740
  function renderPrefs(){
822
741
  const p = prefsOf();
823
742
  $('swEnabled').checked = p.enabled;
824
- $('swRotate').checked = p.rotateByCredits;
825
- $('numInterval').value = p.intervalMinutes;
826
743
  $('selModel').value = p.targetModel;
827
- $('swRotate').disabled = !p.enabled;
828
- $('numInterval').disabled = !p.enabled;
829
744
  $('selModel').disabled = !p.enabled;
830
745
 
831
746
  const dirty = isDirty();
@@ -840,8 +755,8 @@ function renderPrefs(){
840
755
  // 标出被改过的行(演示用:整块提示)
841
756
  $('autoPickRow').classList.add('is-dirty');
842
757
  }
843
- syncConflict();
844
758
  renderAutoPick();
759
+ renderCurrent();
845
760
  }
846
761
 
847
762
  function savePrefs(){
@@ -864,7 +779,7 @@ function discardPrefs(){
864
779
  /* 运行时事实的"自动落盘":只记录,不参与草稿 */
865
780
  function persistRuntime(note){
866
781
  runtimeSnap.updatedAt = clock();
867
- addLog(`(自动落盘)${note}`, 'log-rot');
782
+ addLog(`(自动落盘)${note}`, 'log-swap');
868
783
  }
869
784
 
870
785
  /* ---- 目标测试模型:按区域 catalog 自动挑倍率为 0 的 ---- */
@@ -919,56 +834,86 @@ function renderAutoPick(){
919
834
  —— 从本区域 catalog 内倍率为 0 的候选中取窗口最大的。`;
920
835
  }
921
836
 
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 = '';
837
+ /* ---- 模拟一次失败请求:把「自动换号」跑给人看 ---- */
838
+ /*
839
+ * 真机里这条路径是:请求先发给「当前使用」的账号 → 上游报 429/402/401/502 →
840
+ * 插件依次取池内其他**可用**账号重发同一个请求 → 有人接住就正常返回 →
841
+ * 全部不可用才把错误报出来。这里按同一顺序演一遍,好让规则可验证,而不只是可读。
842
+ *
843
+ * 两个刻意的设计在这段演示里也能看出来:
844
+ * - **「当前使用」始终不变**:换号只是为这一次请求借用额度;
845
+ * - **从没测试过的账号算可用**:所以「当前账号被限流」时,只要池里还有没测过的
846
+ * 账号,请求依然能跑完 —— 这正是「不点测试也能用」的意思。
847
+ */
848
+ async function runSimulatedRequest(){
849
+ if (running) return;
850
+ running = true; setBusy(true);
851
+
852
+ const primary = accounts.find(a => a.id === currentId);
853
+ addLog(`发起请求 → 当前使用:${primary ? primary.name : '(无)'}`, 'log-swap');
854
+ await sleep(450);
855
+
856
+ if (!$('swEnabled').checked){
857
+ // 池关闭:不换号,如实报错 —— 这正是开关关闭时的承诺。
858
+ addLog(`账号池已关闭:${primary ? primary.name : '当前账号'} 失败即报错,不尝试池内其他账号`, 'log-err');
859
+ if (primary) primary.note = '上游:429(未换号 —— 账号池已关闭)';
860
+ render();
861
+ running = false; setBusy(false);
862
+ return;
936
863
  }
937
- }
938
864
 
939
- function disableRotate(from){
940
- // 走草稿:与其它偏好一致,保存后才真正生效
941
- editPref('rotateByCredits', false);
942
- addLog(from === 'notice'
943
- ? '(草稿)已从冲突提示关闭自动轮换 —— 点「保存」后生效,手动选择随之恢复'
944
- : '(草稿)已关闭自动轮换', 'log-warn');
945
- renderCurrent(); render();
865
+ // 让「当前使用」的账号这一次失败,制造需要换号的局面。
866
+ if (primary){
867
+ primary.probe = 'rate-limited';
868
+ primary.probeRetryDue = false; // 冷却未过:真实场景里它会被排除
869
+ primary.note = '上游:将在 02:30:30 重置';
870
+ primary.probeAgo = 0;
871
+ addLog(`${primary.name}:soft_rate(429)—— 使用量超出频率限制`, 'log-warn');
872
+ render();
873
+ }
874
+ await sleep(550);
875
+
876
+ // 依次找下一个可用账号:跳过的会说明原因,而不是默默略过。
877
+ const tried = [currentId];
878
+ for (const cand of accounts){
879
+ if (cand.probe === 'testing'){
880
+ addLog(`${cand.name}:跳过 —— 上一次测试还在进行,结果未知`, 'log-warn');
881
+ continue;
882
+ }
883
+ if (cand.probe === 'rate-limited' || cand.probe === 'out-of-credit'){
884
+ addLog(`${cand.name}:跳过 —— 已知不可用(${probeText(cand.probe)},未到恢复时间)`, 'log-warn');
885
+ continue;
886
+ }
887
+ tried.push(cand.id);
888
+ if (cand.probe === undefined || cand.probe === 'untested'){
889
+ addLog(`${cand.name}:从未测试过,按「默认可用」参与换号`, 'log-swap');
890
+ }
891
+ await sleep(450);
892
+ addLog(`改用 ${cand.name} 重发同一个请求 → 成功返回`, 'log-ok');
893
+ persistRuntime(`本次请求由 ${cand.name} 承接;当前使用仍是 ${primary ? primary.name : '(无)'}`);
894
+ addLog('排序不写回你保存的选择:关闭账号池后仍然是你选定的那个账号生效', 'log-swap');
895
+ render();
896
+ running = false; setBusy(false);
897
+ return;
898
+ }
899
+
900
+ // 全部不可用:把原始错误报出来,并注明尝试过几个账号。
901
+ addLog(`池内没有其他可用账号(尝试过 ${tried.length} 个):原样报出上游错误 429 soft_rate`, 'log-err');
902
+ addLog('错误里会带上上游给的重置时间,用户据此知道何时再试', 'log-warn');
903
+ render();
904
+ running = false; setBusy(false);
946
905
  }
947
906
 
948
907
  /* ---- 绑定 ---- */
949
908
  $('btnCheckin').addEventListener('click', runCheckin);
950
909
  $('btnTest').addEventListener('click', runTest);
910
+ $('btnSimulate').addEventListener('click', runSimulatedRequest);
951
911
  $('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
912
  $('swEnabled').addEventListener('change', e => {
957
913
  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
914
  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} 分钟`);
915
+ ? '(草稿)账号池:启用 —— 失败时依次用池内其他可用账号重试'
916
+ : '(草稿)账号池:关闭 —— 跟随你选的账号,失败如实报错', e.target.checked ? 'log-ok' : 'log-warn');
972
917
  });
973
918
  $('btnSavePool').addEventListener('click', savePrefs);
974
919
  $('btnDiscardPool').addEventListener('click', discardPrefs);
@@ -979,8 +924,8 @@ $('selModel').addEventListener('change', e => {
979
924
  renderPrefs();
980
925
  render();
981
926
  addLog('账号池就绪:池内 3 个账号,目标模型由本区域 catalog 自动挑选');
982
- addLog('本区域已保存「自动轮换=开」,因此上方手动选账号处于停用状态');
983
- addLog('注意:本区域已开启自动轮换,上方手动选账号已相应停用');
927
+ addLog('点「模拟一次失败请求」可以看到自动换号:当前账号被限流后,请求改由池内下一个可用账号跑完');
928
+ if (!$('swEnabled').checked) addLog('账号池当前关闭:失败不会换号,会如实报错');
984
929
  </script>
985
930
  </body>
986
931
  </html>