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.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,8 +110,8 @@ const zh = {
108
110
  bundleDeleteOrphanHint: "仅对记录过 keyId 的分组生效",
109
111
  bundleEnrichLabel: "补全模型元数据",
110
112
  bundleEnrichHint: "同步时按模型 id 从 models.dev 补全容量元数据;关闭则仅用内置快照(离线)",
111
- bundleAllowRemoteLabel: "允许私网设备调用动作接口",
112
- bundleAllowRemoteWarning: "开启后同源或私网页面可改写模型路由与凭据——仅在可信内网开启",
113
+ bundleAllowRemoteLabel: "允许局域网内其他设备操作面板",
114
+ bundleAllowRemoteWarning: "开启后,同一局域网里的设备无需在本机登录,也能修改模型路由和登录信息——请只在家中等可信网络开启",
113
115
  bundleAdvancedHint: "protocolOverrides / compatTemplate 高级字段请编辑 cordis.patch.yml",
114
116
  badgeManagedTag: "Sub2API 同步",
115
117
  badgeManageHint: "管理入口:设置 → 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",
@@ -224,8 +230,8 @@ const en = {
224
230
  bundleDeleteOrphanHint: "Only affects groups whose keyId was recorded",
225
231
  bundleEnrichLabel: "Enrich model metadata",
226
232
  bundleEnrichHint: "Fill per-model capacity facts from models.dev at sync; off = bundled snapshot only (offline)",
227
- bundleAllowRemoteLabel: "Allow private-network devices to call action routes",
228
- bundleAllowRemoteWarning: "When enabled, same-origin or private-network pages can rewrite model routes and credentials — enable only on a trusted network",
233
+ bundleAllowRemoteLabel: "Allow other devices on this network to use the panel",
234
+ 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)",
229
235
  bundleAdvancedHint: "Edit cordis.patch.yml for the advanced protocolOverrides / compatTemplate fields",
230
236
  badgeManagedTag: "Sub2API Sync",
231
237
  badgeManageHint: "Manage in Settings → Sub2API Sync",
@@ -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
  }
@@ -577,13 +613,20 @@ const CSS = `
577
613
  border-top: 0.5px solid var(--dsw-alias-border-l2);
578
614
  }
579
615
 
616
+ /* flex: 1 1 auto is load-bearing: a shrink-to-fit (flex: none) WRAPPING flex
617
+ container sizes itself by its largest item's contribution, not the sum of
618
+ items, so an input+save pair inside sized .s2s-actions to the input alone
619
+ and pushed the button onto a second right-aligned line (the stacked
620
+ config-field rows). Filling the row keeps one line on desktop; the wrap
621
+ below still degrades gracefully when the row itself runs out of space. */
580
622
  .s2s-actions {
581
623
  display: flex;
582
624
  justify-content: flex-end;
583
625
  align-items: center;
584
626
  gap: 10px;
585
627
  flex-wrap: wrap;
586
- flex: none;
628
+ flex: 1 1 auto;
629
+ min-width: 0;
587
630
  }
588
631
 
589
632
  /* The account-balance pill in the identity header's action row. */
@@ -934,6 +977,88 @@ const CSS = `
934
977
  font-size: 13px;
935
978
  line-height: 22px;
936
979
  }
980
+
981
+ /* Mobile (dsh-Go web island / narrow windows): stack the row layouts, keep
982
+ interactive rows at Android's 44dp touch floor, and let the anchored group
983
+ panel clear the soft keyboard. The desktop spec above is untouched — every
984
+ rule here is scoped to the narrow viewport. */
985
+ @media (max-width: 640px) {
986
+ .s2s-section {
987
+ gap: 12px;
988
+ }
989
+
990
+ .s2s-row,
991
+ .s2s-inline {
992
+ flex-direction: column;
993
+ align-items: stretch;
994
+ gap: 8px;
995
+ }
996
+
997
+ .s2s-actions {
998
+ justify-content: flex-start;
999
+ }
1000
+
1001
+ .s2s-actions .s2s-feedback {
1002
+ white-space: normal;
1003
+ }
1004
+
1005
+ .s2s-keyRow {
1006
+ grid-template-columns: minmax(0, 1fr);
1007
+ min-height: 44px;
1008
+ padding: 8px 10px;
1009
+ }
1010
+
1011
+ .s2s-keyRow > :last-child {
1012
+ justify-self: start;
1013
+ text-align: left;
1014
+ }
1015
+
1016
+ .s2s-selectField,
1017
+ .s2s-groupOption {
1018
+ min-height: 44px;
1019
+ }
1020
+
1021
+ .s2s-input,
1022
+ .s2s-groupPicker {
1023
+ flex-basis: 100%;
1024
+ }
1025
+
1026
+ .s2s-searchInput {
1027
+ width: 100%;
1028
+ flex: 1 1 auto;
1029
+ }
1030
+
1031
+ .s2s-pager {
1032
+ justify-content: center;
1033
+ }
1034
+
1035
+ .s2s-footerStrip {
1036
+ flex-direction: column;
1037
+ align-items: flex-start;
1038
+ gap: 6px;
1039
+ }
1040
+
1041
+ .s2s-reportRemove {
1042
+ margin-left: 0;
1043
+ align-self: flex-end;
1044
+ }
1045
+
1046
+ /* Keyboard clearance: the fixed-position panel must not run under the
1047
+ soft keyboard; dvh tracks the visual viewport on Android WebViews. */
1048
+ .s2s-groupPanel {
1049
+ max-height: 60dvh;
1050
+ }
1051
+ }
1052
+
1053
+ /* Notch and gesture-bar insets on standalone web islands. */
1054
+ @supports (padding: max(0px)) {
1055
+ @media (max-width: 640px) {
1056
+ .s2s-section {
1057
+ padding-left: max(8px, env(safe-area-inset-left));
1058
+ padding-right: max(8px, env(safe-area-inset-right));
1059
+ }
1060
+ }
1061
+ }
937
1062
  `;
938
1063
  function ensureCss() {
939
1064
  if (document.querySelector("style[data-plugin=\"dsh-sub2api-sync\"]") !== null) return;
@@ -1824,6 +1949,7 @@ function actionToneOf(action) {
1824
1949
  }
1825
1950
  function Sub2apiSyncSection({ t, api, config, useStatus: useStatus$1 }) {
1826
1951
  const status = useStatus$1((value) => value);
1952
+ const routesReachable = useStatusHealth();
1827
1953
  const snapshot$1 = useConfigForm(config);
1828
1954
  const configValues = snapshot$1.value;
1829
1955
  const writable = snapshot$1.writable;
@@ -2019,6 +2145,10 @@ function Sub2apiSyncSection({ t, api, config, useStatus: useStatus$1 }) {
2019
2145
  className: "s2s-warn",
2020
2146
  children: t("configUnavailable")
2021
2147
  }),
2148
+ !routesReachable && (0, react_jsx_runtime.jsx)("p", {
2149
+ className: "s2s-warn",
2150
+ children: t("routesUnreachableWarning")
2151
+ }),
2022
2152
  (0, react_jsx_runtime.jsx)("section", {
2023
2153
  className: "s2s-card",
2024
2154
  children: (0, react_jsx_runtime.jsxs)("div", {
@@ -2072,7 +2202,7 @@ function Sub2apiSyncSection({ t, api, config, useStatus: useStatus$1 }) {
2072
2202
  onClick: () => {
2073
2203
  setSignInOpen(true);
2074
2204
  },
2075
- children: t("signIn")
2205
+ children: t("connectPanel")
2076
2206
  })
2077
2207
  ]
2078
2208
  })]
@@ -2248,22 +2378,32 @@ function parseNumber(text) {
2248
2378
  function messageOf(error) {
2249
2379
  return error instanceof Error ? error.message : String(error);
2250
2380
  }
2251
- function BundleConfigCard({ view, form, t }) {
2252
- if (view !== "page" || form === undefined) return null;
2381
+ function BundleConfigCard({ view, t, config }) {
2382
+ if (view !== "page") return null;
2253
2383
  return (0, react_jsx_runtime.jsx)(BundleConfigFields, {
2254
- form,
2384
+ config,
2255
2385
  t
2256
2386
  });
2257
2387
  }
2258
- function BundleConfigFields({ form, t }) {
2259
- const values = form.state.value ?? {};
2260
- const writable = form.state.writable;
2388
+ function BundleConfigFields({ config, t }) {
2389
+ const snapshot$1 = useConfigForm(config);
2390
+ const values = snapshot$1.value ?? {};
2391
+ const writable = snapshot$1.writable;
2261
2392
  const [busy, setBusy] = (0, react.useState)(false);
2262
2393
  const [feedback, setFeedback] = (0, react.useState)(undefined);
2263
2394
  const run = (ops) => {
2264
2395
  setBusy(true);
2265
2396
  setFeedback(undefined);
2266
- form.mutate(ops).then((accepted) => {
2397
+ const apply$1 = async () => {
2398
+ for (const op of ops) {
2399
+ const field = op.path[0];
2400
+ if (field === undefined || field === "") return false;
2401
+ const accepted = op.op === "set" ? await setField(config, field, op.value) : await unsetField(config, field);
2402
+ if (!accepted) return false;
2403
+ }
2404
+ return true;
2405
+ };
2406
+ apply$1().then((accepted) => {
2267
2407
  setFeedback(accepted ? {
2268
2408
  tone: "ok",
2269
2409
  text: t("savedNotice")
@@ -2523,6 +2663,9 @@ function footerModel(status) {
2523
2663
  ...status?.lastReportAt !== undefined ? { lastReportAt: status.lastReportAt } : {}
2524
2664
  };
2525
2665
  }
2666
+ function detailCardModel(status) {
2667
+ return status?.auth?.signedIn === true ? "report" : "connect";
2668
+ }
2526
2669
  function relativeMinutes(at, now) {
2527
2670
  const minutes = Math.max(0, Math.floor((now - at) / 6e4));
2528
2671
  if (minutes < 1) return { key: "footerJustNow" };
@@ -2608,11 +2751,64 @@ function DetailBadgeBody({ t }) {
2608
2751
  });
2609
2752
  }
2610
2753
  function DetailSection(props) {
2611
- const { subject, t } = props;
2754
+ const { subject, t, api, config } = props;
2612
2755
  if (!isOwnBundle(subject)) return null;
2613
- return (0, react_jsx_runtime.jsx)(DetailSectionBody, { t });
2756
+ return (0, react_jsx_runtime.jsx)(DetailSectionBody, {
2757
+ t,
2758
+ api,
2759
+ config
2760
+ });
2761
+ }
2762
+ function DetailSectionBody({ t, api, config }) {
2763
+ const status = useStatus();
2764
+ const reachable$1 = useStatusHealth();
2765
+ const snapshot$1 = useConfigForm(config);
2766
+ const [signInOpen, setSignInOpen] = (0, react.useState)(false);
2767
+ if (detailCardModel(status) === "connect") return (0, react_jsx_runtime.jsxs)("section", {
2768
+ className: "s2s-card",
2769
+ children: [
2770
+ (0, react_jsx_runtime.jsx)("span", {
2771
+ className: "s2s-name",
2772
+ children: t("detailConnectTitle")
2773
+ }),
2774
+ (0, react_jsx_runtime.jsx)("p", {
2775
+ className: "s2s-hint",
2776
+ children: t("detailConnectHint")
2777
+ }),
2778
+ (0, react_jsx_runtime.jsx)("div", {
2779
+ className: "s2s-actions",
2780
+ children: (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Button, {
2781
+ variant: "primary",
2782
+ disabled: !reachable$1,
2783
+ onClick: () => {
2784
+ setSignInOpen(true);
2785
+ },
2786
+ children: t("connectPanel")
2787
+ })
2788
+ }),
2789
+ !reachable$1 && (0, react_jsx_runtime.jsx)("p", {
2790
+ className: "s2s-warn",
2791
+ children: t("routesUnreachableWarning")
2792
+ }),
2793
+ (0, react_jsx_runtime.jsx)(SignInDialog, {
2794
+ open: signInOpen,
2795
+ onClose: () => {
2796
+ setSignInOpen(false);
2797
+ },
2798
+ api,
2799
+ config,
2800
+ baseURL: snapshot$1.value?.baseURL,
2801
+ writable: snapshot$1.writable,
2802
+ onSignedIn: () => {
2803
+ void refreshStatus();
2804
+ },
2805
+ t
2806
+ })
2807
+ ]
2808
+ });
2809
+ return (0, react_jsx_runtime.jsx)(DetailReportCard, { t });
2614
2810
  }
2615
- function DetailSectionBody({ t }) {
2811
+ function DetailReportCard({ t }) {
2616
2812
  const status = useStatus();
2617
2813
  const report = status?.lastReport;
2618
2814
  return (0, react_jsx_runtime.jsxs)("section", {
@@ -2702,10 +2898,12 @@ function mount(ctx) {
2702
2898
  locale: NS,
2703
2899
  inject: () => sectionOps
2704
2900
  }, Sub2apiSyncSection));
2901
+ const bundleConfigOps = { config };
2705
2902
  ctx.slots.inject("plugins.bundle.config", () => ctx.slots.register({
2706
2903
  name: "plugins.bundle.config",
2707
2904
  key: PKG_NAME,
2708
- locale: NS
2905
+ locale: NS,
2906
+ inject: () => bundleConfigOps
2709
2907
  }, BundleConfigCard));
2710
2908
  ctx.slots.inject("settings.models.provider-card", () => ctx.slots.register({
2711
2909
  name: "settings.models.provider-card",
@@ -2723,10 +2921,15 @@ function mount(ctx) {
2723
2921
  id: "sub2api-sync",
2724
2922
  locale: NS
2725
2923
  }, DetailBadge));
2924
+ const detailOps = {
2925
+ api: sectionOps.api,
2926
+ config
2927
+ };
2726
2928
  ctx.slots.inject("plugins.detail.section", () => ctx.slots.register({
2727
2929
  name: "plugins.detail.section",
2728
2930
  id: "sub2api-sync",
2729
- locale: NS
2931
+ locale: NS,
2932
+ inject: () => detailOps
2730
2933
  }, DetailSection));
2731
2934
  const activationOps = { openBundleDetails: () => {
2732
2935
  const nav = ctx.pluginNavigation;