@mangoc/dsh-sub2api-sync 0.2.8 → 0.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.
- package/README.md +62 -80
- package/README.zh.md +15 -3
- package/docs/DETAIL.md +1 -0
- package/docs/DETAIL.zh.md +1 -0
- package/docs/preview.svg +36 -0
- package/docs/preview.zh.svg +36 -0
- package/icon.svg +10 -0
- package/lib/client.cjs +239 -36
- package/lib/client.js +239 -36
- package/lib/config.d.ts +12 -2
- package/lib/dshgo.d.ts +35 -0
- package/lib/index.js +329 -32
- package/lib/oauth.d.ts +15 -6
- package/lib/types.d.ts +7 -0
- package/locale/en.json +2 -1
- package/locale/zh.json +2 -1
- package/package.json +36 -2
package/lib/client.js
CHANGED
|
@@ -40,15 +40,17 @@ const PKG_NAME = "@mangoc/dsh-sub2api-sync";
|
|
|
40
40
|
const NS = "sub2api-sync";
|
|
41
41
|
const zh = {
|
|
42
42
|
nav: "Sub2API 同步",
|
|
43
|
-
localOnlyWarning: "
|
|
43
|
+
localOnlyWarning: "当前为跨设备连接:配置只读,仅本页生效",
|
|
44
44
|
configUnavailable: "配置暂不可用:宿主未连接或未暴露该设置条目",
|
|
45
|
+
routesUnreachableWarning: "动作路由不可达:当前连接可能经 DSH Remote 中继,或宿主半侧未就绪——登录、勾选与同步请回到电脑上的 DSH Web",
|
|
45
46
|
nameLine: "sub2api · {instance}",
|
|
46
47
|
nameLineSite: "{name} · {instance}",
|
|
47
48
|
balanceLabel: "余额 {amount}",
|
|
48
49
|
balanceTitle: "面板账号余额(同步结束自动刷新)",
|
|
49
|
-
signedOut: "
|
|
50
|
+
signedOut: "未连接面板",
|
|
50
51
|
signedInAs: "已登录:{email}",
|
|
51
52
|
signIn: "登录",
|
|
53
|
+
connectPanel: "连接面板",
|
|
52
54
|
signOut: "登出",
|
|
53
55
|
signOutDone: "已登出",
|
|
54
56
|
baseURLLabel: "实例地址",
|
|
@@ -57,8 +59,8 @@ const zh = {
|
|
|
57
59
|
savedNotice: "已保存",
|
|
58
60
|
writeRefused: "写入未被接受,已恢复为最新状态",
|
|
59
61
|
accountTitle: "面板账号",
|
|
60
|
-
accountSignedOutHint: "
|
|
61
|
-
signInTitle: "
|
|
62
|
+
accountSignedOutHint: "连接面板后自动发现分组并准备 key,勾选即可同步",
|
|
63
|
+
signInTitle: "连接 sub2api 面板",
|
|
62
64
|
channelLabel: "登录方式",
|
|
63
65
|
channelPassword: "密码登录",
|
|
64
66
|
channelOAuth: "网页授权",
|
|
@@ -72,7 +74,7 @@ const zh = {
|
|
|
72
74
|
signedInOAuthAccount: "{account} · 已通过网页授权登录",
|
|
73
75
|
emailPlaceholder: "you@example.com",
|
|
74
76
|
passwordLabel: "密码",
|
|
75
|
-
passwordHint: "
|
|
77
|
+
passwordHint: "留空则使用已保存的密码重新登录",
|
|
76
78
|
twoFactorPlaceholder: "6 位数字验证码",
|
|
77
79
|
twoFactorSubmit: "验证",
|
|
78
80
|
back: "返回",
|
|
@@ -100,7 +102,7 @@ const zh = {
|
|
|
100
102
|
actionFailed: "失败",
|
|
101
103
|
actionFailedLabel: "操作失败:{message}",
|
|
102
104
|
bundleTemplateLabel: "供应商名模板",
|
|
103
|
-
bundleTemplateHint: "
|
|
105
|
+
bundleTemplateHint: "供应商显示名模板,须包含 {keyName} 占位符",
|
|
104
106
|
bundleTemplateInvalid: "模板必须包含 {keyName} 占位符",
|
|
105
107
|
bundleKeyQuotaLabel: "新建 key 配额(USD)",
|
|
106
108
|
bundleKeyQuotaHint: "自动建 key 时应用;留空不设置",
|
|
@@ -114,8 +116,8 @@ const zh = {
|
|
|
114
116
|
bundleDeleteOrphanHint: "仅对记录过 keyId 的分组生效",
|
|
115
117
|
bundleEnrichLabel: "补全模型元数据",
|
|
116
118
|
bundleEnrichHint: "同步时按模型 id 从 models.dev 补全容量元数据;关闭则仅用内置快照(离线)",
|
|
117
|
-
bundleAllowRemoteLabel: "
|
|
118
|
-
bundleAllowRemoteWarning: "
|
|
119
|
+
bundleAllowRemoteLabel: "允许局域网内其他设备操作面板",
|
|
120
|
+
bundleAllowRemoteWarning: "开启后,同一局域网里的设备无需在本机登录,也能修改模型路由和登录信息——请只在家中等可信网络开启",
|
|
119
121
|
bundleAdvancedHint: "protocolOverrides / compatTemplate 高级字段请编辑 cordis.patch.yml",
|
|
120
122
|
badgeManagedTag: "Sub2API 同步",
|
|
121
123
|
badgeManageHint: "管理入口:设置 → Sub2API 同步",
|
|
@@ -128,7 +130,9 @@ const zh = {
|
|
|
128
130
|
detailBadgeSynced: "已同步 {count} 组",
|
|
129
131
|
detailBadgeSignedOut: "未连接面板",
|
|
130
132
|
detailSectionTitle: "最近同步",
|
|
131
|
-
detailSectionEmpty: "
|
|
133
|
+
detailSectionEmpty: "还没有同步过",
|
|
134
|
+
detailConnectTitle: "面板连接",
|
|
135
|
+
detailConnectHint: "连接面板后即可勾选 key 并同步,无需前往设置页",
|
|
132
136
|
activationTitle: "开始使用 Sub2API 同步",
|
|
133
137
|
activationStep1: "1. 在设置中连接面板(账号密码或网页授权)",
|
|
134
138
|
activationStep2: "2. 在 Keys 卡勾选要同步的 key(可跨分组)",
|
|
@@ -143,9 +147,9 @@ const zh = {
|
|
|
143
147
|
keyNameLabel: "Key 名称",
|
|
144
148
|
keyNameHint: "默认与分组同名",
|
|
145
149
|
keyGroupPlaceholder: "选择分组",
|
|
146
|
-
keyCreated: "
|
|
150
|
+
keyCreated: "已创建并勾选:{name}",
|
|
147
151
|
keysListEmpty: "还没有 key",
|
|
148
|
-
keysListLoading: "正在读取
|
|
152
|
+
keysListLoading: "正在读取 key…",
|
|
149
153
|
keysSearchPlaceholder: "搜索 key…",
|
|
150
154
|
keysSearchEmpty: "没有匹配的 key",
|
|
151
155
|
syncSelected: "同步所选({count})",
|
|
@@ -156,15 +160,17 @@ const zh = {
|
|
|
156
160
|
};
|
|
157
161
|
const en = {
|
|
158
162
|
nav: "Sub2API Sync",
|
|
159
|
-
localOnlyWarning: "
|
|
163
|
+
localOnlyWarning: "Cross-device connection: configuration is read-only and applies to this page only",
|
|
160
164
|
configUnavailable: "Configuration is unavailable: the host is not connected or does not expose this entry",
|
|
165
|
+
routesUnreachableWarning: "Action routes are unreachable: this connection may go through a DSH-Remote relay or the host half is not ready — sign in, select keys and sync from the desktop DSH Web",
|
|
161
166
|
nameLine: "sub2api · {instance}",
|
|
162
167
|
nameLineSite: "{name} · {instance}",
|
|
163
168
|
balanceLabel: "Balance {amount}",
|
|
164
169
|
balanceTitle: "Panel account balance (refreshed after each sync)",
|
|
165
|
-
signedOut: "
|
|
170
|
+
signedOut: "Not connected",
|
|
166
171
|
signedInAs: "Signed in as {email}",
|
|
167
172
|
signIn: "Sign in",
|
|
173
|
+
connectPanel: "Connect panel",
|
|
168
174
|
signOut: "Sign out",
|
|
169
175
|
signOutDone: "Signed out",
|
|
170
176
|
baseURLLabel: "Instance address",
|
|
@@ -173,8 +179,8 @@ const en = {
|
|
|
173
179
|
savedNotice: "Saved",
|
|
174
180
|
writeRefused: "The write was not accepted; the latest state was restored",
|
|
175
181
|
accountTitle: "Panel account",
|
|
176
|
-
accountSignedOutHint: "
|
|
177
|
-
signInTitle: "
|
|
182
|
+
accountSignedOutHint: "Connect the panel to discover its groups and prepare a key per group; tick the ones to sync",
|
|
183
|
+
signInTitle: "Connect to the sub2api panel",
|
|
178
184
|
channelLabel: "Sign-in channel",
|
|
179
185
|
channelPassword: "Password",
|
|
180
186
|
channelOAuth: "Web authorization",
|
|
@@ -188,7 +194,7 @@ const en = {
|
|
|
188
194
|
signedInOAuthAccount: "{account} · Signed in via web authorization",
|
|
189
195
|
emailPlaceholder: "you@example.com",
|
|
190
196
|
passwordLabel: "Password",
|
|
191
|
-
passwordHint: "Leave empty to
|
|
197
|
+
passwordHint: "Leave empty to sign in again with the stored password",
|
|
192
198
|
twoFactorPlaceholder: "6-digit code",
|
|
193
199
|
twoFactorSubmit: "Verify",
|
|
194
200
|
back: "Back",
|
|
@@ -216,7 +222,7 @@ const en = {
|
|
|
216
222
|
actionFailed: "failed",
|
|
217
223
|
actionFailedLabel: "Operation failed: {message}",
|
|
218
224
|
bundleTemplateLabel: "Provider name template",
|
|
219
|
-
bundleTemplateHint: "
|
|
225
|
+
bundleTemplateHint: "Provider display-name template; must contain the {keyName} placeholder",
|
|
220
226
|
bundleTemplateInvalid: "The template must contain the {keyName} placeholder",
|
|
221
227
|
bundleKeyQuotaLabel: "Created key quota (USD)",
|
|
222
228
|
bundleKeyQuotaHint: "Applied to auto-created keys; empty leaves it unset",
|
|
@@ -230,8 +236,8 @@ const en = {
|
|
|
230
236
|
bundleDeleteOrphanHint: "Only affects groups whose keyId was recorded",
|
|
231
237
|
bundleEnrichLabel: "Enrich model metadata",
|
|
232
238
|
bundleEnrichHint: "Fill per-model capacity facts from models.dev at sync; off = bundled snapshot only (offline)",
|
|
233
|
-
bundleAllowRemoteLabel: "Allow
|
|
234
|
-
bundleAllowRemoteWarning: "When enabled, same
|
|
239
|
+
bundleAllowRemoteLabel: "Allow other devices on this network to use the panel",
|
|
240
|
+
bundleAllowRemoteWarning: "When enabled, any device on the same network can change model routes and sign-in details — turn on only on networks you trust (such as home)",
|
|
235
241
|
bundleAdvancedHint: "Edit cordis.patch.yml for the advanced protocolOverrides / compatTemplate fields",
|
|
236
242
|
badgeManagedTag: "Sub2API Sync",
|
|
237
243
|
badgeManageHint: "Manage in Settings → Sub2API Sync",
|
|
@@ -244,7 +250,9 @@ const en = {
|
|
|
244
250
|
detailBadgeSynced: "{count} group(s) synced",
|
|
245
251
|
detailBadgeSignedOut: "Panel not connected",
|
|
246
252
|
detailSectionTitle: "Last sync",
|
|
247
|
-
detailSectionEmpty: "Never synced
|
|
253
|
+
detailSectionEmpty: "Never synced",
|
|
254
|
+
detailConnectTitle: "Panel connection",
|
|
255
|
+
detailConnectHint: "Connect the panel to pick keys and sync — no need to visit the settings page",
|
|
248
256
|
activationTitle: "Get started with Sub2API Sync",
|
|
249
257
|
activationStep1: "1. Connect the panel in Settings (account password or web authorization)",
|
|
250
258
|
activationStep2: "2. Tick the keys to sync on the Keys card (cross-group)",
|
|
@@ -259,7 +267,7 @@ const en = {
|
|
|
259
267
|
keyNameLabel: "Key name",
|
|
260
268
|
keyNameHint: "Defaults to the group name",
|
|
261
269
|
keyGroupPlaceholder: "Pick a group",
|
|
262
|
-
keyCreated: "Created and
|
|
270
|
+
keyCreated: "Created and ticked: {name}",
|
|
263
271
|
keysListEmpty: "No keys yet",
|
|
264
272
|
keysListLoading: "Loading keys…",
|
|
265
273
|
keysSearchPlaceholder: "Search keys…",
|
|
@@ -439,9 +447,20 @@ function createPluginApi() {
|
|
|
439
447
|
//#endregion
|
|
440
448
|
//#region src/client/status.ts
|
|
441
449
|
const POLL_MS = 5e3;
|
|
450
|
+
/** Consecutive failed rounds before the card shows the unreachable banner. */
|
|
451
|
+
const DEGRADED_AFTER = 2;
|
|
442
452
|
let snapshot;
|
|
443
453
|
let timer;
|
|
444
454
|
const listeners = new Set();
|
|
455
|
+
/** Reachability of the plugin's action routes, as observed by the poller. */
|
|
456
|
+
let reachable = true;
|
|
457
|
+
let failures = 0;
|
|
458
|
+
const healthListeners = new Set();
|
|
459
|
+
function publishHealth(next) {
|
|
460
|
+
if (next === reachable) return;
|
|
461
|
+
reachable = next;
|
|
462
|
+
for (const listener of healthListeners) listener();
|
|
463
|
+
}
|
|
445
464
|
function publish(next) {
|
|
446
465
|
if (next === snapshot) return;
|
|
447
466
|
snapshot = next;
|
|
@@ -451,7 +470,12 @@ function publish(next) {
|
|
|
451
470
|
async function poll() {
|
|
452
471
|
try {
|
|
453
472
|
publish(await callJson(ROUTES.status));
|
|
454
|
-
|
|
473
|
+
failures = 0;
|
|
474
|
+
publishHealth(true);
|
|
475
|
+
} catch {
|
|
476
|
+
failures += 1;
|
|
477
|
+
if (failures >= DEGRADED_AFTER) publishHealth(false);
|
|
478
|
+
}
|
|
455
479
|
}
|
|
456
480
|
function subscribe(listener) {
|
|
457
481
|
listeners.add(listener);
|
|
@@ -476,6 +500,18 @@ const statusSource = {
|
|
|
476
500
|
function useStatus() {
|
|
477
501
|
return (0, react.useSyncExternalStore)(subscribe, getSnapshot);
|
|
478
502
|
}
|
|
503
|
+
const statusHealthSource = {
|
|
504
|
+
getSnapshot: () => reachable,
|
|
505
|
+
subscribe(fn) {
|
|
506
|
+
healthListeners.add(fn);
|
|
507
|
+
return () => {
|
|
508
|
+
healthListeners.delete(fn);
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
function useStatusHealth() {
|
|
513
|
+
return (0, react.useSyncExternalStore)(statusHealthSource.subscribe, statusHealthSource.getSnapshot);
|
|
514
|
+
}
|
|
479
515
|
function getSnapshot() {
|
|
480
516
|
return snapshot;
|
|
481
517
|
}
|
|
@@ -583,13 +619,20 @@ const CSS = `
|
|
|
583
619
|
border-top: 0.5px solid var(--dsw-alias-border-l2);
|
|
584
620
|
}
|
|
585
621
|
|
|
622
|
+
/* flex: 1 1 auto is load-bearing: a shrink-to-fit (flex: none) WRAPPING flex
|
|
623
|
+
container sizes itself by its largest item's contribution, not the sum of
|
|
624
|
+
items, so an input+save pair inside sized .s2s-actions to the input alone
|
|
625
|
+
and pushed the button onto a second right-aligned line (the stacked
|
|
626
|
+
config-field rows). Filling the row keeps one line on desktop; the wrap
|
|
627
|
+
below still degrades gracefully when the row itself runs out of space. */
|
|
586
628
|
.s2s-actions {
|
|
587
629
|
display: flex;
|
|
588
630
|
justify-content: flex-end;
|
|
589
631
|
align-items: center;
|
|
590
632
|
gap: 10px;
|
|
591
633
|
flex-wrap: wrap;
|
|
592
|
-
flex:
|
|
634
|
+
flex: 1 1 auto;
|
|
635
|
+
min-width: 0;
|
|
593
636
|
}
|
|
594
637
|
|
|
595
638
|
/* The account-balance pill in the identity header's action row. */
|
|
@@ -940,6 +983,88 @@ const CSS = `
|
|
|
940
983
|
font-size: 13px;
|
|
941
984
|
line-height: 22px;
|
|
942
985
|
}
|
|
986
|
+
|
|
987
|
+
/* Mobile (dsh-Go web island / narrow windows): stack the row layouts, keep
|
|
988
|
+
interactive rows at Android's 44dp touch floor, and let the anchored group
|
|
989
|
+
panel clear the soft keyboard. The desktop spec above is untouched — every
|
|
990
|
+
rule here is scoped to the narrow viewport. */
|
|
991
|
+
@media (max-width: 640px) {
|
|
992
|
+
.s2s-section {
|
|
993
|
+
gap: 12px;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
.s2s-row,
|
|
997
|
+
.s2s-inline {
|
|
998
|
+
flex-direction: column;
|
|
999
|
+
align-items: stretch;
|
|
1000
|
+
gap: 8px;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
.s2s-actions {
|
|
1004
|
+
justify-content: flex-start;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
.s2s-actions .s2s-feedback {
|
|
1008
|
+
white-space: normal;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
.s2s-keyRow {
|
|
1012
|
+
grid-template-columns: minmax(0, 1fr);
|
|
1013
|
+
min-height: 44px;
|
|
1014
|
+
padding: 8px 10px;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
.s2s-keyRow > :last-child {
|
|
1018
|
+
justify-self: start;
|
|
1019
|
+
text-align: left;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
.s2s-selectField,
|
|
1023
|
+
.s2s-groupOption {
|
|
1024
|
+
min-height: 44px;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
.s2s-input,
|
|
1028
|
+
.s2s-groupPicker {
|
|
1029
|
+
flex-basis: 100%;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
.s2s-searchInput {
|
|
1033
|
+
width: 100%;
|
|
1034
|
+
flex: 1 1 auto;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
.s2s-pager {
|
|
1038
|
+
justify-content: center;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
.s2s-footerStrip {
|
|
1042
|
+
flex-direction: column;
|
|
1043
|
+
align-items: flex-start;
|
|
1044
|
+
gap: 6px;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
.s2s-reportRemove {
|
|
1048
|
+
margin-left: 0;
|
|
1049
|
+
align-self: flex-end;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/* Keyboard clearance: the fixed-position panel must not run under the
|
|
1053
|
+
soft keyboard; dvh tracks the visual viewport on Android WebViews. */
|
|
1054
|
+
.s2s-groupPanel {
|
|
1055
|
+
max-height: 60dvh;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
/* Notch and gesture-bar insets on standalone web islands. */
|
|
1060
|
+
@supports (padding: max(0px)) {
|
|
1061
|
+
@media (max-width: 640px) {
|
|
1062
|
+
.s2s-section {
|
|
1063
|
+
padding-left: max(8px, env(safe-area-inset-left));
|
|
1064
|
+
padding-right: max(8px, env(safe-area-inset-right));
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
943
1068
|
`;
|
|
944
1069
|
function ensureCss() {
|
|
945
1070
|
if (document.querySelector("style[data-plugin=\"dsh-sub2api-sync\"]") !== null) return;
|
|
@@ -1830,6 +1955,7 @@ function actionToneOf(action) {
|
|
|
1830
1955
|
}
|
|
1831
1956
|
function Sub2apiSyncSection({ t, api, config, useStatus: useStatus$1 }) {
|
|
1832
1957
|
const status = useStatus$1((value) => value);
|
|
1958
|
+
const routesReachable = useStatusHealth();
|
|
1833
1959
|
const snapshot$1 = useConfigForm(config);
|
|
1834
1960
|
const configValues = snapshot$1.value;
|
|
1835
1961
|
const writable = snapshot$1.writable;
|
|
@@ -2025,6 +2151,10 @@ function Sub2apiSyncSection({ t, api, config, useStatus: useStatus$1 }) {
|
|
|
2025
2151
|
className: "s2s-warn",
|
|
2026
2152
|
children: t("configUnavailable")
|
|
2027
2153
|
}),
|
|
2154
|
+
!routesReachable && (0, react_jsx_runtime.jsx)("p", {
|
|
2155
|
+
className: "s2s-warn",
|
|
2156
|
+
children: t("routesUnreachableWarning")
|
|
2157
|
+
}),
|
|
2028
2158
|
(0, react_jsx_runtime.jsx)("section", {
|
|
2029
2159
|
className: "s2s-card",
|
|
2030
2160
|
children: (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -2078,7 +2208,7 @@ function Sub2apiSyncSection({ t, api, config, useStatus: useStatus$1 }) {
|
|
|
2078
2208
|
onClick: () => {
|
|
2079
2209
|
setSignInOpen(true);
|
|
2080
2210
|
},
|
|
2081
|
-
children: t("
|
|
2211
|
+
children: t("connectPanel")
|
|
2082
2212
|
})
|
|
2083
2213
|
]
|
|
2084
2214
|
})]
|
|
@@ -2254,22 +2384,32 @@ function parseNumber(text) {
|
|
|
2254
2384
|
function messageOf(error) {
|
|
2255
2385
|
return error instanceof Error ? error.message : String(error);
|
|
2256
2386
|
}
|
|
2257
|
-
function BundleConfigCard({ view,
|
|
2258
|
-
if (view !== "page"
|
|
2387
|
+
function BundleConfigCard({ view, t, config }) {
|
|
2388
|
+
if (view !== "page") return null;
|
|
2259
2389
|
return (0, react_jsx_runtime.jsx)(BundleConfigFields, {
|
|
2260
|
-
|
|
2390
|
+
config,
|
|
2261
2391
|
t
|
|
2262
2392
|
});
|
|
2263
2393
|
}
|
|
2264
|
-
function BundleConfigFields({
|
|
2265
|
-
const
|
|
2266
|
-
const
|
|
2394
|
+
function BundleConfigFields({ config, t }) {
|
|
2395
|
+
const snapshot$1 = useConfigForm(config);
|
|
2396
|
+
const values = snapshot$1.value ?? {};
|
|
2397
|
+
const writable = snapshot$1.writable;
|
|
2267
2398
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
2268
2399
|
const [feedback, setFeedback] = (0, react.useState)(undefined);
|
|
2269
2400
|
const run = (ops) => {
|
|
2270
2401
|
setBusy(true);
|
|
2271
2402
|
setFeedback(undefined);
|
|
2272
|
-
|
|
2403
|
+
const apply$1 = async () => {
|
|
2404
|
+
for (const op of ops) {
|
|
2405
|
+
const field = op.path[0];
|
|
2406
|
+
if (field === undefined || field === "") return false;
|
|
2407
|
+
const accepted = op.op === "set" ? await setField(config, field, op.value) : await unsetField(config, field);
|
|
2408
|
+
if (!accepted) return false;
|
|
2409
|
+
}
|
|
2410
|
+
return true;
|
|
2411
|
+
};
|
|
2412
|
+
apply$1().then((accepted) => {
|
|
2273
2413
|
setFeedback(accepted ? {
|
|
2274
2414
|
tone: "ok",
|
|
2275
2415
|
text: t("savedNotice")
|
|
@@ -2529,6 +2669,9 @@ function footerModel(status) {
|
|
|
2529
2669
|
...status?.lastReportAt !== undefined ? { lastReportAt: status.lastReportAt } : {}
|
|
2530
2670
|
};
|
|
2531
2671
|
}
|
|
2672
|
+
function detailCardModel(status) {
|
|
2673
|
+
return status?.auth?.signedIn === true ? "report" : "connect";
|
|
2674
|
+
}
|
|
2532
2675
|
function relativeMinutes(at, now) {
|
|
2533
2676
|
const minutes = Math.max(0, Math.floor((now - at) / 6e4));
|
|
2534
2677
|
if (minutes < 1) return { key: "footerJustNow" };
|
|
@@ -2614,11 +2757,64 @@ function DetailBadgeBody({ t }) {
|
|
|
2614
2757
|
});
|
|
2615
2758
|
}
|
|
2616
2759
|
function DetailSection(props) {
|
|
2617
|
-
const { subject, t } = props;
|
|
2760
|
+
const { subject, t, api, config } = props;
|
|
2618
2761
|
if (!isOwnBundle(subject)) return null;
|
|
2619
|
-
return (0, react_jsx_runtime.jsx)(DetailSectionBody, {
|
|
2762
|
+
return (0, react_jsx_runtime.jsx)(DetailSectionBody, {
|
|
2763
|
+
t,
|
|
2764
|
+
api,
|
|
2765
|
+
config
|
|
2766
|
+
});
|
|
2767
|
+
}
|
|
2768
|
+
function DetailSectionBody({ t, api, config }) {
|
|
2769
|
+
const status = useStatus();
|
|
2770
|
+
const reachable$1 = useStatusHealth();
|
|
2771
|
+
const snapshot$1 = useConfigForm(config);
|
|
2772
|
+
const [signInOpen, setSignInOpen] = (0, react.useState)(false);
|
|
2773
|
+
if (detailCardModel(status) === "connect") return (0, react_jsx_runtime.jsxs)("section", {
|
|
2774
|
+
className: "s2s-card",
|
|
2775
|
+
children: [
|
|
2776
|
+
(0, react_jsx_runtime.jsx)("span", {
|
|
2777
|
+
className: "s2s-name",
|
|
2778
|
+
children: t("detailConnectTitle")
|
|
2779
|
+
}),
|
|
2780
|
+
(0, react_jsx_runtime.jsx)("p", {
|
|
2781
|
+
className: "s2s-hint",
|
|
2782
|
+
children: t("detailConnectHint")
|
|
2783
|
+
}),
|
|
2784
|
+
(0, react_jsx_runtime.jsx)("div", {
|
|
2785
|
+
className: "s2s-actions",
|
|
2786
|
+
children: (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
2787
|
+
variant: "primary",
|
|
2788
|
+
disabled: !reachable$1,
|
|
2789
|
+
onClick: () => {
|
|
2790
|
+
setSignInOpen(true);
|
|
2791
|
+
},
|
|
2792
|
+
children: t("connectPanel")
|
|
2793
|
+
})
|
|
2794
|
+
}),
|
|
2795
|
+
!reachable$1 && (0, react_jsx_runtime.jsx)("p", {
|
|
2796
|
+
className: "s2s-warn",
|
|
2797
|
+
children: t("routesUnreachableWarning")
|
|
2798
|
+
}),
|
|
2799
|
+
(0, react_jsx_runtime.jsx)(SignInDialog, {
|
|
2800
|
+
open: signInOpen,
|
|
2801
|
+
onClose: () => {
|
|
2802
|
+
setSignInOpen(false);
|
|
2803
|
+
},
|
|
2804
|
+
api,
|
|
2805
|
+
config,
|
|
2806
|
+
baseURL: snapshot$1.value?.baseURL,
|
|
2807
|
+
writable: snapshot$1.writable,
|
|
2808
|
+
onSignedIn: () => {
|
|
2809
|
+
void refreshStatus();
|
|
2810
|
+
},
|
|
2811
|
+
t
|
|
2812
|
+
})
|
|
2813
|
+
]
|
|
2814
|
+
});
|
|
2815
|
+
return (0, react_jsx_runtime.jsx)(DetailReportCard, { t });
|
|
2620
2816
|
}
|
|
2621
|
-
function
|
|
2817
|
+
function DetailReportCard({ t }) {
|
|
2622
2818
|
const status = useStatus();
|
|
2623
2819
|
const report = status?.lastReport;
|
|
2624
2820
|
return (0, react_jsx_runtime.jsxs)("section", {
|
|
@@ -2708,10 +2904,12 @@ function mount(ctx) {
|
|
|
2708
2904
|
locale: NS,
|
|
2709
2905
|
inject: () => sectionOps
|
|
2710
2906
|
}, Sub2apiSyncSection));
|
|
2907
|
+
const bundleConfigOps = { config };
|
|
2711
2908
|
ctx.slots.inject("plugins.bundle.config", () => ctx.slots.register({
|
|
2712
2909
|
name: "plugins.bundle.config",
|
|
2713
2910
|
key: PKG_NAME,
|
|
2714
|
-
locale: NS
|
|
2911
|
+
locale: NS,
|
|
2912
|
+
inject: () => bundleConfigOps
|
|
2715
2913
|
}, BundleConfigCard));
|
|
2716
2914
|
ctx.slots.inject("settings.models.provider-card", () => ctx.slots.register({
|
|
2717
2915
|
name: "settings.models.provider-card",
|
|
@@ -2729,10 +2927,15 @@ function mount(ctx) {
|
|
|
2729
2927
|
id: "sub2api-sync",
|
|
2730
2928
|
locale: NS
|
|
2731
2929
|
}, DetailBadge));
|
|
2930
|
+
const detailOps = {
|
|
2931
|
+
api: sectionOps.api,
|
|
2932
|
+
config
|
|
2933
|
+
};
|
|
2732
2934
|
ctx.slots.inject("plugins.detail.section", () => ctx.slots.register({
|
|
2733
2935
|
name: "plugins.detail.section",
|
|
2734
2936
|
id: "sub2api-sync",
|
|
2735
|
-
locale: NS
|
|
2937
|
+
locale: NS,
|
|
2938
|
+
inject: () => detailOps
|
|
2736
2939
|
}, DetailSection));
|
|
2737
2940
|
const activationOps = { openBundleDetails: () => {
|
|
2738
2941
|
const nav = ctx.pluginNavigation;
|
package/lib/config.d.ts
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
*/
|
|
26
26
|
import z from '@deepseek-ai/schemastery';
|
|
27
27
|
import type { ModelTemplate, PiAiRetryPolicy } from './protocols.ts';
|
|
28
|
-
./protocols.
|
|
28
|
+
./protocols.jsE_PANEL_PASSWORD_REF, type ApiProtocol } from './types.ts';
|
|
29
29
|
/** ./types.jsn overrides over the plugin's built-in lenient retry policy
|
|
30
30
|
* (`SUB2API_RETRY_POLICY`): an absent key inherits the built-in value.
|
|
31
31
|
* YAML-only advanced field (一字段一写面 — the tuning card does not render it). */
|
|
@@ -80,12 +80,22 @@ export interface Sub2apiSyncConfig {
|
|
|
80
80
|
defaultMaxTokens?: number;
|
|
81
81
|
/** Escape hatch: allow action routes from private-network peers (§11). Default false. */
|
|
82
82
|
allowRemoteActions?: boolean;
|
|
83
|
+
/** dsh-Go native-settings credential slot for the panel password (fixed
|
|
84
|
+
* ref). The secret value itself lives in the credential store — the
|
|
85
|
+
* schema only carries the binding and a write-refusing sentinel. */
|
|
86
|
+
panelCredential?: {
|
|
87
|
+
/** Mirrors the schema's const projection — the slot ref is fixed. */
|
|
88
|
+
credentialRef?: typeof NATIVE_PANEL_PASSWORD_REF | null;
|
|
89
|
+
/** Sentinel input side: the transform refuses any non-empty value, so
|
|
90
|
+
* this only ever carries the empty default in the config document. */
|
|
91
|
+
password?: any;
|
|
92
|
+
};
|
|
83
93
|
}
|
|
84
94
|
export declare const Config: z<Sub2apiSyncConfig, any>;
|
|
85
95
|
/** The default provider-name template — owned by `types.ts` (browser-safe), re-exported here. */
|
|
86
96
|
export { DEFAULT_PROVIDER_NAME_TEMPLATE } from './types.ts';
|
|
87
97
|
/**
|
|
88
|
-
* One resolved volatile field: a stable
|
|
98
|
+
* One resolved volatile field: a stable referenc./types.jset()` yields the
|
|
89
99
|
* current immutable snapshot. Structurally `cosmokit`'s `Volatile<T>`; kept
|
|
90
100
|
* local so this package need not depend on it.
|
|
91
101
|
*/
|
package/lib/dshgo.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-Go native surfaces (NA protocol v1): a 'sync-status' panel plus a
|
|
3
|
+
* 'sync-strip' composer strip, declared in package.json 'dshGo' and fed
|
|
4
|
+
* here from the same RouteDeps face the web card's action routes use —
|
|
5
|
+
* one contract, no duplicated host logic.
|
|
6
|
+
*
|
|
7
|
+
* Registration is inert without the dsh-go bridge: ctx.inject(['dshGo'], …)
|
|
8
|
+
* simply never fires on a host without one (desktop), and 'dshGo' is NOT in
|
|
9
|
+
* the plugin's top-level 'inject' (per the NA protocol — a top-level entry
|
|
10
|
+
* would keep the whole plugin from booting there). Do not write ['dshGo?']
|
|
11
|
+
* either: cordis 4.0.5 has no optional-dependency syntax and treats it as a
|
|
12
|
+
* permanently-absent service name.
|
|
13
|
+
*
|
|
14
|
+
* Long actions are deliberately fire-and-forget: sync/login/logout start in
|
|
15
|
+
* the background while the invoke answers with a toast immediately. That
|
|
16
|
+
* keeps the 3-second invoke window irrelevant and behaves identically on
|
|
17
|
+
* v1.0 Apps (no v1.1 pending channel needed). Fresh state reaches the App
|
|
18
|
+
* through the data sources' interval re-emit (credential-store reads only —
|
|
19
|
+
* no network on the tick) plus an immediate publish when a background
|
|
20
|
+
* action settles; failures surface in the panel's callout instead of a
|
|
21
|
+
* late toast the v1 effect whitelist cannot deliver.
|
|
22
|
+
*
|
|
23
|
+
* @module sub2api-sync/dshgo
|
|
24
|
+
*/
|
|
25
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
26
|
+
import type { RouteDeps } from './routes./routes.jsxtra host half the RouteDeps face does not carry. */
|
|
27
|
+
export interface DshGoHooks {
|
|
28
|
+
/** The configured panel email (login needs it; deps only closes over it). */
|
|
29
|
+
email(): string | undefined;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Register the native panel and strip. No-op without the bridge; the
|
|
33
|
+
* callback body only runs on hosts that provide the 'dshGo' service.
|
|
34
|
+
*/
|
|
35
|
+
export declare function registerDshGo(ctx: Context, deps: RouteDeps, hooks: DshGoHooks): void;
|