dsh-model-proxy-switch 0.0.0-stage → 1.0.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.
Files changed (220) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +76 -2
  3. package/client.js +473 -0
  4. package/cordis.patch.yml +7 -0
  5. package/icon.svg +13 -0
  6. package/index.js +341 -0
  7. package/locale/en.json +25 -0
  8. package/locale/zh.json +25 -0
  9. package/package.json +55 -4
  10. package/vendor/http-proxy.js +542 -0
  11. package/vendor/node_modules/undici/LICENSE +21 -0
  12. package/vendor/node_modules/undici/README.md +741 -0
  13. package/vendor/node_modules/undici/docs/docs/api/Agent.md +84 -0
  14. package/vendor/node_modules/undici/docs/docs/api/BalancedPool.md +99 -0
  15. package/vendor/node_modules/undici/docs/docs/api/CacheStorage.md +30 -0
  16. package/vendor/node_modules/undici/docs/docs/api/CacheStore.md +164 -0
  17. package/vendor/node_modules/undici/docs/docs/api/Client.md +290 -0
  18. package/vendor/node_modules/undici/docs/docs/api/ClientStats.md +27 -0
  19. package/vendor/node_modules/undici/docs/docs/api/Connector.md +115 -0
  20. package/vendor/node_modules/undici/docs/docs/api/ContentType.md +57 -0
  21. package/vendor/node_modules/undici/docs/docs/api/Cookies.md +128 -0
  22. package/vendor/node_modules/undici/docs/docs/api/Debug.md +62 -0
  23. package/vendor/node_modules/undici/docs/docs/api/DiagnosticsChannel.md +317 -0
  24. package/vendor/node_modules/undici/docs/docs/api/Dispatcher.md +1393 -0
  25. package/vendor/node_modules/undici/docs/docs/api/EnvHttpProxyAgent.md +159 -0
  26. package/vendor/node_modules/undici/docs/docs/api/Errors.md +49 -0
  27. package/vendor/node_modules/undici/docs/docs/api/EventSource.md +45 -0
  28. package/vendor/node_modules/undici/docs/docs/api/Fetch.md +60 -0
  29. package/vendor/node_modules/undici/docs/docs/api/GlobalInstallation.md +139 -0
  30. package/vendor/node_modules/undici/docs/docs/api/H2CClient.md +263 -0
  31. package/vendor/node_modules/undici/docs/docs/api/MockAgent.md +603 -0
  32. package/vendor/node_modules/undici/docs/docs/api/MockCallHistory.md +197 -0
  33. package/vendor/node_modules/undici/docs/docs/api/MockCallHistoryLog.md +43 -0
  34. package/vendor/node_modules/undici/docs/docs/api/MockClient.md +81 -0
  35. package/vendor/node_modules/undici/docs/docs/api/MockErrors.md +12 -0
  36. package/vendor/node_modules/undici/docs/docs/api/MockPool.md +555 -0
  37. package/vendor/node_modules/undici/docs/docs/api/Pool.md +84 -0
  38. package/vendor/node_modules/undici/docs/docs/api/PoolStats.md +35 -0
  39. package/vendor/node_modules/undici/docs/docs/api/ProxyAgent.md +229 -0
  40. package/vendor/node_modules/undici/docs/docs/api/RedirectHandler.md +93 -0
  41. package/vendor/node_modules/undici/docs/docs/api/RetryAgent.md +50 -0
  42. package/vendor/node_modules/undici/docs/docs/api/RetryHandler.md +118 -0
  43. package/vendor/node_modules/undici/docs/docs/api/RoundRobinPool.md +145 -0
  44. package/vendor/node_modules/undici/docs/docs/api/SnapshotAgent.md +616 -0
  45. package/vendor/node_modules/undici/docs/docs/api/Socks5ProxyAgent.md +275 -0
  46. package/vendor/node_modules/undici/docs/docs/api/Util.md +25 -0
  47. package/vendor/node_modules/undici/docs/docs/api/WebSocket.md +141 -0
  48. package/vendor/node_modules/undici/docs/docs/api/api-lifecycle.md +91 -0
  49. package/vendor/node_modules/undici/docs/docs/best-practices/client-certificate.md +64 -0
  50. package/vendor/node_modules/undici/docs/docs/best-practices/crawling.md +58 -0
  51. package/vendor/node_modules/undici/docs/docs/best-practices/mocking-request.md +190 -0
  52. package/vendor/node_modules/undici/docs/docs/best-practices/proxy.md +127 -0
  53. package/vendor/node_modules/undici/docs/docs/best-practices/undici-vs-builtin-fetch.md +224 -0
  54. package/vendor/node_modules/undici/docs/docs/best-practices/writing-tests.md +63 -0
  55. package/vendor/node_modules/undici/index-fetch.js +65 -0
  56. package/vendor/node_modules/undici/index.d.ts +3 -0
  57. package/vendor/node_modules/undici/index.js +234 -0
  58. package/vendor/node_modules/undici/lib/api/abort-signal.js +59 -0
  59. package/vendor/node_modules/undici/lib/api/api-connect.js +110 -0
  60. package/vendor/node_modules/undici/lib/api/api-pipeline.js +252 -0
  61. package/vendor/node_modules/undici/lib/api/api-request.js +214 -0
  62. package/vendor/node_modules/undici/lib/api/api-stream.js +209 -0
  63. package/vendor/node_modules/undici/lib/api/api-upgrade.js +111 -0
  64. package/vendor/node_modules/undici/lib/api/index.js +7 -0
  65. package/vendor/node_modules/undici/lib/api/readable.js +580 -0
  66. package/vendor/node_modules/undici/lib/cache/memory-cache-store.js +279 -0
  67. package/vendor/node_modules/undici/lib/cache/sqlite-cache-store.js +467 -0
  68. package/vendor/node_modules/undici/lib/core/connect.js +159 -0
  69. package/vendor/node_modules/undici/lib/core/constants.js +143 -0
  70. package/vendor/node_modules/undici/lib/core/diagnostics.js +227 -0
  71. package/vendor/node_modules/undici/lib/core/errors.js +477 -0
  72. package/vendor/node_modules/undici/lib/core/request.js +485 -0
  73. package/vendor/node_modules/undici/lib/core/socks5-client.js +422 -0
  74. package/vendor/node_modules/undici/lib/core/socks5-utils.js +212 -0
  75. package/vendor/node_modules/undici/lib/core/symbols.js +76 -0
  76. package/vendor/node_modules/undici/lib/core/tree.js +160 -0
  77. package/vendor/node_modules/undici/lib/core/util.js +992 -0
  78. package/vendor/node_modules/undici/lib/dispatcher/agent.js +162 -0
  79. package/vendor/node_modules/undici/lib/dispatcher/balanced-pool.js +221 -0
  80. package/vendor/node_modules/undici/lib/dispatcher/client-h1.js +1744 -0
  81. package/vendor/node_modules/undici/lib/dispatcher/client-h2.js +1056 -0
  82. package/vendor/node_modules/undici/lib/dispatcher/client.js +669 -0
  83. package/vendor/node_modules/undici/lib/dispatcher/dispatcher-base.js +196 -0
  84. package/vendor/node_modules/undici/lib/dispatcher/dispatcher.js +48 -0
  85. package/vendor/node_modules/undici/lib/dispatcher/env-http-proxy-agent.js +146 -0
  86. package/vendor/node_modules/undici/lib/dispatcher/fixed-queue.js +135 -0
  87. package/vendor/node_modules/undici/lib/dispatcher/h2c-client.js +51 -0
  88. package/vendor/node_modules/undici/lib/dispatcher/pool-base.js +214 -0
  89. package/vendor/node_modules/undici/lib/dispatcher/pool.js +118 -0
  90. package/vendor/node_modules/undici/lib/dispatcher/proxy-agent.js +319 -0
  91. package/vendor/node_modules/undici/lib/dispatcher/retry-agent.js +35 -0
  92. package/vendor/node_modules/undici/lib/dispatcher/round-robin-pool.js +137 -0
  93. package/vendor/node_modules/undici/lib/dispatcher/socks5-proxy-agent.js +260 -0
  94. package/vendor/node_modules/undici/lib/encoding/index.js +33 -0
  95. package/vendor/node_modules/undici/lib/global.js +59 -0
  96. package/vendor/node_modules/undici/lib/handler/cache-handler.js +824 -0
  97. package/vendor/node_modules/undici/lib/handler/cache-revalidation-handler.js +124 -0
  98. package/vendor/node_modules/undici/lib/handler/decorator-handler.js +67 -0
  99. package/vendor/node_modules/undici/lib/handler/deduplication-handler.js +504 -0
  100. package/vendor/node_modules/undici/lib/handler/redirect-handler.js +245 -0
  101. package/vendor/node_modules/undici/lib/handler/retry-handler.js +504 -0
  102. package/vendor/node_modules/undici/lib/handler/unwrap-handler.js +106 -0
  103. package/vendor/node_modules/undici/lib/handler/wrap-handler.js +105 -0
  104. package/vendor/node_modules/undici/lib/interceptor/cache.js +621 -0
  105. package/vendor/node_modules/undici/lib/interceptor/decompress.js +561 -0
  106. package/vendor/node_modules/undici/lib/interceptor/deduplicate.js +117 -0
  107. package/vendor/node_modules/undici/lib/interceptor/dns.js +571 -0
  108. package/vendor/node_modules/undici/lib/interceptor/dump.js +99 -0
  109. package/vendor/node_modules/undici/lib/interceptor/redirect.js +21 -0
  110. package/vendor/node_modules/undici/lib/interceptor/response-error.js +95 -0
  111. package/vendor/node_modules/undici/lib/interceptor/retry.js +19 -0
  112. package/vendor/node_modules/undici/lib/llhttp/.gitkeep +0 -0
  113. package/vendor/node_modules/undici/lib/llhttp/constants.d.ts +195 -0
  114. package/vendor/node_modules/undici/lib/llhttp/constants.js +531 -0
  115. package/vendor/node_modules/undici/lib/llhttp/llhttp-wasm.js +15 -0
  116. package/vendor/node_modules/undici/lib/llhttp/llhttp_simd-wasm.js +15 -0
  117. package/vendor/node_modules/undici/lib/llhttp/utils.d.ts +2 -0
  118. package/vendor/node_modules/undici/lib/llhttp/utils.js +12 -0
  119. package/vendor/node_modules/undici/lib/mock/mock-agent.js +232 -0
  120. package/vendor/node_modules/undici/lib/mock/mock-call-history.js +248 -0
  121. package/vendor/node_modules/undici/lib/mock/mock-client.js +68 -0
  122. package/vendor/node_modules/undici/lib/mock/mock-errors.js +29 -0
  123. package/vendor/node_modules/undici/lib/mock/mock-interceptor.js +209 -0
  124. package/vendor/node_modules/undici/lib/mock/mock-pool.js +68 -0
  125. package/vendor/node_modules/undici/lib/mock/mock-symbols.js +32 -0
  126. package/vendor/node_modules/undici/lib/mock/mock-utils.js +486 -0
  127. package/vendor/node_modules/undici/lib/mock/pending-interceptors-formatter.js +43 -0
  128. package/vendor/node_modules/undici/lib/mock/snapshot-agent.js +353 -0
  129. package/vendor/node_modules/undici/lib/mock/snapshot-recorder.js +588 -0
  130. package/vendor/node_modules/undici/lib/mock/snapshot-utils.js +158 -0
  131. package/vendor/node_modules/undici/lib/util/cache.js +676 -0
  132. package/vendor/node_modules/undici/lib/util/date.js +670 -0
  133. package/vendor/node_modules/undici/lib/util/promise.js +28 -0
  134. package/vendor/node_modules/undici/lib/util/runtime-features.js +124 -0
  135. package/vendor/node_modules/undici/lib/util/stats.js +32 -0
  136. package/vendor/node_modules/undici/lib/util/timers.js +425 -0
  137. package/vendor/node_modules/undici/lib/web/cache/cache.js +864 -0
  138. package/vendor/node_modules/undici/lib/web/cache/cachestorage.js +152 -0
  139. package/vendor/node_modules/undici/lib/web/cache/util.js +45 -0
  140. package/vendor/node_modules/undici/lib/web/cookies/constants.js +12 -0
  141. package/vendor/node_modules/undici/lib/web/cookies/index.js +201 -0
  142. package/vendor/node_modules/undici/lib/web/cookies/parse.js +315 -0
  143. package/vendor/node_modules/undici/lib/web/cookies/util.js +371 -0
  144. package/vendor/node_modules/undici/lib/web/eventsource/eventsource-stream.js +575 -0
  145. package/vendor/node_modules/undici/lib/web/eventsource/eventsource.js +504 -0
  146. package/vendor/node_modules/undici/lib/web/eventsource/util.js +29 -0
  147. package/vendor/node_modules/undici/lib/web/fetch/LICENSE +21 -0
  148. package/vendor/node_modules/undici/lib/web/fetch/body.js +509 -0
  149. package/vendor/node_modules/undici/lib/web/fetch/constants.js +131 -0
  150. package/vendor/node_modules/undici/lib/web/fetch/data-url.js +596 -0
  151. package/vendor/node_modules/undici/lib/web/fetch/formdata-parser.js +586 -0
  152. package/vendor/node_modules/undici/lib/web/fetch/formdata.js +259 -0
  153. package/vendor/node_modules/undici/lib/web/fetch/global.js +40 -0
  154. package/vendor/node_modules/undici/lib/web/fetch/headers.js +719 -0
  155. package/vendor/node_modules/undici/lib/web/fetch/index.js +2413 -0
  156. package/vendor/node_modules/undici/lib/web/fetch/request.js +1115 -0
  157. package/vendor/node_modules/undici/lib/web/fetch/response.js +641 -0
  158. package/vendor/node_modules/undici/lib/web/fetch/util.js +1522 -0
  159. package/vendor/node_modules/undici/lib/web/infra/index.js +243 -0
  160. package/vendor/node_modules/undici/lib/web/subresource-integrity/Readme.md +9 -0
  161. package/vendor/node_modules/undici/lib/web/subresource-integrity/subresource-integrity.js +307 -0
  162. package/vendor/node_modules/undici/lib/web/webidl/index.js +1006 -0
  163. package/vendor/node_modules/undici/lib/web/websocket/connection.js +339 -0
  164. package/vendor/node_modules/undici/lib/web/websocket/constants.js +126 -0
  165. package/vendor/node_modules/undici/lib/web/websocket/events.js +331 -0
  166. package/vendor/node_modules/undici/lib/web/websocket/frame.js +133 -0
  167. package/vendor/node_modules/undici/lib/web/websocket/permessage-deflate.js +105 -0
  168. package/vendor/node_modules/undici/lib/web/websocket/receiver.js +520 -0
  169. package/vendor/node_modules/undici/lib/web/websocket/sender.js +109 -0
  170. package/vendor/node_modules/undici/lib/web/websocket/stream/websocketerror.js +104 -0
  171. package/vendor/node_modules/undici/lib/web/websocket/stream/websocketstream.js +496 -0
  172. package/vendor/node_modules/undici/lib/web/websocket/util.js +347 -0
  173. package/vendor/node_modules/undici/lib/web/websocket/websocket.js +758 -0
  174. package/vendor/node_modules/undici/package.json +152 -0
  175. package/vendor/node_modules/undici/scripts/strip-comments.js +10 -0
  176. package/vendor/node_modules/undici/types/README.md +6 -0
  177. package/vendor/node_modules/undici/types/agent.d.ts +32 -0
  178. package/vendor/node_modules/undici/types/api.d.ts +43 -0
  179. package/vendor/node_modules/undici/types/balanced-pool.d.ts +30 -0
  180. package/vendor/node_modules/undici/types/cache-interceptor.d.ts +179 -0
  181. package/vendor/node_modules/undici/types/cache.d.ts +36 -0
  182. package/vendor/node_modules/undici/types/client-stats.d.ts +15 -0
  183. package/vendor/node_modules/undici/types/client.d.ts +153 -0
  184. package/vendor/node_modules/undici/types/connector.d.ts +36 -0
  185. package/vendor/node_modules/undici/types/content-type.d.ts +21 -0
  186. package/vendor/node_modules/undici/types/cookies.d.ts +30 -0
  187. package/vendor/node_modules/undici/types/diagnostics-channel.d.ts +74 -0
  188. package/vendor/node_modules/undici/types/dispatcher.d.ts +275 -0
  189. package/vendor/node_modules/undici/types/env-http-proxy-agent.d.ts +22 -0
  190. package/vendor/node_modules/undici/types/errors.d.ts +177 -0
  191. package/vendor/node_modules/undici/types/eventsource.d.ts +66 -0
  192. package/vendor/node_modules/undici/types/fetch.d.ts +231 -0
  193. package/vendor/node_modules/undici/types/formdata.d.ts +114 -0
  194. package/vendor/node_modules/undici/types/global-dispatcher.d.ts +9 -0
  195. package/vendor/node_modules/undici/types/global-origin.d.ts +7 -0
  196. package/vendor/node_modules/undici/types/h2c-client.d.ts +73 -0
  197. package/vendor/node_modules/undici/types/handlers.d.ts +14 -0
  198. package/vendor/node_modules/undici/types/header.d.ts +160 -0
  199. package/vendor/node_modules/undici/types/index.d.ts +91 -0
  200. package/vendor/node_modules/undici/types/interceptors.d.ts +82 -0
  201. package/vendor/node_modules/undici/types/mock-agent.d.ts +68 -0
  202. package/vendor/node_modules/undici/types/mock-call-history.d.ts +111 -0
  203. package/vendor/node_modules/undici/types/mock-client.d.ts +27 -0
  204. package/vendor/node_modules/undici/types/mock-errors.d.ts +12 -0
  205. package/vendor/node_modules/undici/types/mock-interceptor.d.ts +94 -0
  206. package/vendor/node_modules/undici/types/mock-pool.d.ts +27 -0
  207. package/vendor/node_modules/undici/types/patch.d.ts +29 -0
  208. package/vendor/node_modules/undici/types/pool-stats.d.ts +19 -0
  209. package/vendor/node_modules/undici/types/pool.d.ts +41 -0
  210. package/vendor/node_modules/undici/types/proxy-agent.d.ts +29 -0
  211. package/vendor/node_modules/undici/types/readable.d.ts +68 -0
  212. package/vendor/node_modules/undici/types/retry-agent.d.ts +8 -0
  213. package/vendor/node_modules/undici/types/retry-handler.d.ts +125 -0
  214. package/vendor/node_modules/undici/types/round-robin-pool.d.ts +41 -0
  215. package/vendor/node_modules/undici/types/snapshot-agent.d.ts +109 -0
  216. package/vendor/node_modules/undici/types/socks5-proxy-agent.d.ts +25 -0
  217. package/vendor/node_modules/undici/types/util.d.ts +18 -0
  218. package/vendor/node_modules/undici/types/utility.d.ts +7 -0
  219. package/vendor/node_modules/undici/types/webidl.d.ts +347 -0
  220. package/vendor/node_modules/undici/types/websocket.d.ts +188 -0
package/index.js ADDED
@@ -0,0 +1,341 @@
1
+ /**
2
+ * Model proxy switch.
3
+ *
4
+ * While enabled, this plugin installs a process-wide outbound HTTP proxy
5
+ * policy, so the requests Node's fetch would send direct (model API, model
6
+ * discovery, web search, MCP over HTTP) are routed through the configured
7
+ * proxy instead. Disabling it restores the launcher's original routing.
8
+ *
9
+ * The proxy address follows the operating system's proxy setting by default
10
+ * (Windows: the "Internet Settings" registry values), falling back to the
11
+ * plugin's configured URL when detection finds nothing. An optional host
12
+ * allowlist narrows routing to named domains only — loopback always stays
13
+ * direct either way.
14
+ *
15
+ * The harness reads proxy names from its launch environment only: the
16
+ * launcher resolves and installs the policy before the first plugin loads, so
17
+ * writing `process.env` later changes nothing on its own. The policy installer
18
+ * is vendored under `vendor/`, with its `undici` dependency in this package's
19
+ * `node_modules/`: the harness's installation-scope module resolution only
20
+ * reaches packages installed inside the profile, and this one is linked from
21
+ * outside it.
22
+ *
23
+ * The switch is exposed to the browser half through a Typert remote
24
+ * (`modelProxy.status` / `setEnabled` / `setHosts`), and its position is
25
+ * remembered in the harness home so a restart keeps the last choice.
26
+ */
27
+ import { execFileSync } from 'node:child_process'
28
+ import { appendFileSync, readFileSync, writeFileSync } from 'node:fs'
29
+ import { homedir } from 'node:os'
30
+ import { join } from 'node:path'
31
+ import { installProxyFromEnvironment } from './vendor/http-proxy.js'
32
+
33
+ export const name = 'model-proxy'
34
+
35
+ const DEFAULTS = {
36
+ proxy: 'auto',
37
+ bypass: 'localhost,127.0.0.1,::1',
38
+ hosts: '',
39
+ }
40
+
41
+ const PACKAGE = 'dsh-model-proxy-switch'
42
+ const SERVICE = 'modelProxy'
43
+
44
+ const HOME = process.env.DSH_HOME || join(homedir(), '.dsh')
45
+
46
+ /** Append one line to the plugin's own diagnostic log, best effort. */
47
+ function diag(message) {
48
+ try {
49
+ appendFileSync(join(HOME, 'model-proxy.log'), `${new Date().toISOString()} ${message}\n`)
50
+ } catch {
51
+ // Diagnostics must never break the plugin.
52
+ }
53
+ }
54
+
55
+ diag('module loaded')
56
+
57
+ //#region system proxy detection
58
+ function readRegistryValue(name) {
59
+ try {
60
+ const out = execFileSync(
61
+ 'reg',
62
+ ['query', 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings', '/v', name],
63
+ { encoding: 'utf8', timeout: 5000, windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'] },
64
+ )
65
+ const match = new RegExp(`${name}\\s+REG_(?:SZ|DWORD)\\s+(\\S+)`).exec(out)
66
+ return match?.[1]
67
+ } catch {
68
+ return undefined
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Read the operating system's HTTP proxy address.
74
+ *
75
+ * Windows stores it under HKCU Internet Settings: `ProxyServer` is either one
76
+ * `host:port` for every protocol or a semicolon list of `scheme=host:port`
77
+ * entries, and `ProxyEnable` says whether apps should follow it. This plugin
78
+ * is itself an explicit opt-in, so the recorded address is used whenever it
79
+ * is present; `enabled` only changes how the panel labels the source.
80
+ *
81
+ * @returns `{ url, enabled }` with an http(s):// URL, or undefined when
82
+ * nothing usable is recorded.
83
+ */
84
+ function detectSystemProxy() {
85
+ if (process.platform !== 'win32') return undefined
86
+ try {
87
+ const server = readRegistryValue('ProxyServer')
88
+ if (!server) return undefined
89
+ const byScheme = {}
90
+ for (const part of server.split(';')) {
91
+ const eq = part.indexOf('=')
92
+ if (eq > 0) byScheme[part.slice(0, eq).trim().toLowerCase()] = part.slice(eq + 1).trim()
93
+ }
94
+ const picked = byScheme.https ?? byScheme.http ?? (Object.keys(byScheme).length === 0 ? server.trim() : undefined)
95
+ if (!picked) return undefined
96
+ const url = picked.includes('://') ? picked : `http://${picked}`
97
+ return { url, enabled: readRegistryValue('ProxyEnable') === '0x1' }
98
+ } catch (error) {
99
+ diag(`system proxy detection failed: ${error?.message ?? error}`)
100
+ return undefined
101
+ }
102
+ }
103
+ //#endregion
104
+
105
+ /** The launch environment is a Map of name to `{ value }`, not a plain object. */
106
+ function proxyEnvironment(proxy, bypass) {
107
+ return new Map([
108
+ ['http_proxy', { value: proxy }],
109
+ ['https_proxy', { value: proxy }],
110
+ ['all_proxy', { value: proxy }],
111
+ ['no_proxy', { value: bypass }],
112
+ ])
113
+ }
114
+
115
+ function stateFile() {
116
+ return join(HOME, 'model-proxy.json')
117
+ }
118
+
119
+ function readState(fallback) {
120
+ try {
121
+ const parsed = JSON.parse(readFileSync(stateFile(), 'utf8'))
122
+ return {
123
+ enabled: typeof parsed?.enabled === 'boolean' ? parsed.enabled : fallback.enabled,
124
+ hosts: typeof parsed?.hosts === 'string' ? parsed.hosts : fallback.hosts,
125
+ }
126
+ } catch {
127
+ return fallback
128
+ }
129
+ }
130
+
131
+ function writeState(state) {
132
+ try {
133
+ writeFileSync(stateFile(), `${JSON.stringify(state, null, 2)}\n`, 'utf8')
134
+ } catch {
135
+ // A read-only home must not break the switch for this run.
136
+ }
137
+ }
138
+
139
+ //#region typert face
140
+ const codec = (typeSymbol, parse) => ({ mode: 'strict', typeSymbol: `${PACKAGE}#${typeSymbol}`, create: () => ({ parse }) })
141
+ const booleanValue = codec('Boolean', (value) => {
142
+ if (typeof value !== 'boolean') throw new Error(`Expected a boolean, got ${typeof value}`)
143
+ return value
144
+ })
145
+ const hostsValue = codec('Hosts', (value) => {
146
+ if (typeof value !== 'string') throw new Error(`Expected a string, got ${typeof value}`)
147
+ if (value.length > 2048) throw new Error('Host list is too long')
148
+ return value
149
+ })
150
+ const jsonResult = codec('ModelProxyStatus', (value) => value)
151
+
152
+ const methodDescriptor = (method, parameters) => ({
153
+ id: `${PACKAGE}#${SERVICE}/${method}`,
154
+ service: SERVICE,
155
+ namespace: SERVICE,
156
+ method,
157
+ invocation: { kind: 'direct' },
158
+ result: jsonResult,
159
+ parameters,
160
+ })
161
+ const jsonParameter = (name, parameterCodec) => ({ name, wire: name, source: 'json', codec: parameterCodec })
162
+
163
+ const descriptors = [
164
+ methodDescriptor('status', []),
165
+ methodDescriptor('setEnabled', [jsonParameter('enabled', booleanValue)]),
166
+ methodDescriptor('setHosts', [jsonParameter('hosts', hostsValue)]),
167
+ ]
168
+ //#endregion
169
+
170
+ export function apply(ctx, config) {
171
+ const configuredProxy = String(config?.proxy ?? DEFAULTS.proxy).trim()
172
+ const bypass = String(config?.bypass ?? DEFAULTS.bypass).trim()
173
+ const state = readState({ enabled: true, hosts: String(config?.hosts ?? DEFAULTS.hosts) })
174
+
175
+ let enabled = state.enabled
176
+ let hosts = state.hosts
177
+ let dispose = undefined
178
+ /** The proxy actually installed, and where it came from. */
179
+ let activeProxy = undefined
180
+ let proxySource = 'none'
181
+
182
+ /**
183
+ * Detection spawns a `reg` child process, so reads are cached briefly:
184
+ * status calls (panel refresh) share one read per TTL, while explicit user
185
+ * actions always force a fresh read.
186
+ */
187
+ const DETECTION_TTL_MS = 10_000
188
+ let detectionCache = { at: 0, value: undefined }
189
+
190
+ /**
191
+ * Resolve which proxy URL would be installed right now. `auto` follows the
192
+ * operating system's recorded proxy address so swapping proxy software
193
+ * needs no edit here.
194
+ */
195
+ function resolveProxy(options = {}) {
196
+ if (configuredProxy !== '' && configuredProxy.toLowerCase() !== 'auto') {
197
+ return { url: configuredProxy, source: 'config' }
198
+ }
199
+ const now = Date.now()
200
+ if (!options.fresh && detectionCache.value !== undefined && now - detectionCache.at < DETECTION_TTL_MS) {
201
+ return detectionCache.value
202
+ }
203
+ const detected = detectSystemProxy()
204
+ const value =
205
+ detected === undefined
206
+ ? { url: undefined, source: 'none' }
207
+ : { url: detected.url, source: detected.enabled ? 'system' : 'registry' }
208
+ detectionCache = { at: now, value }
209
+ return value
210
+ }
211
+
212
+ const install = async () => {
213
+ if (!enabled || dispose !== undefined) return
214
+ // Explicit actions (switch on, host-list save, startup) read fresh.
215
+ const { url, source } = resolveProxy({ fresh: true })
216
+ if (url === undefined) {
217
+ activeProxy = undefined
218
+ proxySource = 'none'
219
+ diag('install skipped: no system proxy detected and no fallback configured')
220
+ return
221
+ }
222
+ let scheme
223
+ try {
224
+ scheme = new URL(url).protocol
225
+ } catch {
226
+ diag(`install refused: unparseable proxy URL ${JSON.stringify(url)}`)
227
+ return
228
+ }
229
+ if (scheme !== 'http:' && scheme !== 'https:') {
230
+ diag(`install refused: unsupported ${scheme} proxy`)
231
+ return
232
+ }
233
+ dispose = await installProxyFromEnvironment(
234
+ proxyEnvironment(url, bypass),
235
+ (message) => diag(`policy: ${message}`),
236
+ { hosts },
237
+ )
238
+ activeProxy = url
239
+ proxySource = source
240
+ diag(`policy installed: ${url} (${source}), hosts=${hosts === '' ? '<all>' : hosts}`)
241
+ }
242
+
243
+ const uninstall = async () => {
244
+ if (dispose === undefined) return
245
+ const pending = dispose
246
+ dispose = undefined
247
+ await pending()
248
+ diag('policy disposed')
249
+ }
250
+
251
+ ctx.effect(async () => {
252
+ try {
253
+ await install()
254
+ } catch (error) {
255
+ diag(`install failed: ${error?.stack ?? error}`)
256
+ console.warn('[model-proxy] install failed', error)
257
+ }
258
+ return async () => {
259
+ try {
260
+ await uninstall()
261
+ } catch (error) {
262
+ diag(`uninstall failed: ${error?.stack ?? error}`)
263
+ }
264
+ }
265
+ })
266
+
267
+ /**
268
+ * Re-read the proxy source and bring the installed policy in line with it.
269
+ * Detection used to run only at install time, so flipping the system proxy
270
+ * afterwards left the panel showing a stale address and label.
271
+ */
272
+ const reconcile = async () => {
273
+ const resolved = resolveProxy()
274
+ if (!enabled) {
275
+ activeProxy = resolved.url
276
+ proxySource = resolved.source
277
+ return
278
+ }
279
+ if (dispose !== undefined && resolved.url === activeProxy) {
280
+ // Same address, only the provenance changed (system proxy flipped).
281
+ if (resolved.source !== 'none') proxySource = resolved.source
282
+ return
283
+ }
284
+ if (dispose !== undefined) await uninstall()
285
+ activeProxy = resolved.url
286
+ proxySource = resolved.source
287
+ if (resolved.url !== undefined) await install()
288
+ }
289
+
290
+ const snapshot = () => ({ enabled, proxy: activeProxy, proxySource, hosts })
291
+
292
+ const service = {
293
+ status: () => {
294
+ diag('remote status()')
295
+ return reconcile().then(snapshot)
296
+ },
297
+ setEnabled(next) {
298
+ diag(`remote setEnabled(${next})`)
299
+ enabled = next
300
+ writeState({ enabled, hosts })
301
+ return (next ? install() : uninstall()).then(snapshot)
302
+ },
303
+ setHosts(next) {
304
+ const trimmed = next.trim()
305
+ diag(`remote setHosts(${JSON.stringify(trimmed)})`)
306
+ hosts = trimmed
307
+ writeState({ enabled, hosts })
308
+ const swap = async () => {
309
+ if (dispose === undefined) return
310
+ await uninstall()
311
+ await install()
312
+ }
313
+ return swap().then(snapshot)
314
+ },
315
+ }
316
+
317
+ ctx.inject(['typert'], (inner) => {
318
+ try {
319
+ // The gateway validates the service by reading its `typertRemote`
320
+ // binding, normally created by extending TypertRemoteService from
321
+ // @deepseek-ai/dsh-typert-protocol. That package is unreachable from a
322
+ // link-installed bundle, so the binding is constructed inline: a frozen
323
+ // { service, serviceKey, namespace } triple is the whole contract.
324
+ Object.assign(service, {
325
+ typertRemote: Object.freeze({ service, serviceKey: SERVICE, namespace: SERVICE }),
326
+ })
327
+ inner.provide(SERVICE, service)
328
+ inner.typert.register({
329
+ package: PACKAGE,
330
+ face: 'host',
331
+ schemas: [],
332
+ model: { services: [], events: [], objects: [] },
333
+ invocations: descriptors,
334
+ })
335
+ diag('typert face registered')
336
+ } catch (error) {
337
+ diag(`typert registration failed: ${error?.stack ?? error}`)
338
+ console.warn('[model-proxy] remote registration failed; the switch keeps its last state', error)
339
+ }
340
+ })
341
+ }
package/locale/en.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "meta": {
3
+ "title": "Model Proxy Switch",
4
+ "description": "Sidebar switch: routes model API and model-list requests through a local HTTP proxy (follows the system proxy by default); off restores direct connections."
5
+ },
6
+ "nav": "Proxy",
7
+ "title": "Model proxy",
8
+ "summary": "Routes model API and model-list requests through an HTTP proxy while on; off means direct.",
9
+ "stateOn": "On",
10
+ "stateOff": "Off",
11
+ "label": "Use proxy",
12
+ "proxy": "Proxy URL",
13
+ "sourceSystem": "from system settings",
14
+ "sourceRegistry": "from registry (system proxy off)",
15
+ "sourceConfig": "from plugin config",
16
+ "sourceNone": "no system proxy detected",
17
+ "hostsLabel": "Only proxy these hosts",
18
+ "hostsHint": "Comma-separated; empty means every external address. Example: api.wufannao.cc.cd",
19
+ "save": "Save",
20
+ "saved": "Saved",
21
+ "scope": "Process-wide switch: model discovery, model calls, web search and MCP share this route. Loopback always stays direct.",
22
+ "saving": "Switching…",
23
+ "readFailed": "Could not read the state",
24
+ "writeFailed": "Switch failed"
25
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "meta": {
3
+ "title": "模型代理开关",
4
+ "description": "侧栏开关:让模型 API、获取模型列表等出网请求走本地 HTTP 代理(默认跟随系统代理设置),关闭即恢复直连。"
5
+ },
6
+ "nav": "代理",
7
+ "title": "模型代理",
8
+ "summary": "让模型 API、获取模型列表等出网请求走 HTTP 代理;关闭即直连。",
9
+ "stateOn": "已开启",
10
+ "stateOff": "已关闭",
11
+ "label": "使用代理",
12
+ "proxy": "代理地址",
13
+ "sourceSystem": "跟随系统代理",
14
+ "sourceRegistry": "注册表记录(系统代理未开启)",
15
+ "sourceConfig": "插件配置",
16
+ "sourceNone": "未检测到系统代理",
17
+ "hostsLabel": "只对这些域名走代理",
18
+ "hostsHint": "逗号分隔,留空表示所有外部地址。例:api.wufannao.cc.cd",
19
+ "save": "保存",
20
+ "saved": "已保存",
21
+ "scope": "开关作用于整个宿主进程的出网请求:获取模型列表、模型调用、网页搜索、MCP 都走同一条链路;本机回环始终直连。",
22
+ "saving": "正在切换…",
23
+ "readFailed": "读取状态失败",
24
+ "writeFailed": "切换失败"
25
+ }
package/package.json CHANGED
@@ -1,6 +1,57 @@
1
1
  {
2
2
  "name": "dsh-model-proxy-switch",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "private": false,
5
+ "description": "Sidebar switch that routes model API and model-list requests through an HTTP proxy (follows the system proxy by default); off restores direct connections. Optional per-host allowlist.",
6
+ "keywords": [
7
+ "dsh",
8
+ "dsh-plugin",
9
+ "deepseek-harness",
10
+ "cordis-plugin",
11
+ "proxy",
12
+ "http-proxy",
13
+ "model"
14
+ ],
15
+ "license": "MIT",
16
+ "type": "module",
17
+ "main": "index.js",
18
+ "exports": {
19
+ ".": "./index.js",
20
+ "./client": "./client.js",
21
+ "./cordis.patch.yml": "./cordis.patch.yml",
22
+ "./locale/*.json": "./locale/*.json",
23
+ "./icon": "./icon.svg",
24
+ "./package.json": "./package.json"
25
+ },
26
+ "files": [
27
+ "index.js",
28
+ "client.js",
29
+ "cordis.patch.yml",
30
+ "locale/*.json",
31
+ "icon.svg",
32
+ "vendor/",
33
+ "README.md"
34
+ ],
35
+ "icon": "./icon.svg",
36
+ "engines": {
37
+ "node": ">=22",
38
+ "dsh": ">=0.2.0-0 <0.3.0-0"
39
+ },
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "git+https://github.com/qinghuan50/dsh-model-proxy-switch.git"
43
+ },
44
+ "dsh": {
45
+ "bundle": {
46
+ "patch": "./cordis.patch.yml"
47
+ },
48
+ "client": {
49
+ "platform": "web",
50
+ "immediately": true,
51
+ "inject": [
52
+ "@deepseek-ai/dsh-client-ui-sidebar",
53
+ "@deepseek-ai/dsh-client-locale"
54
+ ]
55
+ }
56
+ }
57
+ }