@jingyi0605/codingns4dsh 0.1.0 → 0.1.1

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.
Files changed (216) hide show
  1. package/README.en.md +306 -0
  2. package/README.md +190 -993
  3. package/data/build/dist/client/account-bar.d.ts +2 -2
  4. package/data/build/dist/client/account-bar.js +96 -39
  5. package/data/build/dist/client/bundle.js +13722 -3611
  6. package/data/build/dist/client/cli-catalog.js +20 -3
  7. package/data/build/dist/client/cli-slots.d.ts +5 -0
  8. package/data/build/dist/client/cli-slots.js +57 -7
  9. package/data/build/dist/client/debug/ui.d.ts +1 -1
  10. package/data/build/dist/client/debug/ui.js +62 -32
  11. package/data/build/dist/client/dsh-runtime-version.d.ts +9 -2
  12. package/data/build/dist/client/dsh-runtime-version.js +24 -2
  13. package/data/build/dist/client/features/cli-adapters.d.ts +1 -1
  14. package/data/build/dist/client/features/cli-adapters.js +19 -16
  15. package/data/build/dist/client/features/debug.js +2 -2
  16. package/data/build/dist/client/features/index.d.ts +2 -1
  17. package/data/build/dist/client/features/index.js +3 -1
  18. package/data/build/dist/client/features/lan-access-panel.d.ts +1 -1
  19. package/data/build/dist/client/features/lan-access-panel.js +8 -10
  20. package/data/build/dist/client/features/lan-access.js +1 -1
  21. package/data/build/dist/client/features/login-protection-panel.d.ts +1 -1
  22. package/data/build/dist/client/features/login-protection-panel.js +7 -9
  23. package/data/build/dist/client/features/reverse-proxy-panel.d.ts +1 -1
  24. package/data/build/dist/client/features/reverse-proxy-panel.js +99 -11
  25. package/data/build/dist/client/features/reverse-proxy.d.ts +1 -1
  26. package/data/build/dist/client/features/reverse-proxy.js +19 -18
  27. package/data/build/dist/client/features/terminal-enhancement-panel.d.ts +1 -1
  28. package/data/build/dist/client/features/terminal-enhancement-panel.js +62 -55
  29. package/data/build/dist/client/features/terminal-enhancement.d.ts +1 -1
  30. package/data/build/dist/client/features/terminal-enhancement.js +3 -3
  31. package/data/build/dist/client/features/types.d.ts +14 -3
  32. package/data/build/dist/client/features/workspace-session-enhancement-panel.d.ts +1 -1
  33. package/data/build/dist/client/features/workspace-session-enhancement-panel.js +31 -9
  34. package/data/build/dist/client/features/workspace-session-enhancement.js +72 -11
  35. package/data/build/dist/client/git-management.d.ts +16 -0
  36. package/data/build/dist/client/git-management.js +757 -0
  37. package/data/build/dist/client/index.d.ts +3 -2
  38. package/data/build/dist/client/index.js +152 -27
  39. package/data/build/dist/client/locale.js +91 -29
  40. package/data/build/dist/client/quick-phrase-layout.d.ts +49 -0
  41. package/data/build/dist/client/quick-phrase-layout.js +95 -0
  42. package/data/build/dist/client/quick-phrase-slot.d.ts +7 -0
  43. package/data/build/dist/client/quick-phrase-slot.js +370 -0
  44. package/data/build/dist/client/remote-web-context.js +1 -1
  45. package/data/build/dist/client/settings-bridge.d.ts +9 -6
  46. package/data/build/dist/client/settings-bridge.js +51 -27
  47. package/data/build/dist/client/settings-section.d.ts +3 -2
  48. package/data/build/dist/client/settings-section.js +33 -19
  49. package/data/build/dist/client/subscription-slot.d.ts +3 -0
  50. package/data/build/dist/client/subscription-slot.js +108 -16
  51. package/data/build/dist/client/terminal/model.d.ts +14 -2
  52. package/data/build/dist/client/terminal/model.js +109 -21
  53. package/data/build/dist/client/terminal/recovery.d.ts +50 -0
  54. package/data/build/dist/client/terminal/recovery.js +101 -0
  55. package/data/build/dist/client/terminal/ui.d.ts +2 -2
  56. package/data/build/dist/client/terminal/ui.js +49 -11
  57. package/data/build/dist/client/terminal/xterm-view.d.ts +2 -2
  58. package/data/build/dist/client/terminal/xterm-view.js +2 -1
  59. package/data/build/dist/client/theme.d.ts +6 -1
  60. package/data/build/dist/client/theme.js +32 -1
  61. package/data/build/dist/client/workspace-session-archive-dom.d.ts +12 -0
  62. package/data/build/dist/client/workspace-session-archive-dom.js +27 -5
  63. package/data/build/dist/client/workspace-session-visibility-dom.d.ts +30 -0
  64. package/data/build/dist/client/workspace-session-visibility-dom.js +604 -0
  65. package/data/build/dist/dsh-capabilities/client/config-forms-adapter.d.ts +50 -0
  66. package/data/build/dist/dsh-capabilities/client/config-forms-adapter.js +139 -0
  67. package/data/build/dist/dsh-capabilities/client/conversation-adapter.d.ts +7 -0
  68. package/data/build/dist/dsh-capabilities/client/conversation-adapter.js +6 -0
  69. package/data/build/dist/dsh-capabilities/client/locale-adapter.d.ts +9 -0
  70. package/data/build/dist/dsh-capabilities/client/locale-adapter.js +4 -0
  71. package/data/build/dist/dsh-capabilities/client/primitives-adapter.d.ts +10 -0
  72. package/data/build/dist/dsh-capabilities/client/primitives-adapter.js +14 -0
  73. package/data/build/dist/dsh-capabilities/client/settings-scope-adapter.d.ts +8 -0
  74. package/data/build/dist/dsh-capabilities/client/settings-scope-adapter.js +16 -0
  75. package/data/build/dist/dsh-capabilities/client/sidebar-adapter.d.ts +7 -0
  76. package/data/build/dist/dsh-capabilities/client/sidebar-adapter.js +5 -0
  77. package/data/build/dist/dsh-capabilities/client/theme-adapter.d.ts +6 -0
  78. package/data/build/dist/dsh-capabilities/client/theme-adapter.js +4 -0
  79. package/data/build/dist/dsh-capabilities/client/typert-adapter.d.ts +5 -0
  80. package/data/build/dist/dsh-capabilities/client/typert-adapter.js +4 -0
  81. package/data/build/dist/dsh-capabilities/host/config-forms-adapter.d.ts +19 -0
  82. package/data/build/dist/dsh-capabilities/host/config-forms-adapter.js +45 -0
  83. package/data/build/dist/dsh-capabilities/host/connection-rpc-adapter.d.ts +19 -0
  84. package/data/build/dist/dsh-capabilities/host/connection-rpc-adapter.js +13 -0
  85. package/data/build/dist/dsh-capabilities/host/settings-scope-adapter.d.ts +8 -0
  86. package/data/build/dist/dsh-capabilities/host/settings-scope-adapter.js +47 -0
  87. package/data/build/dist/dsh-capabilities/host/typert-adapter.d.ts +6 -0
  88. package/data/build/dist/dsh-capabilities/host/typert-adapter.js +4 -0
  89. package/data/build/dist/dsh-capabilities/index.d.ts +6 -0
  90. package/data/build/dist/dsh-capabilities/index.js +6 -0
  91. package/data/build/dist/dsh-capabilities/matrix.d.ts +15 -0
  92. package/data/build/dist/dsh-capabilities/matrix.js +32 -0
  93. package/data/build/dist/dsh-capabilities/registry.d.ts +22 -0
  94. package/data/build/dist/dsh-capabilities/registry.js +196 -0
  95. package/data/build/dist/dsh-capabilities/routes.d.ts +10 -0
  96. package/data/build/dist/dsh-capabilities/routes.js +34 -0
  97. package/data/build/dist/dsh-capabilities/settings-store.d.ts +29 -0
  98. package/data/build/dist/dsh-capabilities/settings-store.js +5 -0
  99. package/data/build/dist/dsh-capabilities/types.d.ts +50 -0
  100. package/data/build/dist/dsh-capabilities/types.js +23 -0
  101. package/data/build/dist/features/registry.d.ts +8 -2
  102. package/data/build/dist/features/registry.js +104 -1
  103. package/data/build/dist/host/auth-session.d.ts +8 -1
  104. package/data/build/dist/host/auth-session.js +28 -6
  105. package/data/build/dist/host/cli-adapters/claude-driver.d.ts +2 -0
  106. package/data/build/dist/host/cli-adapters/claude-driver.js +19 -4
  107. package/data/build/dist/host/cli-adapters/claude-model-options.d.ts +14 -0
  108. package/data/build/dist/host/cli-adapters/claude-model-options.js +157 -0
  109. package/data/build/dist/host/cli-adapters/codex-driver.d.ts +11 -0
  110. package/data/build/dist/host/cli-adapters/codex-driver.js +166 -14
  111. package/data/build/dist/host/cli-adapters/driver.d.ts +2 -0
  112. package/data/build/dist/host/cli-adapters/dsh-message-projector.d.ts +15 -0
  113. package/data/build/dist/host/cli-adapters/dsh-message-projector.js +120 -29
  114. package/data/build/dist/host/cli-adapters/dsh-tool-history.d.ts +4 -1
  115. package/data/build/dist/host/cli-adapters/dsh-tool-history.js +25 -7
  116. package/data/build/dist/host/cli-adapters/feature.js +74 -8
  117. package/data/build/dist/host/cli-adapters/gemini-driver.js +2 -2
  118. package/data/build/dist/host/cli-adapters/grok-driver.js +2 -2
  119. package/data/build/dist/host/cli-adapters/json-rpc-process.d.ts +4 -2
  120. package/data/build/dist/host/cli-adapters/json-rpc-process.js +8 -9
  121. package/data/build/dist/host/cli-adapters/kimi-driver.js +1 -1
  122. package/data/build/dist/host/cli-adapters/legacy-session-adapter.d.ts +17 -0
  123. package/data/build/dist/host/cli-adapters/legacy-session-adapter.js +88 -0
  124. package/data/build/dist/host/cli-adapters/legacy-session-settings.d.ts +24 -0
  125. package/data/build/dist/host/cli-adapters/legacy-session-settings.js +162 -0
  126. package/data/build/dist/host/cli-adapters/official-provider-subscription.d.ts +29 -0
  127. package/data/build/dist/host/cli-adapters/official-provider-subscription.js +361 -0
  128. package/data/build/dist/host/cli-adapters/opencode-driver.d.ts +2 -0
  129. package/data/build/dist/host/cli-adapters/opencode-driver.js +97 -12
  130. package/data/build/dist/host/cli-adapters/pi-driver.js +69 -23
  131. package/data/build/dist/host/cli-adapters/provider-registry.d.ts +33 -0
  132. package/data/build/dist/host/cli-adapters/provider-registry.js +83 -0
  133. package/data/build/dist/host/cli-adapters/provider-subscription.d.ts +25 -3
  134. package/data/build/dist/host/cli-adapters/provider-subscription.js +346 -30
  135. package/data/build/dist/host/cli-adapters/registry.d.ts +6 -0
  136. package/data/build/dist/host/cli-adapters/registry.js +170 -13
  137. package/data/build/dist/host/cli-adapters/rpc-driver-utils.js +10 -0
  138. package/data/build/dist/host/cli-adapters/session-store.d.ts +7 -0
  139. package/data/build/dist/host/cli-adapters/session-store.js +39 -1
  140. package/data/build/dist/host/cli-adapters/standard-stream-driver.d.ts +3 -0
  141. package/data/build/dist/host/cli-adapters/stream-normalizer.d.ts +3 -0
  142. package/data/build/dist/host/cli-adapters/stream-normalizer.js +22 -2
  143. package/data/build/dist/host/control-api-client.d.ts +3 -3
  144. package/data/build/dist/host/control-api-client.js +2 -1
  145. package/data/build/dist/host/credential-store.js +11 -1
  146. package/data/build/dist/host/dsh-device-runtime.d.ts +4 -0
  147. package/data/build/dist/host/dsh-device-runtime.js +20 -9
  148. package/data/build/dist/host/dsh-runtime-version.d.ts +3 -2
  149. package/data/build/dist/host/dsh-runtime-version.js +71 -2
  150. package/data/build/dist/host/features/auth.js +8 -5
  151. package/data/build/dist/host/features/debug.js +1 -1
  152. package/data/build/dist/host/features/git-management.d.ts +5 -0
  153. package/data/build/dist/host/features/git-management.js +290 -0
  154. package/data/build/dist/host/features/host-status.js +1 -1
  155. package/data/build/dist/host/features/index.d.ts +1 -0
  156. package/data/build/dist/host/features/index.js +3 -0
  157. package/data/build/dist/host/features/terminal-process.js +1 -1
  158. package/data/build/dist/host/features/terminal-status.d.ts +3 -1
  159. package/data/build/dist/host/features/terminal-status.js +3 -1
  160. package/data/build/dist/host/index.d.ts +7 -3
  161. package/data/build/dist/host/index.js +79 -15
  162. package/data/build/dist/host/lan-access-dsh.js +18 -5
  163. package/data/build/dist/host/native-session-bridge.d.ts +23 -1
  164. package/data/build/dist/host/native-session-bridge.js +157 -10
  165. package/data/build/dist/host/rpc-table.d.ts +1 -1
  166. package/data/build/dist/host/rpc.js +60 -10
  167. package/data/build/dist/host/session-migration-repair.d.ts +19 -0
  168. package/data/build/dist/host/session-migration-repair.js +395 -0
  169. package/data/build/dist/host/settings.d.ts +8 -1
  170. package/data/build/dist/host/settings.js +94 -3
  171. package/data/build/dist/host/terminal/backends/tmux-backend.d.ts +1 -0
  172. package/data/build/dist/host/terminal/backends/tmux-backend.js +4 -2
  173. package/data/build/dist/host/terminal/controller-factory.d.ts +4 -0
  174. package/data/build/dist/host/terminal/controller-factory.js +26 -7
  175. package/data/build/dist/host/terminal/runtime-manager.d.ts +2 -1
  176. package/data/build/dist/host/terminal/runtime-manager.js +3 -0
  177. package/data/build/dist/host/terminal/startup.d.ts +2 -0
  178. package/data/build/dist/host/terminal/startup.js +2 -1
  179. package/data/build/dist/host/terminal/terminal-controller.d.ts +10 -3
  180. package/data/build/dist/host/terminal/terminal-controller.js +101 -15
  181. package/data/build/dist/host/terminal/terminal-process-service.js +3 -1
  182. package/data/build/dist/host/terminal/terminal-service.d.ts +2 -0
  183. package/data/build/dist/host/terminal/terminal-service.js +10 -0
  184. package/data/build/dist/index.d.ts +6 -2
  185. package/data/build/dist/index.js +8 -2
  186. package/data/build/dist/shared/contracts/cli-adapter.d.ts +14 -0
  187. package/data/build/dist/shared/contracts/config.d.ts +31 -1
  188. package/data/build/dist/shared/contracts/config.js +25 -0
  189. package/data/build/dist/shared/contracts/dsh-device.d.ts +13 -1
  190. package/data/build/dist/shared/contracts/errors.d.ts +1 -1
  191. package/data/build/dist/shared/contracts/feature.d.ts +6 -0
  192. package/data/build/dist/shared/contracts/git.d.ts +82 -0
  193. package/data/build/dist/shared/contracts/git.js +3 -0
  194. package/data/build/dist/shared/contracts/subscription.d.ts +46 -0
  195. package/data/build/dist/shared/contracts/terminal.d.ts +4 -1
  196. package/data/build/dist/shared/contracts/version.d.ts +2 -2
  197. package/data/build/dist/shared/contracts/version.js +4 -6
  198. package/data/build/dist/shared/debug.d.ts +14 -0
  199. package/data/build/dist/shared/debug.js +53 -0
  200. package/data/build/dist/shared/index.d.ts +7 -4
  201. package/data/build/dist/shared/index.js +2 -1
  202. package/data/build/dist/transport/debug.d.ts +1 -1
  203. package/data/build/dist/transport/debug.js +3 -30
  204. package/data/build/dist/transport/dsh-session.js +3 -3
  205. package/data/build/dist/typert.host.d.ts +1 -1
  206. package/data/build/dist/typert.host.js +6 -3
  207. package/data/build/dist/typert.remote-client.d.ts +11 -0
  208. package/data/build/dist/typert.remote-client.js +7 -0
  209. package/dsh.bundle.patch +1 -1
  210. package/package.json +13 -5
  211. package/profile/README.md +4 -4
  212. package/profile/package.json +6 -4
  213. package/profile/scripts/check-dsh-install.mjs +2 -2
  214. package/profile/version.json +2 -2
  215. package/scripts/check-dsh-install.mjs +2 -2
  216. package/version.json +2 -2
@@ -5,6 +5,7 @@ import { cliAdaptersFeature } from './cli-adapters.js';
5
5
  import { terminalEnhancementFeature } from './terminal-enhancement.js';
6
6
  import { workspaceSessionEnhancementFeature } from './workspace-session-enhancement.js';
7
7
  import { debugFeature } from './debug.js';
8
+ import { gitManagementFeature } from '../git-management.js';
8
9
  /** Client 侧功能模块清单:新增模块在这里登记一行,不需要改动设置页和入口。 */
9
10
  export const CLIENT_FEATURES = [
10
11
  lanAccessFeature,
@@ -14,6 +15,7 @@ export const CLIENT_FEATURES = [
14
15
  workspaceSessionEnhancementFeature,
15
16
  terminalEnhancementFeature,
16
17
  debugFeature,
18
+ gitManagementFeature,
17
19
  ];
18
20
  /**
19
21
  * 汇总需要在设置页显示的模块,按 ui.order 升序。
@@ -33,7 +35,7 @@ export function settingsModules(registry) {
33
35
  entries.sort((left, right) => (left.ui.order ?? 0) - (right.ui.order ?? 0));
34
36
  return entries;
35
37
  }
36
- export { lanAccessFeature, loginProtectionFeature, reverseProxyFeature, cliAdaptersFeature, workspaceSessionEnhancementFeature, terminalEnhancementFeature, debugFeature };
38
+ export { lanAccessFeature, loginProtectionFeature, reverseProxyFeature, cliAdaptersFeature, workspaceSessionEnhancementFeature, terminalEnhancementFeature, debugFeature, gitManagementFeature };
37
39
  export { startBrowserRelayConnection } from './reverse-proxy.js';
38
40
  export { LanAccessPanel } from './lan-access-panel.js';
39
41
  export { LoginProtectionPanel } from './login-protection-panel.js';
@@ -1,5 +1,5 @@
1
1
  import type { ReactElement } from 'react';
2
2
  import type { FeaturePanelProps } from './types.js';
3
3
  /** “局域网访问DSH”设置卡片:只配置一条监听并转发到当前 DSH Web。 */
4
- export declare function LanAccessPanel({ services, enabled, snapshot: settingsSnapshot }: FeaturePanelProps): ReactElement;
4
+ export declare function LanAccessPanel({ services, enabled, snapshot: settingsSnapshot, notify }: FeaturePanelProps): ReactElement;
5
5
  //# sourceMappingURL=lan-access-panel.d.ts.map
@@ -3,7 +3,7 @@ import { CODINGNS_RPC_CHANNEL } from '../../shared/contracts/transport.js';
3
3
  import { dshFormRootStyle, dshSettingsButtonStyle, dshSettingsFieldLabelStyle, dshSettingsFieldStyle, dshSettingsNoteStyle, dshSettingsPrimaryButtonStyle, dshSettingsRowStyle, dshThemeColor, } from '../theme.js';
4
4
  import { useCodingNsTranslator } from '../locale.js';
5
5
  /** “局域网访问DSH”设置卡片:只配置一条监听并转发到当前 DSH Web。 */
6
- export function LanAccessPanel({ services, enabled, snapshot: settingsSnapshot }) {
6
+ export function LanAccessPanel({ services, enabled, snapshot: settingsSnapshot, notify }) {
7
7
  const { rpc, settings } = services;
8
8
  const t = useCodingNsTranslator(services.locale);
9
9
  const [savedSettings, setSavedSettings] = useState();
@@ -17,7 +17,6 @@ export function LanAccessPanel({ services, enabled, snapshot: settingsSnapshot }
17
17
  const [detectedDshPorts, setDetectedDshPorts] = useState([]);
18
18
  const [snapshot, setSnapshot] = useState(null);
19
19
  const [busy, setBusy] = useState(false);
20
- const [message, setMessage] = useState('');
21
20
  useEffect(() => {
22
21
  if (disabled)
23
22
  return;
@@ -53,12 +52,11 @@ export function LanAccessPanel({ services, enabled, snapshot: settingsSnapshot }
53
52
  }, [savedSettings]);
54
53
  const run = async (operation) => {
55
54
  setBusy(true);
56
- setMessage('');
57
55
  try {
58
56
  await operation();
59
57
  }
60
58
  catch (error) {
61
- setMessage(error instanceof Error ? error.message : String(error));
59
+ notify({ kind: 'error', message: error instanceof Error ? error.message : String(error) });
62
60
  }
63
61
  finally {
64
62
  setBusy(false);
@@ -69,7 +67,7 @@ export function LanAccessPanel({ services, enabled, snapshot: settingsSnapshot }
69
67
  setDetectedDshPorts(result.ports);
70
68
  if (result.ports.length === 1)
71
69
  setDshPort(String(result.ports[0]));
72
- setMessage(result.ports.length === 0 ? t('lan.detectNone') : t('lan.detected', { ports: result.ports.join('、') }));
70
+ notify({ kind: 'info', message: result.ports.length === 0 ? t('lan.detectNone') : t('lan.detected', { ports: result.ports.join('、') }) });
73
71
  });
74
72
  const saveMapping = async (nextAutoStart = autoStart) => {
75
73
  const saved = await callRpc(rpc, 'lanAccessDsh/settings/set', readMapping(listenHost, listenPort, dshPort, nextAutoStart));
@@ -79,7 +77,7 @@ export function LanAccessPanel({ services, enabled, snapshot: settingsSnapshot }
79
77
  if (!autoStart)
80
78
  return;
81
79
  void saveMapping().catch((error) => {
82
- setMessage(error instanceof Error ? error.message : String(error));
80
+ notify({ kind: 'error', message: error instanceof Error ? error.message : String(error) });
83
81
  });
84
82
  };
85
83
  const start = () => run(async () => {
@@ -94,22 +92,22 @@ export function LanAccessPanel({ services, enabled, snapshot: settingsSnapshot }
94
92
  setSnapshot(current);
95
93
  setDshPort(String(current.dshPort));
96
94
  setListenPort(String(current.listenPort));
97
- setMessage(t('lan.started', { host: current.listenHost, port: current.actualListenPort ?? current.listenPort, dshPort: current.dshPort }));
95
+ notify({ kind: 'success', message: t('lan.started', { host: current.listenHost, port: current.actualListenPort ?? current.listenPort, dshPort: current.dshPort }) });
98
96
  });
99
97
  const stop = () => run(async () => {
100
98
  await callRpc(rpc, 'lanAccessDsh/stop', {});
101
99
  setSnapshot(null);
102
- setMessage(t('lan.stopped'));
100
+ notify({ kind: 'success', message: t('lan.stopped') });
103
101
  });
104
102
  const toggleAutoStart = () => run(async () => {
105
103
  const next = !autoStart;
106
104
  await saveMapping(next);
107
105
  setAutoStart(next);
108
- setMessage(next ? t('lan.autoStartOn') : t('lan.autoStartOff'));
106
+ notify({ kind: 'success', message: next ? t('lan.autoStartOn') : t('lan.autoStartOff') });
109
107
  });
110
108
  const fieldStyle = dshSettingsFieldStyle;
111
109
  const buttonStyle = dshSettingsButtonStyle;
112
- return createElement('div', { 'aria-disabled': controlsDisabled, style: { ...dshFormRootStyle, display: 'flex', flexDirection: 'column', gap: 14, opacity: controlsDisabled ? 0.5 : 1, pointerEvents: controlsDisabled ? 'none' : 'auto' } }, createElement('p', { style: { margin: 0, color: dshThemeColor.labelSecondary, fontSize: 13, lineHeight: 1.5 } }, t('lan.description')), createElement('label', { style: { display: 'flex', flexDirection: 'column', gap: 6 } }, createElement('span', { style: dshSettingsFieldLabelStyle }, t('lan.listenHost')), createElement('select', { value: listenHost, disabled: controlsDisabled || busy, onChange: (event) => setListenHost(event.currentTarget.value), onBlur: saveMappingOnBlur, style: fieldStyle }, ...listenHosts.map((host) => createElement('option', { key: host, value: host }, host === '0.0.0.0' ? t('lan.allInterfaces') : host)))), createElement('label', { style: { display: 'flex', flexDirection: 'column', gap: 6 } }, createElement('span', { style: dshSettingsFieldLabelStyle }, t('lan.listenPort')), createElement('input', { type: 'number', min: 0, max: 65535, placeholder: t('lan.listenPort'), value: listenPort, disabled: controlsDisabled || busy, onChange: (event) => setListenPort(event.currentTarget.value), onBlur: saveMappingOnBlur, style: fieldStyle })), createElement('label', { style: { display: 'flex', flexDirection: 'column', gap: 6 } }, createElement('span', { style: dshSettingsFieldLabelStyle }, t('lan.dshPort')), createElement('div', { style: dshSettingsRowStyle }, createElement('input', { type: 'number', min: 1, max: 65535, placeholder: t('lan.dshPort'), value: dshPort, disabled: controlsDisabled || busy, onChange: (event) => setDshPort(event.currentTarget.value), onBlur: saveMappingOnBlur, style: { ...fieldStyle, flex: 1, minWidth: 0 } }), createElement('button', { type: 'button', disabled: controlsDisabled || busy, onClick: () => void detect(), style: buttonStyle }, t('lan.detect')))), detectedDshPorts.length > 1 && createElement('div', { style: dshSettingsNoteStyle }, t('lan.detectMultiple', { ports: detectedDshPorts.join('、') })), createElement('label', { style: { display: 'flex', alignItems: 'center', gap: 8, cursor: controlsDisabled || busy ? 'not-allowed' : 'pointer', color: dshThemeColor.labelSecondary, fontSize: 13 } }, createElement('input', { type: 'checkbox', checked: autoStart, disabled: controlsDisabled || busy, onChange: () => void toggleAutoStart(), style: { accentColor: dshThemeColor.accent } }), createElement('span', undefined, t('lan.autoStart'))), createElement('div', { style: dshSettingsRowStyle }, createElement('button', { type: 'button', disabled: controlsDisabled || busy || !listenPort, onClick: () => void start(), style: { ...dshSettingsPrimaryButtonStyle, flex: 1 } }, busy ? t('lan.processing') : snapshot ? t('lan.update') : t('lan.start')), snapshot && createElement('button', { type: 'button', disabled: controlsDisabled || busy, onClick: () => void stop(), style: buttonStyle }, t('lan.stop'))), snapshot && createElement('div', { role: 'status', style: dshSettingsNoteStyle }, t('lan.forwarding', { host: snapshot.listenHost, port: snapshot.actualListenPort ?? snapshot.listenPort, dshPort: snapshot.dshPort })), message && createElement('div', { role: 'status', style: { color: message.includes('已') ? dshThemeColor.success : dshThemeColor.error } }, message));
110
+ return createElement('div', { 'aria-disabled': controlsDisabled, style: { ...dshFormRootStyle, display: 'flex', flexDirection: 'column', gap: 14, opacity: controlsDisabled ? 0.5 : 1, pointerEvents: controlsDisabled ? 'none' : 'auto' } }, createElement('p', { style: { margin: 0, color: dshThemeColor.labelSecondary, fontSize: 13, lineHeight: 1.5 } }, t('lan.description')), createElement('label', { style: { display: 'flex', flexDirection: 'column', gap: 6 } }, createElement('span', { style: dshSettingsFieldLabelStyle }, t('lan.listenHost')), createElement('select', { value: listenHost, disabled: controlsDisabled || busy, onChange: (event) => setListenHost(event.currentTarget.value), onBlur: saveMappingOnBlur, style: fieldStyle }, ...listenHosts.map((host) => createElement('option', { key: host, value: host }, host === '0.0.0.0' ? t('lan.allInterfaces') : host)))), createElement('label', { style: { display: 'flex', flexDirection: 'column', gap: 6 } }, createElement('span', { style: dshSettingsFieldLabelStyle }, t('lan.listenPort')), createElement('input', { type: 'number', min: 0, max: 65535, placeholder: t('lan.listenPort'), value: listenPort, disabled: controlsDisabled || busy, onChange: (event) => setListenPort(event.currentTarget.value), onBlur: saveMappingOnBlur, style: fieldStyle })), createElement('label', { style: { display: 'flex', flexDirection: 'column', gap: 6 } }, createElement('span', { style: dshSettingsFieldLabelStyle }, t('lan.dshPort')), createElement('div', { style: dshSettingsRowStyle }, createElement('input', { type: 'number', min: 1, max: 65535, placeholder: t('lan.dshPort'), value: dshPort, disabled: controlsDisabled || busy, onChange: (event) => setDshPort(event.currentTarget.value), onBlur: saveMappingOnBlur, style: { ...fieldStyle, flex: 1, minWidth: 0 } }), createElement('button', { type: 'button', disabled: controlsDisabled || busy, onClick: () => void detect(), style: buttonStyle }, t('lan.detect')))), detectedDshPorts.length > 1 && createElement('div', { style: dshSettingsNoteStyle }, t('lan.detectMultiple', { ports: detectedDshPorts.join('、') })), createElement('label', { style: { display: 'flex', alignItems: 'center', gap: 8, cursor: controlsDisabled || busy ? 'not-allowed' : 'pointer', color: dshThemeColor.labelSecondary, fontSize: 13 } }, createElement('input', { type: 'checkbox', checked: autoStart, disabled: controlsDisabled || busy, onChange: () => void toggleAutoStart(), style: { accentColor: dshThemeColor.accent } }), createElement('span', undefined, t('lan.autoStart'))), createElement('div', { style: dshSettingsRowStyle }, createElement('button', { type: 'button', disabled: controlsDisabled || busy || !listenPort, onClick: () => void start(), style: { ...dshSettingsPrimaryButtonStyle, flex: 1 } }, busy ? t('lan.processing') : snapshot ? t('lan.update') : t('lan.start')), snapshot && createElement('button', { type: 'button', disabled: controlsDisabled || busy, onClick: () => void stop(), style: buttonStyle }, t('lan.stop'))), snapshot && createElement('div', { role: 'status', style: dshSettingsNoteStyle }, t('lan.forwarding', { host: snapshot.listenHost, port: snapshot.actualListenPort ?? snapshot.listenPort, dshPort: snapshot.dshPort })));
113
111
  }
114
112
  function readMapping(listenHost, listenPort, dshPort, autoStart) {
115
113
  return {
@@ -9,7 +9,7 @@ import { LanAccessPanel } from './lan-access-panel.js';
9
9
  export const lanAccessFeature = {
10
10
  descriptor: {
11
11
  name: 'lanAccess',
12
- version: '0.1.0',
12
+ version: '0.1.1',
13
13
  enabledByDefault: true,
14
14
  dependencies: [],
15
15
  runtime: 'client',
@@ -1,5 +1,5 @@
1
1
  import type { ReactElement } from 'react';
2
2
  import type { FeaturePanelProps } from './types.js';
3
3
  /** 独立的登录保护设置卡片,凭据实际由 Host 保存和校验。 */
4
- export declare function LoginProtectionPanel({ services, enabled, snapshot }: FeaturePanelProps): ReactElement;
4
+ export declare function LoginProtectionPanel({ services, enabled, snapshot, notify }: FeaturePanelProps): ReactElement;
5
5
  //# sourceMappingURL=login-protection-panel.d.ts.map
@@ -1,10 +1,10 @@
1
1
  import { createElement, useEffect, useState } from 'react';
2
2
  import { CODINGNS_RPC_CHANNEL } from '../../shared/contracts/transport.js';
3
- import { dshFormRootStyle, dshSettingsButtonStyle, dshSettingsFieldLabelStyle, dshSettingsFieldStyle, dshSettingsNoteStyle, dshSettingsPrimaryButtonStyle, dshThemeColor, } from '../theme.js';
3
+ import { dshFormRootStyle, dshSettingsButtonStyle, dshSettingsFieldLabelStyle, dshSettingsFieldStyle, dshSettingsPrimaryButtonStyle, dshThemeColor, } from '../theme.js';
4
4
  import { writeLoginProtectionSession } from './login-protection-session.js';
5
5
  const DEFAULT_SCOPES = { lan: true, relay: true };
6
6
  /** 独立的登录保护设置卡片,凭据实际由 Host 保存和校验。 */
7
- export function LoginProtectionPanel({ services, enabled, snapshot }) {
7
+ export function LoginProtectionPanel({ services, enabled, snapshot, notify }) {
8
8
  const { rpc } = services;
9
9
  const controlsDisabled = !enabled || snapshot.status === 'loading' || !snapshot.writable;
10
10
  const [saved, setSaved] = useState({ enabled: false, username: '', passwordConfigured: false, timeoutSeconds: 1800, scopes: DEFAULT_SCOPES });
@@ -14,7 +14,6 @@ export function LoginProtectionPanel({ services, enabled, snapshot }) {
14
14
  const [timeoutSeconds, setTimeoutSeconds] = useState('1800');
15
15
  const [scopes, setScopes] = useState(DEFAULT_SCOPES);
16
16
  const [busy, setBusy] = useState(false);
17
- const [message, setMessage] = useState('');
18
17
  useEffect(() => {
19
18
  if (!enabled)
20
19
  return;
@@ -26,11 +25,10 @@ export function LoginProtectionPanel({ services, enabled, snapshot }) {
26
25
  setTimeoutSeconds(String(value.timeoutSeconds));
27
26
  setScopes(value.scopes ?? DEFAULT_SCOPES);
28
27
  })
29
- .catch((error) => setMessage(error instanceof Error ? error.message : String(error)));
28
+ .catch((error) => notify({ kind: 'error', message: error instanceof Error ? error.message : String(error) }));
30
29
  }, [enabled, rpc]);
31
30
  const save = async () => {
32
31
  setBusy(true);
33
- setMessage('');
34
32
  try {
35
33
  const parsedTimeout = Number(timeoutSeconds);
36
34
  if (!Number.isInteger(parsedTimeout) || parsedTimeout < 60 || parsedTimeout > 604800)
@@ -53,10 +51,10 @@ export function LoginProtectionPanel({ services, enabled, snapshot }) {
53
51
  writeLoginProtectionSession(undefined);
54
52
  }
55
53
  setPassword('');
56
- setMessage(value.enabled && value.scopes.relay && password === '' ? '设置已保存;中继访问需要重新输入密码以建立当前标签页会话' : '登录保护设置已保存');
54
+ notify({ kind: 'success', message: value.enabled && value.scopes.relay && password === '' ? '设置已保存;中继访问需要重新输入密码以建立当前标签页会话' : '登录保护设置已保存' });
57
55
  }
58
56
  catch (error) {
59
- setMessage(error instanceof Error ? error.message : String(error));
57
+ notify({ kind: 'error', message: error instanceof Error ? error.message : String(error) });
60
58
  }
61
59
  finally {
62
60
  setBusy(false);
@@ -70,8 +68,8 @@ export function LoginProtectionPanel({ services, enabled, snapshot }) {
70
68
  'aria-disabled': controlsDisabled,
71
69
  }, createElement('p', { style: { margin: 0, color: dshThemeColor.labelSecondary, fontSize: 13, lineHeight: 1.5 } }, '为局域网和中继访问统一增加本地账号验证。本机 127.0.0.1 / ::1 永远不受此保护。密码只在 Host 侧验证,不会进入浏览器设置或 URL。'), createElement('label', { style: { display: 'flex', alignItems: 'center', gap: 8, color: dshThemeColor.labelSecondary, fontSize: 13 } }, createElement('input', { type: 'checkbox', checked: active, disabled: controlsDisabled || busy, onChange: (event) => setActive(event.currentTarget.checked), style: { accentColor: dshThemeColor.accent } }), createElement('span', undefined, '启用登录保护')), createElement('label', { style: { display: 'flex', flexDirection: 'column', gap: 6 } }, createElement('span', { style: dshSettingsFieldLabelStyle }, '本地用户名'), createElement('input', { value: username, disabled: controlsDisabled || busy || !active, autoComplete: 'username', onChange: (event) => setUsername(event.currentTarget.value), style: field })), createElement('label', { style: { display: 'flex', flexDirection: 'column', gap: 6 } }, createElement('span', { style: dshSettingsFieldLabelStyle }, saved.passwordConfigured ? '新密码(留空保持不变)' : '密码(至少 8 位)'), createElement('input', { type: 'password', value: password, disabled: controlsDisabled || busy || !active, autoComplete: 'new-password', onChange: (event) => setPassword(event.currentTarget.value), style: field })), createElement('label', { style: { display: 'flex', flexDirection: 'column', gap: 6 } }, createElement('span', { style: dshSettingsFieldLabelStyle }, '会话超时(秒)'), createElement('input', { type: 'number', min: 60, max: 604800, value: timeoutSeconds, disabled: controlsDisabled || busy || !active, onChange: (event) => setTimeoutSeconds(event.currentTarget.value), style: field })), createElement('fieldset', { disabled: controlsDisabled || busy || !active, style: { display: 'flex', flexDirection: 'column', gap: 8, margin: 0, padding: 12, border: `1px solid ${dshThemeColor.border}`, borderRadius: 6 } }, createElement('legend', { style: dshSettingsFieldLabelStyle }, '应用范围'), ...[
72
70
  ['lan', '局域网访问', '局域网网卡入口'],
73
- ['relay', '中继访问', 'DSH-Codingns4DSH 中继'],
74
- ].map(([key, label, hint]) => createElement('label', { key, style: { display: 'flex', alignItems: 'center', gap: 8, color: dshThemeColor.labelSecondary, fontSize: 13 } }, createElement('input', { type: 'checkbox', checked: scopes[key], onChange: () => toggleScope(key), style: { accentColor: dshThemeColor.accent } }), createElement('span', undefined, `${label}(${hint})`)))), createElement('button', { type: 'button', disabled: controlsDisabled || busy, onClick: () => void save(), style: dshSettingsPrimaryButtonStyle }, busy ? '保存中…' : '保存登录保护'), message && createElement('div', { role: 'status', style: message.includes('已') ? { color: dshThemeColor.success } : { ...dshSettingsNoteStyle, color: dshThemeColor.error } }, message));
71
+ ['relay', '中继访问', 'Codingns4DSH 中继'],
72
+ ].map(([key, label, hint]) => createElement('label', { key, style: { display: 'flex', alignItems: 'center', gap: 8, color: dshThemeColor.labelSecondary, fontSize: 13 } }, createElement('input', { type: 'checkbox', checked: scopes[key], onChange: () => toggleScope(key), style: { accentColor: dshThemeColor.accent } }), createElement('span', undefined, `${label}(${hint})`)))), createElement('button', { type: 'button', disabled: controlsDisabled || busy, onClick: () => void save(), style: dshSettingsPrimaryButtonStyle }, busy ? '保存中…' : '保存登录保护'));
75
73
  }
76
74
  async function callRpc(rpc, endpoint, payload) {
77
75
  let response;
@@ -5,5 +5,5 @@ import type { FeaturePanelProps } from './types.js';
5
5
  *
6
6
  * 密码只在单次 RPC 中经过 Host,表单不保存它;refresh token 只存在于 Host。
7
7
  */
8
- export declare function ReverseProxyPanel({ services, enabled, snapshot }: FeaturePanelProps): ReactElement;
8
+ export declare function ReverseProxyPanel({ services, enabled, snapshot, notify }: FeaturePanelProps): ReactElement;
9
9
  //# sourceMappingURL=reverse-proxy-panel.d.ts.map
@@ -1,5 +1,5 @@
1
1
  import { createElement, useEffect, useState } from 'react';
2
- import { CODINGNS_CONTROL_BASE_URL_FIELD, CODINGNS_CONTROL_BASE_URLS_FIELD, DEFAULT_CODINGNS_CONTROL_BASE_URLS, } from '../../shared/contracts/config.js';
2
+ import { CODINGNS_CONTROL_STATION_URL, CODINGNS_CONTROL_BASE_URL_FIELD, CODINGNS_CONTROL_BASE_URLS_FIELD, CODINGNS_H5_LOGIN_URL, DEFAULT_CODINGNS_CONTROL_BASE_URLS, } from '../../shared/contracts/config.js';
3
3
  import { CODINGNS_RPC_CHANNEL } from '../../shared/contracts/transport.js';
4
4
  import { dshFormRootStyle, dshPopupSurfaceStyle, dshSettingsButtonStyle, dshSettingsFieldLabelStyle, dshSettingsFieldStyle, dshSettingsNoteStyle, dshSettingsPrimaryButtonStyle, dshSettingsRowStyle, dshThemeColor, } from '../theme.js';
5
5
  import { useCodingNsTranslator } from '../locale.js';
@@ -8,7 +8,7 @@ import { useCodingNsTranslator } from '../locale.js';
8
8
  *
9
9
  * 密码只在单次 RPC 中经过 Host,表单不保存它;refresh token 只存在于 Host。
10
10
  */
11
- export function ReverseProxyPanel({ services, enabled, snapshot }) {
11
+ export function ReverseProxyPanel({ services, enabled, snapshot, notify }) {
12
12
  const { settings, rpc } = services;
13
13
  const t = useCodingNsTranslator(services.locale);
14
14
  const disabled = !enabled;
@@ -24,7 +24,7 @@ export function ReverseProxyPanel({ services, enabled, snapshot }) {
24
24
  const [devices, setDevices] = useState(null);
25
25
  const [selectedDeviceId, setSelectedDeviceId] = useState('');
26
26
  const [busy, setBusy] = useState(false);
27
- const [message, setMessage] = useState('');
27
+ const [h5UrlCopied, setH5UrlCopied] = useState(false);
28
28
  useEffect(() => {
29
29
  const saved = snapshot.value?.controlBaseUrl;
30
30
  const resolved = resolveControlBaseUrl(saved);
@@ -45,17 +45,16 @@ export function ReverseProxyPanel({ services, enabled, snapshot }) {
45
45
  if (!enabled || auth.status !== 'authenticated')
46
46
  return;
47
47
  void loadDevicesInternal().catch((error) => {
48
- setMessage(error instanceof Error ? error.message : String(error));
48
+ notify({ kind: 'error', message: error instanceof Error ? error.message : String(error) });
49
49
  });
50
50
  }, [auth.status, enabled, rpc]);
51
51
  const run = async (operation) => {
52
52
  setBusy(true);
53
- setMessage('');
54
53
  try {
55
54
  await operation();
56
55
  }
57
56
  catch (error) {
58
- setMessage(error instanceof Error ? error.message : String(error));
57
+ notify({ kind: 'error', message: error instanceof Error ? error.message : String(error) });
59
58
  }
60
59
  finally {
61
60
  setBusy(false);
@@ -73,15 +72,34 @@ export function ReverseProxyPanel({ services, enabled, snapshot }) {
73
72
  });
74
73
  setPassword('');
75
74
  setAuth(next);
76
- setMessage(t('relay.loginSuccess'));
75
+ setH5UrlCopied(false);
76
+ notify({ kind: 'success', message: t('relay.loginSuccess') });
77
77
  });
78
78
  const logout = () => run(async () => {
79
79
  await callCodingNsRpc(rpc, 'auth/logout', {});
80
80
  setAuth(loggedOutSnapshot());
81
81
  setDevices(null);
82
82
  setSelectedDeviceId('');
83
- setMessage(t('relay.loggedOut'));
83
+ setH5UrlCopied(false);
84
+ notify({ kind: 'success', message: t('relay.loggedOut') });
84
85
  });
86
+ const copyH5LoginUrl = async () => {
87
+ try {
88
+ await copyText(CODINGNS_H5_LOGIN_URL);
89
+ setH5UrlCopied(true);
90
+ notify({ kind: 'success', message: t('relay.copySuccess') });
91
+ }
92
+ catch (error) {
93
+ setH5UrlCopied(false);
94
+ notify({ kind: 'error', message: error instanceof Error ? error.message : t('relay.copyFailed') });
95
+ }
96
+ };
97
+ const handleH5AddressKeyDown = (event) => {
98
+ if (event.key !== 'Enter' && event.key !== ' ')
99
+ return;
100
+ event.preventDefault();
101
+ void copyH5LoginUrl();
102
+ };
85
103
  const loadDevices = () => run(async () => {
86
104
  await loadDevicesInternal();
87
105
  });
@@ -103,7 +121,7 @@ export function ReverseProxyPanel({ services, enabled, snapshot }) {
103
121
  setControlBaseUrl(addedUrl);
104
122
  setNewControlBaseUrl('');
105
123
  setAddAddressOpen(false);
106
- setMessage(t('relay.add'));
124
+ notify({ kind: 'success', message: t('relay.add') });
107
125
  }
108
126
  catch (error) {
109
127
  setAddressError(error instanceof Error ? error.message : String(error));
@@ -115,13 +133,60 @@ export function ReverseProxyPanel({ services, enabled, snapshot }) {
115
133
  const chooseControlBaseUrl = (value) => {
116
134
  setControlBaseUrl(value);
117
135
  void settings.set(CODINGNS_CONTROL_BASE_URL_FIELD, value).catch((error) => {
118
- setMessage(error instanceof Error ? error.message : String(error));
136
+ notify({ kind: 'error', message: error instanceof Error ? error.message : String(error) });
119
137
  });
120
138
  };
121
139
  const fieldStyle = dshSettingsFieldStyle;
122
140
  const buttonStyle = dshSettingsButtonStyle;
123
141
  const authenticated = auth.status === 'authenticated';
124
- return createElement('div', { 'aria-disabled': controlsDisabled, style: { ...dshFormRootStyle, display: 'flex', flexDirection: 'column', gap: 16, opacity: controlsDisabled ? 0.5 : 1, pointerEvents: controlsDisabled ? 'none' : 'auto' } }, createElement('div', undefined, createElement('h3', { style: { margin: 0, fontSize: 17 } }, t('relay.settings')), createElement('p', { style: { margin: '6px 0 0', color: dshThemeColor.labelSecondary, fontSize: 13, lineHeight: 1.5 } }, t('relay.loginHint'))), createElement('label', { style: { display: 'flex', flexDirection: 'column', gap: 6 } }, createElement('span', { style: dshSettingsFieldLabelStyle }, t('relay.controlApi')), createElement('div', { style: dshSettingsRowStyle }, createElement('select', { value: controlBaseUrl, disabled: controlsDisabled || busy, onChange: (event) => chooseControlBaseUrl(event.currentTarget.value), style: { ...fieldStyle, flex: 1, minWidth: 0 } }, ...controlBaseUrls.map((url) => createElement('option', { key: url, value: url }, url))), createElement('button', { type: 'button', 'aria-haspopup': 'dialog', disabled: controlsDisabled || busy, onClick: () => { setAddressError(''); setAddAddressOpen(true); }, style: { ...buttonStyle, flex: '0 0 auto' } }, t('relay.add')))), addAddressOpen && createElement('div', { role: 'dialog', 'aria-modal': true, 'aria-labelledby': 'codingns-add-address-title', style: { position: 'fixed', inset: 0, zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24, background: dshThemeColor.overlay } }, createElement('div', { style: { ...dshPopupSurfaceStyle, width: 'min(100%, 480px)', boxSizing: 'border-box', padding: 24, borderRadius: 8 } }, createElement('h3', { id: 'codingns-add-address-title', style: { margin: 0, fontSize: 18 } }, t('relay.addServer')), createElement('p', { style: { margin: '8px 0 16px', opacity: 0.7 } }, t('relay.addServerHint')), createElement('input', { type: 'url', autoFocus: true, value: newControlBaseUrl, placeholder: 'https://example.com:1443', disabled: disabled || busy, onChange: (event) => setNewControlBaseUrl(event.currentTarget.value), style: fieldStyle }), addressError && createElement('div', { role: 'alert', style: { marginTop: 8, color: dshThemeColor.error } }, addressError), createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 20 } }, createElement('button', { type: 'button', disabled: busy, onClick: () => { setAddAddressOpen(false); setAddressError(''); }, style: buttonStyle }, t('relay.cancel')), createElement('button', { type: 'button', disabled: busy || !newControlBaseUrl.trim(), onClick: () => void addControlBaseUrl(), style: buttonStyle }, busy ? t('relay.adding') : t('relay.add'))))), !authenticated && createElement('form', { onSubmit: (event) => { event.preventDefault(); void login(); }, style: { display: 'flex', flexDirection: 'column', gap: 12 } }, createElement('label', { style: { display: 'flex', flexDirection: 'column', gap: 6 } }, createElement('span', { style: dshSettingsFieldLabelStyle }, t('relay.email')), createElement('input', { type: 'email', autoComplete: 'username', value: email, disabled: controlsDisabled || busy, onChange: (event) => setEmail(event.currentTarget.value), style: fieldStyle })), createElement('label', { style: { display: 'flex', flexDirection: 'column', gap: 6 } }, createElement('span', { style: dshSettingsFieldLabelStyle }, t('relay.password')), createElement('input', { type: 'password', autoComplete: 'current-password', value: password, disabled: controlsDisabled || busy, onChange: (event) => setPassword(event.currentTarget.value), style: fieldStyle })), createElement('button', { type: 'submit', disabled: controlsDisabled || busy || !controlBaseUrl || !email || !password, style: dshSettingsPrimaryButtonStyle }, busy ? t('relay.loggingIn') : t('relay.login'))), authenticated && createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 12 } }, createElement('div', { style: dshSettingsNoteStyle }, createElement('strong', undefined, auth.account?.email ?? t('relay.loggedIn')), createElement('div', { style: { marginTop: 6, opacity: 0.7 } }, t('relay.device', { value: devices?.devices.find((device) => device.dshDeviceId === selectedDeviceId)?.displayName ?? t('relay.unrecognized') })), createElement('div', { style: { marginTop: 4, opacity: 0.7 } }, t('relay.host', { value: selectedDeviceId || t('relay.unbound') }))), createElement('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap' } }, createElement('button', { type: 'button', disabled: controlsDisabled || busy, onClick: () => void loadDevices(), style: buttonStyle }, t('relay.refreshDevices')), createElement('button', { type: 'button', disabled: controlsDisabled || busy, onClick: () => void logout(), style: buttonStyle }, t('relay.logout'))), devices && createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 8 } }, createElement('strong', { style: dshSettingsFieldLabelStyle }, t('relay.dshDevices')), createElement('select', { value: selectedDeviceId, disabled: controlsDisabled || busy, onChange: (event) => setSelectedDeviceId(event.currentTarget.value), style: fieldStyle }, createElement('option', { value: '' }, t('relay.selectDevice')), ...devices.devices.map((device) => createElement('option', { key: device.dshDeviceId, value: device.dshDeviceId, disabled: !device.online || device.status !== 'active' }, `${device.displayName} · ${device.online ? t('relay.online') : t('relay.offline')}`))), createElement('div', { style: { fontSize: 13, opacity: 0.75 } }, t('relay.devicesSummary', { current: selectedDeviceId || t('relay.unknown'), count: devices.devices.length })))), message && createElement('div', { role: 'status', style: { color: message.includes('成功') ? dshThemeColor.success : dshThemeColor.error } }, message));
142
+ const selectedDevice = devices?.devices.find((device) => device.dshDeviceId === selectedDeviceId);
143
+ const deviceStatus = selectedDevice === undefined
144
+ ? { label: t('relay.unknown'), color: dshThemeColor.labelTertiary }
145
+ : selectedDevice.online && selectedDevice.status === 'active'
146
+ ? { label: t('relay.online'), color: dshThemeColor.success }
147
+ : { label: t('relay.offline'), color: dshThemeColor.error };
148
+ return createElement('div', { 'aria-disabled': controlsDisabled, style: { ...dshFormRootStyle, display: 'flex', flexDirection: 'column', gap: 16, opacity: controlsDisabled ? 0.5 : 1, pointerEvents: controlsDisabled ? 'none' : 'auto' } }, createElement('label', { style: { display: 'flex', flexDirection: 'column', gap: 6 } }, createElement('span', { style: dshSettingsFieldLabelStyle }, t('relay.controlApi')), createElement('div', { style: dshSettingsRowStyle }, createElement('select', { value: controlBaseUrl, disabled: controlsDisabled || busy, onChange: (event) => chooseControlBaseUrl(event.currentTarget.value), style: { ...fieldStyle, flex: 1, minWidth: 0 } }, ...controlBaseUrls.map((url) => createElement('option', { key: url, value: url }, url))), createElement('button', { type: 'button', 'aria-haspopup': 'dialog', disabled: controlsDisabled || busy, onClick: () => { setAddressError(''); setAddAddressOpen(true); }, style: { ...buttonStyle, flex: '0 0 auto' } }, t('relay.add')))), addAddressOpen && createElement('div', { role: 'dialog', 'aria-modal': true, 'aria-labelledby': 'codingns-add-address-title', style: { position: 'fixed', inset: 0, zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24, background: dshThemeColor.overlay } }, createElement('div', { style: { ...dshPopupSurfaceStyle, width: 'min(100%, 480px)', boxSizing: 'border-box', padding: 24, borderRadius: 8 } }, createElement('h3', { id: 'codingns-add-address-title', style: { margin: 0, fontSize: 18 } }, t('relay.addServer')), createElement('p', { style: { margin: '8px 0 16px', opacity: 0.7 } }, t('relay.addServerHint')), createElement('input', { type: 'url', autoFocus: true, value: newControlBaseUrl, placeholder: 'https://example.com:1443', disabled: disabled || busy, onChange: (event) => setNewControlBaseUrl(event.currentTarget.value), style: fieldStyle }), addressError && createElement('div', { role: 'alert', style: { marginTop: 8, color: dshThemeColor.error } }, addressError), createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 20 } }, createElement('button', { type: 'button', disabled: busy, onClick: () => { setAddAddressOpen(false); setAddressError(''); }, style: buttonStyle }, t('relay.cancel')), createElement('button', { type: 'button', disabled: busy || !newControlBaseUrl.trim(), onClick: () => void addControlBaseUrl(), style: buttonStyle }, busy ? t('relay.adding') : t('relay.add'))))), !authenticated && createElement('form', { onSubmit: (event) => { event.preventDefault(); void login(); }, style: { display: 'flex', flexDirection: 'column', gap: 12 } }, createElement('label', { style: { display: 'flex', flexDirection: 'column', gap: 6 } }, createElement('span', { style: dshSettingsFieldLabelStyle }, t('relay.email')), createElement('input', { type: 'email', autoComplete: 'username', value: email, disabled: controlsDisabled || busy, onChange: (event) => setEmail(event.currentTarget.value), style: fieldStyle })), createElement('label', { style: { display: 'flex', flexDirection: 'column', gap: 6 } }, createElement('span', { style: dshSettingsFieldLabelStyle }, t('relay.password')), createElement('input', { type: 'password', autoComplete: 'current-password', value: password, disabled: controlsDisabled || busy, onChange: (event) => setPassword(event.currentTarget.value), style: fieldStyle })), createElement('button', { type: 'submit', disabled: controlsDisabled || busy || !controlBaseUrl || !email || !password, style: dshSettingsPrimaryButtonStyle }, busy ? t('relay.loggingIn') : t('relay.login')), createElement('p', { style: { margin: 0, color: dshThemeColor.labelSecondary, fontSize: 13, lineHeight: 1.5 } }, t('relay.noAccountPrefix'), createElement('a', { href: CODINGNS_CONTROL_STATION_URL, target: '_blank', rel: 'noreferrer', style: { color: dshThemeColor.accent } }, t('relay.register')), t('relay.noAccountSuffix'))), authenticated && createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 12 } }, createElement('div', { style: dshSettingsNoteStyle }, createElement('strong', undefined, auth.account?.email ?? t('relay.loggedIn')), createElement('div', { style: { marginTop: 6, opacity: 0.7 } }, t('relay.device', { value: selectedDevice?.displayName ?? t('relay.unrecognized') })), createElement('div', { style: { marginTop: 4, opacity: 0.7 } }, t('relay.dshVersion', { value: selectedDevice?.dshVersion ?? t('relay.unknown') })), createElement('div', { style: { marginTop: 4, opacity: 0.7 } }, t('relay.computerName', { value: selectedDevice?.computerName ?? t('relay.unknown') })), createElement('div', { style: { marginTop: 4, opacity: 0.7 } }, t('relay.host', { value: selectedDeviceId || t('relay.unbound') }))), createElement('div', { style: dshSettingsNoteStyle }, createElement('strong', undefined, t('relay.h5LoginAddress')), createElement('div', {
149
+ role: 'button',
150
+ tabIndex: controlsDisabled || busy ? -1 : 0,
151
+ 'aria-disabled': controlsDisabled || busy,
152
+ 'aria-label': t('relay.copyAddressHint'),
153
+ title: t('relay.copyAddressHint'),
154
+ onClick: () => { if (!controlsDisabled && !busy)
155
+ void copyH5LoginUrl(); },
156
+ onKeyDown: (event) => { if (!controlsDisabled && !busy)
157
+ handleH5AddressKeyDown(event); },
158
+ style: {
159
+ display: 'flex',
160
+ alignItems: 'center',
161
+ gap: 12,
162
+ marginTop: 8,
163
+ minWidth: 0,
164
+ padding: '10px 12px',
165
+ border: `1px solid ${h5UrlCopied ? dshThemeColor.success : dshThemeColor.border}`,
166
+ borderRadius: 8,
167
+ color: dshThemeColor.accent,
168
+ background: dshThemeColor.pageBackground,
169
+ cursor: controlsDisabled || busy ? 'default' : 'copy',
170
+ transition: 'border-color 160ms ease, background 160ms ease',
171
+ },
172
+ }, createElement('span', { style: { minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace', fontSize: 13, fontWeight: 600 } }, CODINGNS_H5_LOGIN_URL))), createElement('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap' } }, createElement('button', { type: 'button', disabled: controlsDisabled || busy, onClick: () => void loadDevices(), style: buttonStyle }, t('relay.refreshDevices')), createElement('button', { type: 'button', disabled: controlsDisabled || busy, onClick: () => void logout(), style: buttonStyle }, t('relay.logout'))), devices && createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 8 } }, createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, minWidth: 0 } }, createElement('strong', { style: dshSettingsFieldLabelStyle }, t('relay.dshDevices')), createElement('span', {
173
+ role: 'status',
174
+ 'aria-label': t('relay.deviceStatus', { value: deviceStatus.label }),
175
+ style: {
176
+ display: 'inline-flex',
177
+ alignItems: 'center',
178
+ gap: 6,
179
+ flex: '0 0 auto',
180
+ padding: '4px 9px',
181
+ border: `1px solid ${deviceStatus.color}`,
182
+ borderRadius: 999,
183
+ color: deviceStatus.color,
184
+ background: dshThemeColor.surfaceSubtle,
185
+ fontSize: 12,
186
+ lineHeight: 1.2,
187
+ fontWeight: 600,
188
+ },
189
+ }, createElement('span', { 'aria-hidden': true, style: { width: 7, height: 7, flex: '0 0 auto', borderRadius: '50%', background: deviceStatus.color } }), deviceStatus.label)), createElement('select', { value: selectedDeviceId, disabled: controlsDisabled || busy, onChange: (event) => setSelectedDeviceId(event.currentTarget.value), style: fieldStyle }, createElement('option', { value: '' }, t('relay.selectDevice')), ...devices.devices.map((device) => createElement('option', { key: device.dshDeviceId, value: device.dshDeviceId, disabled: !device.online || device.status !== 'active' }, `${device.displayName} · ${device.computerName ?? t('relay.unknown')} · ${device.dshVersion ?? t('relay.unknown')} · ${device.online ? t('relay.online') : t('relay.offline')}`))), createElement('div', { style: { fontSize: 13, opacity: 0.75 } }, t('relay.devicesSummary', { current: selectedDeviceId || t('relay.unknown'), count: devices.devices.length })))));
125
190
  }
126
191
  async function callCodingNsRpc(rpc, endpoint, payload) {
127
192
  let response;
@@ -156,4 +221,27 @@ function normalizeControlBaseUrl(value) {
156
221
  throw new TypeError('Control API 地址必须使用 HTTP(S)');
157
222
  return parsed.toString().replace(/\/+$/u, '');
158
223
  }
224
+ /** 复制 H5 地址;非安全上下文下回退到传统 DOM 复制接口。 */
225
+ async function copyText(value) {
226
+ if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
227
+ await navigator.clipboard.writeText(value);
228
+ return;
229
+ }
230
+ if (typeof document === 'undefined')
231
+ throw new Error('当前环境不支持复制');
232
+ const input = document.createElement('textarea');
233
+ input.value = value;
234
+ input.setAttribute('readonly', '');
235
+ input.style.position = 'fixed';
236
+ input.style.opacity = '0';
237
+ document.body.appendChild(input);
238
+ input.select();
239
+ try {
240
+ if (!document.execCommand('copy'))
241
+ throw new Error('当前环境不支持复制');
242
+ }
243
+ finally {
244
+ input.remove();
245
+ }
246
+ }
159
247
  //# sourceMappingURL=reverse-proxy-panel.js.map
@@ -3,7 +3,7 @@ import type { CodingNsRpcClient } from './types.js';
3
3
  /**
4
4
  * 中转访问服务模块。
5
5
  *
6
- * 它把 DSH 页面接入 DSH-Codingns4DSH 独立设备隧道。登录和设备列表由 Host RPC 提供,
6
+ * 它把 DSH 页面接入 Codingns4DSH 独立设备隧道。登录和设备列表由 Host RPC 提供,
7
7
  * 连接状态和设备选择由设置面板承载;隧道本身在模块启用时建立。
8
8
  */
9
9
  export declare const reverseProxyFeature: CodingNsClientFeatureModule;
@@ -1,26 +1,26 @@
1
1
  import { ReverseProxyPanel } from './reverse-proxy-panel.js';
2
2
  import { startDshH5Bootstrap } from '../dsh-h5-bootstrap.js';
3
+ import { callCodingNsRpc } from '../settings-bridge.js';
3
4
  import { LOGIN_PROTECTION_SESSION_EVENT, readLoginProtectionSession } from './login-protection-session.js';
4
- import { CODINGNS_RPC_CHANNEL } from '../../shared/contracts/transport.js';
5
5
  function isRemoteWebContext() {
6
6
  return globalThis.__CODINGNS4DSH_REMOTE_WEB_CONTEXT__ === true;
7
7
  }
8
8
  /**
9
9
  * 中转访问服务模块。
10
10
  *
11
- * 它把 DSH 页面接入 DSH-Codingns4DSH 独立设备隧道。登录和设备列表由 Host RPC 提供,
11
+ * 它把 DSH 页面接入 Codingns4DSH 独立设备隧道。登录和设备列表由 Host RPC 提供,
12
12
  * 连接状态和设备选择由设置面板承载;隧道本身在模块启用时建立。
13
13
  */
14
14
  export const reverseProxyFeature = {
15
15
  descriptor: {
16
16
  name: 'reverseProxy',
17
- version: '0.1.0',
17
+ version: '0.1.1',
18
18
  enabledByDefault: false,
19
19
  dependencies: [],
20
20
  runtime: 'client',
21
21
  ui: {
22
22
  label: '中转访问服务',
23
- description: '通过 DSH-Codingns4DSH 独立设备隧道访问 Host。',
23
+ description: '通过 Codingns4DSH 独立设备隧道访问 Host。',
24
24
  labelKey: 'feature.reverseProxy.label',
25
25
  descriptionKey: 'feature.reverseProxy.description',
26
26
  order: 20,
@@ -51,6 +51,11 @@ export const reverseProxyFeature = {
51
51
  if (stopped || waitingForLogin)
52
52
  return;
53
53
  try {
54
+ const auth = await readAuthSnapshot(context.services.rpc);
55
+ if (auth.status !== 'authenticated') {
56
+ scheduleRetry();
57
+ return;
58
+ }
54
59
  const protection = await readLoginProtectionSettings(context.services.rpc);
55
60
  const loginProtectionToken = readLoginProtectionSession();
56
61
  if (protection.enabled && protection.scopes.relay && loginProtectionToken === undefined) {
@@ -68,9 +73,14 @@ export const reverseProxyFeature = {
68
73
  if (stopped || abort.signal.aborted)
69
74
  return;
70
75
  console.error('codingns4dsh: 中继连接建立失败,将在稍后重试', error);
71
- retryTimer = setTimeout(() => { void attempt(); }, 5_000);
76
+ scheduleRetry();
72
77
  }
73
78
  };
79
+ const scheduleRetry = () => {
80
+ if (retryTimer !== undefined || stopped || abort.signal.aborted)
81
+ return;
82
+ retryTimer = setTimeout(() => { retryTimer = undefined; void attempt(); }, 5_000);
83
+ };
74
84
  void attempt();
75
85
  return async () => {
76
86
  stopped = true;
@@ -95,18 +105,9 @@ export async function startBrowserRelayConnection(rpc, signal, loginProtectionTo
95
105
  };
96
106
  }
97
107
  async function readLoginProtectionSettings(rpc) {
98
- let response;
99
- try {
100
- response = await rpc.call(CODINGNS_RPC_CHANNEL, 'lanAccessDsh/login/get', {});
101
- }
102
- catch (error) {
103
- const message = error instanceof Error ? error.message : String(error);
104
- if (!/HTTP (?:404|405)\b/u.test(message))
105
- throw error;
106
- response = await rpc.call('/api', 'codingns/lanAccessDsh/login/get', {});
107
- }
108
- if (!response.ok)
109
- throw new Error(response.error.message);
110
- return response.value;
108
+ return callCodingNsRpc(rpc, 'lanAccessDsh/login/get', {});
109
+ }
110
+ async function readAuthSnapshot(rpc) {
111
+ return callCodingNsRpc(rpc, 'auth/snapshot', {});
111
112
  }
112
113
  //# sourceMappingURL=reverse-proxy.js.map
@@ -1,5 +1,5 @@
1
1
  import type { ReactElement } from 'react';
2
2
  import type { FeaturePanelProps } from './types.js';
3
3
  /** 终端默认行为与外观设置;终端 UI 由独立 Sidebar 模块负责。 */
4
- export declare function TerminalEnhancementPanel({ services, enabled, snapshot }: FeaturePanelProps): ReactElement;
4
+ export declare function TerminalEnhancementPanel({ services, enabled, snapshot, notify }: FeaturePanelProps): ReactElement;
5
5
  //# sourceMappingURL=terminal-enhancement-panel.d.ts.map