kdczyz-dsh-model 1.2.2-beta → 1.2.3-beta
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.
|
@@ -286,15 +286,15 @@ trae 页改为本包自写(`console.mjs` 的 `TraeConfigTab`),与 workbudd
|
|
|
286
286
|
那个区域里正在服务的那一个账号**(WorkBuddy 用 `activeAccountId`、Trae 用
|
|
287
287
|
`lastServedId` 退回「当前账号」、Qoder 用 `serving.accountId`),与卡片是两个口径。
|
|
288
288
|
|
|
289
|
-
|
|
289
|
+
每一行只有一个控件(原先还有一个「停用 / 启用」开关,已移除 —— 完全手动语义下
|
|
290
|
+
它只对「已选中的那个」有效,停用别的账号对请求毫无影响;停用的管理仍在设置页):
|
|
290
291
|
|
|
291
292
|
| 控件 | 干什么 | 打到哪 |
|
|
292
293
|
|---|---|---|
|
|
293
|
-
| **「参与轮换」开关** | 把这个账号移出 / 放回轮换 | WorkBuddy:`POST /plugins/dsh-workbuddy-xdpool/accounts/disabled`(服务端合并);Trae / Qoder:设置 `regions` / `disabledAccountIds`,**读-改-写合并** |
|
|
294
294
|
| **「切换到此账号」** | 下一次请求优先用它 | WorkBuddy:`POST …/accounts/select?region=cn\|global`;Trae:`POST /pool/account?region=` **且必须补一次 `accounts` 设置写**;Qoder:写 `selectedAccountId` |
|
|
295
295
|
|
|
296
296
|
已经是「当前账号」的那一行:WorkBuddy 把按钮换成「**取消固定**」(`{accountId:null}`,
|
|
297
|
-
|
|
297
|
+
交回手动状态(未选 → 请求失败));Trae / Qoder **不给按钮** —— 这两家的池永远有一个当前账号,
|
|
298
298
|
「切换到这个已经是当前的账号」是纯 no-op(两个配置页也是这么做的)。
|
|
299
299
|
|
|
300
300
|
### 四个「做反了也不会报错」的坑
|
|
@@ -303,7 +303,7 @@ trae 页改为本包自写(`console.mjs` 的 `TraeConfigTab`),与 workbudd
|
|
|
303
303
|
|---|---|---|
|
|
304
304
|
| Trae 的 `credits.total` 是**发放总量**,不是余额 | 真机上把 3,050(发放)当成余额显示,而账号只剩 796.88 | `accountCreditsOf(row, family)` 按平台取字段:Trae 取 `generalAvailable → available → total`,WorkBuddy / Qoder 取 `total → …`(Qoder 就是裸数字) |
|
|
305
305
|
| WorkBuddy 的 `disabledAccountIds` 是**两个区域共用的一份扁平数组**(两个 provider 注册同一个 `settingsNs` + `settingsPath: []`) | 配置页只看得见本区域的账号,照本区域拼一份完整名单写回设置,会把**另一个区域**已停用的账号静默重新启用 | 启停只走 `/accounts/disabled` 路由(服务端对 `current().disabledAccountIds` 合并);单测里除了断言「请求发出去了」,还断言**没有**顺手写那份设置 —— 只断言前者的话,「路由 + 顺手再写一次」也能全绿 |
|
|
306
|
-
| 卡片开着的时候轮询额度 | 上游按账号计费(Trae 的 `/usage` 是一次实时计费读),卡片开着不动也在持续花钱 | 只在**打开卡片 / 手动刷新 /
|
|
306
|
+
| 卡片开着的时候轮询额度 | 上游按账号计费(Trae 的 `/usage` 是一次实时计费读),卡片开着不动也在持续花钱 | 只在**打开卡片 / 手动刷新 / 写完一次切换之后**取一次;`CREDIT_REFRESH_MS = 30000` 只作用于胶囊那颗数字。单测里钉着「展开卡片不新增任何 interval」 |
|
|
307
307
|
| 行里的名字直接用上游昵称 | Qoder / Trae 的 label 是 uuid,卡片被撑破 | `shortAccountName()` 截到 8 个字符 + `…`,完整值放 `title` |
|
|
308
308
|
|
|
309
309
|
### 浮层为什么是 portal + fixed
|
|
@@ -337,7 +337,7 @@ node scripts/gen-xdpool-client.mjs # 改 vendor/xdpool/lib/client.js 后
|
|
|
337
337
|
```sh
|
|
338
338
|
node test/credit-store.test.mjs # 14 项:积分/历史记录读写、去重、坏行容错
|
|
339
339
|
node test/client-apply.test.mjs # 43 项:仿浏览器执行 client.js 并真正调用 apply
|
|
340
|
-
node test/console-render.test.mjs #
|
|
340
|
+
node test/console-render.test.mjs # 420 项:真正渲染三个 tab(三个配置页的同一套骨架与页头按钮、Qoder 单列)与 **composer 胶囊展开的账号卡片**,空/坏数据与边界值、自动刷新定时器、分配策略读写、Qoder 模型列(图片/上下文跟随上游 + 「从 Qoder 刷新」拉取即落盘)、Trae 与 Qoder 的「签到」(每张卡带自己的 accountId、不写 selectedAccountId、重放不许说成「已领 +100」)、卡片的启停/切换两条写路径与「展开卡片不新增任何定时器」
|
|
341
341
|
node test/host-assembly.test.mjs # 10 项:cordis 装配层(exports 白名单、cordis.patch.yml 条目)
|
|
342
342
|
node test/trae-pool.test.mjs # 46 项:Trae 号池(scan/分配策略/冷却换号/停用/保留额度)
|
|
343
343
|
node test/trae-checkin-route.test.mjs # 36 项:Trae 签到路由(指定账号走 clientOfAccount 且绝不碰绑定账号、设备门禁 9095 与「账号已签」分开、未知账号 404、方法/区域/来源/坏 JSON 边界)
|
|
@@ -332,14 +332,6 @@ const __WBC_CONSOLE_FACTORY__ = (react, jsxRuntime, embedded) => {
|
|
|
332
332
|
.wotr-credit-row-credits{flex:none;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-primary)}
|
|
333
333
|
.wotr-credit-row-foot{display:flex;align-items:center;justify-content:space-between;gap:8px}
|
|
334
334
|
.wotr-credit-tags{display:flex;align-items:center;gap:4px;flex-wrap:wrap;min-width:0}
|
|
335
|
-
/* 启停开关:本页自绘(不引 primitives 的 Switch),配色沿用本文件
|
|
336
|
-
既有的 token 组合 —— 开 = label-primary 底 + bg-layer-1 滑块,
|
|
337
|
-
与 .wbc-btn-primary 同一套对比关系,深浅色主题下都不会糊。 */
|
|
338
|
-
.wotr-credit-switch{position:relative;flex:none;width:30px;height:16px;padding:0;border:none;border-radius:8px;background:var(--dsw-alias-bg-layer-3,rgba(127,127,127,.35));cursor:pointer;transition:background .12s ease-out}
|
|
339
|
-
.wotr-credit-switch::after{content:'';position:absolute;top:2px;left:2px;width:12px;height:12px;border-radius:50%;background:var(--dsw-alias-label-secondary,#888);transition:transform .12s ease-out,background .12s ease-out}
|
|
340
|
-
.wotr-credit-switch[data-on="1"]{background:var(--dsw-alias-label-primary,#4c8dff)}
|
|
341
|
-
.wotr-credit-switch[data-on="1"]::after{transform:translateX(14px);background:var(--dsw-alias-bg-layer-1,#fff)}
|
|
342
|
-
.wotr-credit-switch:disabled{opacity:.5;cursor:default}
|
|
343
335
|
.wotr-credit-foot{margin-top:10px;padding-top:8px;border-top:.5px solid var(--dsw-alias-border-l2);font-size:11px;line-height:16px}
|
|
344
336
|
.wotr-credit-note{margin-top:6px;font-size:11px;line-height:16px;color:var(--dsw-alias-label-primary)}
|
|
345
337
|
.wotr-credit-err{margin-top:6px;font-size:11px;line-height:16px;color:var(--dsw-alias-label-error,#e5484d)}
|
|
@@ -4097,8 +4089,8 @@ const __WBC_CONSOLE_FACTORY__ = (react, jsxRuntime, embedded) => {
|
|
|
4097
4089
|
* 就地改缓存里的行(写完之后的即时反馈)。
|
|
4098
4090
|
*
|
|
4099
4091
|
* 刻意**不**为了这一次改动再打一遍状态接口 —— 那个接口每个账号都会触发一次
|
|
4100
|
-
* 计费调用。写路径本身已经做过读回确认(见
|
|
4101
|
-
*
|
|
4092
|
+
* 计费调用。写路径本身已经做过读回确认(见 selectAccountRemote),
|
|
4093
|
+
* 所以就地改动是有依据的;下一次打开卡片或点「刷新」
|
|
4102
4094
|
* 会用真实快照把它对齐。
|
|
4103
4095
|
*/
|
|
4104
4096
|
function patchCreditRows(family, match, patch) {
|
|
@@ -4111,81 +4103,11 @@ const __WBC_CONSOLE_FACTORY__ = (react, jsxRuntime, embedded) => {
|
|
|
4111
4103
|
});
|
|
4112
4104
|
}
|
|
4113
4105
|
|
|
4114
|
-
/** 停用名单的加减:**只动这一个 id**,其余原样带过 */
|
|
4115
|
-
function mergeDisabledIds(ids, accountId, disabled) {
|
|
4116
|
-
const current = asArray(ids).filter((id) => typeof id === 'string' && id !== '');
|
|
4117
|
-
return disabled
|
|
4118
|
-
? [...new Set([...current, accountId])]
|
|
4119
|
-
: current.filter((id) => id !== accountId);
|
|
4120
|
-
}
|
|
4121
|
-
|
|
4122
|
-
/** 两份停用名单是否等价(读回确认用;顺序无关) */
|
|
4123
|
-
function sameIdSet(a, b) {
|
|
4124
|
-
const left = asArray(a).filter((id) => typeof id === 'string' && id !== '');
|
|
4125
|
-
const right = asArray(b).filter((id) => typeof id === 'string' && id !== '');
|
|
4126
|
-
if (left.length !== right.length) return false;
|
|
4127
|
-
const set = new Set(left);
|
|
4128
|
-
return right.every((id) => set.has(id));
|
|
4129
|
-
}
|
|
4130
|
-
|
|
4131
4106
|
/** 该平台设置作用域(拿不到就是 null,调用方按只读处理) */
|
|
4132
4107
|
function creditScopeOf(family) {
|
|
4133
4108
|
return poolScopeOf(family === 'qoder' ? qoderSettings : traeSettings);
|
|
4134
4109
|
}
|
|
4135
4110
|
|
|
4136
|
-
/**
|
|
4137
|
-
* 停用 / 启用一个账号 —— 三家写法不同,语义是同一条。
|
|
4138
|
-
*
|
|
4139
|
-
* WorkBuddy:走路由 `POST /accounts/disabled`。**不要**在这里自己拼
|
|
4140
|
-
* `disabledAccountIds` 写设置:国内版与国际版共用**同一份**设置文档
|
|
4141
|
-
* (两个 provider 用同一个 settingsNs、`settingsPath: []`),只按当前区域的
|
|
4142
|
-
* 行拼出来的列表会把另一个区域的停用项整段抹掉。路由在服务端按整份设置合并
|
|
4143
|
-
* (`setAccountDisabled` 读 `current().disabledAccountIds` 再加减一个 id)。
|
|
4144
|
-
*
|
|
4145
|
-
* Trae / Qoder:没有可用的写路由(Trae 的 `/pool/account` 只改进程内的池,
|
|
4146
|
-
* 下一次设置同步就被覆盖回去),所以写设置字段 —— 但**从当前设置文档里读旧值
|
|
4147
|
-
* 再合并**,而不是从行里拼。
|
|
4148
|
-
*/
|
|
4149
|
-
async function setAccountDisabledRemote(family, region, accountId, disabled) {
|
|
4150
|
-
if (typeof accountId !== 'string' || accountId === '') throw new Error('缺少账号 id');
|
|
4151
|
-
const want = disabled === true;
|
|
4152
|
-
if (family === 'wb') {
|
|
4153
|
-
await fetchJson(`${POOL_API}/accounts/disabled`, {
|
|
4154
|
-
method: 'POST',
|
|
4155
|
-
headers: { 'Content-Type': 'application/json' },
|
|
4156
|
-
body: JSON.stringify({ accountId, disabled: want }),
|
|
4157
|
-
});
|
|
4158
|
-
return;
|
|
4159
|
-
}
|
|
4160
|
-
const scope = creditScopeOf(family);
|
|
4161
|
-
const who = family === 'qoder' ? 'qoder' : 'trae';
|
|
4162
|
-
if (scope === undefined || scope === null) {
|
|
4163
|
-
throw new Error(`${who}: 拿不到设置作用域,无法保存(配置页同一处也只读)`);
|
|
4164
|
-
}
|
|
4165
|
-
if (!poolWritable(scope)) throw new Error(`${who}: 设置当前不可写`);
|
|
4166
|
-
if (family === 'qoder') {
|
|
4167
|
-
const currentIds = asArray(unwrapVolatileDeep(scope.getSnapshot?.().value)?.disabledAccountIds);
|
|
4168
|
-
const next = mergeDisabledIds(currentIds, accountId, want);
|
|
4169
|
-
await writeSettingsField(
|
|
4170
|
-
scope,
|
|
4171
|
-
'disabledAccountIds',
|
|
4172
|
-
next,
|
|
4173
|
-
(doc) => sameIdSet(doc?.disabledAccountIds, next),
|
|
4174
|
-
'qoder',
|
|
4175
|
-
);
|
|
4176
|
-
return;
|
|
4177
|
-
}
|
|
4178
|
-
const value = scope.getSnapshot?.().value;
|
|
4179
|
-
const next = mergeDisabledIds(traeRegionSlotOf(value, region).disabledAccountIds, accountId, want);
|
|
4180
|
-
const slot = { ...traeRegionSlotOf(value, region), disabledAccountIds: next };
|
|
4181
|
-
await writeTraeField(
|
|
4182
|
-
scope,
|
|
4183
|
-
'regions',
|
|
4184
|
-
nextRegionSlots(regionsMapOf(value), region, slot),
|
|
4185
|
-
(readBack) => sameIdSet(traeRegionSlotOf(readBack, region).disabledAccountIds, next),
|
|
4186
|
-
);
|
|
4187
|
-
}
|
|
4188
|
-
|
|
4189
4111
|
/**
|
|
4190
4112
|
* 把某个账号设成「当前账号」。
|
|
4191
4113
|
*
|
|
@@ -4325,8 +4247,7 @@ const __WBC_CONSOLE_FACTORY__ = (react, jsxRuntime, embedded) => {
|
|
|
4325
4247
|
*
|
|
4326
4248
|
* 展开的卡片里是**当前这个平台的全部账号**(不是「所有平台的账号」):
|
|
4327
4249
|
* WorkBuddy 与 Trae 各自的国内版 + 国际版都列出来,Qoder 只有一个池。
|
|
4328
|
-
*
|
|
4329
|
-
* setAccountDisabledRemote / selectAccountRemote 的语义说明。
|
|
4250
|
+
* 每行带一个控件 —— 「切换到此账号」,见 selectAccountRemote 的语义说明。
|
|
4330
4251
|
*
|
|
4331
4252
|
* 刷新策略分两档,因为两档的成本差一个量级:
|
|
4332
4253
|
* - 胶囊那颗数字:**每 30 秒自动刷一次**(见 CREDIT_REFRESH_MS 的成本说明),
|
|
@@ -4602,28 +4523,6 @@ const __WBC_CONSOLE_FACTORY__ = (react, jsxRuntime, embedded) => {
|
|
|
4602
4523
|
refreshHeadline();
|
|
4603
4524
|
};
|
|
4604
4525
|
|
|
4605
|
-
/** 停用 / 启用某一行 */
|
|
4606
|
-
const onToggleRow = async (row) => {
|
|
4607
|
-
const next = row.disabled !== true;
|
|
4608
|
-
setBusyRow(row.key);
|
|
4609
|
-
setWriteError('');
|
|
4610
|
-
setNote('');
|
|
4611
|
-
try {
|
|
4612
|
-
await setAccountDisabledRemote(row.family, row.region, row.id, next);
|
|
4613
|
-
patchCreditRows(row.family, (r) => r.key === row.key, { disabled: next });
|
|
4614
|
-
// 停掉的正好是「这一轮在用的那个」→ 胶囊上的主语马上就会变,补一发
|
|
4615
|
-
if (next === true && row.serving === true && row.region === source.region) {
|
|
4616
|
-
refreshHeadline();
|
|
4617
|
-
}
|
|
4618
|
-
setNote(next ? `已停用 ${row.name},请求失败时请手动切换` : `已重新启用 ${row.name}`);
|
|
4619
|
-
} catch (e) {
|
|
4620
|
-
setWriteError(e?.message ?? String(e));
|
|
4621
|
-
} finally {
|
|
4622
|
-
setBusyRow('');
|
|
4623
|
-
setTick((n) => n + 1);
|
|
4624
|
-
}
|
|
4625
|
-
};
|
|
4626
|
-
|
|
4627
4526
|
/** 切换到此账号 / 取消固定 */
|
|
4628
4527
|
const onSelectRow = async (row) => {
|
|
4629
4528
|
// WorkBuddy 独有的「取消固定」:完全手动语义下它的池把「没有当前账号」
|
|
@@ -4730,22 +4629,6 @@ const __WBC_CONSOLE_FACTORY__ = (react, jsxRuntime, embedded) => {
|
|
|
4730
4629
|
},
|
|
4731
4630
|
children: label,
|
|
4732
4631
|
}),
|
|
4733
|
-
jsx('button', {
|
|
4734
|
-
type: 'button',
|
|
4735
|
-
role: 'switch',
|
|
4736
|
-
className: 'wotr-credit-switch',
|
|
4737
|
-
'aria-checked': row.disabled === true ? 'false' : 'true',
|
|
4738
|
-
'aria-label': `${row.name} 停用/启用`,
|
|
4739
|
-
'data-on': row.disabled === true ? '0' : '1',
|
|
4740
|
-
disabled: busy,
|
|
4741
|
-
title:
|
|
4742
|
-
row.disabled === true
|
|
4743
|
-
? `启用 ${row.fullName}(恢复参与取号)`
|
|
4744
|
-
: `停用 ${row.fullName}(选中它时请求会失败)`,
|
|
4745
|
-
onClick: () => {
|
|
4746
|
-
void onToggleRow(row);
|
|
4747
|
-
},
|
|
4748
|
-
}),
|
|
4749
4632
|
],
|
|
4750
4633
|
}),
|
|
4751
4634
|
],
|
|
@@ -280,14 +280,6 @@ export function createConsoleSection({ react, jsxRuntime, embedded }) {
|
|
|
280
280
|
.wotr-credit-row-credits{flex:none;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-primary)}
|
|
281
281
|
.wotr-credit-row-foot{display:flex;align-items:center;justify-content:space-between;gap:8px}
|
|
282
282
|
.wotr-credit-tags{display:flex;align-items:center;gap:4px;flex-wrap:wrap;min-width:0}
|
|
283
|
-
/* 启停开关:本页自绘(不引 primitives 的 Switch),配色沿用本文件
|
|
284
|
-
既有的 token 组合 —— 开 = label-primary 底 + bg-layer-1 滑块,
|
|
285
|
-
与 .wbc-btn-primary 同一套对比关系,深浅色主题下都不会糊。 */
|
|
286
|
-
.wotr-credit-switch{position:relative;flex:none;width:30px;height:16px;padding:0;border:none;border-radius:8px;background:var(--dsw-alias-bg-layer-3,rgba(127,127,127,.35));cursor:pointer;transition:background .12s ease-out}
|
|
287
|
-
.wotr-credit-switch::after{content:'';position:absolute;top:2px;left:2px;width:12px;height:12px;border-radius:50%;background:var(--dsw-alias-label-secondary,#888);transition:transform .12s ease-out,background .12s ease-out}
|
|
288
|
-
.wotr-credit-switch[data-on="1"]{background:var(--dsw-alias-label-primary,#4c8dff)}
|
|
289
|
-
.wotr-credit-switch[data-on="1"]::after{transform:translateX(14px);background:var(--dsw-alias-bg-layer-1,#fff)}
|
|
290
|
-
.wotr-credit-switch:disabled{opacity:.5;cursor:default}
|
|
291
283
|
.wotr-credit-foot{margin-top:10px;padding-top:8px;border-top:.5px solid var(--dsw-alias-border-l2);font-size:11px;line-height:16px}
|
|
292
284
|
.wotr-credit-note{margin-top:6px;font-size:11px;line-height:16px;color:var(--dsw-alias-label-primary)}
|
|
293
285
|
.wotr-credit-err{margin-top:6px;font-size:11px;line-height:16px;color:var(--dsw-alias-label-error,#e5484d)}
|
|
@@ -4045,8 +4037,8 @@ export function createConsoleSection({ react, jsxRuntime, embedded }) {
|
|
|
4045
4037
|
* 就地改缓存里的行(写完之后的即时反馈)。
|
|
4046
4038
|
*
|
|
4047
4039
|
* 刻意**不**为了这一次改动再打一遍状态接口 —— 那个接口每个账号都会触发一次
|
|
4048
|
-
* 计费调用。写路径本身已经做过读回确认(见
|
|
4049
|
-
*
|
|
4040
|
+
* 计费调用。写路径本身已经做过读回确认(见 selectAccountRemote),
|
|
4041
|
+
* 所以就地改动是有依据的;下一次打开卡片或点「刷新」
|
|
4050
4042
|
* 会用真实快照把它对齐。
|
|
4051
4043
|
*/
|
|
4052
4044
|
function patchCreditRows(family, match, patch) {
|
|
@@ -4059,81 +4051,11 @@ export function createConsoleSection({ react, jsxRuntime, embedded }) {
|
|
|
4059
4051
|
});
|
|
4060
4052
|
}
|
|
4061
4053
|
|
|
4062
|
-
/** 停用名单的加减:**只动这一个 id**,其余原样带过 */
|
|
4063
|
-
function mergeDisabledIds(ids, accountId, disabled) {
|
|
4064
|
-
const current = asArray(ids).filter((id) => typeof id === 'string' && id !== '');
|
|
4065
|
-
return disabled
|
|
4066
|
-
? [...new Set([...current, accountId])]
|
|
4067
|
-
: current.filter((id) => id !== accountId);
|
|
4068
|
-
}
|
|
4069
|
-
|
|
4070
|
-
/** 两份停用名单是否等价(读回确认用;顺序无关) */
|
|
4071
|
-
function sameIdSet(a, b) {
|
|
4072
|
-
const left = asArray(a).filter((id) => typeof id === 'string' && id !== '');
|
|
4073
|
-
const right = asArray(b).filter((id) => typeof id === 'string' && id !== '');
|
|
4074
|
-
if (left.length !== right.length) return false;
|
|
4075
|
-
const set = new Set(left);
|
|
4076
|
-
return right.every((id) => set.has(id));
|
|
4077
|
-
}
|
|
4078
|
-
|
|
4079
4054
|
/** 该平台设置作用域(拿不到就是 null,调用方按只读处理) */
|
|
4080
4055
|
function creditScopeOf(family) {
|
|
4081
4056
|
return poolScopeOf(family === 'qoder' ? qoderSettings : traeSettings);
|
|
4082
4057
|
}
|
|
4083
4058
|
|
|
4084
|
-
/**
|
|
4085
|
-
* 停用 / 启用一个账号 —— 三家写法不同,语义是同一条。
|
|
4086
|
-
*
|
|
4087
|
-
* WorkBuddy:走路由 `POST /accounts/disabled`。**不要**在这里自己拼
|
|
4088
|
-
* `disabledAccountIds` 写设置:国内版与国际版共用**同一份**设置文档
|
|
4089
|
-
* (两个 provider 用同一个 settingsNs、`settingsPath: []`),只按当前区域的
|
|
4090
|
-
* 行拼出来的列表会把另一个区域的停用项整段抹掉。路由在服务端按整份设置合并
|
|
4091
|
-
* (`setAccountDisabled` 读 `current().disabledAccountIds` 再加减一个 id)。
|
|
4092
|
-
*
|
|
4093
|
-
* Trae / Qoder:没有可用的写路由(Trae 的 `/pool/account` 只改进程内的池,
|
|
4094
|
-
* 下一次设置同步就被覆盖回去),所以写设置字段 —— 但**从当前设置文档里读旧值
|
|
4095
|
-
* 再合并**,而不是从行里拼。
|
|
4096
|
-
*/
|
|
4097
|
-
async function setAccountDisabledRemote(family, region, accountId, disabled) {
|
|
4098
|
-
if (typeof accountId !== 'string' || accountId === '') throw new Error('缺少账号 id');
|
|
4099
|
-
const want = disabled === true;
|
|
4100
|
-
if (family === 'wb') {
|
|
4101
|
-
await fetchJson(`${POOL_API}/accounts/disabled`, {
|
|
4102
|
-
method: 'POST',
|
|
4103
|
-
headers: { 'Content-Type': 'application/json' },
|
|
4104
|
-
body: JSON.stringify({ accountId, disabled: want }),
|
|
4105
|
-
});
|
|
4106
|
-
return;
|
|
4107
|
-
}
|
|
4108
|
-
const scope = creditScopeOf(family);
|
|
4109
|
-
const who = family === 'qoder' ? 'qoder' : 'trae';
|
|
4110
|
-
if (scope === undefined || scope === null) {
|
|
4111
|
-
throw new Error(`${who}: 拿不到设置作用域,无法保存(配置页同一处也只读)`);
|
|
4112
|
-
}
|
|
4113
|
-
if (!poolWritable(scope)) throw new Error(`${who}: 设置当前不可写`);
|
|
4114
|
-
if (family === 'qoder') {
|
|
4115
|
-
const currentIds = asArray(unwrapVolatileDeep(scope.getSnapshot?.().value)?.disabledAccountIds);
|
|
4116
|
-
const next = mergeDisabledIds(currentIds, accountId, want);
|
|
4117
|
-
await writeSettingsField(
|
|
4118
|
-
scope,
|
|
4119
|
-
'disabledAccountIds',
|
|
4120
|
-
next,
|
|
4121
|
-
(doc) => sameIdSet(doc?.disabledAccountIds, next),
|
|
4122
|
-
'qoder',
|
|
4123
|
-
);
|
|
4124
|
-
return;
|
|
4125
|
-
}
|
|
4126
|
-
const value = scope.getSnapshot?.().value;
|
|
4127
|
-
const next = mergeDisabledIds(traeRegionSlotOf(value, region).disabledAccountIds, accountId, want);
|
|
4128
|
-
const slot = { ...traeRegionSlotOf(value, region), disabledAccountIds: next };
|
|
4129
|
-
await writeTraeField(
|
|
4130
|
-
scope,
|
|
4131
|
-
'regions',
|
|
4132
|
-
nextRegionSlots(regionsMapOf(value), region, slot),
|
|
4133
|
-
(readBack) => sameIdSet(traeRegionSlotOf(readBack, region).disabledAccountIds, next),
|
|
4134
|
-
);
|
|
4135
|
-
}
|
|
4136
|
-
|
|
4137
4059
|
/**
|
|
4138
4060
|
* 把某个账号设成「当前账号」。
|
|
4139
4061
|
*
|
|
@@ -4273,8 +4195,7 @@ export function createConsoleSection({ react, jsxRuntime, embedded }) {
|
|
|
4273
4195
|
*
|
|
4274
4196
|
* 展开的卡片里是**当前这个平台的全部账号**(不是「所有平台的账号」):
|
|
4275
4197
|
* WorkBuddy 与 Trae 各自的国内版 + 国际版都列出来,Qoder 只有一个池。
|
|
4276
|
-
*
|
|
4277
|
-
* setAccountDisabledRemote / selectAccountRemote 的语义说明。
|
|
4198
|
+
* 每行带一个控件 —— 「切换到此账号」,见 selectAccountRemote 的语义说明。
|
|
4278
4199
|
*
|
|
4279
4200
|
* 刷新策略分两档,因为两档的成本差一个量级:
|
|
4280
4201
|
* - 胶囊那颗数字:**每 30 秒自动刷一次**(见 CREDIT_REFRESH_MS 的成本说明),
|
|
@@ -4550,28 +4471,6 @@ export function createConsoleSection({ react, jsxRuntime, embedded }) {
|
|
|
4550
4471
|
refreshHeadline();
|
|
4551
4472
|
};
|
|
4552
4473
|
|
|
4553
|
-
/** 停用 / 启用某一行 */
|
|
4554
|
-
const onToggleRow = async (row) => {
|
|
4555
|
-
const next = row.disabled !== true;
|
|
4556
|
-
setBusyRow(row.key);
|
|
4557
|
-
setWriteError('');
|
|
4558
|
-
setNote('');
|
|
4559
|
-
try {
|
|
4560
|
-
await setAccountDisabledRemote(row.family, row.region, row.id, next);
|
|
4561
|
-
patchCreditRows(row.family, (r) => r.key === row.key, { disabled: next });
|
|
4562
|
-
// 停掉的正好是「这一轮在用的那个」→ 胶囊上的主语马上就会变,补一发
|
|
4563
|
-
if (next === true && row.serving === true && row.region === source.region) {
|
|
4564
|
-
refreshHeadline();
|
|
4565
|
-
}
|
|
4566
|
-
setNote(next ? `已停用 ${row.name},请求失败时请手动切换` : `已重新启用 ${row.name}`);
|
|
4567
|
-
} catch (e) {
|
|
4568
|
-
setWriteError(e?.message ?? String(e));
|
|
4569
|
-
} finally {
|
|
4570
|
-
setBusyRow('');
|
|
4571
|
-
setTick((n) => n + 1);
|
|
4572
|
-
}
|
|
4573
|
-
};
|
|
4574
|
-
|
|
4575
4474
|
/** 切换到此账号 / 取消固定 */
|
|
4576
4475
|
const onSelectRow = async (row) => {
|
|
4577
4476
|
// WorkBuddy 独有的「取消固定」:完全手动语义下它的池把「没有当前账号」
|
|
@@ -4678,22 +4577,6 @@ export function createConsoleSection({ react, jsxRuntime, embedded }) {
|
|
|
4678
4577
|
},
|
|
4679
4578
|
children: label,
|
|
4680
4579
|
}),
|
|
4681
|
-
jsx('button', {
|
|
4682
|
-
type: 'button',
|
|
4683
|
-
role: 'switch',
|
|
4684
|
-
className: 'wotr-credit-switch',
|
|
4685
|
-
'aria-checked': row.disabled === true ? 'false' : 'true',
|
|
4686
|
-
'aria-label': `${row.name} 停用/启用`,
|
|
4687
|
-
'data-on': row.disabled === true ? '0' : '1',
|
|
4688
|
-
disabled: busy,
|
|
4689
|
-
title:
|
|
4690
|
-
row.disabled === true
|
|
4691
|
-
? `启用 ${row.fullName}(恢复参与取号)`
|
|
4692
|
-
: `停用 ${row.fullName}(选中它时请求会失败)`,
|
|
4693
|
-
onClick: () => {
|
|
4694
|
-
void onToggleRow(row);
|
|
4695
|
-
},
|
|
4696
|
-
}),
|
|
4697
4580
|
],
|
|
4698
4581
|
}),
|
|
4699
4582
|
],
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-kdczyz-mode",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.3-beta",
|
|
4
4
|
"description": "WorkBuddy + Trae + Qoder merged into one DSH plugin: the WorkBuddy desktop account/model pool, the Trae connector and the Qoder account pool as three independent providers, plus one settings page with six tabs — two configuration tabs (XD Pool / Trae config: provider switch, account binding, model selection and its own multi-account pool) and four management/read-only tabs (accounts [WorkBuddy + Trae + Qoder, three columns] / Qoder pool / credits / usage history).",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -4599,7 +4599,7 @@ console.log('\n胶囊展开的账号卡片\n');
|
|
|
4599
4599
|
calls.map((c) => c.url).join(' '),
|
|
4600
4600
|
);
|
|
4601
4601
|
const row = findRow(node, '国内号');
|
|
4602
|
-
ok('
|
|
4602
|
+
ok('每行都有「切换到此账号」按钮', findByClass(row, 'wotr-credit-mini') !== null, text);
|
|
4603
4603
|
ok(
|
|
4604
4604
|
'被固定的那一行标明「已固定」,并且按钮变成「取消固定」',
|
|
4605
4605
|
(() => {
|
|
@@ -4618,53 +4618,6 @@ console.log('\n胶囊展开的账号卡片\n');
|
|
|
4618
4618
|
globalThis.fetch = prevFetch;
|
|
4619
4619
|
}
|
|
4620
4620
|
|
|
4621
|
-
// B) WorkBuddy 的开关:走路由(服务端按整份设置合并,跨区域安全)
|
|
4622
|
-
{
|
|
4623
|
-
const prevFetch = globalThis.fetch;
|
|
4624
|
-
const calls = routeFetch([
|
|
4625
|
-
['region=cn', { ok: true, selectedAccountId: 'cn1', activeAccountId: 'cn1', accounts: [{ id: 'cn1', nickname: '国内号', credits: { total: 1200 } }, { id: 'cn2', nickname: '国内备用', credits: { total: 40 } }] }],
|
|
4626
|
-
['region=global', { ok: true, activeAccountId: 'g1', accounts: [{ id: 'g1', nickname: '国际号', credits: { total: 80 } }] }],
|
|
4627
|
-
]);
|
|
4628
|
-
let node = null;
|
|
4629
|
-
let err = null;
|
|
4630
|
-
try {
|
|
4631
|
-
const first = renderOpenPill('workbuddy-xdpool', makeEmbedded());
|
|
4632
|
-
node = first.node;
|
|
4633
|
-
cardRefresh(node)?.props?.onClick?.();
|
|
4634
|
-
await new Promise((r) => setTimeout(r, 0));
|
|
4635
|
-
first.react.reset();
|
|
4636
|
-
node = first.Pill({ sessionId: 's1', resolveModelDirectory: () => makeModelStore('workbuddy-xdpool').store });
|
|
4637
|
-
// 点「国内备用」那一行的开关
|
|
4638
|
-
const row = findRow(node, '国内备用');
|
|
4639
|
-
findByClass(row, 'wotr-credit-switch')?.props?.onClick?.();
|
|
4640
|
-
await new Promise((r) => setTimeout(r, 0));
|
|
4641
|
-
first.react.reset();
|
|
4642
|
-
node = first.Pill({ sessionId: 's1', resolveModelDirectory: () => makeModelStore('workbuddy-xdpool').store });
|
|
4643
|
-
} catch (e) {
|
|
4644
|
-
err = e;
|
|
4645
|
-
}
|
|
4646
|
-
const write = calls.find((c) => c.url.includes('/accounts/disabled'));
|
|
4647
|
-
ok(
|
|
4648
|
-
'WorkBuddy 的开关走 /accounts/disabled(POST)',
|
|
4649
|
-
err === null && write !== undefined && write.method === 'POST',
|
|
4650
|
-
err?.message ?? calls.map((c) => `${c.method} ${c.url}`).join(' | '),
|
|
4651
|
-
);
|
|
4652
|
-
ok(
|
|
4653
|
-
'请求体只带 accountId 与 disabled',
|
|
4654
|
-
write !== undefined && JSON.parse(String(write.body))?.accountId === 'cn2' && JSON.parse(String(write.body))?.disabled === true,
|
|
4655
|
-
String(write?.body),
|
|
4656
|
-
);
|
|
4657
|
-
const out = [];
|
|
4658
|
-
collectText(node, out);
|
|
4659
|
-
ok('停用之后卡片上那一行的开关变成关闭态', findByClass(findRow(node, '国内备用'), 'wotr-credit-switch')?.props?.['data-on'] === '0', out.join(''));
|
|
4660
|
-
ok(
|
|
4661
|
-
'写的是国内版那一行,国际版没被连带改动',
|
|
4662
|
-
calls.filter((c) => c.url.includes('/accounts/disabled')).length === 1,
|
|
4663
|
-
calls.map((c) => c.url).join(' '),
|
|
4664
|
-
);
|
|
4665
|
-
globalThis.fetch = prevFetch;
|
|
4666
|
-
}
|
|
4667
|
-
|
|
4668
4621
|
// C) WorkBuddy 的「切换到此账号」:走 /accounts/select?region=,已固定的那行是取消固定
|
|
4669
4622
|
{
|
|
4670
4623
|
const prevFetch = globalThis.fetch;
|
|
@@ -4735,8 +4688,6 @@ console.log('\n胶囊展开的账号卡片\n');
|
|
|
4735
4688
|
await new Promise((r) => setTimeout(r, 0));
|
|
4736
4689
|
first.react.reset();
|
|
4737
4690
|
node = first.Pill({ sessionId: 's1', resolveModelDirectory: () => makeModelStore('trae').store });
|
|
4738
|
-
// 停用「国内备用」
|
|
4739
|
-
findByClass(findRow(node, '国内备用'), 'wotr-credit-switch')?.props?.onClick?.();
|
|
4740
4691
|
await new Promise((r) => setTimeout(r, 0));
|
|
4741
4692
|
first.react.reset();
|
|
4742
4693
|
node = first.Pill({ sessionId: 's1', resolveModelDirectory: () => makeModelStore('trae').store });
|
|
@@ -4754,19 +4705,6 @@ console.log('\n胶囊展开的账号卡片\n');
|
|
|
4754
4705
|
ok('Trae 卡片把国内版与国际版都列出来', err === null && text.includes('国内版') && text.includes('国际版'), err?.message ?? text);
|
|
4755
4706
|
ok('Trae 两边的账号都在(国际版没有余额接口 → 显示 –)', text.includes('国内号') && text.includes('4,500') && text.includes('国际号'), text);
|
|
4756
4707
|
ok('Trae 卡片行显示的也是通用池剩余,不是发放总量 9,000', !text.includes('9,000'), text);
|
|
4757
|
-
const disabledWrite = scope.writes.find((w) => w.field === 'regions');
|
|
4758
|
-
ok(
|
|
4759
|
-
'Trae 的开关写 regions(不动 ai 那边的 enabled)',
|
|
4760
|
-
disabledWrite?.value?.ai?.enabled === true,
|
|
4761
|
-
JSON.stringify(disabledWrite?.value),
|
|
4762
|
-
);
|
|
4763
|
-
ok(
|
|
4764
|
-
'Trae 的停用是**合并**:既有的 keep-me 没被抹掉,新 id 加进去了',
|
|
4765
|
-
Array.isArray(disabledWrite?.value?.cn?.disabledAccountIds) &&
|
|
4766
|
-
disabledWrite.value.cn.disabledAccountIds.includes('keep-me') &&
|
|
4767
|
-
disabledWrite.value.cn.disabledAccountIds.includes('acct-2'),
|
|
4768
|
-
JSON.stringify(disabledWrite?.value?.cn?.disabledAccountIds),
|
|
4769
|
-
);
|
|
4770
4708
|
const accountsWrite = scope.writes.find((w) => w.field === 'accounts');
|
|
4771
4709
|
ok('Trae 的切换写 accounts.cn = acct-2', accountsWrite?.value?.cn === 'acct-2', JSON.stringify(accountsWrite?.value));
|
|
4772
4710
|
ok(
|
|
@@ -4779,7 +4717,7 @@ console.log('\n胶囊展开的账号卡片\n');
|
|
|
4779
4717
|
globalThis.fetch = prevFetch;
|
|
4780
4718
|
}
|
|
4781
4719
|
|
|
4782
|
-
// E) Qoder
|
|
4720
|
+
// E) Qoder:只有一个池,所以不分组;控件(切换)写各自字段
|
|
4783
4721
|
{
|
|
4784
4722
|
const prevFetch = globalThis.fetch;
|
|
4785
4723
|
const scope = makeLiveScope({ disabledAccountIds: ['other'], selectedAccountId: 'q1' });
|
|
@@ -4807,7 +4745,6 @@ console.log('\n胶囊展开的账号卡片\n');
|
|
|
4807
4745
|
await new Promise((r) => setTimeout(r, 0));
|
|
4808
4746
|
first.react.reset();
|
|
4809
4747
|
node = first.Pill({ sessionId: 's1', resolveModelDirectory: () => makeModelStore('qoder').store });
|
|
4810
|
-
findByClass(findRow(node, 'q2-accou'), 'wotr-credit-switch')?.props?.onClick?.();
|
|
4811
4748
|
await new Promise((r) => setTimeout(r, 0));
|
|
4812
4749
|
findByClass(findRow(node, 'q2-accou'), 'wotr-credit-mini')?.props?.onClick?.();
|
|
4813
4750
|
await new Promise((r) => setTimeout(r, 0));
|
|
@@ -4820,12 +4757,6 @@ console.log('\n胶囊展开的账号卡片\n');
|
|
|
4820
4757
|
collectText(node, out);
|
|
4821
4758
|
const text = out.join('');
|
|
4822
4759
|
ok('Qoder 卡片渲染且不出现区域分组标题', err === null && text.includes('Qoder · 2 个账号') && !text.includes('国内版'), err?.message ?? text);
|
|
4823
|
-
const disabledWrite = scope.writes.find((w) => w.field === 'disabledAccountIds');
|
|
4824
|
-
ok(
|
|
4825
|
-
'Qoder 的停用写 disabledAccountIds 且保留 other',
|
|
4826
|
-
Array.isArray(disabledWrite?.value) && disabledWrite.value.includes('other') && disabledWrite.value.includes('q2'),
|
|
4827
|
-
JSON.stringify(disabledWrite?.value),
|
|
4828
|
-
);
|
|
4829
4760
|
const selectWrite = scope.writes.find((w) => w.field === 'selectedAccountId');
|
|
4830
4761
|
ok('Qoder 的切换写 selectedAccountId = q2', selectWrite?.value === 'q2', JSON.stringify(selectWrite?.value));
|
|
4831
4762
|
ok(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kdczyz-dsh-model",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.3-beta",
|
|
4
4
|
"description": "Installable wrapper for the DSH plugin in dsh-kdczyz-mode/: WorkBuddy model pool + Trae connector + Qoder account pool as three independent providers, plus one settings page with six tabs (XD Pool / Trae config [provider switch, account binding, model selection, account pool] / accounts [WorkBuddy + Trae + Qoder, three columns] / Qoder pool [device-flow login, per-account credits, cooldown] / credits / usage history). This root manifest exists so the DSH plugin manager can install the package: it declares dsh.bundle.patch at its root and re-exports the inner package's entry points. It deliberately declares NO dsh.client and NO ./client export - the client half belongs to the inner dsh-kdczyz-mode package. Re-exporting ./client here would make two manifests claim the same client.js; DSH emits one client bundle per claiming package, and the duplicate makes the renderer throw 'Identifier XDPOOL_CLIENT_SRC has already been declared', which fails web boot and prevents the app from starting.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|