@liyang8246/dsh-accounts 0.1.8 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +180 -6
- package/lib/index.js +23 -2
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -27,7 +27,9 @@
|
|
|
27
27
|
const en = {
|
|
28
28
|
title: "Model Accounts",
|
|
29
29
|
refresh: "Refresh quota",
|
|
30
|
-
|
|
30
|
+
modelFilter: "Model name filter",
|
|
31
|
+
modelFilterPlaceholder: "e.g. deepseek gpt minimax",
|
|
32
|
+
modelFilterHint: "Space-separated keywords, case-insensitive. The chat model picker only offers models whose name contains one of them; empty offers every model.",
|
|
31
33
|
add: "Add account",
|
|
32
34
|
label: "Account label",
|
|
33
35
|
key: "API key",
|
|
@@ -57,7 +59,9 @@
|
|
|
57
59
|
const zh = {
|
|
58
60
|
title: "模型账号",
|
|
59
61
|
refresh: "刷新额度",
|
|
60
|
-
|
|
62
|
+
modelFilter: "模型筛选",
|
|
63
|
+
modelFilterPlaceholder: "例如 deepseek gpt minimax",
|
|
64
|
+
modelFilterHint: "用空格分隔关键词,不区分大小写。聊天框的模型选择只列出名称含任一关键词的模型;留空列出全部。",
|
|
61
65
|
add: "添加账号",
|
|
62
66
|
label: "账号名称",
|
|
63
67
|
key: "API Key",
|
|
@@ -106,6 +110,7 @@
|
|
|
106
110
|
.zph-accounts .zph-quota{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:8px}.zph-accounts .zph-window{padding:8px 10px;border-radius:8px;background:var(--dsw-alias-bg-layer-2);font-size:11px;overflow-wrap:anywhere}.zph-accounts .zph-window-head{display:flex;align-items:baseline;justify-content:space-between;gap:8px}
|
|
107
111
|
.zph-accounts .zph-track{margin:6px 0 0;width:100%}.zph-accounts .zph-error{color:var(--dsw-alias-state-error-primary);overflow-wrap:anywhere}.zph-accounts .zph-quota-unavailable{padding-left:5px}.zph-accounts .zph-quota-unavailable > p{display:inline}.zph-accounts .zph-quota-unavailable > p + p{margin-left:8px}
|
|
108
112
|
.zph-accounts .zph-add{padding:12px 16px;border-top:1px solid var(--dsw-alias-border-l1)}.zph-accounts .zph-form{display:flex;flex-direction:column;gap:10px;margin-top:10px}.zph-accounts .zph-form label{display:flex;flex-direction:column;gap:4px}.zph-accounts .zph-grow{flex:1 1 180px;min-width:0}.zph-accounts .zph-align-end{align-items:flex-end}
|
|
113
|
+
.zph-accounts .zph-filter{display:flex;flex-direction:column;gap:4px}.zph-accounts .zph-filter label{display:flex;flex-direction:column;gap:4px}.zph-accounts .zph-filter input{width:100%}
|
|
109
114
|
.zph-accounts .zph-switch{display:flex;align-items:center;cursor:pointer}.zph-accounts .zph-switch input{appearance:none;-webkit-appearance:none;position:relative;width:36px;height:20px;min-height:0;padding:0;border:0;border-radius:10px;background:var(--dsw-alias-bg-layer-2);cursor:pointer;transition:background .15s}
|
|
110
115
|
.zph-accounts .zph-switch input::after{content:'';position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:50%;background:var(--dsw-alias-label-secondary);transition:transform .15s,background .15s}
|
|
111
116
|
.zph-accounts .zph-switch input:checked{background:var(--dsw-alias-label-primary)}.zph-accounts .zph-switch input:checked::after{transform:translateX(16px);background:var(--dsw-alias-bg-base)}.zph-accounts .zph-switch input:disabled{opacity:.5;cursor:default}
|
|
@@ -150,7 +155,7 @@
|
|
|
150
155
|
return isObject(value) && typeof value.id === "string" && isChannelId(value.channel) && (value.status === "pending" || value.status === "authorized" || value.status === "cancelled" || value.status === "error") && optionalString(value.prompt) && optionalString(value.error) && (value.notice === void 0 || isNotice(value.notice));
|
|
151
156
|
}
|
|
152
157
|
function isSnapshot(value) {
|
|
153
|
-
return isObject(value) && Array.isArray(value.channels) && value.channels.every(isChannel) && Array.isArray(value.accounts) && value.accounts.every(isAccount) && isObject(value.catalog) && typeof value.catalog.source === "string" && typeof value.catalog.generatedAt === "string" && typeof value.catalog.sha256 === "string" && (value.login === void 0 || isLogin(value.login));
|
|
158
|
+
return isObject(value) && Array.isArray(value.channels) && value.channels.every(isChannel) && Array.isArray(value.accounts) && value.accounts.every(isAccount) && isObject(value.catalog) && typeof value.catalog.source === "string" && typeof value.catalog.generatedAt === "string" && typeof value.catalog.sha256 === "string" && optionalString(value.filter) && (value.login === void 0 || isLogin(value.login));
|
|
154
159
|
}
|
|
155
160
|
function readSnapshot(value) {
|
|
156
161
|
if (!isSnapshot(value)) throw new Error("Invalid account response");
|
|
@@ -225,11 +230,12 @@
|
|
|
225
230
|
return isLoginRequest(request) ? readLogin(value) : readSnapshot(value);
|
|
226
231
|
}
|
|
227
232
|
// src/client/accounts-page.tsx
|
|
228
|
-
function createAccountsPage(React, ctx, store, t, Quota) {
|
|
233
|
+
function createAccountsPage(React, ctx, store, t, Quota, filter) {
|
|
229
234
|
const errorMessage = (error) => error instanceof Error ? error.message : t("failed");
|
|
230
235
|
return function AccountsPage() {
|
|
231
236
|
React.useSyncExternalStore((fn) => ctx.locale.subscribe(fn), () => ctx.locale.getSnapshot());
|
|
232
237
|
const data = React.useSyncExternalStore(store.subscribe, store.getSnapshot);
|
|
238
|
+
const filterText = React.useSyncExternalStore(filter.subscribe, filter.getSnapshot);
|
|
233
239
|
const [busy, setBusy] = React.useState(false);
|
|
234
240
|
const [error, setError] = React.useState("");
|
|
235
241
|
const [form, setForm] = React.useState(null);
|
|
@@ -393,7 +399,14 @@
|
|
|
393
399
|
className: "zph-accounts",
|
|
394
400
|
"aria-label": t("title"),
|
|
395
401
|
"aria-busy": busy
|
|
396
|
-
}, /* @__PURE__ */ React.createElement("style", null, css), /* @__PURE__ */ React.createElement("div", { className: "zph-
|
|
402
|
+
}, /* @__PURE__ */ React.createElement("style", null, css), /* @__PURE__ */ React.createElement("h2", null, t("title")), /* @__PURE__ */ React.createElement("div", { className: "zph-filter" }, /* @__PURE__ */ React.createElement("label", null, /* @__PURE__ */ React.createElement("span", null, t("modelFilter")), /* @__PURE__ */ React.createElement("input", {
|
|
403
|
+
type: "text",
|
|
404
|
+
value: filterText,
|
|
405
|
+
placeholder: t("modelFilterPlaceholder"),
|
|
406
|
+
autoComplete: "off",
|
|
407
|
+
spellCheck: false,
|
|
408
|
+
onChange: (event) => filter.setText(event.currentTarget.value)
|
|
409
|
+
})), /* @__PURE__ */ React.createElement("p", { className: "zph-muted" }, t("modelFilterHint"))), error && /* @__PURE__ */ React.createElement("p", {
|
|
397
410
|
role: "alert",
|
|
398
411
|
className: "zph-error"
|
|
399
412
|
}, error), !data ? /* @__PURE__ */ React.createElement("p", { role: "status" }, t("loading")) : data.channels.map((channel) => {
|
|
@@ -548,6 +561,158 @@
|
|
|
548
561
|
return /* @__PURE__ */ React.createElement(QuotaBars, { windows: activeQuota(data, channel) });
|
|
549
562
|
};
|
|
550
563
|
}
|
|
564
|
+
// src/client/model-filter.ts
|
|
565
|
+
/** Typing must not write the credential record per keystroke. */
|
|
566
|
+
const saveDelay = 400;
|
|
567
|
+
function keywordsOf(text) {
|
|
568
|
+
return [...new Set(text.toLowerCase().split(/\s+/).filter(Boolean))];
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Create the composer model filter.
|
|
572
|
+
* @param save - persists a settled text change; typing alone never reaches it.
|
|
573
|
+
* @returns the filter, hydrated once from the Host's saved text.
|
|
574
|
+
*/
|
|
575
|
+
function createModelFilter(save) {
|
|
576
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
577
|
+
let text = "";
|
|
578
|
+
let parsed = [];
|
|
579
|
+
let hydrated = false;
|
|
580
|
+
let dirty = false;
|
|
581
|
+
let timer;
|
|
582
|
+
const publish = () => {
|
|
583
|
+
for (const listener of [...listeners]) listener();
|
|
584
|
+
};
|
|
585
|
+
return {
|
|
586
|
+
getSnapshot: () => text,
|
|
587
|
+
subscribe(listener) {
|
|
588
|
+
listeners.add(listener);
|
|
589
|
+
return () => {
|
|
590
|
+
listeners.delete(listener);
|
|
591
|
+
};
|
|
592
|
+
},
|
|
593
|
+
hydrate(value) {
|
|
594
|
+
if (hydrated) return;
|
|
595
|
+
hydrated = true;
|
|
596
|
+
const next = value.trim();
|
|
597
|
+
if (dirty || next === text) return;
|
|
598
|
+
text = next;
|
|
599
|
+
parsed = keywordsOf(next);
|
|
600
|
+
publish();
|
|
601
|
+
},
|
|
602
|
+
setText(next) {
|
|
603
|
+
const value = next.trim();
|
|
604
|
+
dirty = true;
|
|
605
|
+
if (value === text) return;
|
|
606
|
+
text = value;
|
|
607
|
+
parsed = keywordsOf(value);
|
|
608
|
+
publish();
|
|
609
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
610
|
+
timer = setTimeout(() => {
|
|
611
|
+
timer = void 0;
|
|
612
|
+
save?.(value);
|
|
613
|
+
}, saveDelay);
|
|
614
|
+
},
|
|
615
|
+
keywords: () => parsed,
|
|
616
|
+
dispose() {
|
|
617
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
618
|
+
timer = void 0;
|
|
619
|
+
listeners.clear();
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
function matches(name, keywords) {
|
|
624
|
+
const lower = name.toLowerCase();
|
|
625
|
+
return keywords.some((keyword) => lower.includes(keyword));
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Keep only models whose name contains one of the keywords.
|
|
629
|
+
* @param state - the session's model directory snapshot.
|
|
630
|
+
* @param keywords - lowercase keywords; empty keeps everything.
|
|
631
|
+
* @returns the snapshot with filtered provider groups; the selected model stays visible so the composer trigger keeps naming the model in use.
|
|
632
|
+
*/
|
|
633
|
+
function filterModels(state, keywords) {
|
|
634
|
+
if (!keywords.length) return state;
|
|
635
|
+
const groups = state.groups.map((group) => ({
|
|
636
|
+
...group,
|
|
637
|
+
models: group.models.filter((model) => matches(model.name, keywords))
|
|
638
|
+
}));
|
|
639
|
+
const { current } = state;
|
|
640
|
+
if (current !== null) {
|
|
641
|
+
const source = state.groups.find((group) => group.id === current.provider);
|
|
642
|
+
const model = source?.models.find((item) => item.id === current.model);
|
|
643
|
+
if (source !== void 0 && model !== void 0) {
|
|
644
|
+
const target = groups.find((group) => group.id === source.id);
|
|
645
|
+
if (target === void 0) groups.push({
|
|
646
|
+
...source,
|
|
647
|
+
models: [model]
|
|
648
|
+
});
|
|
649
|
+
else if (!target.models.some((item) => item.id === model.id)) target.models = [...target.models, model];
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return {
|
|
653
|
+
...state,
|
|
654
|
+
groups: groups.filter((group) => group.models.length > 0)
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Filter every session model directory the composer seat and /model popup share.
|
|
659
|
+
* Both entries read the directory's store, so the filter sits on the store
|
|
660
|
+
* instead of on either surface.
|
|
661
|
+
* @param ctx - client root context.
|
|
662
|
+
* @param filter - the page-owned keyword filter.
|
|
663
|
+
*/
|
|
664
|
+
function installModelFilter(ctx, filter) {
|
|
665
|
+
const models = ctx.get("modelDirectories");
|
|
666
|
+
if (models === void 0) return;
|
|
667
|
+
const original = models.directoryFor;
|
|
668
|
+
const patched = /* @__PURE__ */ new WeakSet();
|
|
669
|
+
models.directoryFor = (sessionId) => {
|
|
670
|
+
const directory = original.call(models, sessionId);
|
|
671
|
+
if (!patched.has(directory)) {
|
|
672
|
+
patched.add(directory);
|
|
673
|
+
filterDirectory(directory, filter);
|
|
674
|
+
}
|
|
675
|
+
return directory;
|
|
676
|
+
};
|
|
677
|
+
ctx.effect(() => () => {
|
|
678
|
+
models.directoryFor = original;
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
function filterDirectory(directory, filter) {
|
|
682
|
+
const source = directory.store;
|
|
683
|
+
let cache;
|
|
684
|
+
const read = () => {
|
|
685
|
+
const snapshot = source.getSnapshot();
|
|
686
|
+
const key = filter.getSnapshot();
|
|
687
|
+
if (cache === void 0 || cache.snapshot !== snapshot || cache.key !== key) cache = {
|
|
688
|
+
snapshot,
|
|
689
|
+
key,
|
|
690
|
+
value: filterModels(snapshot, filter.keywords())
|
|
691
|
+
};
|
|
692
|
+
return cache.value;
|
|
693
|
+
};
|
|
694
|
+
const wrapped = {
|
|
695
|
+
getSnapshot: read,
|
|
696
|
+
subscribe(listener) {
|
|
697
|
+
const stopSource = source.subscribe(listener);
|
|
698
|
+
const stopFilter = filter.subscribe(listener);
|
|
699
|
+
return () => {
|
|
700
|
+
stopSource();
|
|
701
|
+
stopFilter();
|
|
702
|
+
};
|
|
703
|
+
},
|
|
704
|
+
set: (next) => {
|
|
705
|
+
source.set(next);
|
|
706
|
+
},
|
|
707
|
+
update: (mutator) => {
|
|
708
|
+
source.update(mutator);
|
|
709
|
+
}
|
|
710
|
+
};
|
|
711
|
+
Object.defineProperty(directory, "store", {
|
|
712
|
+
configurable: true,
|
|
713
|
+
get: () => wrapped
|
|
714
|
+
});
|
|
715
|
+
}
|
|
551
716
|
// src/client/quota.tsx
|
|
552
717
|
function createQuotaComponents(React, t) {
|
|
553
718
|
function Quota({ quota }) {
|
|
@@ -669,7 +834,16 @@
|
|
|
669
834
|
ctx.effect(() => () => store.dispose());
|
|
670
835
|
const { Quota, QuotaBars } = createQuotaComponents(React, t);
|
|
671
836
|
const ComposerQuota = createComposerQuota(React, ctx, store, QuotaBars);
|
|
672
|
-
const
|
|
837
|
+
const filter = createModelFilter((text) => {
|
|
838
|
+
call({
|
|
839
|
+
action: "set_filter",
|
|
840
|
+
filter: text
|
|
841
|
+
}).catch(() => {});
|
|
842
|
+
});
|
|
843
|
+
ctx.effect(() => () => filter.dispose());
|
|
844
|
+
installModelFilter(ctx, filter);
|
|
845
|
+
store.reload().then((snapshot) => filter.hydrate(snapshot.filter ?? "")).catch(() => {});
|
|
846
|
+
const AccountsPage = createAccountsPage(React, ctx, store, t, Quota, filter);
|
|
673
847
|
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
674
848
|
name: "settings.section",
|
|
675
849
|
id: "zephyr-accounts",
|
package/lib/index.js
CHANGED
|
@@ -1395,6 +1395,11 @@ function record(value, label = "request") {
|
|
|
1395
1395
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Invalid ${label}`);
|
|
1396
1396
|
return value;
|
|
1397
1397
|
}
|
|
1398
|
+
/** Model filter text; empty clears the filter, so `text()` cannot validate it. */
|
|
1399
|
+
function filterText(value) {
|
|
1400
|
+
if (typeof value !== "string" || value.length > 1024 || [...value].some((character) => character.charCodeAt(0) < 32 || character === "")) throw new Error("Invalid model filter");
|
|
1401
|
+
return value.trim();
|
|
1402
|
+
}
|
|
1398
1403
|
function validateTokens(value) {
|
|
1399
1404
|
const tokens = record(value, "OAuth tokens");
|
|
1400
1405
|
const access = text(tokens.access, "access token", 32768);
|
|
@@ -6945,7 +6950,8 @@ function readState(payload) {
|
|
|
6945
6950
|
version: 1,
|
|
6946
6951
|
accounts,
|
|
6947
6952
|
active: { ...active },
|
|
6948
|
-
quotas: readQuotas(state.quotas)
|
|
6953
|
+
quotas: readQuotas(state.quotas),
|
|
6954
|
+
...typeof state.filter === "string" && state.filter.trim() ? { filter: state.filter.trim() } : {}
|
|
6949
6955
|
};
|
|
6950
6956
|
}
|
|
6951
6957
|
function stateFrom(stored) {
|
|
@@ -7056,6 +7062,15 @@ var Accounts = class {
|
|
|
7056
7062
|
async quotas() {
|
|
7057
7063
|
return (await this.read()).quotas ?? {};
|
|
7058
7064
|
}
|
|
7065
|
+
async filter() {
|
|
7066
|
+
return (await this.read()).filter ?? "";
|
|
7067
|
+
}
|
|
7068
|
+
async saveFilter(filter) {
|
|
7069
|
+
await this.change((state) => {
|
|
7070
|
+
if (filter) state.filter = filter;
|
|
7071
|
+
else delete state.filter;
|
|
7072
|
+
});
|
|
7073
|
+
}
|
|
7059
7074
|
async saveQuota(id, quota) {
|
|
7060
7075
|
await this.change((state) => {
|
|
7061
7076
|
if (!state.accounts.some((account) => account.id === id)) return;
|
|
@@ -7254,6 +7269,7 @@ var ModelAccountsService = class {
|
|
|
7254
7269
|
async snapshot() {
|
|
7255
7270
|
const views = await this.accounts.list();
|
|
7256
7271
|
const saved = await this.accounts.quotas();
|
|
7272
|
+
const filter = await this.accounts.filter();
|
|
7257
7273
|
return {
|
|
7258
7274
|
channels,
|
|
7259
7275
|
accounts: views.map((account) => {
|
|
@@ -7263,7 +7279,8 @@ var ModelAccountsService = class {
|
|
|
7263
7279
|
quota
|
|
7264
7280
|
} : account;
|
|
7265
7281
|
}),
|
|
7266
|
-
catalog: catalogMetadata
|
|
7282
|
+
catalog: catalogMetadata,
|
|
7283
|
+
filter
|
|
7267
7284
|
};
|
|
7268
7285
|
}
|
|
7269
7286
|
async activeChannels() {
|
|
@@ -7357,6 +7374,10 @@ var ModelAccountsService = class {
|
|
|
7357
7374
|
if (origin !== "ui") throw new Error("Resetting quota is user-only; open Settings → Model Accounts");
|
|
7358
7375
|
await this.resetCredit(text(args.id, "account id"), signal);
|
|
7359
7376
|
break;
|
|
7377
|
+
case "set_filter":
|
|
7378
|
+
if (origin !== "ui") throw new Error("The model filter is user-only; open Settings → Model Accounts");
|
|
7379
|
+
await this.accounts.saveFilter(filterText(args.filter));
|
|
7380
|
+
break;
|
|
7360
7381
|
case "refresh_quota": {
|
|
7361
7382
|
const accounts = await this.accounts.list();
|
|
7362
7383
|
const ids = args.id === void 0 ? accounts.map((account) => account.id) : [text(args.id, "account id")];
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@liyang8246/dsh-accounts",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.9",
|
|
5
5
|
"description": "Independent multi-account Codex, OpenCode Go and CommandCode Go plugin for DeepSeek Harness.",
|
|
6
6
|
"repository": { "type": "git", "url": "https://github.com/liyang8246/dsh-zolin.git", "directory": "packages/dsh-accounts" },
|
|
7
7
|
"exports": {
|