@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.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: "登录后可发现分组、勾选同步,自动为每个分组准备 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,7 +116,7 @@ const zh = {
114
116
  bundleDeleteOrphanHint: "仅对记录过 keyId 的分组生效",
115
117
  bundleEnrichLabel: "补全模型元数据",
116
118
  bundleEnrichHint: "同步时按模型 id 从 models.dev 补全容量元数据;关闭则仅用内置快照(离线)",
117
- bundleAllowRemoteLabel: "允许私网设备调用动作接口",
119
+ bundleAllowRemoteLabel: "允许私网设备调用动作路由",
118
120
  bundleAllowRemoteWarning: "开启后同源或私网页面可改写模型路由与凭据——仅在可信内网开启",
119
121
  bundleAdvancedHint: "protocolOverrides / compatTemplate 高级字段请编辑 cordis.patch.yml",
120
122
  badgeManagedTag: "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",
@@ -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
  }
@@ -940,6 +976,88 @@ const CSS = `
940
976
  font-size: 13px;
941
977
  line-height: 22px;
942
978
  }
979
+
980
+ /* Mobile (dsh-Go web island / narrow windows): stack the row layouts, keep
981
+ interactive rows at Android's 44dp touch floor, and let the anchored group
982
+ panel clear the soft keyboard. The desktop spec above is untouched — every
983
+ rule here is scoped to the narrow viewport. */
984
+ @media (max-width: 640px) {
985
+ .s2s-section {
986
+ gap: 12px;
987
+ }
988
+
989
+ .s2s-row,
990
+ .s2s-inline {
991
+ flex-direction: column;
992
+ align-items: stretch;
993
+ gap: 8px;
994
+ }
995
+
996
+ .s2s-actions {
997
+ justify-content: flex-start;
998
+ }
999
+
1000
+ .s2s-actions .s2s-feedback {
1001
+ white-space: normal;
1002
+ }
1003
+
1004
+ .s2s-keyRow {
1005
+ grid-template-columns: minmax(0, 1fr);
1006
+ min-height: 44px;
1007
+ padding: 8px 10px;
1008
+ }
1009
+
1010
+ .s2s-keyRow > :last-child {
1011
+ justify-self: start;
1012
+ text-align: left;
1013
+ }
1014
+
1015
+ .s2s-selectField,
1016
+ .s2s-groupOption {
1017
+ min-height: 44px;
1018
+ }
1019
+
1020
+ .s2s-input,
1021
+ .s2s-groupPicker {
1022
+ flex-basis: 100%;
1023
+ }
1024
+
1025
+ .s2s-searchInput {
1026
+ width: 100%;
1027
+ flex: 1 1 auto;
1028
+ }
1029
+
1030
+ .s2s-pager {
1031
+ justify-content: center;
1032
+ }
1033
+
1034
+ .s2s-footerStrip {
1035
+ flex-direction: column;
1036
+ align-items: flex-start;
1037
+ gap: 6px;
1038
+ }
1039
+
1040
+ .s2s-reportRemove {
1041
+ margin-left: 0;
1042
+ align-self: flex-end;
1043
+ }
1044
+
1045
+ /* Keyboard clearance: the fixed-position panel must not run under the
1046
+ soft keyboard; dvh tracks the visual viewport on Android WebViews. */
1047
+ .s2s-groupPanel {
1048
+ max-height: 60dvh;
1049
+ }
1050
+ }
1051
+
1052
+ /* Notch and gesture-bar insets on standalone web islands. */
1053
+ @supports (padding: max(0px)) {
1054
+ @media (max-width: 640px) {
1055
+ .s2s-section {
1056
+ padding-left: max(8px, env(safe-area-inset-left));
1057
+ padding-right: max(8px, env(safe-area-inset-right));
1058
+ }
1059
+ }
1060
+ }
943
1061
  `;
944
1062
  function ensureCss() {
945
1063
  if (document.querySelector("style[data-plugin=\"dsh-sub2api-sync\"]") !== null) return;
@@ -1830,6 +1948,7 @@ function actionToneOf(action) {
1830
1948
  }
1831
1949
  function Sub2apiSyncSection({ t, api, config, useStatus: useStatus$1 }) {
1832
1950
  const status = useStatus$1((value) => value);
1951
+ const routesReachable = useStatusHealth();
1833
1952
  const snapshot$1 = useConfigForm(config);
1834
1953
  const configValues = snapshot$1.value;
1835
1954
  const writable = snapshot$1.writable;
@@ -2025,6 +2144,10 @@ function Sub2apiSyncSection({ t, api, config, useStatus: useStatus$1 }) {
2025
2144
  className: "s2s-warn",
2026
2145
  children: t("configUnavailable")
2027
2146
  }),
2147
+ !routesReachable && (0, react_jsx_runtime.jsx)("p", {
2148
+ className: "s2s-warn",
2149
+ children: t("routesUnreachableWarning")
2150
+ }),
2028
2151
  (0, react_jsx_runtime.jsx)("section", {
2029
2152
  className: "s2s-card",
2030
2153
  children: (0, react_jsx_runtime.jsxs)("div", {
@@ -2078,7 +2201,7 @@ function Sub2apiSyncSection({ t, api, config, useStatus: useStatus$1 }) {
2078
2201
  onClick: () => {
2079
2202
  setSignInOpen(true);
2080
2203
  },
2081
- children: t("signIn")
2204
+ children: t("connectPanel")
2082
2205
  })
2083
2206
  ]
2084
2207
  })]
@@ -2254,22 +2377,32 @@ function parseNumber(text) {
2254
2377
  function messageOf(error) {
2255
2378
  return error instanceof Error ? error.message : String(error);
2256
2379
  }
2257
- function BundleConfigCard({ view, form, t }) {
2258
- if (view !== "page" || form === undefined) return null;
2380
+ function BundleConfigCard({ view, t, config }) {
2381
+ if (view !== "page") return null;
2259
2382
  return (0, react_jsx_runtime.jsx)(BundleConfigFields, {
2260
- form,
2383
+ config,
2261
2384
  t
2262
2385
  });
2263
2386
  }
2264
- function BundleConfigFields({ form, t }) {
2265
- const values = form.state.value ?? {};
2266
- const writable = form.state.writable;
2387
+ function BundleConfigFields({ config, t }) {
2388
+ const snapshot$1 = useConfigForm(config);
2389
+ const values = snapshot$1.value ?? {};
2390
+ const writable = snapshot$1.writable;
2267
2391
  const [busy, setBusy] = (0, react.useState)(false);
2268
2392
  const [feedback, setFeedback] = (0, react.useState)(undefined);
2269
2393
  const run = (ops) => {
2270
2394
  setBusy(true);
2271
2395
  setFeedback(undefined);
2272
- form.mutate(ops).then((accepted) => {
2396
+ const apply$1 = async () => {
2397
+ for (const op of ops) {
2398
+ const field = op.path[0];
2399
+ if (field === undefined || field === "") return false;
2400
+ const accepted = op.op === "set" ? await setField(config, field, op.value) : await unsetField(config, field);
2401
+ if (!accepted) return false;
2402
+ }
2403
+ return true;
2404
+ };
2405
+ apply$1().then((accepted) => {
2273
2406
  setFeedback(accepted ? {
2274
2407
  tone: "ok",
2275
2408
  text: t("savedNotice")
@@ -2529,6 +2662,9 @@ function footerModel(status) {
2529
2662
  ...status?.lastReportAt !== undefined ? { lastReportAt: status.lastReportAt } : {}
2530
2663
  };
2531
2664
  }
2665
+ function detailCardModel(status) {
2666
+ return status?.auth?.signedIn === true ? "report" : "connect";
2667
+ }
2532
2668
  function relativeMinutes(at, now) {
2533
2669
  const minutes = Math.max(0, Math.floor((now - at) / 6e4));
2534
2670
  if (minutes < 1) return { key: "footerJustNow" };
@@ -2614,11 +2750,64 @@ function DetailBadgeBody({ t }) {
2614
2750
  });
2615
2751
  }
2616
2752
  function DetailSection(props) {
2617
- const { subject, t } = props;
2753
+ const { subject, t, api, config } = props;
2618
2754
  if (!isOwnBundle(subject)) return null;
2619
- return (0, react_jsx_runtime.jsx)(DetailSectionBody, { t });
2755
+ return (0, react_jsx_runtime.jsx)(DetailSectionBody, {
2756
+ t,
2757
+ api,
2758
+ config
2759
+ });
2620
2760
  }
2621
- function DetailSectionBody({ t }) {
2761
+ function DetailSectionBody({ t, api, config }) {
2762
+ const status = useStatus();
2763
+ const reachable$1 = useStatusHealth();
2764
+ const snapshot$1 = useConfigForm(config);
2765
+ const [signInOpen, setSignInOpen] = (0, react.useState)(false);
2766
+ if (detailCardModel(status) === "connect") return (0, react_jsx_runtime.jsxs)("section", {
2767
+ className: "s2s-card",
2768
+ children: [
2769
+ (0, react_jsx_runtime.jsx)("span", {
2770
+ className: "s2s-name",
2771
+ children: t("detailConnectTitle")
2772
+ }),
2773
+ (0, react_jsx_runtime.jsx)("p", {
2774
+ className: "s2s-hint",
2775
+ children: t("detailConnectHint")
2776
+ }),
2777
+ (0, react_jsx_runtime.jsx)("div", {
2778
+ className: "s2s-actions",
2779
+ children: (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Button, {
2780
+ variant: "primary",
2781
+ disabled: !reachable$1,
2782
+ onClick: () => {
2783
+ setSignInOpen(true);
2784
+ },
2785
+ children: t("connectPanel")
2786
+ })
2787
+ }),
2788
+ !reachable$1 && (0, react_jsx_runtime.jsx)("p", {
2789
+ className: "s2s-warn",
2790
+ children: t("routesUnreachableWarning")
2791
+ }),
2792
+ (0, react_jsx_runtime.jsx)(SignInDialog, {
2793
+ open: signInOpen,
2794
+ onClose: () => {
2795
+ setSignInOpen(false);
2796
+ },
2797
+ api,
2798
+ config,
2799
+ baseURL: snapshot$1.value?.baseURL,
2800
+ writable: snapshot$1.writable,
2801
+ onSignedIn: () => {
2802
+ void refreshStatus();
2803
+ },
2804
+ t
2805
+ })
2806
+ ]
2807
+ });
2808
+ return (0, react_jsx_runtime.jsx)(DetailReportCard, { t });
2809
+ }
2810
+ function DetailReportCard({ t }) {
2622
2811
  const status = useStatus();
2623
2812
  const report = status?.lastReport;
2624
2813
  return (0, react_jsx_runtime.jsxs)("section", {
@@ -2708,10 +2897,12 @@ function mount(ctx) {
2708
2897
  locale: NS,
2709
2898
  inject: () => sectionOps
2710
2899
  }, Sub2apiSyncSection));
2900
+ const bundleConfigOps = { config };
2711
2901
  ctx.slots.inject("plugins.bundle.config", () => ctx.slots.register({
2712
2902
  name: "plugins.bundle.config",
2713
2903
  key: PKG_NAME,
2714
- locale: NS
2904
+ locale: NS,
2905
+ inject: () => bundleConfigOps
2715
2906
  }, BundleConfigCard));
2716
2907
  ctx.slots.inject("settings.models.provider-card", () => ctx.slots.register({
2717
2908
  name: "settings.models.provider-card",
@@ -2729,10 +2920,15 @@ function mount(ctx) {
2729
2920
  id: "sub2api-sync",
2730
2921
  locale: NS
2731
2922
  }, DetailBadge));
2923
+ const detailOps = {
2924
+ api: sectionOps.api,
2925
+ config
2926
+ };
2732
2927
  ctx.slots.inject("plugins.detail.section", () => ctx.slots.register({
2733
2928
  name: "plugins.detail.section",
2734
2929
  id: "sub2api-sync",
2735
- locale: NS
2930
+ locale: NS,
2931
+ inject: () => detailOps
2736
2932
  }, DetailSection));
2737
2933
  const activationOps = { openBundleDetails: () => {
2738
2934
  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;