@linxin666/dsh-remote-web-ui 0.3.5 → 0.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (238) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +77 -468
  3. package/README.zh.md +83 -113
  4. package/lib/client.js +955 -203
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +1136 -1466
  7. package/lib/types/client/FooterRemoteEntry.d.ts +6 -7
  8. package/lib/types/client/FooterRemoteEntry.d.ts.map +1 -1
  9. package/lib/types/client/FooterRemoteEntry.js +1 -1
  10. package/lib/types/client/RemoteEntry.d.ts +7 -4
  11. package/lib/types/client/RemoteEntry.d.ts.map +1 -1
  12. package/lib/types/client/RemoteEntry.js +7 -10
  13. package/lib/types/client/RemotePanel.d.ts +2 -2
  14. package/lib/types/client/RemotePanel.d.ts.map +1 -1
  15. package/lib/types/client/RemotePanel.js +2 -2
  16. package/lib/types/client/RemoteSettingsCard.d.ts +9 -5
  17. package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
  18. package/lib/types/client/RemoteSettingsCard.js +56 -3
  19. package/lib/types/client/deep-link.d.ts +10 -13
  20. package/lib/types/client/deep-link.d.ts.map +1 -1
  21. package/lib/types/client/deep-link.js +14 -74
  22. package/lib/types/client/index.d.ts +2 -6
  23. package/lib/types/client/index.d.ts.map +1 -1
  24. package/lib/types/client/index.js +73 -39
  25. package/lib/types/client/locales.d.ts +26 -12
  26. package/lib/types/client/locales.d.ts.map +1 -1
  27. package/lib/types/client/locales.js +36 -22
  28. package/lib/types/client/mobile-adapt.d.ts +40 -0
  29. package/lib/types/client/mobile-adapt.d.ts.map +1 -0
  30. package/lib/types/client/mobile-adapt.js +967 -0
  31. package/lib/types/client/pair-api.d.ts +24 -6
  32. package/lib/types/client/pair-api.d.ts.map +1 -1
  33. package/lib/types/client/pair-api.js +9 -14
  34. package/lib/types/client/remote-channel.d.ts +3 -0
  35. package/lib/types/client/remote-channel.d.ts.map +1 -1
  36. package/lib/types/client/remote-channel.js +35 -3
  37. package/lib/types/client/settings-form.d.ts +35 -43
  38. package/lib/types/client/settings-form.d.ts.map +1 -1
  39. package/lib/types/client/settings-form.js +60 -53
  40. package/lib/types/firewall.d.ts +75 -0
  41. package/lib/types/firewall.d.ts.map +1 -0
  42. package/lib/types/firewall.js +184 -0
  43. package/lib/types/gate.d.ts +11 -5
  44. package/lib/types/gate.d.ts.map +1 -1
  45. package/lib/types/gate.js +11 -5
  46. package/lib/types/index.d.ts +39 -10
  47. package/lib/types/index.d.ts.map +1 -1
  48. package/lib/types/index.js +206 -58
  49. package/lib/types/inner-auth.d.ts +32 -0
  50. package/lib/types/inner-auth.d.ts.map +1 -0
  51. package/lib/types/inner-auth.js +72 -0
  52. package/lib/types/lan-bind-plan.d.ts +55 -0
  53. package/lib/types/lan-bind-plan.d.ts.map +1 -0
  54. package/lib/types/lan-bind-plan.js +60 -0
  55. package/lib/types/lan-bind.d.ts +69 -0
  56. package/lib/types/lan-bind.d.ts.map +1 -0
  57. package/lib/types/lan-bind.js +128 -0
  58. package/lib/types/loopback-proxy.d.ts +9 -2
  59. package/lib/types/loopback-proxy.d.ts.map +1 -1
  60. package/lib/types/loopback-proxy.js +24 -2
  61. package/lib/types/pairing.d.ts +15 -3
  62. package/lib/types/pairing.d.ts.map +1 -1
  63. package/lib/types/pairing.js +20 -6
  64. package/lib/types/remote-api.d.ts +30 -13
  65. package/lib/types/remote-api.d.ts.map +1 -1
  66. package/lib/types/remote-api.js +55 -42
  67. package/lib/types/remote-channel-boot.d.ts +21 -2
  68. package/lib/types/remote-channel-boot.d.ts.map +1 -1
  69. package/lib/types/remote-channel-boot.js +65 -3
  70. package/lib/types/remote-channel-rules.d.ts +8 -0
  71. package/lib/types/remote-channel-rules.d.ts.map +1 -1
  72. package/lib/types/remote-channel-rules.js +9 -2
  73. package/lib/types/remote-methods.d.ts +45 -11
  74. package/lib/types/remote-methods.d.ts.map +1 -1
  75. package/lib/types/remote-methods.js +56 -27
  76. package/lib/types/remote-presence-pet.d.ts +51 -0
  77. package/lib/types/remote-presence-pet.d.ts.map +1 -0
  78. package/lib/types/remote-presence-pet.js +101 -0
  79. package/lib/types/routes.d.ts +76 -4
  80. package/lib/types/routes.d.ts.map +1 -1
  81. package/lib/types/routes.js +336 -19
  82. package/lib/types/tunnel.d.ts.map +1 -1
  83. package/lib/types/tunnel.js +7 -1
  84. package/package.json +20 -18
  85. package/src/client/FooterRemoteEntry.tsx +9 -10
  86. package/src/client/RemoteEntry.tsx +13 -14
  87. package/src/client/RemotePanel.tsx +5 -16
  88. package/src/client/RemoteSettingsCard.tsx +72 -13
  89. package/src/client/deep-link.ts +14 -75
  90. package/src/client/index.ts +77 -39
  91. package/src/client/locales.ts +36 -22
  92. package/src/client/mobile-adapt.ts +913 -0
  93. package/src/client/pair-api.ts +26 -15
  94. package/src/client/remote-channel.ts +31 -3
  95. package/src/client/settings-form.ts +78 -92
  96. package/src/dsh-home.ts +1 -1
  97. package/src/firewall.ts +230 -0
  98. package/src/gate.ts +11 -5
  99. package/src/index.ts +235 -65
  100. package/src/inner-auth.ts +85 -0
  101. package/src/lan-bind-plan.ts +81 -0
  102. package/src/lan-bind.ts +145 -0
  103. package/src/loopback-proxy.ts +34 -0
  104. package/src/pairing.persist.test.ts +1 -1
  105. package/src/pairing.ts +20 -6
  106. package/src/remote-api.ts +67 -45
  107. package/src/remote-channel-boot.ts +70 -3
  108. package/src/remote-channel-rules.ts +17 -2
  109. package/src/remote-methods.ts +58 -27
  110. package/src/remote-presence-pet.ts +129 -0
  111. package/src/routes.ts +358 -19
  112. package/src/tunnel.ts +7 -1
  113. package/assets/apple-touch-icon.png +0 -0
  114. package/assets/mobile-icon-192.png +0 -0
  115. package/assets/mobile-icon-512.png +0 -0
  116. package/assets/mobile-service-worker.js +0 -63
  117. package/lib/mobile.js +0 -17621
  118. package/lib/mobile.js.map +0 -1
  119. package/lib/types/mobile/PairRequiredView.d.ts +0 -7
  120. package/lib/types/mobile/PairRequiredView.d.ts.map +0 -1
  121. package/lib/types/mobile/PairRequiredView.js +0 -28
  122. package/lib/types/mobile/PairRequiredView.test.d.ts +0 -2
  123. package/lib/types/mobile/PairRequiredView.test.d.ts.map +0 -1
  124. package/lib/types/mobile/PairRequiredView.test.js +0 -24
  125. package/lib/types/mobile/api.d.ts +0 -145
  126. package/lib/types/mobile/api.d.ts.map +0 -1
  127. package/lib/types/mobile/api.js +0 -103
  128. package/lib/types/mobile/index.d.ts +0 -10
  129. package/lib/types/mobile/index.d.ts.map +0 -1
  130. package/lib/types/mobile/index.js +0 -38
  131. package/lib/types/mobile/markdown.d.ts +0 -34
  132. package/lib/types/mobile/markdown.d.ts.map +0 -1
  133. package/lib/types/mobile/markdown.js +0 -285
  134. package/lib/types/mobile/markdown.test.d.ts +0 -2
  135. package/lib/types/mobile/markdown.test.d.ts.map +0 -1
  136. package/lib/types/mobile/markdown.test.js +0 -79
  137. package/lib/types/mobile/messages.d.ts +0 -141
  138. package/lib/types/mobile/messages.d.ts.map +0 -1
  139. package/lib/types/mobile/messages.js +0 -553
  140. package/lib/types/mobile/messages.test.d.ts +0 -2
  141. package/lib/types/mobile/messages.test.d.ts.map +0 -1
  142. package/lib/types/mobile/messages.test.js +0 -356
  143. package/lib/types/mobile/mobile-styles.d.ts +0 -6
  144. package/lib/types/mobile/mobile-styles.d.ts.map +0 -1
  145. package/lib/types/mobile/mobile-styles.js +0 -1438
  146. package/lib/types/mobile/mobile-theme.d.ts +0 -22
  147. package/lib/types/mobile/mobile-theme.d.ts.map +0 -1
  148. package/lib/types/mobile/mobile-theme.js +0 -70
  149. package/lib/types/mobile/mobile-theme.test.d.ts +0 -2
  150. package/lib/types/mobile/mobile-theme.test.d.ts.map +0 -1
  151. package/lib/types/mobile/mobile-theme.test.js +0 -71
  152. package/lib/types/mobile/mux.d.ts +0 -128
  153. package/lib/types/mobile/mux.d.ts.map +0 -1
  154. package/lib/types/mobile/mux.js +0 -305
  155. package/lib/types/mobile/mux.test.d.ts +0 -2
  156. package/lib/types/mobile/mux.test.d.ts.map +0 -1
  157. package/lib/types/mobile/mux.test.js +0 -267
  158. package/lib/types/mobile/pairing.d.ts +0 -31
  159. package/lib/types/mobile/pairing.d.ts.map +0 -1
  160. package/lib/types/mobile/pairing.js +0 -62
  161. package/lib/types/mobile/rpc.d.ts +0 -38
  162. package/lib/types/mobile/rpc.d.ts.map +0 -1
  163. package/lib/types/mobile/rpc.js +0 -83
  164. package/lib/types/mobile/theme-toggle.d.ts +0 -12
  165. package/lib/types/mobile/theme-toggle.d.ts.map +0 -1
  166. package/lib/types/mobile/theme-toggle.js +0 -26
  167. package/lib/types/mobile/views/App.d.ts +0 -57
  168. package/lib/types/mobile/views/App.d.ts.map +0 -1
  169. package/lib/types/mobile/views/App.js +0 -150
  170. package/lib/types/mobile/views/App.test.d.ts +0 -2
  171. package/lib/types/mobile/views/App.test.d.ts.map +0 -1
  172. package/lib/types/mobile/views/App.test.js +0 -52
  173. package/lib/types/mobile/views/ChatView.d.ts +0 -56
  174. package/lib/types/mobile/views/ChatView.d.ts.map +0 -1
  175. package/lib/types/mobile/views/ChatView.js +0 -773
  176. package/lib/types/mobile/views/ChatView.stream.test.d.ts +0 -2
  177. package/lib/types/mobile/views/ChatView.stream.test.d.ts.map +0 -1
  178. package/lib/types/mobile/views/ChatView.stream.test.js +0 -231
  179. package/lib/types/mobile/views/ChatView.test.d.ts +0 -2
  180. package/lib/types/mobile/views/ChatView.test.d.ts.map +0 -1
  181. package/lib/types/mobile/views/ChatView.test.js +0 -718
  182. package/lib/types/mobile/views/SessionListView.d.ts +0 -27
  183. package/lib/types/mobile/views/SessionListView.d.ts.map +0 -1
  184. package/lib/types/mobile/views/SessionListView.js +0 -139
  185. package/lib/types/mobile/views/SessionListView.test.d.ts +0 -2
  186. package/lib/types/mobile/views/SessionListView.test.d.ts.map +0 -1
  187. package/lib/types/mobile/views/SessionListView.test.js +0 -167
  188. package/lib/types/mobile/views/WorkspaceView.d.ts +0 -21
  189. package/lib/types/mobile/views/WorkspaceView.d.ts.map +0 -1
  190. package/lib/types/mobile/views/WorkspaceView.js +0 -93
  191. package/lib/types/mobile/views/WorkspaceView.test.d.ts +0 -2
  192. package/lib/types/mobile/views/WorkspaceView.test.d.ts.map +0 -1
  193. package/lib/types/mobile/views/WorkspaceView.test.js +0 -183
  194. package/lib/types/mobile-api.d.ts +0 -65
  195. package/lib/types/mobile-api.d.ts.map +0 -1
  196. package/lib/types/mobile-api.js +0 -404
  197. package/lib/types/mobile-pending.d.ts +0 -38
  198. package/lib/types/mobile-pending.d.ts.map +0 -1
  199. package/lib/types/mobile-pending.js +0 -59
  200. package/lib/types/mobile-pwa.d.ts +0 -6
  201. package/lib/types/mobile-pwa.d.ts.map +0 -1
  202. package/lib/types/mobile-pwa.js +0 -17
  203. package/lib/types/mobile-routes.d.ts +0 -13
  204. package/lib/types/mobile-routes.d.ts.map +0 -1
  205. package/lib/types/mobile-routes.js +0 -217
  206. package/lib/types/paired-model-catalog.d.ts +0 -17
  207. package/lib/types/paired-model-catalog.d.ts.map +0 -1
  208. package/lib/types/paired-model-catalog.js +0 -311
  209. package/src/mobile/PairRequiredView.test.tsx +0 -28
  210. package/src/mobile/PairRequiredView.tsx +0 -58
  211. package/src/mobile/api.ts +0 -229
  212. package/src/mobile/index.tsx +0 -41
  213. package/src/mobile/markdown.test.ts +0 -91
  214. package/src/mobile/markdown.ts +0 -291
  215. package/src/mobile/messages.test.ts +0 -402
  216. package/src/mobile/messages.ts +0 -677
  217. package/src/mobile/mobile-styles.ts +0 -1439
  218. package/src/mobile/mobile-theme.test.ts +0 -84
  219. package/src/mobile/mobile-theme.ts +0 -76
  220. package/src/mobile/mux.test.ts +0 -314
  221. package/src/mobile/mux.ts +0 -336
  222. package/src/mobile/pairing.ts +0 -77
  223. package/src/mobile/rpc.ts +0 -90
  224. package/src/mobile/theme-toggle.tsx +0 -47
  225. package/src/mobile/views/App.test.tsx +0 -62
  226. package/src/mobile/views/App.tsx +0 -213
  227. package/src/mobile/views/ChatView.stream.test.tsx +0 -263
  228. package/src/mobile/views/ChatView.test.tsx +0 -816
  229. package/src/mobile/views/ChatView.tsx +0 -1295
  230. package/src/mobile/views/SessionListView.test.tsx +0 -207
  231. package/src/mobile/views/SessionListView.tsx +0 -244
  232. package/src/mobile/views/WorkspaceView.test.tsx +0 -227
  233. package/src/mobile/views/WorkspaceView.tsx +0 -245
  234. package/src/mobile-api.ts +0 -418
  235. package/src/mobile-pending.ts +0 -88
  236. package/src/mobile-pwa.ts +0 -19
  237. package/src/mobile-routes.ts +0 -231
  238. package/src/paired-model-catalog.ts +0 -272
@@ -1,311 +0,0 @@
1
- import { z } from 'zod';
2
- import { readCookie } from "./gate.js";
3
- import { readBoundedJson, writeJson } from "./http.js";
4
- import { isTrustedApiRequest, publicHostOf } from "./routes.js";
5
- const MAX_BODY_BYTES = 16 * 1024;
6
- const MAX_IDENTIFIER_LENGTH = 160;
7
- const MAX_DISPLAY_NAME_LENGTH = 240;
8
- const MAX_TOKEN_LIMIT = 10_000_000;
9
- const REASONING_EFFORTS = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
10
- const stringField = (max) => z.string().trim().min(1).max(max).refine(value => !/[\r\n\0]/.test(value), 'must not contain a newline or NUL');
11
- const providerSchema = z.object({ provider: stringField(MAX_IDENTIFIER_LENGTH) }).strict();
12
- const modelSchema = z.object({
13
- id: stringField(MAX_IDENTIFIER_LENGTH),
14
- name: stringField(MAX_DISPLAY_NAME_LENGTH).optional(),
15
- contextWindow: z.number().int().safe().positive().max(MAX_TOKEN_LIMIT).optional(),
16
- maxTokens: z.number().int().safe().positive().max(MAX_TOKEN_LIMIT).optional(),
17
- reasoningEfforts: z.array(z.enum(REASONING_EFFORTS)).min(1).refine(efforts => efforts.every((effort, index) => index === 0 || REASONING_EFFORTS.indexOf(efforts[index - 1]) < REASONING_EFFORTS.indexOf(effort)), 'reasoning efforts must be unique and in canonical order').optional(),
18
- }).strict();
19
- const upsertSchema = z.object({ provider: stringField(MAX_IDENTIFIER_LENGTH), model: modelSchema }).strict();
20
- export const PAIRED_MODEL_CATALOG_PATHS = {
21
- catalog: '/api/pair/model-catalog',
22
- discover: '/api/pair/model-catalog/discover',
23
- upsert: '/api/pair/model-catalog/upsert',
24
- };
25
- let rpcSequence = 0;
26
- function request(payload) {
27
- rpcSequence += 1;
28
- return { rpcId: `paired-model-catalog-${Date.now().toString(36)}-${rpcSequence.toString(36)}`, payload };
29
- }
30
- function isRecord(value) { return typeof value === 'object' && value !== null && !Array.isArray(value); }
31
- function valueOf(response) { return response.result.ok ? response.result.value : undefined; }
32
- function errorCode(response) { return response.result.ok ? undefined : response.result.error.code; }
33
- function reject(res, status, error) { writeJson(res, status, { error }); }
34
- function publicProviderId(value) {
35
- const normalized = value.replace(/[\r\n\0]/g, ' ').trim().slice(0, MAX_IDENTIFIER_LENGTH);
36
- return normalized === '' ? 'unknown' : normalized;
37
- }
38
- function publicCatalog(value) {
39
- return {
40
- groups: value.groups.map(group => ({ id: group.id, name: group.name, models: group.models.map(model => ({
41
- id: model.id, name: model.name,
42
- ...model.description === undefined ? {} : { description: model.description },
43
- ...model.reasoning === undefined ? {} : { reasoning: model.reasoning },
44
- })) })),
45
- failures: value.failures.map(failure => ({
46
- id: failure.id,
47
- name: failure.name,
48
- message: `model catalog unavailable for provider ${publicProviderId(failure.id)}`,
49
- })),
50
- };
51
- }
52
- function publicCandidates(models) {
53
- return models.map(model => ({ id: model.id,
54
- ...model.name === undefined ? {} : { name: model.name },
55
- ...model.contextWindow === undefined ? {} : { contextWindow: model.contextWindow },
56
- ...model.maxTokens === undefined ? {} : { maxTokens: model.maxTokens },
57
- }));
58
- }
59
- function fieldsOf(model, includeId) {
60
- const fields = {
61
- ...includeId ? { id: model.id } : {},
62
- ...model.name === undefined ? {} : { name: model.name },
63
- ...model.contextWindow === undefined ? {} : { contextWindow: model.contextWindow },
64
- ...model.maxTokens === undefined ? {} : { maxTokens: model.maxTokens },
65
- };
66
- if (model.reasoningEfforts !== undefined)
67
- fields.reasoningEfforts = Object.fromEntries(model.reasoningEfforts.map(effort => [effort, effort === 'off' ? null : effort]));
68
- return fields;
69
- }
70
- function groupFor(catalog, provider) {
71
- if (catalog.failures.some(failure => failure.id === provider))
72
- return undefined;
73
- return catalog.groups.find(group => group.id === provider);
74
- }
75
- async function readCatalog(apiProxy) {
76
- try {
77
- return valueOf(await apiProxy.llm.models(request({})));
78
- }
79
- catch {
80
- return undefined;
81
- }
82
- }
83
- /** Resolve one exact active llm-pi-ai provider address, never an input-supplied path. */
84
- async function eligibleProvider(apiProxy, requested) {
85
- let providersResponse;
86
- let settingsResponse;
87
- try {
88
- providersResponse = await apiProxy.llm.providers(request({}));
89
- settingsResponse = await apiProxy.settings.describe(request({}));
90
- }
91
- catch {
92
- return { status: 502, error: 'model catalog capability is unavailable' };
93
- }
94
- const providers = valueOf(providersResponse);
95
- const settings = valueOf(settingsResponse);
96
- if (providers === undefined || settings === undefined)
97
- return { status: 502, error: 'model catalog capability is unavailable' };
98
- const route = providers.providers.find(candidate => candidate.provider === requested);
99
- if (route === undefined)
100
- return { status: 404, error: `unknown provider ${requested}` };
101
- if (!settings.writable || !route.active || route.settingsNs !== 'llm-pi-ai' || route.settingsPath.length !== 2
102
- || route.settingsPath[0] !== 'providers' || route.settingsPath[1] !== requested)
103
- return { status: 403, error: `provider ${requested} is not eligible for the paired model catalog` };
104
- const namespace = settings.namespaces.find(candidate => candidate.ns === 'llm-pi-ai');
105
- const root = namespace !== undefined && isRecord(namespace.value) ? namespace.value : undefined;
106
- const providersValue = root !== undefined && isRecord(root.providers) ? root.providers : undefined;
107
- const profile = providersValue !== undefined && isRecord(providersValue[requested]) ? providersValue[requested] : undefined;
108
- if (namespace === undefined || profile === undefined)
109
- return { status: 403, error: `provider ${requested} is not eligible for the paired model catalog` };
110
- return { provider: { provider: requested, displayName: route.displayName, namespace, profile } };
111
- }
112
- function validIdentifier(value) {
113
- const parsed = stringField(MAX_IDENTIFIER_LENGTH).safeParse(value);
114
- return parsed.success && parsed.data === value;
115
- }
116
- function configuredModels(profile) {
117
- if (!Object.hasOwn(profile, 'models'))
118
- return { inherited: true };
119
- if (!Array.isArray(profile.models))
120
- return undefined;
121
- if (profile.models.length === 0)
122
- return { inherited: true };
123
- const values = [];
124
- for (const entry of profile.models) {
125
- if (!isRecord(entry) || !validIdentifier(entry.id))
126
- return undefined;
127
- values.push({ ...entry });
128
- }
129
- return { inherited: false, values };
130
- }
131
- function configuredOverrides(profile) {
132
- if (!Object.hasOwn(profile, 'modelOverrides'))
133
- return { present: false, values: new Map() };
134
- if (!isRecord(profile.modelOverrides))
135
- return undefined;
136
- const values = new Map();
137
- for (const [id, value] of Object.entries(profile.modelOverrides)) {
138
- if (!validIdentifier(id) || !isRecord(value))
139
- return undefined;
140
- values.set(id, { ...value });
141
- }
142
- return { present: true, values };
143
- }
144
- function updatedModels(existing, model) {
145
- const next = existing.map(entry => ({ ...entry }));
146
- const index = next.findIndex(entry => entry.id === model.id);
147
- const patch = fieldsOf(model, true);
148
- if (index >= 0)
149
- next[index] = { ...next[index], ...patch };
150
- else
151
- next.push(patch);
152
- return next;
153
- }
154
- function mutationPlan(profile, provider, model, catalog) {
155
- const configured = configuredModels(profile);
156
- const overrides = configuredOverrides(profile);
157
- if (configured === undefined || overrides === undefined)
158
- return undefined;
159
- const modelsPath = ['providers', provider, 'models'];
160
- const overridesPath = ['providers', provider, 'modelOverrides'];
161
- if (!configured.inherited) {
162
- if (overrides.values.size > 0)
163
- return undefined;
164
- const ops = [{ op: 'set', path: modelsPath, value: updatedModels(configured.values, model) }];
165
- if (overrides.present)
166
- ops.push({ op: 'unset', path: overridesPath });
167
- return { ops };
168
- }
169
- const group = groupFor(catalog, provider);
170
- if (group?.models.some(entry => entry.id === model.id) === true) {
171
- const existing = overrides.values.get(model.id) ?? {};
172
- return { ops: [{ op: 'set', path: [...overridesPath, model.id], value: { ...existing, ...fieldsOf(model, false) } }] };
173
- }
174
- if (group === undefined)
175
- return undefined;
176
- const materialized = [];
177
- const materializedIds = new Set();
178
- for (const installed of group.models) {
179
- materialized.push({ ...overrides.values.get(installed.id), id: installed.id });
180
- materializedIds.add(installed.id);
181
- }
182
- for (const [id, override] of overrides.values) {
183
- if (!materializedIds.has(id))
184
- materialized.push({ ...override, id });
185
- }
186
- const ops = [{ op: 'set', path: modelsPath, value: updatedModels(materialized, model) }];
187
- if (overrides.present)
188
- ops.push({ op: 'unset', path: overridesPath });
189
- return { ops };
190
- }
191
- function paired(req, service) {
192
- const deviceId = readCookie(req.headers.cookie, service.config.cookieName);
193
- return deviceId !== undefined && service.touchDevice(deviceId);
194
- }
195
- async function bodyOf(req, schema) {
196
- try {
197
- return schema.parse(await readBoundedJson(req, MAX_BODY_BYTES));
198
- }
199
- catch {
200
- return undefined;
201
- }
202
- }
203
- /** Build the narrow paired-device model catalog routes. */
204
- export function makePairedModelCatalogRoutes(deps) {
205
- const { service, apiProxy, lanAddresses } = deps;
206
- /** Same fence as the /api/pair family: loopback, the advertised LAN literals, or the configured public host. */
207
- const fence = (req) => {
208
- const publicHost = publicHostOf(service.publicBaseUrl);
209
- return isTrustedApiRequest(req, publicHost === undefined ? lanAddresses : [...lanAddresses, publicHost]);
210
- };
211
- const gate = (req, res) => {
212
- if (!fence(req)) {
213
- req.resume();
214
- reject(res, 403, 'paired model catalog is not reachable for this origin');
215
- return false;
216
- }
217
- if (!paired(req, service)) {
218
- req.resume();
219
- reject(res, 403, 'paired model catalog requires a live paired device');
220
- return false;
221
- }
222
- return true;
223
- };
224
- const catalog = async (req, res) => {
225
- if (!gate(req, res))
226
- return;
227
- if (req.method !== 'GET')
228
- return reject(res, 405, 'method not allowed');
229
- let providersResponse;
230
- let settingsResponse;
231
- try {
232
- providersResponse = await apiProxy.llm.providers(request({}));
233
- settingsResponse = await apiProxy.settings.describe(request({}));
234
- }
235
- catch {
236
- return reject(res, 502, 'model catalog capability is unavailable');
237
- }
238
- const providers = valueOf(providersResponse);
239
- const settings = valueOf(settingsResponse);
240
- if (providers === undefined || settings === undefined)
241
- return reject(res, 502, 'model catalog capability is unavailable');
242
- const namespace = settings.namespaces.find(candidate => candidate.ns === 'llm-pi-ai');
243
- const providerMap = namespace !== undefined && isRecord(namespace.value) && isRecord(namespace.value.providers) ? namespace.value.providers : undefined;
244
- const eligible = settings.writable && providerMap !== undefined ? providers.providers.filter(candidate => candidate.active && candidate.settingsNs === 'llm-pi-ai'
245
- && candidate.settingsPath.length === 2 && candidate.settingsPath[0] === 'providers' && candidate.settingsPath[1] === candidate.provider && isRecord(providerMap[candidate.provider]))
246
- .map(candidate => ({ provider: candidate.provider, displayName: candidate.displayName })) : [];
247
- writeJson(res, 200, { capability: 'paired-model-catalog', providers: eligible });
248
- };
249
- const discover = async (req, res) => {
250
- if (!gate(req, res))
251
- return;
252
- if (req.method !== 'POST')
253
- return reject(res, 405, 'method not allowed');
254
- const body = await bodyOf(req, providerSchema);
255
- if (body === undefined)
256
- return reject(res, 400, 'invalid model catalog request');
257
- const eligible = await eligibleProvider(apiProxy, body.provider);
258
- if (eligible.provider === undefined)
259
- return reject(res, eligible.status ?? 502, eligible.error ?? 'model catalog capability is unavailable');
260
- try {
261
- const result = valueOf(await apiProxy.llm.discoverModels(request({ settingsNs: 'llm-pi-ai', provider: eligible.provider.provider })));
262
- if (result === undefined)
263
- return reject(res, 502, `model discovery failed for provider ${eligible.provider.provider}`);
264
- writeJson(res, 200, { models: publicCandidates(result.models) });
265
- }
266
- catch {
267
- reject(res, 502, `model discovery failed for provider ${eligible.provider.provider}`);
268
- }
269
- };
270
- const upsert = async (req, res) => {
271
- if (!gate(req, res))
272
- return;
273
- if (req.method !== 'POST')
274
- return reject(res, 405, 'method not allowed');
275
- const body = await bodyOf(req, upsertSchema);
276
- if (body === undefined)
277
- return reject(res, 400, 'invalid model catalog request');
278
- const eligible = await eligibleProvider(apiProxy, body.provider);
279
- if (eligible.provider === undefined)
280
- return reject(res, eligible.status ?? 502, eligible.error ?? 'model catalog capability is unavailable');
281
- const before = await readCatalog(apiProxy);
282
- if (before === undefined)
283
- return reject(res, 502, `model catalog is unavailable for provider ${body.provider}`);
284
- const plan = mutationPlan(eligible.provider.profile, body.provider, body.model, before);
285
- if (plan === undefined)
286
- return reject(res, 502, `model catalog is unavailable for provider ${body.provider}`);
287
- let mutation;
288
- try {
289
- mutation = await apiProxy.settings.mutate(request({ ns: 'llm-pi-ai', expectedRevision: eligible.provider.namespace.revision, ops: plan.ops }));
290
- }
291
- catch {
292
- return reject(res, 502, `model update failed for provider ${body.provider}`);
293
- }
294
- const code = errorCode(mutation);
295
- if (code === 'settings-conflict')
296
- return reject(res, 409, `model update conflicted for provider ${body.provider}`);
297
- if (code === 'settings-rejected')
298
- return reject(res, 422, `model update was rejected for provider ${body.provider}`);
299
- if (code !== undefined)
300
- return reject(res, 502, `model update failed for provider ${body.provider}`);
301
- const after = await readCatalog(apiProxy);
302
- if (after === undefined)
303
- return reject(res, 502, `model catalog is unavailable for provider ${body.provider}`);
304
- writeJson(res, 200, publicCatalog(after));
305
- };
306
- return [
307
- { kind: 'exact', path: PAIRED_MODEL_CATALOG_PATHS.catalog, handler: catalog },
308
- { kind: 'exact', path: PAIRED_MODEL_CATALOG_PATHS.discover, handler: discover },
309
- { kind: 'exact', path: PAIRED_MODEL_CATALOG_PATHS.upsert, handler: upsert },
310
- ];
311
- }
@@ -1,28 +0,0 @@
1
- // @vitest-environment jsdom
2
- import { afterEach, describe, expect, it, vi } from 'vitest'
3
- import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
4
- import { PairRequiredView } from './PairRequiredView.tsx'
5
-
6
- afterEach(() => {
7
- cleanup()
8
- vi.unstubAllGlobals()
9
- })
10
-
11
- describe('PairRequiredView', () => {
12
- it('accepts a pasted link in the installed app context', async () => {
13
- const fetch = vi.fn().mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 }))
14
- vi.stubGlobal('fetch', fetch)
15
- const onPaired = vi.fn()
16
- render(<PairRequiredView onPaired={onPaired} />)
17
-
18
- fireEvent.change(screen.getByLabelText('配对链接'), { target: { value: 'https://phone.example/m/?pair=tok-1&workspace=ws-7' } })
19
- fireEvent.click(screen.getByRole('button', { name: '配对' }))
20
-
21
- await waitFor(() => expect(onPaired).toHaveBeenCalledWith('/m/?workspace=ws-7'))
22
- })
23
-
24
- it('shows an initial QR failure without starting the mobile data channel', () => {
25
- render(<PairRequiredView initialError="配对链接已被使用。" onPaired={vi.fn()} />)
26
- expect(screen.getByRole('alert').textContent).toContain('配对链接已被使用。')
27
- })
28
- })
@@ -1,58 +0,0 @@
1
- /** Device-pairing gate for installed mobile web-app contexts with isolated storage. */
2
- import { type FormEvent, useState } from 'react'
3
- import { acceptMobilePair, mobilePairPath, parseMobilePairInput } from './pairing.ts'
4
-
5
- export interface PairRequiredViewProps {
6
- initialError?: string
7
- onPaired(path: string): void
8
- }
9
-
10
- /** Collect a fresh desktop-issued pairing link when this client has no paired cookie. */
11
- export function PairRequiredView({ initialError, onPaired }: PairRequiredViewProps) {
12
- const [value, setValue] = useState('')
13
- const [error, setError] = useState<string | undefined>(initialError)
14
- const [submitting, setSubmitting] = useState(false)
15
-
16
- const submit = async (event: FormEvent<HTMLFormElement>): Promise<void> => {
17
- event.preventDefault()
18
- const input = parseMobilePairInput(value)
19
- if (input === undefined) {
20
- setError('请输入有效的配对链接。')
21
- return
22
- }
23
-
24
- setSubmitting(true)
25
- setError(undefined)
26
- const result = await acceptMobilePair(input.token)
27
- setSubmitting(false)
28
- if (!result.ok) {
29
- setError(result.message)
30
- return
31
- }
32
-
33
- onPaired(mobilePairPath(input.workspaceId))
34
- }
35
-
36
- return (
37
- <main className="mobile mobile-pair" aria-labelledby="mobile-pair-title">
38
- <form className="mobile-pairCard" onSubmit={(event) => { void submit(event) }}>
39
- <h1 id="mobile-pair-title" className="mobile-title">设备配对</h1>
40
- <p className="mobile-muted">粘贴桌面端复制的配对链接以连接此设备。</p>
41
- <label className="mobile-pairLabel" htmlFor="mobile-pair-link">配对链接</label>
42
- <input
43
- id="mobile-pair-link"
44
- className="mobile-pairInput"
45
- value={value}
46
- onChange={(event) => { setValue(event.target.value) }}
47
- autoComplete="off"
48
- autoCapitalize="off"
49
- spellCheck={false}
50
- />
51
- {error === undefined ? null : <p className="mobile-error" role="alert">{error}</p>}
52
- <button className="mobile-new mobile-pairSubmit" type="submit" disabled={submitting}>
53
- {submitting ? '正在配对' : '配对'}
54
- </button>
55
- </form>
56
- </main>
57
- )
58
- }
package/src/mobile/api.ts DELETED
@@ -1,229 +0,0 @@
1
- /**
2
- * Mobile-surface business API: the handful of host RPC methods the
3
- * simplified surface needs. Types come from the harness apiproxy contract
4
- * (type-only imports; the wire schemas stay in the bundle only through the
5
- * rpc/mux layers).
6
- */
7
-
8
- import type { WorkspaceView } from '@deepseek-ai/dsh-host-apiproxy/api/workspace'
9
- import type { AgentPresetEntry } from '@deepseek-ai/dsh-host-apiproxy/api/agent-presets'
10
- import type { SessionSummary, SessionModels, SessionProjectionsBlock } from '@deepseek-ai/dsh-host-apiproxy/api/sessions'
11
- import { callUnary } from './rpc.ts'
12
-
13
- /** One session.list page. */
14
- export interface SessionPage {
15
- items: SessionSummary[]
16
- /** Continuation cursor; undefined once the tail is reached. */
17
- nextCursor?: string
18
- hasMore: boolean
19
- }
20
-
21
- /** The session.create result (the id is the commit the caller navigates to). */
22
- export interface CreatedSession {
23
- sessionId: string
24
- /** The composition the new session runs (echoed so the caller can label it). */
25
- agentPreset?: string
26
- }
27
-
28
- /** Agent presets available when composing a new session. */
29
- export interface AgentPresetRoster {
30
- presets: readonly AgentPresetEntry[]
31
- authorable: boolean
32
- hasDocument: boolean
33
- }
34
-
35
- /** One history page (already bounded to whole messages by the host). */
36
- export interface HistoryPage {
37
- events: import('@deepseek-ai/dsh-host-apiproxy/api/sessions').HistoryEntry[]
38
- hasMore: boolean
39
- /**
40
- * Projection baseline riding the tail page (permissions select etc.);
41
- * absent when the deployment mounts no projection registry.
42
- */
43
- projections?: SessionProjectionsBlock
44
- }
45
-
46
- /** Read-only display preferences the plugin answers locally on `/m/api`. */
47
- export interface MobilePreferences {
48
- /** Plain Enter sends the drafted prompt (false: Enter inserts a newline). */
49
- mobileEnterToSend: boolean
50
- }
51
-
52
- /** The workspace roster (session ids come back per workspace). */
53
- export async function listWorkspaces(): Promise<WorkspaceView[]> {
54
- const { items } = await callUnary<{ items: WorkspaceView[] }>('workspace.list', {})
55
- return items
56
- }
57
-
58
- /** Read-only mobile display preferences (answered by the plugin, not the host proxy). */
59
- export async function fetchMobilePreferences(): Promise<MobilePreferences> {
60
- return await callUnary<MobilePreferences>('mobile.preferences', {})
61
- }
62
-
63
- /** One session.list page; omit the cursor for the first page. */
64
- export async function listSessions(cursor?: string): Promise<SessionPage> {
65
- return await callUnary<SessionPage>('session.list', cursor === undefined ? {} : { cursor })
66
- }
67
-
68
- /** Read the available agent compositions for a new session. */
69
- export async function listAgentPresets(): Promise<AgentPresetRoster> {
70
- return await callUnary<AgentPresetRoster>('agentPreset.list', {})
71
- }
72
-
73
- /**
74
- * Create a blank session (entity birth precedes the first message). Name a
75
- * workspace to attach it there, or a cwd; omitting both uses the host cwd.
76
- */
77
- export async function createSession(
78
- options: { workspaceId?: string; cwd?: string; agentPreset?: string } = {},
79
- ): Promise<CreatedSession> {
80
- return await callUnary<CreatedSession>('session.create', options)
81
- }
82
-
83
- /** One history window; omit beforeSeq for the tail page, pass a signal to abort. */
84
- export async function history(
85
- sessionId: string,
86
- beforeSeq?: number,
87
- maxMessages = 30,
88
- signal?: AbortSignal,
89
- ): Promise<HistoryPage> {
90
- return await callUnary<HistoryPage>('session.history', {
91
- sessionId,
92
- maxMessages,
93
- ...(beforeSeq !== undefined ? { beforeSeq } : {}),
94
- }, signal)
95
- }
96
-
97
- /** Send one text prompt (queued: the agent picks it up in order). */
98
- export async function prompt(sessionId: string, text: string): Promise<void> {
99
- await callUnary<{ accepted: true }>('session.prompt', {
100
- sessionId,
101
- mode: 'queue',
102
- content: [{ type: 'text', text }],
103
- })
104
- }
105
-
106
- /** Send one slash command line (e.g. `/permission workspace-write`). */
107
- export async function sendCommand(sessionId: string, line: string): Promise<unknown> {
108
- return await callUnary<unknown>('mobile.command', {
109
- sessionId,
110
- line,
111
- })
112
- }
113
-
114
- /**
115
- * Stop the session's active turn (the mobile stop button). Pending queued
116
- * work is preserved and resumes in FIFO order once cancellation settles.
117
- */
118
- export async function cancelSession(sessionId: string): Promise<{ accepted: true }> {
119
- return await callUnary<{ accepted: true }>('session.cancel', { sessionId })
120
- }
121
-
122
- /** Fresh advisory model directory for one session (current + groups + failures). */
123
- export async function models(sessionId: string): Promise<SessionModels> {
124
- return await callUnary<SessionModels>('session.models', { sessionId })
125
- }
126
-
127
- /** Select the complete model selection (provider/model/reasoning effort) for a session. */
128
- export async function selectModel(
129
- sessionId: string,
130
- selection: { provider: string; model: string; reasoningEffort?: string },
131
- ): Promise<{ selected: { provider: string; model: string; reasoningEffort?: string } }> {
132
- return await callUnary<{ selected: { provider: string; model: string; reasoningEffort?: string } }>('session.selectModel', {
133
- sessionId,
134
- provider: selection.provider,
135
- model: selection.model,
136
- ...(selection.reasoningEffort !== undefined ? { reasoningEffort: selection.reasoningEffort } : {}),
137
- })
138
- }
139
-
140
- /* ── pending approvals / questions (#1025) ───────────────────────────── */
141
-
142
- /** One pending tool approval awaiting the user's decision. */
143
- export interface PendingApproval {
144
- approvalId: string
145
- toolName: string
146
- callId?: string
147
- reason?: string
148
- }
149
-
150
- /** One pending question group awaiting the user's answer. */
151
- export interface PendingQuestionItem {
152
- id: string
153
- question: string
154
- detail?: string
155
- header?: string
156
- options?: Array<{ label: string; description?: string }>
157
- multiSelect?: boolean
158
- }
159
-
160
- /** The pending state for one session. */
161
- export interface PendingState {
162
- approvals: PendingApproval[]
163
- questions: PendingQuestionItem[]
164
- }
165
-
166
- /** Fetch pending approvals and questions for one session (polling fallback data source). */
167
- export async function fetchPending(sessionId: string): Promise<PendingState> {
168
- return await callUnary<PendingState>('mobile.pending', { sessionId })
169
- }
170
-
171
- /** Submit an approval decision (allowed-once or rejected). */
172
- export async function respondApproval(
173
- sessionId: string,
174
- approvalId: string,
175
- outcome: 'allowed-once' | 'rejected',
176
- ): Promise<void> {
177
- await callUnary<unknown>('mobile.respond', {
178
- sessionId,
179
- type: 'approval',
180
- approvalId,
181
- outcome,
182
- })
183
- }
184
-
185
- /** Submit answers to a question group. */
186
- export async function respondQuestion(
187
- sessionId: string,
188
- answers: Array<{ id: string; selected: string[]; custom?: string }>,
189
- ): Promise<void> {
190
- await callUnary<unknown>('mobile.respond', {
191
- sessionId,
192
- type: 'question',
193
- answers,
194
- })
195
- }
196
-
197
- /* ── directory browsing / workspace creation (#977) ──────────────────── */
198
-
199
- /** One entry in a directory listing. */
200
- export interface DirectoryEntry {
201
- name: string
202
- path: string
203
- hidden: boolean
204
- }
205
-
206
- /** The host directory listing result (one level, with ancestor breadcrumbs). */
207
- export interface DirectoryListing {
208
- path: string
209
- home: string
210
- crumbs: DirectoryEntry[]
211
- entries: DirectoryEntry[]
212
- truncated: boolean
213
- }
214
-
215
- /** The result of creating a workspace from an existing directory. */
216
- export interface WorkspaceCreateResult {
217
- workspace: WorkspaceView
218
- created: boolean
219
- }
220
-
221
- /** Browse one directory level on the host (defaults to home directory). */
222
- export async function listDirectory(path?: string): Promise<DirectoryListing> {
223
- return await callUnary<DirectoryListing>('host.listDirectory', path === undefined ? {} : { path })
224
- }
225
-
226
- /** Create a workspace from an existing host directory (does not mkdir). */
227
- export async function createWorkspace(path: string): Promise<WorkspaceCreateResult> {
228
- return await callUnary<WorkspaceCreateResult>('workspace.create', { path })
229
- }
@@ -1,41 +0,0 @@
1
- /**
2
- * Mobile surface entry: the standalone phone UI served at /m. Boots its own
3
- * React tree (no main-UI module loader), talks to the host over the shared
4
- * /api transport with the paired-device cookie, and renders a deliberately
5
- * thin three-level surface:
6
- * workspaces (landing, no new-session homepage) → sessions (paged) →
7
- * chat (history paged on demand + live mux stream + prompt input).
8
- */
9
-
10
- import { createRoot } from 'react-dom/client'
11
- import { App } from './views/App.tsx'
12
- import { mobileCss } from './mobile-styles.ts'
13
- import { initMobileTheme } from './mobile-theme.ts'
14
- import { registerMobilePwa } from '../mobile-pwa.ts'
15
- import { consumeMobilePairUrl } from './pairing.ts'
16
-
17
- // Apply the persisted (or default light) theme before first paint, so the
18
- // page never flashes the wrong palette.
19
- initMobileTheme()
20
-
21
- // Inject the standalone stylesheet (the page has no shell to load it for us).
22
- const style = document.createElement('style')
23
- style.dataset.plugin = 'remote-web-ui/mobile'
24
- style.textContent = mobileCss
25
- document.head.appendChild(style)
26
-
27
- const root = document.getElementById('root')
28
- if (root === null) throw new Error('mobile: #root missing')
29
- void bootMobile(root)
30
-
31
- async function bootMobile(mount: HTMLElement): Promise<void> {
32
- const pair = await consumeMobilePairUrl(window.location.href)
33
- if (pair.kind === 'accepted') {
34
- window.location.replace(pair.path)
35
- return
36
- }
37
- if (pair.kind === 'failed') window.history.replaceState(null, '', pair.path)
38
-
39
- void registerMobilePwa()
40
- createRoot(mount).render(<App initialPairError={pair.kind === 'failed' ? pair.message : undefined} />)
41
- }