cj-tauri 1.1.2 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +137 -77
- package/bin/cj-tauri.js +287 -287
- package/cjpm.toml +13 -2
- package/cli/cjpm.toml +22 -13
- package/cli/src/doctor.cj +308 -304
- package/cli/src/main.cj +129 -121
- package/cli/src/manifest.cj +632 -0
- package/cli/src/manifest_test.cj +87 -0
- package/cli/src/project.cj +770 -754
- package/cli/src/resolve.cj +348 -348
- package/cli/src/scaffold.cj +46 -38
- package/cli/src/sync.cj +526 -0
- package/cli/src/util.cj +9 -0
- package/cli/templates/app/README.md +30 -59
- package/cli/templates/app/{ui → assets/ui}/index.html +2 -0
- package/cli/templates/app/cj-tauri.toml +32 -0
- package/cli/templates/app/cjpm.toml +21 -4
- package/cli/templates/app/src/main.cj +29 -16
- package/cli/templates/app-react/README.md +34 -45
- package/cli/templates/app-react/{ui → assets/ui}/index.html +25 -25
- package/cli/templates/app-react/{ui → assets/ui}/src/App.css +67 -67
- package/cli/templates/app-react/{ui → assets/ui}/src/App.jsx +124 -124
- package/cli/templates/app-react/cj-tauri.toml +33 -0
- package/cli/templates/app-react/cjpm.toml +21 -4
- package/cli/templates/app-react/src/main.cj +18 -29
- package/cli/templates/app-vue/README.md +34 -38
- package/cli/templates/app-vue/{ui → assets/ui}/index.html +25 -25
- package/cli/templates/app-vue/{ui → assets/ui}/src/App.vue +176 -176
- package/cli/templates/app-vue/cj-tauri.toml +33 -0
- package/cli/templates/app-vue/cjpm.toml +21 -4
- package/cli/templates/app-vue/src/main.cj +18 -29
- package/cli/templates/{ohos-pc → ohos-skeleton}/README.md +5 -0
- package/cli/templates/{ohos-pc → ohos-skeleton}/build-hap.sh +15 -0
- package/cli/templates/{ohos-pc → ohos-skeleton}/entry/cjpm.toml +8 -0
- package/cli/templates/ohos-skeleton/entry/src/main/cangjie/dialog_host.cj +217 -0
- package/cli/templates/ohos-skeleton/entry/src/main/cangjie/index.cj +527 -0
- package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/module.json5 +8 -0
- package/native/build_win.bat +62 -62
- package/native/core/bridge_core.c +1681 -1626
- package/native/core/bridge_core.h +675 -618
- package/native/core/bridge_js.h +108 -108
- package/native/platforms/harmonyos/README.md +115 -6
- package/native/platforms/linux/bridge_linux.c +2787 -2485
- package/native/platforms/windows/bridge_win.c +3740 -3248
- package/native/tests/gtk_threading_probe.c +143 -143
- package/native/tests/test_bridge_core.c +1339 -1228
- package/package.json +53 -51
- package/prebuilt/linux-x64/cj-tauri +0 -0
- package/src/api_system.cj +67 -67
- package/src/app.cj +325 -940
- package/src/app_events.cj +166 -0
- package/src/app_host_ops.cj +287 -0
- package/src/app_run.cj +283 -0
- package/src/app_spec.cj +76 -0
- package/src/arkweb_menu.cj +216 -0
- package/src/arkweb_script.cj +0 -12
- package/src/capability.cj +409 -409
- package/src/clipboard_bridge.cj +178 -0
- package/src/dialog_spec.cj +79 -0
- package/src/dragdrop.cj +26 -26
- package/src/embedded_assets.cj +98 -98
- package/src/host.cj +752 -604
- package/src/host_arkweb.cj +201 -17
- package/src/host_webkit.cj +663 -548
- package/src/host_webview2.cj +663 -548
- package/src/hotkey.cj +66 -66
- package/src/ipc_hub.cj +337 -337
- package/src/ipc_message.cj +286 -286
- package/src/launch.cj +62 -0
- package/src/logging.cj +127 -127
- package/src/menu.cj +197 -197
- package/src/platform/platform_app_context.cj +77 -0
- package/src/platform/platform_assets.cj +33 -0
- package/src/platform/platform_discover.cj +176 -0
- package/src/platform/platform_env.cj +29 -0
- package/src/platform/platform_feature.cj +31 -0
- package/src/platform/platform_page_source.cj +87 -0
- package/src/platform/platform_target_info.cj +32 -0
- package/src/platform_host_provider.cj +87 -0
- package/src/plugin.cj +333 -299
- package/src/plugins/clipboard/plugin_clipboard.cj +152 -144
- package/src/plugins/dialog/plugin_dialog.cj +209 -209
- package/src/plugins/fs/plugin_fs.cj +105 -1
- package/src/plugins/http/http_backend.cj +131 -0
- package/src/plugins/http/http_desktop.cj +195 -0
- package/src/plugins/http/http_ohos.cj +204 -0
- package/src/plugins/http/plugin_http.cj +310 -459
- package/src/plugins/log/plugin_log.cj +105 -105
- package/src/plugins/menu/plugin_menu.cj +8 -0
- package/src/plugins/notification/plugin_notification.cj +154 -146
- package/src/plugins/serial/plugin_serial.cj +17 -1
- package/src/plugins/serial/serial_bridge.cj +9 -1
- package/src/plugins/serial/serial_manager_ohos.cj +92 -0
- package/src/plugins/serial/serial_ohos.cj +529 -0
- package/src/plugins/shell/plugin_shell.cj +29 -1
- package/src/plugins/sqlite/plugin_sqlite.cj +8 -0
- package/src/plugins/ws/plugin_ws.cj +488 -470
- package/src/process_hidden.cj +174 -174
- package/src/scope.cj +391 -391
- package/src/shell_event.cj +69 -69
- package/src/tests/app_spec_test.cj +126 -0
- package/src/tests/capability_test.cj +243 -243
- package/src/tests/dialog_spec_test.cj +73 -0
- package/src/tests/fake_host.cj +201 -186
- package/src/tests/ipc_hub_test.cj +487 -487
- package/src/tests/logging_test.cj +62 -62
- package/src/tests/platform_contract_test.cj +251 -0
- package/src/tests/plugin_clipboard_test.cj +222 -222
- package/src/tests/plugin_dialog_test.cj +237 -237
- package/src/tests/plugin_fs_test.cj +207 -0
- package/src/tests/plugin_http_test.cj +124 -124
- package/src/tests/plugin_notification_test.cj +205 -205
- package/src/tests/plugin_serial_test.cj +48 -0
- package/src/tests/plugin_test.cj +66 -3
- package/src/tests/plugin_ws_test.cj +40 -40
- package/src/tests/process_hidden_test.cj +129 -129
- package/src/tests/scope_test.cj +350 -350
- package/src/tests/system_host_test.cj +99 -81
- package/src/tests/urlutil_test.cj +53 -53
- package/src/tests/version_test.cj +75 -75
- package/src/tests/window_registry_test.cj +123 -123
- package/src/tray.cj +90 -90
- package/src/urlutil.cj +159 -159
- package/src/version.cj +44 -44
- package/src/window.cj +51 -51
- package/src/window_registry.cj +116 -116
- package/cli/templates/ohos-pc/entry/src/main/cangjie/index.cj +0 -167
- package/prebuilt/win32-x64/cj-tauri.exe +0 -0
- package/src/host_factory.cj +0 -48
- /package/cli/templates/app/{capabilities → assets/capabilities}/default.json +0 -0
- /package/cli/templates/app-react/{capabilities → assets/capabilities}/default.json +0 -0
- /package/cli/templates/app-react/{ui → assets/ui}/package.json +0 -0
- /package/cli/templates/app-react/{ui → assets/ui}/src/main.jsx +0 -0
- /package/cli/templates/app-react/{ui → assets/ui}/vite.config.js +0 -0
- /package/cli/templates/app-vue/{capabilities → assets/capabilities}/default.json +0 -0
- /package/cli/templates/app-vue/{ui → assets/ui}/package.json +0 -0
- /package/cli/templates/app-vue/{ui → assets/ui}/src/main.js +0 -0
- /package/cli/templates/app-vue/{ui → assets/ui}/vite.config.js +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/app.json5 +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/resources/base/element/string.json +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/resources/base/media/background.png +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/resources/base/media/foreground.png +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/resources/base/media/layered_image.json +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/build-profile.json5 +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/build-profile.json5 +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/hvigorfile.ts +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/oh-package.json5 +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/cangjie/ability_stage.cj +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/cangjie/app_host.cj +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/cangjie/main_ability.cj +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/element/color.json +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/element/string.json +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/media/background.png +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/media/foreground.png +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/media/layered_image.json +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/media/startIcon.png +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/profile/main_pages.json +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/rawfile/capabilities.json +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/rawfile/index.html +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/hvigor/hvigor-config.json5 +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/hvigorfile.ts +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/oh-package.json5 +0 -0
|
@@ -1,459 +1,310 @@
|
|
|
1
|
-
package cjTauri.plugins.http
|
|
2
|
-
|
|
3
|
-
import cjTauri.*
|
|
4
|
-
|
|
5
|
-
import std.collection.*
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
public let
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* -
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
public
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
m.add("
|
|
62
|
-
m.add("
|
|
63
|
-
m.add("
|
|
64
|
-
m
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
let
|
|
71
|
-
|
|
72
|
-
all.add("
|
|
73
|
-
all.add("
|
|
74
|
-
all.add("
|
|
75
|
-
|
|
76
|
-
m
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
ev
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
public
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
case "http:
|
|
101
|
-
case "http:
|
|
102
|
-
case "http:
|
|
103
|
-
case
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
let
|
|
121
|
-
|
|
122
|
-
case
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
let
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if (
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
let
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
case None =>
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
case None => ()
|
|
312
|
-
}
|
|
313
|
-
// 按块读到 EOF 或上限:resp.body 的静态类型只有 InputStream,
|
|
314
|
-
// readToEnd 编译不过(泛型约束要 Seekable,AGENTS §4);超限截断并如实标记。
|
|
315
|
-
// ByteBuffer 没有 size 属性:字节数自己累加(music 的 readBodyBytes 同款不查
|
|
316
|
-
// 长度,这里要掐上限,所以另立一版)
|
|
317
|
-
let buf = ByteBuffer()
|
|
318
|
-
let chunk = Array<Byte>(8192, repeat: 0)
|
|
319
|
-
var n = resp.body.read(chunk)
|
|
320
|
-
var total: Int64 = 0
|
|
321
|
-
while (n > 0) {
|
|
322
|
-
if (total + n > CJ_HTTP_MAX_BYTES) {
|
|
323
|
-
buf.write(chunk[0..(CJ_HTTP_MAX_BYTES - total)])
|
|
324
|
-
truncated = true
|
|
325
|
-
break
|
|
326
|
-
}
|
|
327
|
-
buf.write(chunk[0..n])
|
|
328
|
-
total += n
|
|
329
|
-
n = resp.body.read(chunk)
|
|
330
|
-
}
|
|
331
|
-
resp.close()
|
|
332
|
-
text = String.fromUtf8(buf.bytes())
|
|
333
|
-
} finally {
|
|
334
|
-
client.close()
|
|
335
|
-
}
|
|
336
|
-
let obj = JsonObject()
|
|
337
|
-
obj.put("status", JsonInt(status))
|
|
338
|
-
obj.put("contentType", JsonString(ctype))
|
|
339
|
-
obj.put("body", JsonString(text))
|
|
340
|
-
obj.put("truncated", JsonBool(truncated))
|
|
341
|
-
return obj
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
/** 主机白名单判定(共享层语义:先 deny 后 allow,allow 必须显式命中) */
|
|
345
|
-
private func checkHostScope(url: String, host: String, ipc: IpcContext): Unit {
|
|
346
|
-
// 分支体保持单语句(多语句的块会被解析器当 lambda,见 doFetch 注释)
|
|
347
|
-
match (ipc.scopeRuleFor("http")) {
|
|
348
|
-
case None => this.rejectOutsideScope(url, "no host whitelist declared")
|
|
349
|
-
case Some(rule) => this.checkRuleAllows(url, host, rule)
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
private func checkRuleAllows(url: String, host: String, rule: ScopeRule): Unit {
|
|
354
|
-
if (!scopeAllows(rule, host)) {
|
|
355
|
-
this.rejectOutsideScope(url, host)
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
private func rejectOutsideScope(url: String, why: String): Nothing {
|
|
360
|
-
eprintln("[cj-tauri] scope denied (http): ${url} -> ${why}")
|
|
361
|
-
throw CommandException("http: host is outside the authorized scope")
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
/** 页面自带的请求头(可选 headers 对象,string → string),条数与长度有上限 */
|
|
365
|
-
private func applyPageHeaders(builder: HttpRequestBuilder, args: JsonObject): HttpRequestBuilder {
|
|
366
|
-
var b = builder
|
|
367
|
-
match (args.get("headers")) {
|
|
368
|
-
// 分支体保持单语句(块体会被解析器当 lambda,见 doFetch 注释)
|
|
369
|
-
case Some(v) => b = this.applyHeadersObject(b, v)
|
|
370
|
-
case None => ()
|
|
371
|
-
}
|
|
372
|
-
return b
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
private func applyHeadersObject(builder: HttpRequestBuilder, v: JsonValue): HttpRequestBuilder {
|
|
376
|
-
var b = builder
|
|
377
|
-
// JsonKind 没有 ==(AGENTS §4):枚举比较只能走 match
|
|
378
|
-
let isObject = match (v.kind()) {
|
|
379
|
-
case JsonKind.JsObject => true
|
|
380
|
-
case _ => false
|
|
381
|
-
}
|
|
382
|
-
if (!isObject) {
|
|
383
|
-
return b
|
|
384
|
-
}
|
|
385
|
-
let hs = v.asObject()
|
|
386
|
-
var count: Int64 = 0
|
|
387
|
-
// 遍历用 getFields()(store 插件同款先例):JsonObject 不是按索引取的容器
|
|
388
|
-
for ((name0, value) in hs.getFields()) {
|
|
389
|
-
if (count >= CJ_HTTP_MAX_HEADERS) {
|
|
390
|
-
break
|
|
391
|
-
}
|
|
392
|
-
// 键小写化后跳过禁改的头
|
|
393
|
-
let name = name0.toAsciiLower()
|
|
394
|
-
if (name == "host" || name == "content-length") {
|
|
395
|
-
// 白名单判的是 URL 里的 host,页面不许改写它
|
|
396
|
-
continue
|
|
397
|
-
}
|
|
398
|
-
if (this.isStringKind(value)) {
|
|
399
|
-
let hval = value.asString().getValue()
|
|
400
|
-
if (hval.size <= CJ_HTTP_MAX_HEADER_VALUE) {
|
|
401
|
-
b = b.header(name, hval)
|
|
402
|
-
count++
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
return b
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
private func isStringKind(v: JsonValue): Bool {
|
|
410
|
-
let r = match (v.kind()) {
|
|
411
|
-
case JsonKind.JsString => true
|
|
412
|
-
case _ => false
|
|
413
|
-
}
|
|
414
|
-
return r
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
private func requireString(args: JsonObject, key: String): String {
|
|
418
|
-
match (args.get(key)) {
|
|
419
|
-
case Some(v) => match (v.kind()) {
|
|
420
|
-
case JsonKind.JsString => return v.asString().getValue()
|
|
421
|
-
case _ => throw CommandException("http: argument \"${key}\" must be a string")
|
|
422
|
-
}
|
|
423
|
-
case None => throw CommandException("http: missing argument \"${key}\"")
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
/**
|
|
429
|
-
* 从 URL 提取 host(小写返回):核心 urlHostOf 的 **协议收紧包装**——
|
|
430
|
-
* ws/wss 是 ws 插件的合法目标,但不是可抓取目标;http 插件只认 http/https。
|
|
431
|
-
* 解析与大小写口径统一在核心层(urlutil.cj),这里不再有第二份字节扫描。
|
|
432
|
-
* public 是为了单测可直接构造(跨包须 public)。
|
|
433
|
-
*/
|
|
434
|
-
public func httpHostOf(rawUrl: String): Option<String> {
|
|
435
|
-
let lower = rawUrl.toAsciiLower()
|
|
436
|
-
if (!lower.startsWith("http://") && !lower.startsWith("https://")) {
|
|
437
|
-
return None
|
|
438
|
-
}
|
|
439
|
-
return urlHostOf(rawUrl)
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
/**
|
|
443
|
-
* 建 TLS 客户端配置:**域名必须显式写 `serverName`(SNI)**。
|
|
444
|
-
*
|
|
445
|
-
* stdx 的 `TlsClientConfig()` 默认不发 SNI,而多租户 HTTPS 边缘(实测 Cloudflare 后面的 api 域名)
|
|
446
|
-
* 收不到 SNI 就以 `handshake failure`(40) 拒掉握手——表象是「TLS 握手失败」,同一地址
|
|
447
|
-
* curl / openssl 打起来一切正常,极易误判成对端故障、甚至误判成白名单问题(AGENTS §4 有这条坑)。
|
|
448
|
-
* IP 字面量**不带** SNI:RFC 6066 不许,且「https 直连 IP」原本可用,不能反被改坏。
|
|
449
|
-
*
|
|
450
|
-
* 这七行故意留在插件里而不进核心共享层:核心一引用 `stdx.net.tls`,不用 TLS 的应用也会被拉上
|
|
451
|
-
* OpenSSL 依赖,单文件打包契约跟着变形(§2 的包粒度纪律);ws 插件里那份是同样的七行。
|
|
452
|
-
*/
|
|
453
|
-
private func tlsConfigForHost(host: String): TlsClientConfig {
|
|
454
|
-
var cfg = TlsClientConfig()
|
|
455
|
-
if (!isIpLiteralHost(host)) {
|
|
456
|
-
cfg.serverName = host
|
|
457
|
-
}
|
|
458
|
-
return cfg
|
|
459
|
-
}
|
|
1
|
+
package cjTauri.plugins.http
|
|
2
|
+
|
|
3
|
+
import cjTauri.*
|
|
4
|
+
|
|
5
|
+
import std.collection.*
|
|
6
|
+
import stdx.encoding.json.*
|
|
7
|
+
|
|
8
|
+
/** 单次响应体上限:8 MiB(LLM 非流式回复通常远小于它;够装一份大 README) */
|
|
9
|
+
public let CJ_HTTP_MAX_BYTES: Int64 = 8 * 1024 * 1024
|
|
10
|
+
|
|
11
|
+
/** 请求超时(读写同一预算):Desktop 侧 ClientBuilder 没有 connectTimeout,目标不可达会卡在
|
|
12
|
+
* TCP connect 上——超时预算按「整体别太久」给,长挂由前端超时/取消兜 */
|
|
13
|
+
public let CJ_HTTP_TIMEOUT_SECONDS: Int64 = 60
|
|
14
|
+
|
|
15
|
+
/** 页面可带的请求头上限(条数与单条长度):防用一堆超长头把内存撑爆 */
|
|
16
|
+
public let CJ_HTTP_MAX_HEADERS: Int64 = 16
|
|
17
|
+
public let CJ_HTTP_MAX_HEADER_VALUE: Int64 = 4096
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 官方插件 http:**页面可控的出网口**(RFC-003 HTTP 白名单收口)
|
|
21
|
+
*
|
|
22
|
+
* 命令:http:get({url})/ http:post({url, body, headers?})/ http:delete({url, body}),返回
|
|
23
|
+
* { status, contentType, body, truncated };失败抛 CommandException(业务错误)。
|
|
24
|
+
* 权限:**不自动放行**——"permissions": ["http:default"] 或在 commands 里明文写。
|
|
25
|
+
*
|
|
26
|
+
* 安全语义(与 fs 的路径 scope 同构,判定逻辑复用共享层 scopeAllows/globMatch):
|
|
27
|
+
* - **主机白名单**:清单写 "scopes": { "http": { "allow": ["api.deepseek.com"] } }。
|
|
28
|
+
* allow 必须显式命中、先 deny 后 allow;**没写 scopes.http = 一律拒绝**并打一行 stderr
|
|
29
|
+
* (与 fs「空 allow = 全拒」同方向:出网是高危面,不做「没写就放行」的旧行为)。
|
|
30
|
+
* - **只认 host,不认端口**:匹配目标是 URL 里的 host(小写化);端口不参与匹配——
|
|
31
|
+
* 白名单声明「我信这个主机」,同一台主机的端口不会让信任变多。
|
|
32
|
+
* - **禁自动重定向**(autoRedirect(false)):3xx 原样回给页面——重定向到别的主机
|
|
33
|
+
* 不会替页面**追出去**(白名单只判第一跳;要跟就由页面显式再发一次、再过一遍白名单)。
|
|
34
|
+
* - Host / Content-Length 头不许页面自带(前者能改写白名单判过的目标,后者由库自己算)。
|
|
35
|
+
*
|
|
36
|
+
* 平台分层(2026-10-25 起,见 `http_backend.cj` 顶部注释):本文件只有**命令面与判定**,
|
|
37
|
+
* 真正出网交给 `httpTransportSend` / `httpTransportOpenStream`——桌面是 stdx + 显式 TLS,
|
|
38
|
+
* 鸿蒙是系统网络栈 `ohos.net.http`(仓颉运行时在鸿蒙上装不了 OpenSSL 3,stdx 的 TLS 必然失败)。
|
|
39
|
+
* **命令名、入参、返回值、白名单判定两平台一致**,前端一行都不用改。
|
|
40
|
+
*
|
|
41
|
+
* 性能口径(为什么**每个请求新建 Client**而不是共享一个):
|
|
42
|
+
* - 最大的性能收益本来就在**流式**:一次 LLM 对话只有一次请求,而这条流要挂几十秒——
|
|
43
|
+
* 建连 + TLS 握手(约百毫秒量级)摊在整条流里可忽略;频繁短请求不是本插件的靶场景。
|
|
44
|
+
* - 跨线程共享 Client 的连接池是否线程安全,本仓**没有验证过**(music 示例宁可每次
|
|
45
|
+
* 建连也不埋「偶发串包」的坑,见 examples/music/src/api.cj 的注释);命令 worker 是
|
|
46
|
+
* 并发的,用锁包共享 Client 会把并发请求串行化,反而伤多窗口/多会话场景。
|
|
47
|
+
* - 真到了「高频短请求」成为瓶颈那天,先实测 stdx 连接池的线程安全再改共享——
|
|
48
|
+
* 那是一次有判据的优化,不是现在凭感觉做。
|
|
49
|
+
*/
|
|
50
|
+
public class HttpPlugin <: Plugin {
|
|
51
|
+
public init() {}
|
|
52
|
+
|
|
53
|
+
public func name(): String {
|
|
54
|
+
return "http"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public func commands(): HashMap<String, CommandHandler> {
|
|
58
|
+
let m = HashMap<String, CommandHandler>()
|
|
59
|
+
// 键写**短名**:前缀由框架补(写全名会得到 http:http:get,且所有命令都成未注册)
|
|
60
|
+
m.add("get", HttpCommands())
|
|
61
|
+
m.add("post", HttpCommands())
|
|
62
|
+
m.add("delete", HttpCommands())
|
|
63
|
+
m.add("postStream", HttpCommands())
|
|
64
|
+
return m
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 命名权限集(v1.1):default = GET + POST + POST 流式 */
|
|
68
|
+
public func permissions(): HashMap<String, ArrayList<String>> {
|
|
69
|
+
let m = HashMap<String, ArrayList<String>>()
|
|
70
|
+
let all = ArrayList<String>()
|
|
71
|
+
all.add("get")
|
|
72
|
+
all.add("post")
|
|
73
|
+
all.add("delete")
|
|
74
|
+
all.add("postStream")
|
|
75
|
+
m.add("default", all)
|
|
76
|
+
return m
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** 流式命令推事件:每个 body 块经 http:chunk 回投(应用要在清单 events 里放行)。
|
|
80
|
+
* 这里写**短名**——框架自动补「<插件名>:」前缀,写全名会得到 http:http:chunk(实测)。 */
|
|
81
|
+
public func events(): ArrayList<String> {
|
|
82
|
+
let ev = ArrayList<String>()
|
|
83
|
+
ev.add("chunk")
|
|
84
|
+
return ev
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** 吃 scope:主机白名单的判定在清单 scopes.http(没写 = 全拒,见类注释) */
|
|
88
|
+
public func scopeAware(): Bool {
|
|
89
|
+
return true
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** http 命令处理器:get 收 {url},post / delete 收 {url, body, headers?}(delete 的体同样是 JSON 文本) */
|
|
94
|
+
public class HttpCommands <: CommandHandler {
|
|
95
|
+
public init() {}
|
|
96
|
+
|
|
97
|
+
public func handle(cmd: String, args: JsonObject, ipc: IpcContext): JsonValue {
|
|
98
|
+
match (cmd) {
|
|
99
|
+
case "http:get" => return this.fetch("GET", args, ipc)
|
|
100
|
+
case "http:post" => return this.fetch("POST", args, ipc)
|
|
101
|
+
case "http:delete" => return this.fetch("DELETE", args, ipc)
|
|
102
|
+
case "http:postStream" => return this.fetchStream(args, ipc)
|
|
103
|
+
case _ => throw CommandException("unknown http command: ${cmd}")
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 流式 POST(SSE):与非流式同一套白名单/请求头判定,差别只在**读响应体**——
|
|
109
|
+
* 按块读、每读一块就把增量经事件通道回投(emit:同一帧内多条都要到,不合并)。
|
|
110
|
+
*
|
|
111
|
+
* 事件 payload:{ status, contentType, delta, received, done, truncated }。
|
|
112
|
+
* delta 是**本轮新增**的文本;UTF-8 多字节被块边界切开时,不完整的那几字节
|
|
113
|
+
* 留到下一块拼齐再发(按 lead byte 回扫补齐边界,见 takeCompleteUtf8)。
|
|
114
|
+
* 结束(EOF / 超限)后发 done=true 的最后一条,命令再返回总字节数。
|
|
115
|
+
*
|
|
116
|
+
* 鸿蒙侧由 `httpTransportOpenStream` 当场如实拒绝(系统网络栈的回调形态不暴露增量块),
|
|
117
|
+
* 所以「命令能注册、能通过白名单、调用时明确报不可用」——不会静默返回空流。
|
|
118
|
+
*/
|
|
119
|
+
private func fetchStream(args: JsonObject, ipc: IpcContext): JsonValue {
|
|
120
|
+
let url = this.requireString(args, "url")
|
|
121
|
+
let host = match (httpHostOf(url)) {
|
|
122
|
+
case Some(h) => h
|
|
123
|
+
case None => throw CommandException("http: host is outside the authorized scope")
|
|
124
|
+
}
|
|
125
|
+
this.checkHostScope(url, host, ipc)
|
|
126
|
+
|
|
127
|
+
let headers = parsePageHeaders(args)
|
|
128
|
+
let stream = httpTransportOpenStream(url, host, headers, this.requireString(args, "body"))
|
|
129
|
+
let status = stream.status()
|
|
130
|
+
let ctype = stream.contentType()
|
|
131
|
+
var total: Int64 = 0
|
|
132
|
+
var truncated = false
|
|
133
|
+
try {
|
|
134
|
+
let chunk = Array<Byte>(8192, repeat: 0)
|
|
135
|
+
let pending = ArrayList<Byte>()
|
|
136
|
+
var n = stream.read(chunk)
|
|
137
|
+
while (n > 0) {
|
|
138
|
+
if (total + n > CJ_HTTP_MAX_BYTES) {
|
|
139
|
+
truncated = true
|
|
140
|
+
break
|
|
141
|
+
}
|
|
142
|
+
for (i in 0..n) {
|
|
143
|
+
pending.add(chunk[i])
|
|
144
|
+
}
|
|
145
|
+
total += n
|
|
146
|
+
n = stream.read(chunk)
|
|
147
|
+
// 发走所有完整 UTF-8 序列,尾巴(不完整的那几字节)留在 pending。
|
|
148
|
+
// 必须 emit(每条都到)而不是 emitLatest:后者按事件名「同帧只保最新」,
|
|
149
|
+
// 会把中间 delta 连同数据块一起覆盖掉——实测 chunks 数时好时坏、
|
|
150
|
+
// 前端拼文本缺字;对话流的语义是每一块都到达,不合并。
|
|
151
|
+
let delta = this.takeCompleteUtf8(pending)
|
|
152
|
+
if (delta.size > 0) {
|
|
153
|
+
ipc.emit("http:chunk", this.chunkPayload(status, ctype, delta, total, false, false))
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// 收尾:流在汉字中间被掐断时,pending 里剩下的是补不齐的不完整序列(最多 3 字节)。
|
|
157
|
+
// 这里**不能**直接 fromUtf8:它对非法序列抛 IllegalArgumentException,一抛就把整条
|
|
158
|
+
// 流式命令 reject 掉——页面显示「请求失败」,而正文其实早就收完了。丢这几个字节更诚实,
|
|
159
|
+
// 但要把丢弃这件事留一行 stderr(别让失败无声)。
|
|
160
|
+
var tail = ""
|
|
161
|
+
if (pending.size > 0) {
|
|
162
|
+
try {
|
|
163
|
+
tail = String.fromUtf8(pending.toArray())
|
|
164
|
+
} catch (e: Exception) {
|
|
165
|
+
eprintln("[cj-tauri] http:postStream 尾部 ${pending.size} 字节不是合法 UTF-8(流被掐断),已丢弃")
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
ipc.emit("http:chunk", this.chunkPayload(status, ctype, tail, total, true, truncated))
|
|
169
|
+
} finally {
|
|
170
|
+
// 句柄不关等于泄漏连接(桌面侧连客户端一起关);异常路径同样要收
|
|
171
|
+
stream.close()
|
|
172
|
+
}
|
|
173
|
+
let obj = JsonObject()
|
|
174
|
+
obj.put("status", JsonInt(status))
|
|
175
|
+
obj.put("contentType", JsonString(ctype))
|
|
176
|
+
obj.put("received", JsonInt(total))
|
|
177
|
+
obj.put("truncated", JsonBool(truncated))
|
|
178
|
+
return obj
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* 从 pending 里取走「完整的 UTF-8 序列」并转文本;不完整的尾巴留在原地。
|
|
183
|
+
* `public` 是为了让单测可见(`cjTauri.tests` 是子包,跨包只认 public):
|
|
184
|
+
* 这是「少移 1 字节」那类坑的唯一判据点,用例在 `src/tests/plugin_http_test.cj`。
|
|
185
|
+
*/
|
|
186
|
+
public func takeCompleteUtf8(pending: ArrayList<Byte>): String {
|
|
187
|
+
if (pending.size == 0) {
|
|
188
|
+
return ""
|
|
189
|
+
}
|
|
190
|
+
var keep: Int64 = 0
|
|
191
|
+
// 回扫最多 3 字节找 lead byte(110xxxxx/1110xxxx/11110xxx),判序列是否齐
|
|
192
|
+
let start = if (pending.size > 3) {
|
|
193
|
+
pending.size - 3
|
|
194
|
+
} else {
|
|
195
|
+
0
|
|
196
|
+
}
|
|
197
|
+
var i = pending.size - 1
|
|
198
|
+
while (i >= start) {
|
|
199
|
+
// ArrayList.get 返回 Option<UInt8>(下标在界内才取值,这里恒在界内)
|
|
200
|
+
let b = pending.get(i).getOrThrow()
|
|
201
|
+
if (b < 0x80u8) {
|
|
202
|
+
keep = 0
|
|
203
|
+
break
|
|
204
|
+
}
|
|
205
|
+
if ((b & 0xC0u8) != 0x80u8) {
|
|
206
|
+
// lead byte:判这串还缺几字节
|
|
207
|
+
let need: Int64 = if (b >= 0xF0u8) {
|
|
208
|
+
3
|
|
209
|
+
} else if (b >= 0xE0u8) {
|
|
210
|
+
2
|
|
211
|
+
} else {
|
|
212
|
+
1
|
|
213
|
+
}
|
|
214
|
+
let have = pending.size - i
|
|
215
|
+
keep = if (have < need + 1) {
|
|
216
|
+
have
|
|
217
|
+
} else {
|
|
218
|
+
0
|
|
219
|
+
}
|
|
220
|
+
break
|
|
221
|
+
}
|
|
222
|
+
i--
|
|
223
|
+
}
|
|
224
|
+
let cut = pending.size - keep
|
|
225
|
+
if (cut <= 0) {
|
|
226
|
+
return ""
|
|
227
|
+
}
|
|
228
|
+
let out = String.fromUtf8(pending.toArray()[0..cut])
|
|
229
|
+
// 移除已发走的部分(ArrayList.remove 收 Range)。Range 在本机工具链上是**右开**的
|
|
230
|
+
// (实测:a[0..3] 取 3 个、remove(0..2) 移 2 个),所以这里必须写 0..cut。
|
|
231
|
+
// 写成 0..(cut-1) 会少移 1 字节:上一个序列的末字节留在 pending 里,下一轮拼出来的
|
|
232
|
+
// 字节序列非法,fromUtf8 当场抛 IllegalArgumentException,整条流式命令变成
|
|
233
|
+
// 「请求失败」(2026-10-16 实机:command failed (http:postStream): Invalid utf8 byte sequence.
|
|
234
|
+
// 且中文长回复才偶发——ASCII 结尾的块少移那个字节仍是合法 UTF-8,所以以前看不太出来)。
|
|
235
|
+
pending.remove(0..cut)
|
|
236
|
+
return out
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private func chunkPayload(status: Int64, ctype: String, delta: String, received: Int64, done: Bool, truncated: Bool): JsonObject {
|
|
240
|
+
let obj = JsonObject()
|
|
241
|
+
obj.put("status", JsonInt(status))
|
|
242
|
+
obj.put("contentType", JsonString(ctype))
|
|
243
|
+
obj.put("delta", JsonString(delta))
|
|
244
|
+
obj.put("received", JsonInt(received))
|
|
245
|
+
obj.put("done", JsonBool(done))
|
|
246
|
+
obj.put("truncated", JsonBool(truncated))
|
|
247
|
+
return obj
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private func fetch(method: String, args: JsonObject, ipc: IpcContext): JsonValue {
|
|
251
|
+
let url = this.requireString(args, "url")
|
|
252
|
+
let host = match (httpHostOf(url)) {
|
|
253
|
+
case Some(h) => h
|
|
254
|
+
// 解析不出 host / 协议不是 http(s):按白名单未命中处理(不透露解析细节)
|
|
255
|
+
case None => throw CommandException("http: host is outside the authorized scope")
|
|
256
|
+
}
|
|
257
|
+
this.checkHostScope(url, host, ipc)
|
|
258
|
+
// GET 不带体;POST / DELETE 的体是必填(缺了就是业务错误,与改造前一致)
|
|
259
|
+
let body = if (method == "GET") {
|
|
260
|
+
""
|
|
261
|
+
} else {
|
|
262
|
+
this.requireString(args, "body")
|
|
263
|
+
}
|
|
264
|
+
return httpReplyJson(httpTransportSend(method, url, host, parsePageHeaders(args), body))
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** 主机白名单判定(共享层语义:先 deny 后 allow,allow 必须显式命中) */
|
|
268
|
+
private func checkHostScope(url: String, host: String, ipc: IpcContext): Unit {
|
|
269
|
+
// 分支体保持单语句(多语句的块会被解析器当 lambda,见下)
|
|
270
|
+
match (ipc.scopeRuleFor("http")) {
|
|
271
|
+
case None => this.rejectOutsideScope(url, "no host whitelist declared")
|
|
272
|
+
case Some(rule) => this.checkRuleAllows(url, host, rule)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private func checkRuleAllows(url: String, host: String, rule: ScopeRule): Unit {
|
|
277
|
+
if (!scopeAllows(rule, host)) {
|
|
278
|
+
this.rejectOutsideScope(url, host)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private func rejectOutsideScope(url: String, why: String): Nothing {
|
|
283
|
+
eprintln("[cj-tauri] scope denied (http): ${url} -> ${why}")
|
|
284
|
+
throw CommandException("http: host is outside the authorized scope")
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private func requireString(args: JsonObject, key: String): String {
|
|
288
|
+
match (args.get(key)) {
|
|
289
|
+
case Some(v) => match (v.kind()) {
|
|
290
|
+
case JsonKind.JsString => return v.asString().getValue()
|
|
291
|
+
case _ => throw CommandException("http: argument \"${key}\" must be a string")
|
|
292
|
+
}
|
|
293
|
+
case None => throw CommandException("http: missing argument \"${key}\"")
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* 从 URL 提取 host(小写返回):核心 urlHostOf 的 **协议收紧包装**——
|
|
300
|
+
* ws/wss 是 ws 插件的合法目标,但不是可抓取目标;http 插件只认 http/https。
|
|
301
|
+
* 解析与大小写口径统一在核心层(urlutil.cj),这里不再有第二份字节扫描。
|
|
302
|
+
* public 是为了单测可直接构造(跨包须 public)。
|
|
303
|
+
*/
|
|
304
|
+
public func httpHostOf(rawUrl: String): Option<String> {
|
|
305
|
+
let lower = rawUrl.toAsciiLower()
|
|
306
|
+
if (!lower.startsWith("http://") && !lower.startsWith("https://")) {
|
|
307
|
+
return None
|
|
308
|
+
}
|
|
309
|
+
return urlHostOf(rawUrl)
|
|
310
|
+
}
|