@mangoc/dsh-sub2api-sync 0.2.0 → 0.2.2
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.cjs +127 -34
- package/lib/client.js +127 -34
- package/lib/index.js +12 -5
- package/lib/panel.d.ts +1 -0
- package/lib/routes.d.ts +13 -3
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +2 -2
package/lib/client.cjs
CHANGED
|
@@ -125,8 +125,8 @@ const zh = {
|
|
|
125
125
|
detailSectionEmpty: "还没有同步过——在设置 → Sub2API 同步 完成首次同步",
|
|
126
126
|
activationTitle: "开始使用 Sub2API 同步",
|
|
127
127
|
activationStep1: "1. 在设置中连接面板(账号密码或网页授权)",
|
|
128
|
-
activationStep2: "2.
|
|
129
|
-
activationStep3: "3.
|
|
128
|
+
activationStep2: "2. 在 Keys 卡勾选要同步的 key(可跨分组)",
|
|
129
|
+
activationStep3: "3. 点击「同步所选」,模型即可使用",
|
|
130
130
|
activationOpenConfig: "打开配置页",
|
|
131
131
|
activationDismiss: "知道了",
|
|
132
132
|
keysTitle: "Keys",
|
|
@@ -138,9 +138,13 @@ const zh = {
|
|
|
138
138
|
keyCreated: "已创建并绑定:{name}",
|
|
139
139
|
keysListEmpty: "还没有 key",
|
|
140
140
|
keysListLoading: "正在读取 keys…",
|
|
141
|
+
keysSearchPlaceholder: "搜索 key…",
|
|
142
|
+
keysSearchEmpty: "没有匹配的 key",
|
|
141
143
|
syncSelected: "同步所选({count})",
|
|
142
144
|
syncSelectedDone: "同步完成:{ok}/{total} 条成功",
|
|
143
|
-
|
|
145
|
+
keysPagePrev: "上一页",
|
|
146
|
+
keysPageNext: "下一页",
|
|
147
|
+
keysPageStatus: "第 {page} / {pages} 页 · 共 {total} 个"
|
|
144
148
|
};
|
|
145
149
|
const en = {
|
|
146
150
|
nav: "Sub2API Sync",
|
|
@@ -235,8 +239,8 @@ const en = {
|
|
|
235
239
|
detailSectionEmpty: "Never synced — finish the first sync in Settings → Sub2API Sync",
|
|
236
240
|
activationTitle: "Get started with Sub2API Sync",
|
|
237
241
|
activationStep1: "1. Connect the panel in Settings (account password or web authorization)",
|
|
238
|
-
activationStep2: "2.
|
|
239
|
-
activationStep3: "3. Click \"Sync
|
|
242
|
+
activationStep2: "2. Tick the keys to sync on the Keys card (cross-group)",
|
|
243
|
+
activationStep3: "3. Click \"Sync selected\" and the models are ready",
|
|
240
244
|
activationOpenConfig: "Open configuration",
|
|
241
245
|
activationDismiss: "Got it",
|
|
242
246
|
keysTitle: "Keys",
|
|
@@ -248,9 +252,13 @@ const en = {
|
|
|
248
252
|
keyCreated: "Created and bound: {name}",
|
|
249
253
|
keysListEmpty: "No keys yet",
|
|
250
254
|
keysListLoading: "Loading keys…",
|
|
255
|
+
keysSearchPlaceholder: "Search keys…",
|
|
256
|
+
keysSearchEmpty: "No keys match",
|
|
251
257
|
syncSelected: "Sync selected ({count})",
|
|
252
258
|
syncSelectedDone: "Synced: {ok}/{total} succeeded",
|
|
253
|
-
|
|
259
|
+
keysPagePrev: "Previous page",
|
|
260
|
+
keysPageNext: "Next page",
|
|
261
|
+
keysPageStatus: "Page {page} / {pages} · {total} keys"
|
|
254
262
|
};
|
|
255
263
|
const dictionaries = {
|
|
256
264
|
zh,
|
|
@@ -404,7 +412,7 @@ function createPluginApi() {
|
|
|
404
412
|
login2fa: (code) => callJson(ROUTES.login2fa, post({ code })),
|
|
405
413
|
logout: () => callJson(ROUTES.logout, post()),
|
|
406
414
|
groups: () => callJson(ROUTES.groups),
|
|
407
|
-
listKeys: (page, pageSize) => callJson(`${ROUTES.keys}?page=${page}&pageSize=${pageSize}`),
|
|
415
|
+
listKeys: (page, pageSize, search) => callJson(`${ROUTES.keys}?page=${page}&pageSize=${pageSize}${search !== undefined && search.length > 0 ? `&search=${encodeURIComponent(search)}` : ""}`),
|
|
408
416
|
createKey: (input) => callJson(ROUTES.keys, post(input)),
|
|
409
417
|
site: () => callJson(ROUTES.site),
|
|
410
418
|
account: () => callJson(ROUTES.account),
|
|
@@ -743,6 +751,31 @@ const CSS = `
|
|
|
743
751
|
opacity: 0.55;
|
|
744
752
|
}
|
|
745
753
|
|
|
754
|
+
/* The keys-card search input: fixed-width slot between the title and the
|
|
755
|
+
action buttons (see the s2s-actions flex layout). */
|
|
756
|
+
.s2s-searchInput {
|
|
757
|
+
flex: none;
|
|
758
|
+
width: 220px;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/* The keys-card pager: centered prev / status / next strip. */
|
|
762
|
+
.s2s-pager {
|
|
763
|
+
display: flex;
|
|
764
|
+
align-items: center;
|
|
765
|
+
justify-content: flex-end;
|
|
766
|
+
gap: 10px;
|
|
767
|
+
flex-wrap: wrap;
|
|
768
|
+
min-height: 32px;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
.s2s-pageStatus {
|
|
772
|
+
color: var(--dsw-alias-label-secondary);
|
|
773
|
+
font-size: 12px;
|
|
774
|
+
line-height: 18px;
|
|
775
|
+
font-variant-numeric: tabular-nums;
|
|
776
|
+
white-space: nowrap;
|
|
777
|
+
}
|
|
778
|
+
|
|
746
779
|
/* U2 placements: the provider-card badge, the Models-page footer strip, and
|
|
747
780
|
the Plugins-page activation steps. */
|
|
748
781
|
.s2s-badgeRow {
|
|
@@ -1148,12 +1181,16 @@ function protocolForPlatform(platform) {
|
|
|
1148
1181
|
//#endregion
|
|
1149
1182
|
//#region src/client/section/KeysCard.tsx
|
|
1150
1183
|
const PAGE_SIZE = 5;
|
|
1184
|
+
const SEARCH_DEBOUNCE_MS = 300;
|
|
1151
1185
|
function KeysCard({ api, config, groups, t }) {
|
|
1152
1186
|
const snapshot$1 = useConfigForm(config);
|
|
1153
1187
|
const [keys, setKeys] = (0, react.useState)([]);
|
|
1154
|
-
const [
|
|
1188
|
+
const [search, setSearch] = (0, react.useState)("");
|
|
1189
|
+
const [query, setQuery] = (0, react.useState)("");
|
|
1190
|
+
const [page, setPage] = (0, react.useState)(1);
|
|
1155
1191
|
const [pages, setPages] = (0, react.useState)(1);
|
|
1156
1192
|
const [total, setTotal] = (0, react.useState)(0);
|
|
1193
|
+
const [reloadTick, setReloadTick] = (0, react.useState)(0);
|
|
1157
1194
|
const [loading, setLoading] = (0, react.useState)(false);
|
|
1158
1195
|
const [listError, setListError] = (0, react.useState)(undefined);
|
|
1159
1196
|
const [creating, setCreating] = (0, react.useState)(false);
|
|
@@ -1164,23 +1201,41 @@ function KeysCard({ api, config, groups, t }) {
|
|
|
1164
1201
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
1165
1202
|
const [syncing, setSyncing] = (0, react.useState)(false);
|
|
1166
1203
|
const [feedback, setFeedback] = (0, react.useState)(undefined);
|
|
1167
|
-
const
|
|
1204
|
+
const appliedQueryRef = (0, react.useRef)(query);
|
|
1205
|
+
(0, react.useEffect)(() => {
|
|
1206
|
+
const timer$1 = window.setTimeout(() => {
|
|
1207
|
+
const next = search.trim();
|
|
1208
|
+
if (appliedQueryRef.current !== next) {
|
|
1209
|
+
appliedQueryRef.current = next;
|
|
1210
|
+
setQuery(next);
|
|
1211
|
+
setPage(1);
|
|
1212
|
+
}
|
|
1213
|
+
}, SEARCH_DEBOUNCE_MS);
|
|
1214
|
+
return () => window.clearTimeout(timer$1);
|
|
1215
|
+
}, [search]);
|
|
1216
|
+
const requestSeqRef = (0, react.useRef)(0);
|
|
1217
|
+
(0, react.useEffect)(() => {
|
|
1218
|
+
const requestId = ++requestSeqRef.current;
|
|
1168
1219
|
setLoading(true);
|
|
1169
1220
|
setListError(undefined);
|
|
1170
|
-
api.listKeys(
|
|
1171
|
-
|
|
1221
|
+
api.listKeys(page, PAGE_SIZE, query === "" ? undefined : query).then((response) => {
|
|
1222
|
+
if (requestSeqRef.current !== requestId) return;
|
|
1223
|
+
setKeys(response.keys);
|
|
1172
1224
|
setPages(response.pages);
|
|
1173
1225
|
setTotal(response.total);
|
|
1174
|
-
setPage(
|
|
1226
|
+
if (response.pages > 0 && page > response.pages) setPage(response.pages);
|
|
1175
1227
|
}).catch((cause) => {
|
|
1228
|
+
if (requestSeqRef.current !== requestId) return;
|
|
1176
1229
|
setListError(cause instanceof Error ? cause.message : String(cause));
|
|
1177
1230
|
}).finally(() => {
|
|
1178
|
-
setLoading(false);
|
|
1231
|
+
if (requestSeqRef.current === requestId) setLoading(false);
|
|
1179
1232
|
});
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1233
|
+
}, [
|
|
1234
|
+
api,
|
|
1235
|
+
page,
|
|
1236
|
+
query,
|
|
1237
|
+
reloadTick
|
|
1238
|
+
]);
|
|
1184
1239
|
const selectedIds = snapshot$1.value?.selectedKeys ?? [];
|
|
1185
1240
|
const toggle = (keyId, next) => {
|
|
1186
1241
|
const nextSet = new Set(selectedIds);
|
|
@@ -1241,7 +1296,8 @@ else nextSet.delete(keyId);
|
|
|
1241
1296
|
text: t("keyCreated", { name: response.key.name })
|
|
1242
1297
|
});
|
|
1243
1298
|
closeCreate();
|
|
1244
|
-
|
|
1299
|
+
setPage(1);
|
|
1300
|
+
setReloadTick((tick) => tick + 1);
|
|
1245
1301
|
}).catch((cause) => {
|
|
1246
1302
|
setFeedback({
|
|
1247
1303
|
tone: "error",
|
|
@@ -1276,10 +1332,19 @@ else nextSet.delete(keyId);
|
|
|
1276
1332
|
}), (0, react_jsx_runtime.jsxs)("div", {
|
|
1277
1333
|
className: "s2s-actions",
|
|
1278
1334
|
children: [
|
|
1335
|
+
(0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Input, {
|
|
1336
|
+
className: "s2s-searchInput",
|
|
1337
|
+
type: "text",
|
|
1338
|
+
value: search,
|
|
1339
|
+
placeholder: t("keysSearchPlaceholder"),
|
|
1340
|
+
onChange: (event) => {
|
|
1341
|
+
setSearch(event.target.value);
|
|
1342
|
+
}
|
|
1343
|
+
}),
|
|
1279
1344
|
(0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1280
1345
|
variant: "outline",
|
|
1281
1346
|
onClick: () => {
|
|
1282
|
-
|
|
1347
|
+
setReloadTick((tick) => tick + 1);
|
|
1283
1348
|
},
|
|
1284
1349
|
children: t("groupsRefresh")
|
|
1285
1350
|
}),
|
|
@@ -1309,7 +1374,7 @@ else nextSet.delete(keyId);
|
|
|
1309
1374
|
}),
|
|
1310
1375
|
!loading && keys.length === 0 && listError === undefined && (0, react_jsx_runtime.jsx)("p", {
|
|
1311
1376
|
className: "s2s-feedback s2s-hint",
|
|
1312
|
-
children: t("keysListEmpty")
|
|
1377
|
+
children: query !== "" ? t("keysSearchEmpty") : t("keysListEmpty")
|
|
1313
1378
|
}),
|
|
1314
1379
|
loading && keys.length === 0 && (0, react_jsx_runtime.jsx)("p", {
|
|
1315
1380
|
className: "s2s-feedback s2s-hint",
|
|
@@ -1354,16 +1419,34 @@ else nextSet.delete(keyId);
|
|
|
1354
1419
|
}, key.id);
|
|
1355
1420
|
})
|
|
1356
1421
|
}),
|
|
1357
|
-
keys.length > 0 &&
|
|
1358
|
-
className: "s2s-
|
|
1359
|
-
children:
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1422
|
+
keys.length > 0 && (pages > 1 || query !== "") && (0, react_jsx_runtime.jsxs)("div", {
|
|
1423
|
+
className: "s2s-pager",
|
|
1424
|
+
children: [
|
|
1425
|
+
(0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1426
|
+
variant: "outline",
|
|
1427
|
+
disabled: loading || page <= 1,
|
|
1428
|
+
onClick: () => {
|
|
1429
|
+
setPage(page - 1);
|
|
1430
|
+
},
|
|
1431
|
+
children: t("keysPagePrev")
|
|
1432
|
+
}),
|
|
1433
|
+
(0, react_jsx_runtime.jsx)("span", {
|
|
1434
|
+
className: "s2s-pageStatus",
|
|
1435
|
+
children: t("keysPageStatus", {
|
|
1436
|
+
page,
|
|
1437
|
+
pages,
|
|
1438
|
+
total
|
|
1439
|
+
})
|
|
1440
|
+
}),
|
|
1441
|
+
(0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1442
|
+
variant: "outline",
|
|
1443
|
+
disabled: loading || page >= pages,
|
|
1444
|
+
onClick: () => {
|
|
1445
|
+
setPage(page + 1);
|
|
1446
|
+
},
|
|
1447
|
+
children: t("keysPageNext")
|
|
1448
|
+
})
|
|
1449
|
+
]
|
|
1367
1450
|
}),
|
|
1368
1451
|
(0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
1369
1452
|
open: creating,
|
|
@@ -2313,7 +2396,7 @@ function DetailSectionBody({ t }) {
|
|
|
2313
2396
|
//#endregion
|
|
2314
2397
|
//#region src/client/plugins/ActivationGuide.tsx
|
|
2315
2398
|
function ActivationGuide(props) {
|
|
2316
|
-
const { t, onDismiss, onOpenDetails } = props;
|
|
2399
|
+
const { t, onDismiss, onOpenDetails, openBundleDetails } = props;
|
|
2317
2400
|
return (0, react_jsx_runtime.jsxs)("div", {
|
|
2318
2401
|
className: "s2s-card",
|
|
2319
2402
|
children: [
|
|
@@ -2333,7 +2416,9 @@ function ActivationGuide(props) {
|
|
|
2333
2416
|
className: "s2s-actions",
|
|
2334
2417
|
children: [(0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
2335
2418
|
variant: "primary",
|
|
2336
|
-
onClick:
|
|
2419
|
+
onClick: () => {
|
|
2420
|
+
if (!(openBundleDetails?.() ?? false)) onOpenDetails();
|
|
2421
|
+
},
|
|
2337
2422
|
children: t("activationOpenConfig")
|
|
2338
2423
|
}), (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
2339
2424
|
variant: "ghost",
|
|
@@ -2391,10 +2476,17 @@ function mount(ctx) {
|
|
|
2391
2476
|
id: "sub2api-sync",
|
|
2392
2477
|
locale: NS
|
|
2393
2478
|
}, DetailSection));
|
|
2479
|
+
const activationOps = { openBundleDetails: () => {
|
|
2480
|
+
const nav = ctx.pluginNavigation;
|
|
2481
|
+
if (nav === undefined) return false;
|
|
2482
|
+
nav.openBundle(PKG_NAME);
|
|
2483
|
+
return true;
|
|
2484
|
+
} };
|
|
2394
2485
|
ctx.slots.inject("plugins.bundle.activation", () => ctx.slots.register({
|
|
2395
2486
|
name: "plugins.bundle.activation",
|
|
2396
2487
|
key: PKG_NAME,
|
|
2397
|
-
locale: NS
|
|
2488
|
+
locale: NS,
|
|
2489
|
+
inject: () => activationOps
|
|
2398
2490
|
}, ActivationGuide));
|
|
2399
2491
|
}
|
|
2400
2492
|
|
|
@@ -2404,7 +2496,8 @@ const name = "dsh-sub2api-sync-client";
|
|
|
2404
2496
|
const inject = [
|
|
2405
2497
|
"slots",
|
|
2406
2498
|
"locale",
|
|
2407
|
-
"configForms"
|
|
2499
|
+
"configForms",
|
|
2500
|
+
"pluginNavigation"
|
|
2408
2501
|
];
|
|
2409
2502
|
function apply(ctx) {
|
|
2410
2503
|
mount(ctx);
|
package/lib/client.js
CHANGED
|
@@ -131,8 +131,8 @@ const zh = {
|
|
|
131
131
|
detailSectionEmpty: "还没有同步过——在设置 → Sub2API 同步 完成首次同步",
|
|
132
132
|
activationTitle: "开始使用 Sub2API 同步",
|
|
133
133
|
activationStep1: "1. 在设置中连接面板(账号密码或网页授权)",
|
|
134
|
-
activationStep2: "2.
|
|
135
|
-
activationStep3: "3.
|
|
134
|
+
activationStep2: "2. 在 Keys 卡勾选要同步的 key(可跨分组)",
|
|
135
|
+
activationStep3: "3. 点击「同步所选」,模型即可使用",
|
|
136
136
|
activationOpenConfig: "打开配置页",
|
|
137
137
|
activationDismiss: "知道了",
|
|
138
138
|
keysTitle: "Keys",
|
|
@@ -144,9 +144,13 @@ const zh = {
|
|
|
144
144
|
keyCreated: "已创建并绑定:{name}",
|
|
145
145
|
keysListEmpty: "还没有 key",
|
|
146
146
|
keysListLoading: "正在读取 keys…",
|
|
147
|
+
keysSearchPlaceholder: "搜索 key…",
|
|
148
|
+
keysSearchEmpty: "没有匹配的 key",
|
|
147
149
|
syncSelected: "同步所选({count})",
|
|
148
150
|
syncSelectedDone: "同步完成:{ok}/{total} 条成功",
|
|
149
|
-
|
|
151
|
+
keysPagePrev: "上一页",
|
|
152
|
+
keysPageNext: "下一页",
|
|
153
|
+
keysPageStatus: "第 {page} / {pages} 页 · 共 {total} 个"
|
|
150
154
|
};
|
|
151
155
|
const en = {
|
|
152
156
|
nav: "Sub2API Sync",
|
|
@@ -241,8 +245,8 @@ const en = {
|
|
|
241
245
|
detailSectionEmpty: "Never synced — finish the first sync in Settings → Sub2API Sync",
|
|
242
246
|
activationTitle: "Get started with Sub2API Sync",
|
|
243
247
|
activationStep1: "1. Connect the panel in Settings (account password or web authorization)",
|
|
244
|
-
activationStep2: "2.
|
|
245
|
-
activationStep3: "3. Click \"Sync
|
|
248
|
+
activationStep2: "2. Tick the keys to sync on the Keys card (cross-group)",
|
|
249
|
+
activationStep3: "3. Click \"Sync selected\" and the models are ready",
|
|
246
250
|
activationOpenConfig: "Open configuration",
|
|
247
251
|
activationDismiss: "Got it",
|
|
248
252
|
keysTitle: "Keys",
|
|
@@ -254,9 +258,13 @@ const en = {
|
|
|
254
258
|
keyCreated: "Created and bound: {name}",
|
|
255
259
|
keysListEmpty: "No keys yet",
|
|
256
260
|
keysListLoading: "Loading keys…",
|
|
261
|
+
keysSearchPlaceholder: "Search keys…",
|
|
262
|
+
keysSearchEmpty: "No keys match",
|
|
257
263
|
syncSelected: "Sync selected ({count})",
|
|
258
264
|
syncSelectedDone: "Synced: {ok}/{total} succeeded",
|
|
259
|
-
|
|
265
|
+
keysPagePrev: "Previous page",
|
|
266
|
+
keysPageNext: "Next page",
|
|
267
|
+
keysPageStatus: "Page {page} / {pages} · {total} keys"
|
|
260
268
|
};
|
|
261
269
|
const dictionaries = {
|
|
262
270
|
zh,
|
|
@@ -410,7 +418,7 @@ function createPluginApi() {
|
|
|
410
418
|
login2fa: (code) => callJson(ROUTES.login2fa, post({ code })),
|
|
411
419
|
logout: () => callJson(ROUTES.logout, post()),
|
|
412
420
|
groups: () => callJson(ROUTES.groups),
|
|
413
|
-
listKeys: (page, pageSize) => callJson(`${ROUTES.keys}?page=${page}&pageSize=${pageSize}`),
|
|
421
|
+
listKeys: (page, pageSize, search) => callJson(`${ROUTES.keys}?page=${page}&pageSize=${pageSize}${search !== undefined && search.length > 0 ? `&search=${encodeURIComponent(search)}` : ""}`),
|
|
414
422
|
createKey: (input) => callJson(ROUTES.keys, post(input)),
|
|
415
423
|
site: () => callJson(ROUTES.site),
|
|
416
424
|
account: () => callJson(ROUTES.account),
|
|
@@ -749,6 +757,31 @@ const CSS = `
|
|
|
749
757
|
opacity: 0.55;
|
|
750
758
|
}
|
|
751
759
|
|
|
760
|
+
/* The keys-card search input: fixed-width slot between the title and the
|
|
761
|
+
action buttons (see the s2s-actions flex layout). */
|
|
762
|
+
.s2s-searchInput {
|
|
763
|
+
flex: none;
|
|
764
|
+
width: 220px;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/* The keys-card pager: centered prev / status / next strip. */
|
|
768
|
+
.s2s-pager {
|
|
769
|
+
display: flex;
|
|
770
|
+
align-items: center;
|
|
771
|
+
justify-content: flex-end;
|
|
772
|
+
gap: 10px;
|
|
773
|
+
flex-wrap: wrap;
|
|
774
|
+
min-height: 32px;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
.s2s-pageStatus {
|
|
778
|
+
color: var(--dsw-alias-label-secondary);
|
|
779
|
+
font-size: 12px;
|
|
780
|
+
line-height: 18px;
|
|
781
|
+
font-variant-numeric: tabular-nums;
|
|
782
|
+
white-space: nowrap;
|
|
783
|
+
}
|
|
784
|
+
|
|
752
785
|
/* U2 placements: the provider-card badge, the Models-page footer strip, and
|
|
753
786
|
the Plugins-page activation steps. */
|
|
754
787
|
.s2s-badgeRow {
|
|
@@ -1154,12 +1187,16 @@ function protocolForPlatform(platform) {
|
|
|
1154
1187
|
//#endregion
|
|
1155
1188
|
//#region src/client/section/KeysCard.tsx
|
|
1156
1189
|
const PAGE_SIZE = 5;
|
|
1190
|
+
const SEARCH_DEBOUNCE_MS = 300;
|
|
1157
1191
|
function KeysCard({ api, config, groups, t }) {
|
|
1158
1192
|
const snapshot$1 = useConfigForm(config);
|
|
1159
1193
|
const [keys, setKeys] = (0, react.useState)([]);
|
|
1160
|
-
const [
|
|
1194
|
+
const [search, setSearch] = (0, react.useState)("");
|
|
1195
|
+
const [query, setQuery] = (0, react.useState)("");
|
|
1196
|
+
const [page, setPage] = (0, react.useState)(1);
|
|
1161
1197
|
const [pages, setPages] = (0, react.useState)(1);
|
|
1162
1198
|
const [total, setTotal] = (0, react.useState)(0);
|
|
1199
|
+
const [reloadTick, setReloadTick] = (0, react.useState)(0);
|
|
1163
1200
|
const [loading, setLoading] = (0, react.useState)(false);
|
|
1164
1201
|
const [listError, setListError] = (0, react.useState)(undefined);
|
|
1165
1202
|
const [creating, setCreating] = (0, react.useState)(false);
|
|
@@ -1170,23 +1207,41 @@ function KeysCard({ api, config, groups, t }) {
|
|
|
1170
1207
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
1171
1208
|
const [syncing, setSyncing] = (0, react.useState)(false);
|
|
1172
1209
|
const [feedback, setFeedback] = (0, react.useState)(undefined);
|
|
1173
|
-
const
|
|
1210
|
+
const appliedQueryRef = (0, react.useRef)(query);
|
|
1211
|
+
(0, react.useEffect)(() => {
|
|
1212
|
+
const timer$1 = window.setTimeout(() => {
|
|
1213
|
+
const next = search.trim();
|
|
1214
|
+
if (appliedQueryRef.current !== next) {
|
|
1215
|
+
appliedQueryRef.current = next;
|
|
1216
|
+
setQuery(next);
|
|
1217
|
+
setPage(1);
|
|
1218
|
+
}
|
|
1219
|
+
}, SEARCH_DEBOUNCE_MS);
|
|
1220
|
+
return () => window.clearTimeout(timer$1);
|
|
1221
|
+
}, [search]);
|
|
1222
|
+
const requestSeqRef = (0, react.useRef)(0);
|
|
1223
|
+
(0, react.useEffect)(() => {
|
|
1224
|
+
const requestId = ++requestSeqRef.current;
|
|
1174
1225
|
setLoading(true);
|
|
1175
1226
|
setListError(undefined);
|
|
1176
|
-
api.listKeys(
|
|
1177
|
-
|
|
1227
|
+
api.listKeys(page, PAGE_SIZE, query === "" ? undefined : query).then((response) => {
|
|
1228
|
+
if (requestSeqRef.current !== requestId) return;
|
|
1229
|
+
setKeys(response.keys);
|
|
1178
1230
|
setPages(response.pages);
|
|
1179
1231
|
setTotal(response.total);
|
|
1180
|
-
setPage(
|
|
1232
|
+
if (response.pages > 0 && page > response.pages) setPage(response.pages);
|
|
1181
1233
|
}).catch((cause) => {
|
|
1234
|
+
if (requestSeqRef.current !== requestId) return;
|
|
1182
1235
|
setListError(cause instanceof Error ? cause.message : String(cause));
|
|
1183
1236
|
}).finally(() => {
|
|
1184
|
-
setLoading(false);
|
|
1237
|
+
if (requestSeqRef.current === requestId) setLoading(false);
|
|
1185
1238
|
});
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1239
|
+
}, [
|
|
1240
|
+
api,
|
|
1241
|
+
page,
|
|
1242
|
+
query,
|
|
1243
|
+
reloadTick
|
|
1244
|
+
]);
|
|
1190
1245
|
const selectedIds = snapshot$1.value?.selectedKeys ?? [];
|
|
1191
1246
|
const toggle = (keyId, next) => {
|
|
1192
1247
|
const nextSet = new Set(selectedIds);
|
|
@@ -1247,7 +1302,8 @@ else nextSet.delete(keyId);
|
|
|
1247
1302
|
text: t("keyCreated", { name: response.key.name })
|
|
1248
1303
|
});
|
|
1249
1304
|
closeCreate();
|
|
1250
|
-
|
|
1305
|
+
setPage(1);
|
|
1306
|
+
setReloadTick((tick) => tick + 1);
|
|
1251
1307
|
}).catch((cause) => {
|
|
1252
1308
|
setFeedback({
|
|
1253
1309
|
tone: "error",
|
|
@@ -1282,10 +1338,19 @@ else nextSet.delete(keyId);
|
|
|
1282
1338
|
}), (0, react_jsx_runtime.jsxs)("div", {
|
|
1283
1339
|
className: "s2s-actions",
|
|
1284
1340
|
children: [
|
|
1341
|
+
(0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Input, {
|
|
1342
|
+
className: "s2s-searchInput",
|
|
1343
|
+
type: "text",
|
|
1344
|
+
value: search,
|
|
1345
|
+
placeholder: t("keysSearchPlaceholder"),
|
|
1346
|
+
onChange: (event) => {
|
|
1347
|
+
setSearch(event.target.value);
|
|
1348
|
+
}
|
|
1349
|
+
}),
|
|
1285
1350
|
(0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1286
1351
|
variant: "outline",
|
|
1287
1352
|
onClick: () => {
|
|
1288
|
-
|
|
1353
|
+
setReloadTick((tick) => tick + 1);
|
|
1289
1354
|
},
|
|
1290
1355
|
children: t("groupsRefresh")
|
|
1291
1356
|
}),
|
|
@@ -1315,7 +1380,7 @@ else nextSet.delete(keyId);
|
|
|
1315
1380
|
}),
|
|
1316
1381
|
!loading && keys.length === 0 && listError === undefined && (0, react_jsx_runtime.jsx)("p", {
|
|
1317
1382
|
className: "s2s-feedback s2s-hint",
|
|
1318
|
-
children: t("keysListEmpty")
|
|
1383
|
+
children: query !== "" ? t("keysSearchEmpty") : t("keysListEmpty")
|
|
1319
1384
|
}),
|
|
1320
1385
|
loading && keys.length === 0 && (0, react_jsx_runtime.jsx)("p", {
|
|
1321
1386
|
className: "s2s-feedback s2s-hint",
|
|
@@ -1360,16 +1425,34 @@ else nextSet.delete(keyId);
|
|
|
1360
1425
|
}, key.id);
|
|
1361
1426
|
})
|
|
1362
1427
|
}),
|
|
1363
|
-
keys.length > 0 &&
|
|
1364
|
-
className: "s2s-
|
|
1365
|
-
children:
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1428
|
+
keys.length > 0 && (pages > 1 || query !== "") && (0, react_jsx_runtime.jsxs)("div", {
|
|
1429
|
+
className: "s2s-pager",
|
|
1430
|
+
children: [
|
|
1431
|
+
(0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1432
|
+
variant: "outline",
|
|
1433
|
+
disabled: loading || page <= 1,
|
|
1434
|
+
onClick: () => {
|
|
1435
|
+
setPage(page - 1);
|
|
1436
|
+
},
|
|
1437
|
+
children: t("keysPagePrev")
|
|
1438
|
+
}),
|
|
1439
|
+
(0, react_jsx_runtime.jsx)("span", {
|
|
1440
|
+
className: "s2s-pageStatus",
|
|
1441
|
+
children: t("keysPageStatus", {
|
|
1442
|
+
page,
|
|
1443
|
+
pages,
|
|
1444
|
+
total
|
|
1445
|
+
})
|
|
1446
|
+
}),
|
|
1447
|
+
(0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1448
|
+
variant: "outline",
|
|
1449
|
+
disabled: loading || page >= pages,
|
|
1450
|
+
onClick: () => {
|
|
1451
|
+
setPage(page + 1);
|
|
1452
|
+
},
|
|
1453
|
+
children: t("keysPageNext")
|
|
1454
|
+
})
|
|
1455
|
+
]
|
|
1373
1456
|
}),
|
|
1374
1457
|
(0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
1375
1458
|
open: creating,
|
|
@@ -2319,7 +2402,7 @@ function DetailSectionBody({ t }) {
|
|
|
2319
2402
|
//#endregion
|
|
2320
2403
|
//#region src/client/plugins/ActivationGuide.tsx
|
|
2321
2404
|
function ActivationGuide(props) {
|
|
2322
|
-
const { t, onDismiss, onOpenDetails } = props;
|
|
2405
|
+
const { t, onDismiss, onOpenDetails, openBundleDetails } = props;
|
|
2323
2406
|
return (0, react_jsx_runtime.jsxs)("div", {
|
|
2324
2407
|
className: "s2s-card",
|
|
2325
2408
|
children: [
|
|
@@ -2339,7 +2422,9 @@ function ActivationGuide(props) {
|
|
|
2339
2422
|
className: "s2s-actions",
|
|
2340
2423
|
children: [(0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
2341
2424
|
variant: "primary",
|
|
2342
|
-
onClick:
|
|
2425
|
+
onClick: () => {
|
|
2426
|
+
if (!(openBundleDetails?.() ?? false)) onOpenDetails();
|
|
2427
|
+
},
|
|
2343
2428
|
children: t("activationOpenConfig")
|
|
2344
2429
|
}), (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
2345
2430
|
variant: "ghost",
|
|
@@ -2397,10 +2482,17 @@ function mount(ctx) {
|
|
|
2397
2482
|
id: "sub2api-sync",
|
|
2398
2483
|
locale: NS
|
|
2399
2484
|
}, DetailSection));
|
|
2485
|
+
const activationOps = { openBundleDetails: () => {
|
|
2486
|
+
const nav = ctx.pluginNavigation;
|
|
2487
|
+
if (nav === undefined) return false;
|
|
2488
|
+
nav.openBundle(PKG_NAME);
|
|
2489
|
+
return true;
|
|
2490
|
+
} };
|
|
2400
2491
|
ctx.slots.inject("plugins.bundle.activation", () => ctx.slots.register({
|
|
2401
2492
|
name: "plugins.bundle.activation",
|
|
2402
2493
|
key: PKG_NAME,
|
|
2403
|
-
locale: NS
|
|
2494
|
+
locale: NS,
|
|
2495
|
+
inject: () => activationOps
|
|
2404
2496
|
}, ActivationGuide));
|
|
2405
2497
|
}
|
|
2406
2498
|
|
|
@@ -2410,7 +2502,8 @@ const name = "dsh-sub2api-sync-client";
|
|
|
2410
2502
|
const inject = [
|
|
2411
2503
|
"slots",
|
|
2412
2504
|
"locale",
|
|
2413
|
-
"configForms"
|
|
2505
|
+
"configForms",
|
|
2506
|
+
"pluginNavigation"
|
|
2414
2507
|
];
|
|
2415
2508
|
function apply(ctx) {
|
|
2416
2509
|
mount(ctx);
|
package/lib/index.js
CHANGED
|
@@ -1829,6 +1829,8 @@ var PanelClient = class {
|
|
|
1829
1829
|
timezone: timeZoneOf()
|
|
1830
1830
|
});
|
|
1831
1831
|
if (options?.groupId !== undefined) query.set("group_id", String(options.groupId));
|
|
1832
|
+
const search = options?.search?.trim();
|
|
1833
|
+
if (search !== undefined && search.length > 0) query.set("search", search);
|
|
1832
1834
|
const data = await this.call(token, "GET", `/keys?${query.toString()}`);
|
|
1833
1835
|
const record = data;
|
|
1834
1836
|
const items = record?.items;
|
|
@@ -2206,11 +2208,11 @@ function conflictStatus(error) {
|
|
|
2206
2208
|
function registerRoutes(ctx, deps) {
|
|
2207
2209
|
ctx.inject(["webServer"], (webCtx) => {
|
|
2208
2210
|
const route = (path, handler) => {
|
|
2209
|
-
webCtx.webServer.register({
|
|
2211
|
+
webCtx.effect(() => webCtx.webServer.register({
|
|
2210
2212
|
kind: "exact",
|
|
2211
2213
|
path,
|
|
2212
2214
|
handler
|
|
2213
|
-
});
|
|
2215
|
+
}));
|
|
2214
2216
|
};
|
|
2215
2217
|
const guard = (req, res) => {
|
|
2216
2218
|
if (!trustedRequest(req, deps.allowRemote())) {
|
|
@@ -2342,7 +2344,9 @@ function registerRoutes(ctx, deps) {
|
|
|
2342
2344
|
const rawSize = Number(url.searchParams.get("pageSize") ?? "5");
|
|
2343
2345
|
const page = Number.isSafeInteger(rawPage) && rawPage > 0 ? rawPage : 1;
|
|
2344
2346
|
const pageSize = Number.isSafeInteger(rawSize) && rawSize > 0 ? Math.min(rawSize, 50) : 5;
|
|
2345
|
-
const
|
|
2347
|
+
const rawSearch = (url.searchParams.get("search") ?? "").trim();
|
|
2348
|
+
const search = rawSearch.length > 0 ? rawSearch : undefined;
|
|
2349
|
+
const result = await deps.listKeys(page, pageSize, search);
|
|
2346
2350
|
json(res, 200, {
|
|
2347
2351
|
ok: true,
|
|
2348
2352
|
keys: result.keys,
|
|
@@ -2528,6 +2532,8 @@ function registerRoutes(ctx, deps) {
|
|
|
2528
2532
|
});
|
|
2529
2533
|
}
|
|
2530
2534
|
});
|
|
2535
|
+
}).then(undefined, (error) => {
|
|
2536
|
+
ctx.logger.error(error);
|
|
2531
2537
|
});
|
|
2532
2538
|
}
|
|
2533
2539
|
/** Standalone failure page when the callback handler itself throws. Inline on
|
|
@@ -2688,10 +2694,11 @@ function apply(ctx, config) {
|
|
|
2688
2694
|
},
|
|
2689
2695
|
lastReport: () => reports.report(),
|
|
2690
2696
|
lastReportAt: () => reports.at(),
|
|
2691
|
-
listKeys: async (page, pageSize) => {
|
|
2697
|
+
listKeys: async (page, pageSize, search) => {
|
|
2692
2698
|
const result = await gateway.use((panel, token) => panel.listKeys(token, {
|
|
2693
2699
|
page,
|
|
2694
|
-
pageSize
|
|
2700
|
+
pageSize,
|
|
2701
|
+
...search !== undefined && search.length > 0 ? { search } : {}
|
|
2695
2702
|
}));
|
|
2696
2703
|
return {
|
|
2697
2704
|
keys: result.items.map((key) => ({
|
package/lib/panel.d.ts
CHANGED
package/lib/routes.d.ts
CHANGED
|
@@ -193,8 +193,9 @@ export interface RouteDeps {
|
|
|
193
193
|
sync(keyIds?: number[]): Promise<SyncReport>;
|
|
194
194
|
lastReport(): SyncReport | undefined;
|
|
195
195
|
lastReportAt(): number | undefined;
|
|
196
|
-
/** One page of the panel's keys (newest first; page is 1-based)
|
|
197
|
-
|
|
196
|
+
/** One page of the panel's keys (newest first; page is 1-based), optional
|
|
197
|
+
* server-side `search` over key names. */
|
|
198
|
+
listKeys(page: number, pageSize: number, search?: string): Promise<KeysListPage>;
|
|
198
199
|
/** Create one relay key (name defaults to the group name upstream of this seam). */
|
|
199
200
|
createKey(input: {
|
|
200
201
|
groupId: number;
|
|
@@ -247,7 +248,16 @@ export interface RouteDeps {
|
|
|
247
248
|
/** Drop a pending attempt (user cancelled the dialog). */
|
|
248
249
|
oauthCancel(attemptId: string): Promise<void>;
|
|
249
250
|
}
|
|
250
|
-
/** Register all card routes on the web server (no-op without one).
|
|
251
|
+
/** Register all card routes on the web server (no-op without one).
|
|
252
|
+
*
|
|
253
|
+
* Lifecycle: every registration's disposer rides the registering inject fiber
|
|
254
|
+
* (`webCtx.effect`). A plugin reload must remove its routes BEFORE the next
|
|
255
|
+
* apply re-registers them — the webserver rejects duplicate (kind, path)
|
|
256
|
+
* entries, and a dropped disposer would orphan the old entries: the new apply
|
|
257
|
+
* fails silently (an inject child failure does not fail the plugin fiber) and
|
|
258
|
+
* the table keeps serving the disposed fiber's closures, whose every service
|
|
259
|
+
* access then throws `cannot get required service … in inactive context`
|
|
260
|
+
* (the OAuth callback's 授权失败). */
|
|
251
261
|
export declare function registerRoutes(ctx: Context, deps: RouteDeps): void;
|
|
252
262
|
/**
|
|
253
263
|
* Assemble the `ownedRoutes` view for the status endpoint from the live
|
package/locale/en.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "Sub2API Sync",
|
|
4
|
-
"description": "Sync one sub2api instance's groups, keys, and models into dsh-llm-pi-ai provider routes: panel-driven login (
|
|
4
|
+
"description": "Sync one sub2api instance's groups, keys, and models into dsh-llm-pi-ai provider routes: panel-driven login (password / OAuth), one relay key one provider."
|
|
5
5
|
}
|
|
6
6
|
}
|
package/locale/zh.json
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mangoc/dsh-sub2api-sync",
|
|
3
|
-
"description": "Sync one sub2api instance's groups, keys, and models into dsh-llm-pi-ai provider routes: panel-driven login (password / OAuth)
|
|
4
|
-
"version": "0.2.
|
|
3
|
+
"description": "Sync one sub2api instance's groups, keys, and models into dsh-llm-pi-ai provider routes: panel-driven login (password / OAuth), one relay key one provider.",
|
|
4
|
+
"version": "0.2.2",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Shuai-S",
|
|
7
7
|
"homepage": "https://github.com/Shuai-S/dsh-sub2api-sync#readme",
|