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/lib/client.js CHANGED
@@ -40,15 +40,17 @@ const PKG_NAME = "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: "登录后可发现分组、勾选同步,自动为每个分组准备 key",
61
- signInTitle: "登录 sub2api 面板",
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: "displayName 模板,必须包含 {keyName} 占位符",
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: "还没有同步过——在设置 → Sub2API 同步 完成首次同步",
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: "已创建并绑定:{name}",
150
+ keyCreated: "已创建并勾选:{name}",
147
151
  keysListEmpty: "还没有 key",
148
- keysListLoading: "正在读取 keys…",
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: "This page runs process-local; configuration applies to this page only",
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: "Signed out",
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: "Sign in to discover groups, pick the ones to sync, and let each group get its key prepared",
177
- signInTitle: "Sign in to the sub2api panel",
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 re-login with the password the host already stored",
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: "displayName template; must contain the {keyName} placeholder",
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 private-network devices to call action routes",
234
- bundleAllowRemoteWarning: "When enabled, same-origin or private-network pages can rewrite model routes and credentials — enable only on a trusted network",
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 — finish the first sync in Settings → Sub2API Sync",
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 bound: {name}",
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
- } catch {}
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: none;
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("signIn")
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, form, t }) {
2258
- if (view !== "page" || form === undefined) return null;
2387
+ function BundleConfigCard({ view, t, config }) {
2388
+ if (view !== "page") return null;
2259
2389
  return (0, react_jsx_runtime.jsx)(BundleConfigFields, {
2260
- form,
2390
+ config,
2261
2391
  t
2262
2392
  });
2263
2393
  }
2264
- function BundleConfigFields({ form, t }) {
2265
- const values = form.state.value ?? {};
2266
- const writable = form.state.writable;
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
- form.mutate(ops).then((accepted) => {
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, { t });
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 DetailSectionBody({ t }) {
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.jsApiProtocol } from './types.ts';
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 refe./types.jse `get()` yields the
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;