@mangoc/dsh-sub2api-sync 0.2.8 → 0.2.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.cjs CHANGED
@@ -34,15 +34,17 @@ const PKG_NAME = __DSH_PLUGIN_NAME__;
34
34
  const NS = "sub2api-sync";
35
35
  const zh = {
36
36
  nav: "Sub2API 同步",
37
- localOnlyWarning: "当前页面进程本地,配置仅本页生效",
37
+ localOnlyWarning: "当前为跨设备连接:配置只读,仅本页生效",
38
38
  configUnavailable: "配置暂不可用:宿主未连接或未暴露该设置条目",
39
+ routesUnreachableWarning: "动作路由不可达:当前连接可能经 DSH Remote 中继,或宿主半侧未就绪——登录、勾选与同步请回到电脑上的 DSH Web",
39
40
  nameLine: "sub2api · {instance}",
40
41
  nameLineSite: "{name} · {instance}",
41
42
  balanceLabel: "余额 {amount}",
42
43
  balanceTitle: "面板账号余额(同步结束自动刷新)",
43
- signedOut: "未登录",
44
+ signedOut: "未连接面板",
44
45
  signedInAs: "已登录:{email}",
45
46
  signIn: "登录",
47
+ connectPanel: "连接面板",
46
48
  signOut: "登出",
47
49
  signOutDone: "已登出",
48
50
  baseURLLabel: "实例地址",
@@ -51,8 +53,8 @@ const zh = {
51
53
  savedNotice: "已保存",
52
54
  writeRefused: "写入未被接受,已恢复为最新状态",
53
55
  accountTitle: "面板账号",
54
- accountSignedOutHint: "登录后可发现分组、勾选同步,自动为每个分组准备 key",
55
- signInTitle: "登录 sub2api 面板",
56
+ accountSignedOutHint: "连接面板后自动发现分组并准备 key,勾选即可同步",
57
+ signInTitle: "连接 sub2api 面板",
56
58
  channelLabel: "登录方式",
57
59
  channelPassword: "密码登录",
58
60
  channelOAuth: "网页授权",
@@ -66,7 +68,7 @@ const zh = {
66
68
  signedInOAuthAccount: "{account} · 已通过网页授权登录",
67
69
  emailPlaceholder: "you@example.com",
68
70
  passwordLabel: "密码",
69
- passwordHint: "留空则使用宿主已保存的密码重新登录",
71
+ passwordHint: "留空则使用已保存的密码重新登录",
70
72
  twoFactorPlaceholder: "6 位数字验证码",
71
73
  twoFactorSubmit: "验证",
72
74
  back: "返回",
@@ -94,7 +96,7 @@ const zh = {
94
96
  actionFailed: "失败",
95
97
  actionFailedLabel: "操作失败:{message}",
96
98
  bundleTemplateLabel: "供应商名模板",
97
- bundleTemplateHint: "displayName 模板,必须包含 {keyName} 占位符",
99
+ bundleTemplateHint: "供应商显示名模板,须包含 {keyName} 占位符",
98
100
  bundleTemplateInvalid: "模板必须包含 {keyName} 占位符",
99
101
  bundleKeyQuotaLabel: "新建 key 配额(USD)",
100
102
  bundleKeyQuotaHint: "自动建 key 时应用;留空不设置",
@@ -108,7 +110,7 @@ const zh = {
108
110
  bundleDeleteOrphanHint: "仅对记录过 keyId 的分组生效",
109
111
  bundleEnrichLabel: "补全模型元数据",
110
112
  bundleEnrichHint: "同步时按模型 id 从 models.dev 补全容量元数据;关闭则仅用内置快照(离线)",
111
- bundleAllowRemoteLabel: "允许私网设备调用动作接口",
113
+ bundleAllowRemoteLabel: "允许私网设备调用动作路由",
112
114
  bundleAllowRemoteWarning: "开启后同源或私网页面可改写模型路由与凭据——仅在可信内网开启",
113
115
  bundleAdvancedHint: "protocolOverrides / compatTemplate 高级字段请编辑 cordis.patch.yml",
114
116
  badgeManagedTag: "Sub2API 同步",
@@ -122,7 +124,9 @@ const zh = {
122
124
  detailBadgeSynced: "已同步 {count} 组",
123
125
  detailBadgeSignedOut: "未连接面板",
124
126
  detailSectionTitle: "最近同步",
125
- detailSectionEmpty: "还没有同步过——在设置 → Sub2API 同步 完成首次同步",
127
+ detailSectionEmpty: "还没有同步过",
128
+ detailConnectTitle: "面板连接",
129
+ detailConnectHint: "连接面板后即可勾选 key 并同步,无需前往设置页",
126
130
  activationTitle: "开始使用 Sub2API 同步",
127
131
  activationStep1: "1. 在设置中连接面板(账号密码或网页授权)",
128
132
  activationStep2: "2. 在 Keys 卡勾选要同步的 key(可跨分组)",
@@ -137,9 +141,9 @@ const zh = {
137
141
  keyNameLabel: "Key 名称",
138
142
  keyNameHint: "默认与分组同名",
139
143
  keyGroupPlaceholder: "选择分组",
140
- keyCreated: "已创建并绑定:{name}",
144
+ keyCreated: "已创建并勾选:{name}",
141
145
  keysListEmpty: "还没有 key",
142
- keysListLoading: "正在读取 keys…",
146
+ keysListLoading: "正在读取 key…",
143
147
  keysSearchPlaceholder: "搜索 key…",
144
148
  keysSearchEmpty: "没有匹配的 key",
145
149
  syncSelected: "同步所选({count})",
@@ -150,15 +154,17 @@ const zh = {
150
154
  };
151
155
  const en = {
152
156
  nav: "Sub2API Sync",
153
- localOnlyWarning: "This page runs process-local; configuration applies to this page only",
157
+ localOnlyWarning: "Cross-device connection: configuration is read-only and applies to this page only",
154
158
  configUnavailable: "Configuration is unavailable: the host is not connected or does not expose this entry",
159
+ 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",
155
160
  nameLine: "sub2api · {instance}",
156
161
  nameLineSite: "{name} · {instance}",
157
162
  balanceLabel: "Balance {amount}",
158
163
  balanceTitle: "Panel account balance (refreshed after each sync)",
159
- signedOut: "Signed out",
164
+ signedOut: "Not connected",
160
165
  signedInAs: "Signed in as {email}",
161
166
  signIn: "Sign in",
167
+ connectPanel: "Connect panel",
162
168
  signOut: "Sign out",
163
169
  signOutDone: "Signed out",
164
170
  baseURLLabel: "Instance address",
@@ -167,8 +173,8 @@ const en = {
167
173
  savedNotice: "Saved",
168
174
  writeRefused: "The write was not accepted; the latest state was restored",
169
175
  accountTitle: "Panel account",
170
- accountSignedOutHint: "Sign in to discover groups, pick the ones to sync, and let each group get its key prepared",
171
- signInTitle: "Sign in to the sub2api panel",
176
+ accountSignedOutHint: "Connect the panel to discover its groups and prepare a key per group; tick the ones to sync",
177
+ signInTitle: "Connect to the sub2api panel",
172
178
  channelLabel: "Sign-in channel",
173
179
  channelPassword: "Password",
174
180
  channelOAuth: "Web authorization",
@@ -182,7 +188,7 @@ const en = {
182
188
  signedInOAuthAccount: "{account} · Signed in via web authorization",
183
189
  emailPlaceholder: "you@example.com",
184
190
  passwordLabel: "Password",
185
- passwordHint: "Leave empty to re-login with the password the host already stored",
191
+ passwordHint: "Leave empty to sign in again with the stored password",
186
192
  twoFactorPlaceholder: "6-digit code",
187
193
  twoFactorSubmit: "Verify",
188
194
  back: "Back",
@@ -210,7 +216,7 @@ const en = {
210
216
  actionFailed: "failed",
211
217
  actionFailedLabel: "Operation failed: {message}",
212
218
  bundleTemplateLabel: "Provider name template",
213
- bundleTemplateHint: "displayName template; must contain the {keyName} placeholder",
219
+ bundleTemplateHint: "Provider display-name template; must contain the {keyName} placeholder",
214
220
  bundleTemplateInvalid: "The template must contain the {keyName} placeholder",
215
221
  bundleKeyQuotaLabel: "Created key quota (USD)",
216
222
  bundleKeyQuotaHint: "Applied to auto-created keys; empty leaves it unset",
@@ -238,7 +244,9 @@ const en = {
238
244
  detailBadgeSynced: "{count} group(s) synced",
239
245
  detailBadgeSignedOut: "Panel not connected",
240
246
  detailSectionTitle: "Last sync",
241
- detailSectionEmpty: "Never synced — finish the first sync in Settings → Sub2API Sync",
247
+ detailSectionEmpty: "Never synced",
248
+ detailConnectTitle: "Panel connection",
249
+ detailConnectHint: "Connect the panel to pick keys and sync — no need to visit the settings page",
242
250
  activationTitle: "Get started with Sub2API Sync",
243
251
  activationStep1: "1. Connect the panel in Settings (account password or web authorization)",
244
252
  activationStep2: "2. Tick the keys to sync on the Keys card (cross-group)",
@@ -253,7 +261,7 @@ const en = {
253
261
  keyNameLabel: "Key name",
254
262
  keyNameHint: "Defaults to the group name",
255
263
  keyGroupPlaceholder: "Pick a group",
256
- keyCreated: "Created and bound: {name}",
264
+ keyCreated: "Created and ticked: {name}",
257
265
  keysListEmpty: "No keys yet",
258
266
  keysListLoading: "Loading keys…",
259
267
  keysSearchPlaceholder: "Search keys…",
@@ -433,9 +441,20 @@ function createPluginApi() {
433
441
  //#endregion
434
442
  //#region src/client/status.ts
435
443
  const POLL_MS = 5e3;
444
+ /** Consecutive failed rounds before the card shows the unreachable banner. */
445
+ const DEGRADED_AFTER = 2;
436
446
  let snapshot;
437
447
  let timer;
438
448
  const listeners = new Set();
449
+ /** Reachability of the plugin's action routes, as observed by the poller. */
450
+ let reachable = true;
451
+ let failures = 0;
452
+ const healthListeners = new Set();
453
+ function publishHealth(next) {
454
+ if (next === reachable) return;
455
+ reachable = next;
456
+ for (const listener of healthListeners) listener();
457
+ }
439
458
  function publish(next) {
440
459
  if (next === snapshot) return;
441
460
  snapshot = next;
@@ -445,7 +464,12 @@ function publish(next) {
445
464
  async function poll() {
446
465
  try {
447
466
  publish(await callJson(ROUTES.status));
448
- } catch {}
467
+ failures = 0;
468
+ publishHealth(true);
469
+ } catch {
470
+ failures += 1;
471
+ if (failures >= DEGRADED_AFTER) publishHealth(false);
472
+ }
449
473
  }
450
474
  function subscribe(listener) {
451
475
  listeners.add(listener);
@@ -470,6 +494,18 @@ const statusSource = {
470
494
  function useStatus() {
471
495
  return (0, react.useSyncExternalStore)(subscribe, getSnapshot);
472
496
  }
497
+ const statusHealthSource = {
498
+ getSnapshot: () => reachable,
499
+ subscribe(fn) {
500
+ healthListeners.add(fn);
501
+ return () => {
502
+ healthListeners.delete(fn);
503
+ };
504
+ }
505
+ };
506
+ function useStatusHealth() {
507
+ return (0, react.useSyncExternalStore)(statusHealthSource.subscribe, statusHealthSource.getSnapshot);
508
+ }
473
509
  function getSnapshot() {
474
510
  return snapshot;
475
511
  }
@@ -934,6 +970,88 @@ const CSS = `
934
970
  font-size: 13px;
935
971
  line-height: 22px;
936
972
  }
973
+
974
+ /* Mobile (dsh-Go web island / narrow windows): stack the row layouts, keep
975
+ interactive rows at Android's 44dp touch floor, and let the anchored group
976
+ panel clear the soft keyboard. The desktop spec above is untouched — every
977
+ rule here is scoped to the narrow viewport. */
978
+ @media (max-width: 640px) {
979
+ .s2s-section {
980
+ gap: 12px;
981
+ }
982
+
983
+ .s2s-row,
984
+ .s2s-inline {
985
+ flex-direction: column;
986
+ align-items: stretch;
987
+ gap: 8px;
988
+ }
989
+
990
+ .s2s-actions {
991
+ justify-content: flex-start;
992
+ }
993
+
994
+ .s2s-actions .s2s-feedback {
995
+ white-space: normal;
996
+ }
997
+
998
+ .s2s-keyRow {
999
+ grid-template-columns: minmax(0, 1fr);
1000
+ min-height: 44px;
1001
+ padding: 8px 10px;
1002
+ }
1003
+
1004
+ .s2s-keyRow > :last-child {
1005
+ justify-self: start;
1006
+ text-align: left;
1007
+ }
1008
+
1009
+ .s2s-selectField,
1010
+ .s2s-groupOption {
1011
+ min-height: 44px;
1012
+ }
1013
+
1014
+ .s2s-input,
1015
+ .s2s-groupPicker {
1016
+ flex-basis: 100%;
1017
+ }
1018
+
1019
+ .s2s-searchInput {
1020
+ width: 100%;
1021
+ flex: 1 1 auto;
1022
+ }
1023
+
1024
+ .s2s-pager {
1025
+ justify-content: center;
1026
+ }
1027
+
1028
+ .s2s-footerStrip {
1029
+ flex-direction: column;
1030
+ align-items: flex-start;
1031
+ gap: 6px;
1032
+ }
1033
+
1034
+ .s2s-reportRemove {
1035
+ margin-left: 0;
1036
+ align-self: flex-end;
1037
+ }
1038
+
1039
+ /* Keyboard clearance: the fixed-position panel must not run under the
1040
+ soft keyboard; dvh tracks the visual viewport on Android WebViews. */
1041
+ .s2s-groupPanel {
1042
+ max-height: 60dvh;
1043
+ }
1044
+ }
1045
+
1046
+ /* Notch and gesture-bar insets on standalone web islands. */
1047
+ @supports (padding: max(0px)) {
1048
+ @media (max-width: 640px) {
1049
+ .s2s-section {
1050
+ padding-left: max(8px, env(safe-area-inset-left));
1051
+ padding-right: max(8px, env(safe-area-inset-right));
1052
+ }
1053
+ }
1054
+ }
937
1055
  `;
938
1056
  function ensureCss() {
939
1057
  if (document.querySelector("style[data-plugin=\"dsh-sub2api-sync\"]") !== null) return;
@@ -1824,6 +1942,7 @@ function actionToneOf(action) {
1824
1942
  }
1825
1943
  function Sub2apiSyncSection({ t, api, config, useStatus: useStatus$1 }) {
1826
1944
  const status = useStatus$1((value) => value);
1945
+ const routesReachable = useStatusHealth();
1827
1946
  const snapshot$1 = useConfigForm(config);
1828
1947
  const configValues = snapshot$1.value;
1829
1948
  const writable = snapshot$1.writable;
@@ -2019,6 +2138,10 @@ function Sub2apiSyncSection({ t, api, config, useStatus: useStatus$1 }) {
2019
2138
  className: "s2s-warn",
2020
2139
  children: t("configUnavailable")
2021
2140
  }),
2141
+ !routesReachable && (0, react_jsx_runtime.jsx)("p", {
2142
+ className: "s2s-warn",
2143
+ children: t("routesUnreachableWarning")
2144
+ }),
2022
2145
  (0, react_jsx_runtime.jsx)("section", {
2023
2146
  className: "s2s-card",
2024
2147
  children: (0, react_jsx_runtime.jsxs)("div", {
@@ -2072,7 +2195,7 @@ function Sub2apiSyncSection({ t, api, config, useStatus: useStatus$1 }) {
2072
2195
  onClick: () => {
2073
2196
  setSignInOpen(true);
2074
2197
  },
2075
- children: t("signIn")
2198
+ children: t("connectPanel")
2076
2199
  })
2077
2200
  ]
2078
2201
  })]
@@ -2248,22 +2371,32 @@ function parseNumber(text) {
2248
2371
  function messageOf(error) {
2249
2372
  return error instanceof Error ? error.message : String(error);
2250
2373
  }
2251
- function BundleConfigCard({ view, form, t }) {
2252
- if (view !== "page" || form === undefined) return null;
2374
+ function BundleConfigCard({ view, t, config }) {
2375
+ if (view !== "page") return null;
2253
2376
  return (0, react_jsx_runtime.jsx)(BundleConfigFields, {
2254
- form,
2377
+ config,
2255
2378
  t
2256
2379
  });
2257
2380
  }
2258
- function BundleConfigFields({ form, t }) {
2259
- const values = form.state.value ?? {};
2260
- const writable = form.state.writable;
2381
+ function BundleConfigFields({ config, t }) {
2382
+ const snapshot$1 = useConfigForm(config);
2383
+ const values = snapshot$1.value ?? {};
2384
+ const writable = snapshot$1.writable;
2261
2385
  const [busy, setBusy] = (0, react.useState)(false);
2262
2386
  const [feedback, setFeedback] = (0, react.useState)(undefined);
2263
2387
  const run = (ops) => {
2264
2388
  setBusy(true);
2265
2389
  setFeedback(undefined);
2266
- form.mutate(ops).then((accepted) => {
2390
+ const apply$1 = async () => {
2391
+ for (const op of ops) {
2392
+ const field = op.path[0];
2393
+ if (field === undefined || field === "") return false;
2394
+ const accepted = op.op === "set" ? await setField(config, field, op.value) : await unsetField(config, field);
2395
+ if (!accepted) return false;
2396
+ }
2397
+ return true;
2398
+ };
2399
+ apply$1().then((accepted) => {
2267
2400
  setFeedback(accepted ? {
2268
2401
  tone: "ok",
2269
2402
  text: t("savedNotice")
@@ -2523,6 +2656,9 @@ function footerModel(status) {
2523
2656
  ...status?.lastReportAt !== undefined ? { lastReportAt: status.lastReportAt } : {}
2524
2657
  };
2525
2658
  }
2659
+ function detailCardModel(status) {
2660
+ return status?.auth?.signedIn === true ? "report" : "connect";
2661
+ }
2526
2662
  function relativeMinutes(at, now) {
2527
2663
  const minutes = Math.max(0, Math.floor((now - at) / 6e4));
2528
2664
  if (minutes < 1) return { key: "footerJustNow" };
@@ -2608,11 +2744,64 @@ function DetailBadgeBody({ t }) {
2608
2744
  });
2609
2745
  }
2610
2746
  function DetailSection(props) {
2611
- const { subject, t } = props;
2747
+ const { subject, t, api, config } = props;
2612
2748
  if (!isOwnBundle(subject)) return null;
2613
- return (0, react_jsx_runtime.jsx)(DetailSectionBody, { t });
2749
+ return (0, react_jsx_runtime.jsx)(DetailSectionBody, {
2750
+ t,
2751
+ api,
2752
+ config
2753
+ });
2614
2754
  }
2615
- function DetailSectionBody({ t }) {
2755
+ function DetailSectionBody({ t, api, config }) {
2756
+ const status = useStatus();
2757
+ const reachable$1 = useStatusHealth();
2758
+ const snapshot$1 = useConfigForm(config);
2759
+ const [signInOpen, setSignInOpen] = (0, react.useState)(false);
2760
+ if (detailCardModel(status) === "connect") return (0, react_jsx_runtime.jsxs)("section", {
2761
+ className: "s2s-card",
2762
+ children: [
2763
+ (0, react_jsx_runtime.jsx)("span", {
2764
+ className: "s2s-name",
2765
+ children: t("detailConnectTitle")
2766
+ }),
2767
+ (0, react_jsx_runtime.jsx)("p", {
2768
+ className: "s2s-hint",
2769
+ children: t("detailConnectHint")
2770
+ }),
2771
+ (0, react_jsx_runtime.jsx)("div", {
2772
+ className: "s2s-actions",
2773
+ children: (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Button, {
2774
+ variant: "primary",
2775
+ disabled: !reachable$1,
2776
+ onClick: () => {
2777
+ setSignInOpen(true);
2778
+ },
2779
+ children: t("connectPanel")
2780
+ })
2781
+ }),
2782
+ !reachable$1 && (0, react_jsx_runtime.jsx)("p", {
2783
+ className: "s2s-warn",
2784
+ children: t("routesUnreachableWarning")
2785
+ }),
2786
+ (0, react_jsx_runtime.jsx)(SignInDialog, {
2787
+ open: signInOpen,
2788
+ onClose: () => {
2789
+ setSignInOpen(false);
2790
+ },
2791
+ api,
2792
+ config,
2793
+ baseURL: snapshot$1.value?.baseURL,
2794
+ writable: snapshot$1.writable,
2795
+ onSignedIn: () => {
2796
+ void refreshStatus();
2797
+ },
2798
+ t
2799
+ })
2800
+ ]
2801
+ });
2802
+ return (0, react_jsx_runtime.jsx)(DetailReportCard, { t });
2803
+ }
2804
+ function DetailReportCard({ t }) {
2616
2805
  const status = useStatus();
2617
2806
  const report = status?.lastReport;
2618
2807
  return (0, react_jsx_runtime.jsxs)("section", {
@@ -2702,10 +2891,12 @@ function mount(ctx) {
2702
2891
  locale: NS,
2703
2892
  inject: () => sectionOps
2704
2893
  }, Sub2apiSyncSection));
2894
+ const bundleConfigOps = { config };
2705
2895
  ctx.slots.inject("plugins.bundle.config", () => ctx.slots.register({
2706
2896
  name: "plugins.bundle.config",
2707
2897
  key: PKG_NAME,
2708
- locale: NS
2898
+ locale: NS,
2899
+ inject: () => bundleConfigOps
2709
2900
  }, BundleConfigCard));
2710
2901
  ctx.slots.inject("settings.models.provider-card", () => ctx.slots.register({
2711
2902
  name: "settings.models.provider-card",
@@ -2723,10 +2914,15 @@ function mount(ctx) {
2723
2914
  id: "sub2api-sync",
2724
2915
  locale: NS
2725
2916
  }, DetailBadge));
2917
+ const detailOps = {
2918
+ api: sectionOps.api,
2919
+ config
2920
+ };
2726
2921
  ctx.slots.inject("plugins.detail.section", () => ctx.slots.register({
2727
2922
  name: "plugins.detail.section",
2728
2923
  id: "sub2api-sync",
2729
- locale: NS
2924
+ locale: NS,
2925
+ inject: () => detailOps
2730
2926
  }, DetailSection));
2731
2927
  const activationOps = { openBundleDetails: () => {
2732
2928
  const nav = ctx.pluginNavigation;