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.
Files changed (162) hide show
  1. package/README.md +137 -77
  2. package/bin/cj-tauri.js +287 -287
  3. package/cjpm.toml +13 -2
  4. package/cli/cjpm.toml +22 -13
  5. package/cli/src/doctor.cj +308 -304
  6. package/cli/src/main.cj +129 -121
  7. package/cli/src/manifest.cj +632 -0
  8. package/cli/src/manifest_test.cj +87 -0
  9. package/cli/src/project.cj +770 -754
  10. package/cli/src/resolve.cj +348 -348
  11. package/cli/src/scaffold.cj +46 -38
  12. package/cli/src/sync.cj +526 -0
  13. package/cli/src/util.cj +9 -0
  14. package/cli/templates/app/README.md +30 -59
  15. package/cli/templates/app/{ui → assets/ui}/index.html +2 -0
  16. package/cli/templates/app/cj-tauri.toml +32 -0
  17. package/cli/templates/app/cjpm.toml +21 -4
  18. package/cli/templates/app/src/main.cj +29 -16
  19. package/cli/templates/app-react/README.md +34 -45
  20. package/cli/templates/app-react/{ui → assets/ui}/index.html +25 -25
  21. package/cli/templates/app-react/{ui → assets/ui}/src/App.css +67 -67
  22. package/cli/templates/app-react/{ui → assets/ui}/src/App.jsx +124 -124
  23. package/cli/templates/app-react/cj-tauri.toml +33 -0
  24. package/cli/templates/app-react/cjpm.toml +21 -4
  25. package/cli/templates/app-react/src/main.cj +18 -29
  26. package/cli/templates/app-vue/README.md +34 -38
  27. package/cli/templates/app-vue/{ui → assets/ui}/index.html +25 -25
  28. package/cli/templates/app-vue/{ui → assets/ui}/src/App.vue +176 -176
  29. package/cli/templates/app-vue/cj-tauri.toml +33 -0
  30. package/cli/templates/app-vue/cjpm.toml +21 -4
  31. package/cli/templates/app-vue/src/main.cj +18 -29
  32. package/cli/templates/{ohos-pc → ohos-skeleton}/README.md +5 -0
  33. package/cli/templates/{ohos-pc → ohos-skeleton}/build-hap.sh +15 -0
  34. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/cjpm.toml +8 -0
  35. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/dialog_host.cj +217 -0
  36. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/index.cj +527 -0
  37. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/module.json5 +8 -0
  38. package/native/build_win.bat +62 -62
  39. package/native/core/bridge_core.c +1681 -1626
  40. package/native/core/bridge_core.h +675 -618
  41. package/native/core/bridge_js.h +108 -108
  42. package/native/platforms/harmonyos/README.md +115 -6
  43. package/native/platforms/linux/bridge_linux.c +2787 -2485
  44. package/native/platforms/windows/bridge_win.c +3740 -3248
  45. package/native/tests/gtk_threading_probe.c +143 -143
  46. package/native/tests/test_bridge_core.c +1339 -1228
  47. package/package.json +53 -51
  48. package/prebuilt/linux-x64/cj-tauri +0 -0
  49. package/src/api_system.cj +67 -67
  50. package/src/app.cj +325 -940
  51. package/src/app_events.cj +166 -0
  52. package/src/app_host_ops.cj +287 -0
  53. package/src/app_run.cj +283 -0
  54. package/src/app_spec.cj +76 -0
  55. package/src/arkweb_menu.cj +216 -0
  56. package/src/arkweb_script.cj +0 -12
  57. package/src/capability.cj +409 -409
  58. package/src/clipboard_bridge.cj +178 -0
  59. package/src/dialog_spec.cj +79 -0
  60. package/src/dragdrop.cj +26 -26
  61. package/src/embedded_assets.cj +98 -98
  62. package/src/host.cj +752 -604
  63. package/src/host_arkweb.cj +201 -17
  64. package/src/host_webkit.cj +663 -548
  65. package/src/host_webview2.cj +663 -548
  66. package/src/hotkey.cj +66 -66
  67. package/src/ipc_hub.cj +337 -337
  68. package/src/ipc_message.cj +286 -286
  69. package/src/launch.cj +62 -0
  70. package/src/logging.cj +127 -127
  71. package/src/menu.cj +197 -197
  72. package/src/platform/platform_app_context.cj +77 -0
  73. package/src/platform/platform_assets.cj +33 -0
  74. package/src/platform/platform_discover.cj +176 -0
  75. package/src/platform/platform_env.cj +29 -0
  76. package/src/platform/platform_feature.cj +31 -0
  77. package/src/platform/platform_page_source.cj +87 -0
  78. package/src/platform/platform_target_info.cj +32 -0
  79. package/src/platform_host_provider.cj +87 -0
  80. package/src/plugin.cj +333 -299
  81. package/src/plugins/clipboard/plugin_clipboard.cj +152 -144
  82. package/src/plugins/dialog/plugin_dialog.cj +209 -209
  83. package/src/plugins/fs/plugin_fs.cj +105 -1
  84. package/src/plugins/http/http_backend.cj +131 -0
  85. package/src/plugins/http/http_desktop.cj +195 -0
  86. package/src/plugins/http/http_ohos.cj +204 -0
  87. package/src/plugins/http/plugin_http.cj +310 -459
  88. package/src/plugins/log/plugin_log.cj +105 -105
  89. package/src/plugins/menu/plugin_menu.cj +8 -0
  90. package/src/plugins/notification/plugin_notification.cj +154 -146
  91. package/src/plugins/serial/plugin_serial.cj +17 -1
  92. package/src/plugins/serial/serial_bridge.cj +9 -1
  93. package/src/plugins/serial/serial_manager_ohos.cj +92 -0
  94. package/src/plugins/serial/serial_ohos.cj +529 -0
  95. package/src/plugins/shell/plugin_shell.cj +29 -1
  96. package/src/plugins/sqlite/plugin_sqlite.cj +8 -0
  97. package/src/plugins/ws/plugin_ws.cj +488 -470
  98. package/src/process_hidden.cj +174 -174
  99. package/src/scope.cj +391 -391
  100. package/src/shell_event.cj +69 -69
  101. package/src/tests/app_spec_test.cj +126 -0
  102. package/src/tests/capability_test.cj +243 -243
  103. package/src/tests/dialog_spec_test.cj +73 -0
  104. package/src/tests/fake_host.cj +201 -186
  105. package/src/tests/ipc_hub_test.cj +487 -487
  106. package/src/tests/logging_test.cj +62 -62
  107. package/src/tests/platform_contract_test.cj +251 -0
  108. package/src/tests/plugin_clipboard_test.cj +222 -222
  109. package/src/tests/plugin_dialog_test.cj +237 -237
  110. package/src/tests/plugin_fs_test.cj +207 -0
  111. package/src/tests/plugin_http_test.cj +124 -124
  112. package/src/tests/plugin_notification_test.cj +205 -205
  113. package/src/tests/plugin_serial_test.cj +48 -0
  114. package/src/tests/plugin_test.cj +66 -3
  115. package/src/tests/plugin_ws_test.cj +40 -40
  116. package/src/tests/process_hidden_test.cj +129 -129
  117. package/src/tests/scope_test.cj +350 -350
  118. package/src/tests/system_host_test.cj +99 -81
  119. package/src/tests/urlutil_test.cj +53 -53
  120. package/src/tests/version_test.cj +75 -75
  121. package/src/tests/window_registry_test.cj +123 -123
  122. package/src/tray.cj +90 -90
  123. package/src/urlutil.cj +159 -159
  124. package/src/version.cj +44 -44
  125. package/src/window.cj +51 -51
  126. package/src/window_registry.cj +116 -116
  127. package/cli/templates/ohos-pc/entry/src/main/cangjie/index.cj +0 -167
  128. package/prebuilt/win32-x64/cj-tauri.exe +0 -0
  129. package/src/host_factory.cj +0 -48
  130. /package/cli/templates/app/{capabilities → assets/capabilities}/default.json +0 -0
  131. /package/cli/templates/app-react/{capabilities → assets/capabilities}/default.json +0 -0
  132. /package/cli/templates/app-react/{ui → assets/ui}/package.json +0 -0
  133. /package/cli/templates/app-react/{ui → assets/ui}/src/main.jsx +0 -0
  134. /package/cli/templates/app-react/{ui → assets/ui}/vite.config.js +0 -0
  135. /package/cli/templates/app-vue/{capabilities → assets/capabilities}/default.json +0 -0
  136. /package/cli/templates/app-vue/{ui → assets/ui}/package.json +0 -0
  137. /package/cli/templates/app-vue/{ui → assets/ui}/src/main.js +0 -0
  138. /package/cli/templates/app-vue/{ui → assets/ui}/vite.config.js +0 -0
  139. /package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/app.json5 +0 -0
  140. /package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/resources/base/element/string.json +0 -0
  141. /package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/resources/base/media/background.png +0 -0
  142. /package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/resources/base/media/foreground.png +0 -0
  143. /package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/resources/base/media/layered_image.json +0 -0
  144. /package/cli/templates/{ohos-pc → ohos-skeleton}/build-profile.json5 +0 -0
  145. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/build-profile.json5 +0 -0
  146. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/hvigorfile.ts +0 -0
  147. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/oh-package.json5 +0 -0
  148. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/cangjie/ability_stage.cj +0 -0
  149. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/cangjie/app_host.cj +0 -0
  150. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/cangjie/main_ability.cj +0 -0
  151. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/element/color.json +0 -0
  152. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/element/string.json +0 -0
  153. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/media/background.png +0 -0
  154. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/media/foreground.png +0 -0
  155. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/media/layered_image.json +0 -0
  156. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/media/startIcon.png +0 -0
  157. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/profile/main_pages.json +0 -0
  158. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/rawfile/capabilities.json +0 -0
  159. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/rawfile/index.html +0 -0
  160. /package/cli/templates/{ohos-pc → ohos-skeleton}/hvigor/hvigor-config.json5 +0 -0
  161. /package/cli/templates/{ohos-pc → ohos-skeleton}/hvigorfile.ts +0 -0
  162. /package/cli/templates/{ohos-pc → ohos-skeleton}/oh-package.json5 +0 -0
@@ -0,0 +1,131 @@
1
+ package cjTauri.plugins.http
2
+
3
+ import std.collection.*
4
+ import stdx.encoding.json.*
5
+
6
+ /*
7
+ * http 插件的**传输层契约**(两平台共用,本文件不含任何平台代码)
8
+ *
9
+ * 分工(照 AGENTS §2 的平台差异纪律):
10
+ * - 本文件:回给页面的形状、流式句柄接口、**请求头解析与安全过滤**——判定只此一份;
11
+ * - `http_desktop.cj`(@When[env != "ohos"]):stdx.net.http + 显式 TLS;
12
+ * - `http_ohos.cj`(@When[env == "ohos"]):系统网络栈 ohos.net.http。
13
+ *
14
+ * 为什么值得抽这一层:仓颉运行时在鸿蒙上装不了 OpenSSL 3(真机
15
+ * `MUSL-LDSO: load libcrypto_openssl.z.so failed … errno=2`),stdx 的 TLS 从第一步就不可用,
16
+ * 于是 `http:get` 在鸿蒙上**必然是 https 全灭、http 照旧**——这种「半死」状态最难排查。
17
+ * 换系统栈之后命令名、入参、返回值、白名单判定都不变,前端契约一行都不用改。
18
+ */
19
+
20
+ /**
21
+ * 一次出网请求的结果(**两平台同一形状**)
22
+ *
23
+ * 回给页面的是 `{status, contentType, body, truncated}` 四项,与 `httpReplyJson` 一一对应。
24
+ * `truncated` 表示「撞上 `CJ_HTTP_MAX_BYTES` 上限被截断」,不是出错。
25
+ */
26
+ public class HttpReply {
27
+ public let status: Int64
28
+ public let contentType: String
29
+ public let body: String
30
+ public let truncated: Bool
31
+
32
+ public init(status: Int64, contentType: String, body: String, truncated: Bool) {
33
+ this.status = status
34
+ this.contentType = contentType
35
+ this.body = body
36
+ this.truncated = truncated
37
+ }
38
+ }
39
+
40
+ /** `HttpReply` → 命令返回值(两边各拼一次迟早会漂字段名,所以收在一处) */
41
+ public func httpReplyJson(reply: HttpReply): JsonValue {
42
+ let obj = JsonObject()
43
+ obj.put("status", JsonInt(reply.status))
44
+ obj.put("contentType", JsonString(reply.contentType))
45
+ obj.put("body", JsonString(reply.body))
46
+ obj.put("truncated", JsonBool(reply.truncated))
47
+ return obj
48
+ }
49
+
50
+ /**
51
+ * 流式响应句柄:把「边收边发」收敛成 read / close 两步
52
+ *
53
+ * 桌面由 stdx 的响应体包装(连接与响应都归它持有,close 一并释放);**没有流式能力的平台
54
+ * 在构造处如实抛错**——返回一个假句柄假装成功是不可接受的(AGENTS §1 第 4 条:
55
+ * 跑不起来就说跑不起来)。
56
+ *
57
+ * 句柄只被 `http:postStream` 的那条命令线程用,**不做跨线程共享**(与 stdx 响应的
58
+ * 单线程语义一致)。
59
+ */
60
+ public interface HttpResponseStream {
61
+ func status(): Int64
62
+ func contentType(): String
63
+ /** 读一块进 buf,返回字节数;0 表示 EOF */
64
+ func read(buf: Array<Byte>): Int64
65
+ func close(): Unit
66
+ }
67
+
68
+ /**
69
+ * 解析页面给的 `headers` 对象(可选)→ 请求头表(键已小写化)
70
+ *
71
+ * 这是**安全判定**,不是格式转换,所以只写一份:条数与单条长度有上限,且
72
+ * **Host / Content-Length 一律剔除**——前者能改写白名单判过的目标(白名单判的是 URL 里的
73
+ * host),后者由库自己算,页面写进来的值与实际发出的一样才不误导。
74
+ * 非字符串值直接跳过(不做隐式转换:`{"X": 1}` 该被忽略,不是变成 "1")。
75
+ */
76
+ public func parsePageHeaders(args: JsonObject): HashMap<String, String> {
77
+ let out = HashMap<String, String>()
78
+ let v = match (args.get("headers")) {
79
+ case Some(h) => h
80
+ case None => return out
81
+ }
82
+ // JsonKind 没有 ==(AGENTS §4):枚举比较只能走 match
83
+ let isObject = match (v.kind()) {
84
+ case JsonKind.JsObject => true
85
+ case _ => false
86
+ }
87
+ if (!isObject) {
88
+ return out
89
+ }
90
+ var count: Int64 = 0
91
+ // 遍历用 getFields()(JsonObject 不是按索引取的容器,store 插件同款先例)
92
+ for ((name0, value) in v.asObject().getFields()) {
93
+ if (count >= CJ_HTTP_MAX_HEADERS) {
94
+ break
95
+ }
96
+ let name = name0.toAsciiLower()
97
+ if (name == "host" || name == "content-length") {
98
+ continue
99
+ }
100
+ let isString = match (value.kind()) {
101
+ case JsonKind.JsString => true
102
+ case _ => false
103
+ }
104
+ if (!isString) {
105
+ continue
106
+ }
107
+ let hval = value.asString().getValue()
108
+ if (hval.size <= CJ_HTTP_MAX_HEADER_VALUE) {
109
+ out[name] = hval
110
+ count++
111
+ }
112
+ }
113
+ return out
114
+ }
115
+
116
+ /**
117
+ * 从响应头里取 Content-Type(同 Y 头名的**大小写不敏感**取值)
118
+ *
119
+ * 桌面侧 stdx 用 `headers.getFirst`(且**没有 get**,AGENTS §4);鸿蒙侧 `HttpResponse.header`
120
+ * 是 `HashMap<String, String>` 且键保留服务端大小写,只能逐个小写比对。
121
+ * 两平台都要「按名字取头」这一件事,所以收成一个只吃表的纯函数,谁都能用。
122
+ */
123
+ public func headerValueOf(header: HashMap<String, String>, want: String): String {
124
+ let lower = want.toAsciiLower()
125
+ for ((k, v) in header) {
126
+ if (k.toAsciiLower() == lower) {
127
+ return v
128
+ }
129
+ }
130
+ return ""
131
+ }
@@ -0,0 +1,195 @@
1
+ package cjTauri.plugins.http
2
+
3
+ import cjTauri.*
4
+
5
+ @When[env != "ohos"]
6
+ import std.collection.*
7
+
8
+ @When[env != "ohos"]
9
+ import std.io.*
10
+
11
+ @When[env != "ohos"]
12
+ import stdx.net.http.*
13
+
14
+ @When[env != "ohos"]
15
+ import stdx.net.tls.*
16
+
17
+ /*
18
+ * 桌面两端的出网实现(Linux / Windows):stdx.net.http
19
+ *
20
+ * 与鸿蒙侧的分界:本文件只在 `env != "ohos"` 编译,`http_ohos.cj` 反之。
21
+ * 上层(`plugin_http.cj`)只认两个函数名,不出现任何平台分支。
22
+ */
23
+
24
+ /**
25
+ * 发一次请求并读完响应体(撞 `CJ_HTTP_MAX_BYTES` 上限则截断并置 `truncated`)
26
+ *
27
+ * 调用方(`HttpCommands`)已经做过主机白名单判定与参数解析——本函数只管「怎么发出去」。
28
+ * 三个口径与改造前完全一致,别顺手改:
29
+ * - **禁自动重定向**:3xx 原样回给页面,白名单只判第一跳;
30
+ * - 每个请求**新建 Client**(共享 Client 的线程安全性本仓没验证过,见 `HttpPlugin` 类注释);
31
+ * - `resp.body` 的静态类型只有 InputStream,`readToEnd` 编译不过(泛型约束要 Seekable,
32
+ * AGENTS §4),所以按块读到 EOF 或上限。
33
+ */
34
+ @When[env != "ohos"]
35
+ public func httpTransportSend(method: String, url: String, host: String,
36
+ headers: HashMap<String, String>, body: String): HttpReply {
37
+ var builder = HttpRequestBuilder().url(url)
38
+ if (method == "GET") {
39
+ builder = builder.get()
40
+ } else if (method == "DELETE") {
41
+ // DELETE 也带 JSON 体:cjh 的工作区删除靠 body 传 path(其 rest_api 的 DELETE 分支
42
+ // 读的就是请求体),所以这里不能走「无体 DELETE」那条路
43
+ builder = builder.delete().body(body)
44
+ } else {
45
+ builder = builder.post().body(body)
46
+ }
47
+ // 默认头:JSON 客户端最常见形态;页面可用 headers 覆盖/追加(Host/Content-Length 已剔除)
48
+ builder = builder.header("Accept", "application/json, text/event-stream, */*")
49
+ for ((name, value) in headers) {
50
+ builder = builder.header(name, value)
51
+ }
52
+
53
+ let client = ClientBuilder()
54
+ .tlsConfig(tlsConfigForHost(host))
55
+ .autoRedirect(false)
56
+ .readTimeout(Duration.second * CJ_HTTP_TIMEOUT_SECONDS)
57
+ .writeTimeout(Duration.second * CJ_HTTP_TIMEOUT_SECONDS)
58
+ .build()
59
+ var status: Int64 = 0
60
+ var ctype = ""
61
+ var text = ""
62
+ var truncated = false
63
+ try {
64
+ let resp = client.send(builder.build())
65
+ status = Int64(resp.status)
66
+ // 头名大小写在 HTTP 里不敏感,但取法只有 getFirst(**没有 get**,AGENTS §4)
67
+ ctype = match (resp.headers.getFirst("Content-Type")) {
68
+ case Some(v) => v
69
+ case None => ""
70
+ }
71
+ // ByteBuffer 没有 size 属性:字节数自己累加(要掐上限,所以不用现成的 readBodyBytes)
72
+ let buf = ByteBuffer()
73
+ let chunk = Array<Byte>(8192, repeat: 0)
74
+ var total: Int64 = 0
75
+ var n = resp.body.read(chunk)
76
+ while (n > 0) {
77
+ if (total + n > CJ_HTTP_MAX_BYTES) {
78
+ buf.write(chunk[0..(CJ_HTTP_MAX_BYTES - total)])
79
+ truncated = true
80
+ break
81
+ }
82
+ buf.write(chunk[0..n])
83
+ total += n
84
+ n = resp.body.read(chunk)
85
+ }
86
+ resp.close()
87
+ text = String.fromUtf8(buf.bytes())
88
+ } finally {
89
+ client.close()
90
+ }
91
+ return HttpReply(status, ctype, text, truncated)
92
+ }
93
+
94
+ /**
95
+ * 打开一个流式响应(SSE / 长回复增量回投用)
96
+ *
97
+ * 句柄持有 body 与 client,`close()` 一并释放——**句柄不关闭等于泄漏连接**,调用方必须
98
+ * 放在 finally 里。连接与响应都归这一条命令线程所有,不跨线程共享。
99
+ */
100
+ @When[env != "ohos"]
101
+ public func httpTransportOpenStream(url: String, host: String,
102
+ headers: HashMap<String, String>, body: String): HttpResponseStream {
103
+ var builder = HttpRequestBuilder().url(url).post().body(body)
104
+ builder = builder.header("Accept", "text/event-stream")
105
+ for ((name, value) in headers) {
106
+ builder = builder.header(name, value)
107
+ }
108
+ let client = ClientBuilder()
109
+ .tlsConfig(tlsConfigForHost(host))
110
+ .autoRedirect(false)
111
+ .readTimeout(Duration.second * CJ_HTTP_TIMEOUT_SECONDS)
112
+ .writeTimeout(Duration.second * CJ_HTTP_TIMEOUT_SECONDS)
113
+ .build()
114
+ try {
115
+ let resp = client.send(builder.build())
116
+ let ctype = match (resp.headers.getFirst("Content-Type")) {
117
+ case Some(v) => v
118
+ case None => ""
119
+ }
120
+ return StdHttpStream(Int64(resp.status), ctype, resp.body, client)
121
+ } catch (e: Exception) {
122
+ // 建连/握手/首字节就失败:client 还没交给句柄,这里必须自己收掉,否则泄漏
123
+ client.close()
124
+ throw e
125
+ }
126
+ }
127
+
128
+ /**
129
+ * stdx 响应体的流式包装
130
+ *
131
+ * 只持 `InputStream` 与 `Client`,**不持响应对象**——响应体的读与关闭已经够用,
132
+ * 少一个类型依赖少一处跨版本形状风险。
133
+ */
134
+ @When[env != "ohos"]
135
+ class StdHttpStream <: HttpResponseStream {
136
+ private let st: Int64
137
+ private let ct: String
138
+ private let body: InputStream
139
+ private let client: Client
140
+ private var closed = false
141
+
142
+ init(st: Int64, ct: String, body: InputStream, client: Client) {
143
+ this.st = st
144
+ this.ct = ct
145
+ this.body = body
146
+ this.client = client
147
+ }
148
+
149
+ // 接口成员隐式 public,实现类里必须显式写 public(否则报「must be at least as visible」)
150
+ public func status(): Int64 {
151
+ return this.st
152
+ }
153
+
154
+ public func contentType(): String {
155
+ return this.ct
156
+ }
157
+
158
+ public func read(buf: Array<Byte>): Int64 {
159
+ return this.body.read(buf)
160
+ }
161
+
162
+ /** 幂等:finally 与异常路径都可能调到,重复关不抛 */
163
+ public func close(): Unit {
164
+ if (this.closed) {
165
+ return
166
+ }
167
+ this.closed = true
168
+ try {
169
+ this.client.close()
170
+ } catch (_: Exception) {
171
+ // 关连接失败不影响已经收到的内容,吞掉
172
+ }
173
+ }
174
+ }
175
+
176
+ /**
177
+ * 建 TLS 客户端配置:**域名必须显式写 `serverName`(SNI)**
178
+ *
179
+ * stdx 的 `TlsClientConfig()` 默认不发 SNI,而多租户 HTTPS 边缘(实测 Cloudflare 后面的
180
+ * api 域名)收不到 SNI 就以 `handshake failure`(40) 拒掉握手——表象是「TLS 握手失败」,
181
+ * 同一地址 curl / openssl 打起来一切正常,极易误判成对端故障、甚至误判成白名单问题
182
+ * (AGENTS §4 有这条坑)。IP 字面量**不带** SNI:RFC 6066 不许,且「https 直连 IP」原本
183
+ * 可用,不能反被改坏。
184
+ *
185
+ * 这七行故意留在插件里而不进核心共享层:核心一引用 `stdx.net.tls`,不用 TLS 的应用也会
186
+ * 被拉上 OpenSSL 依赖,单文件打包契约跟着变形(§2 的包粒度纪律);ws 插件里那份是同样的七行。
187
+ */
188
+ @When[env != "ohos"]
189
+ func tlsConfigForHost(host: String): TlsClientConfig {
190
+ var cfg = TlsClientConfig()
191
+ if (!isIpLiteralHost(host)) {
192
+ cfg.serverName = host
193
+ }
194
+ return cfg
195
+ }
@@ -0,0 +1,204 @@
1
+ package cjTauri.plugins.http
2
+
3
+ @When[env == "ohos"]
4
+ import cjTauri.*
5
+
6
+ @When[env == "ohos"]
7
+ import ohos.net.http.*
8
+
9
+ @When[env == "ohos"]
10
+ import std.collection.*
11
+
12
+ /*
13
+ * 鸿蒙侧的出网实现:系统网络栈 `ohos.net.http`(NetworkKit)
14
+ *
15
+ * ## 为什么不是 stdx.net.http
16
+ *
17
+ * 仓颉运行时在鸿蒙上**装不了 OpenSSL 3**:真机 hilog 是
18
+ * `MUSL-LDSO: load libcrypto_openssl.z.so failed, namespace=moduleNs_default, errno=2`
19
+ * (`libssl_openssl.z.so` 同样),即运行时按名字 dlopen、设备上没有这个库。于是 stdx 的 TLS
20
+ * 从第一步就不可用,全部 https 当场失败、http 照旧——这种「半死」状态最容易误判成网络/权限问题
21
+ * (AGENTS §4 有完整形状,判据见 `docs/鸿蒙ArkWeb接入踩坑.md` §10.15)。
22
+ *
23
+ * 换成系统栈之后 TLS / 证书链 / 代理都交给 OS,本文件只管组请求、等结果、解码。
24
+ * **不要**往 hap 里塞 OpenSSL:运行时是 musl 静态链,AOSP 那份的 ABI 与符号都对不上。
25
+ *
26
+ * ## 为什么这里是阻塞等回调
27
+ *
28
+ * `HttpRequest.request` 只有回调形态(没有同步版本),而命令 handler 跑在框架的 worker
29
+ * 线程上(不是 UI 线程),所以「发请求 → sleep 轮询等回调」是安全的:冻不住界面。
30
+ * **这个写法不能搬到 UI 线程**(那会把界面卡满整个超时预算)。
31
+ *
32
+ * ## 形状依据与未验证项(本机没有鸿蒙仓颉工具链,编不了这一支)
33
+ *
34
+ * 建客户端的调用形状照抄 `examples/ohos/ohos-movie/entry/src/main/cangjie/ohos_movie_image.cj`
35
+ * ——那份是**真机验证过**的(`client.request(url, options) { err, resp => … }` 位置参数 + 尾随
36
+ * 闭包、`HttpDataType.ArrayBuffer`、`resp.responseCode` / `resp.result` / `resp.header`、
37
+ * `client.destroy()`)。两处是本机核不到、要在真机构建时确认的:
38
+ * ① `RequestMethod` 的成员名(`Get` 是 precedent 里的写法,`Post` / `Delete` 按同一命名风格推);
39
+ * ② `HttpRequestOptions.extraData` 承载 POST 体(官方文档确认该字段存在,仓内无用法先例)。
40
+ * 真机首跑若在这两处报错,改的是**这一支**,不要回头动 `plugin_http.cj`(命令面对两平台一致)。
41
+ */
42
+
43
+ /** 轮询回调的间隔(毫秒):与 ohos-movie 取封面的轮询同口径 */
44
+ @When[env == "ohos"]
45
+ let OHOS_HTTP_POLL_MS: Int64 = 20
46
+
47
+ /**
48
+ * 回调与命令线程之间搬结果用的盒子
49
+ *
50
+ * 仓颉的 lambda **不能捕获可变的局部变量**(AGENTS §4),所以结果装在对象里、闭包只改字段。
51
+ * 回调线程与轮询线程之间没有加锁——与真机验证过的 `ohos_movie_image.cj` 同形(那边同样只写
52
+ * `done` / `bytes` 这类简单字段)。真要改成并发安全的,两边得一起改,别只改这边。
53
+ */
54
+ @When[env == "ohos"]
55
+ class OhosHttpFeed {
56
+ var done = false
57
+ var failure: Option<String> = None
58
+ var status: Int64 = 0
59
+ var bytes: Array<Byte> = []
60
+ var contentType: String = ""
61
+ }
62
+
63
+ /** 命令的 HTTP 方法 → 系统栈的枚举(GET 是兜底:本插件只会传这三者之一) */
64
+ @When[env == "ohos"]
65
+ func ohosMethodOf(method: String): RequestMethod {
66
+ if (method == "POST") {
67
+ return RequestMethod.Post
68
+ }
69
+ if (method == "DELETE") {
70
+ return RequestMethod.Delete
71
+ }
72
+ return RequestMethod.Get
73
+ }
74
+
75
+ /** 把响应体统一成字节:期望类型是 ArrayBuffer,个别实现仍可能回字符串 */
76
+ @When[env == "ohos"]
77
+ func ohosResponseBytes(data: HttpData): Array<Byte> {
78
+ match (data) {
79
+ case ArrayData(b) => return b
80
+ case StringData(s) => return s.toArray()
81
+ case _ => return Array<Byte>()
82
+ }
83
+ }
84
+
85
+ /**
86
+ * 发一次请求,返回 `(状态码, Content-Type, 响应体字节)`
87
+ *
88
+ * 失败一律转成 `CommandException`(业务错误):命令面回给页面的是业务错误,不是框架内部异常。
89
+ * 超时既可能是 `request` 同步抛、也可能是回调来得太晚,两种都收口到同一句话。
90
+ */
91
+ @When[env == "ohos"]
92
+ func ohosRequest(method: String, url: String, headers: HashMap<String, String>,
93
+ body: String): (Int64, String, Array<Byte>) {
94
+ let feed = OhosHttpFeed()
95
+ // 页面头原样带上(Host / Content-Length 已在 parsePageHeaders 里剔除——判定只在那份)
96
+ let hdrs = HashMap<String, String>()
97
+ for ((k, v) in headers) {
98
+ hdrs[k] = v
99
+ }
100
+ if (!hdrs.contains("accept")) {
101
+ // 默认头与非流式命令的桌面侧一致(页面给了就尊重页面的)
102
+ hdrs["Accept"] = "application/json, text/event-stream, */*"
103
+ }
104
+ let timeoutMs = UInt32(CJ_HTTP_TIMEOUT_SECONDS * 1000)
105
+ let client = createHttp()
106
+ try {
107
+ // GET 不带体:extraData 在 GET 上会被拼成查询参数,所以两种形状分开构造
108
+ let options = if (method == "GET") {
109
+ HttpRequestOptions(
110
+ method: RequestMethod.Get,
111
+ expectDataType: HttpDataType.ArrayBuffer,
112
+ header: hdrs,
113
+ readTimeout: timeoutMs,
114
+ connectTimeout: timeoutMs
115
+ )
116
+ } else {
117
+ HttpRequestOptions(
118
+ method: ohosMethodOf(method),
119
+ extraData: StringData(body),
120
+ expectDataType: HttpDataType.ArrayBuffer,
121
+ header: hdrs,
122
+ readTimeout: timeoutMs,
123
+ connectTimeout: timeoutMs
124
+ )
125
+ }
126
+ client.request(url, options) { err, resp =>
127
+ match (err) {
128
+ case Some(e) => feed.failure = Some("${e}")
129
+ case None => ()
130
+ }
131
+ match (resp) {
132
+ case Some(r) =>
133
+ feed.status = Int64(r.responseCode)
134
+ feed.bytes = ohosResponseBytes(r.result)
135
+ feed.contentType = headerValueOf(r.header, "content-type")
136
+ case None => ()
137
+ }
138
+ feed.done = true
139
+ }
140
+ } catch (e: Exception) {
141
+ // 同步抛:缺 ohos.permission.INTERNET、明文流量被策略拦、域名不可达等
142
+ feed.failure = Some("${e}")
143
+ feed.done = true
144
+ }
145
+
146
+ // 等回调:命令在 worker 线程上跑,这里阻塞不会冻界面(见文件头「为什么这里是阻塞的」)
147
+ let budgetMs = CJ_HTTP_TIMEOUT_SECONDS * 1000
148
+ var waited: Int64 = 0
149
+ while (!feed.done && waited < budgetMs) {
150
+ sleep(Duration.millisecond * OHOS_HTTP_POLL_MS)
151
+ waited += OHOS_HTTP_POLL_MS
152
+ }
153
+ try {
154
+ client.destroy()
155
+ } catch (_: Exception) {
156
+ // 释放失败不影响结论(这个 client 用完即弃,每条命令自己建一个)
157
+ }
158
+
159
+ if (let Some(msg) <- feed.failure) {
160
+ eprintln("[cj-tauri] http transport failed (ohos): ${msg}")
161
+ throw CommandException("http: request failed on HarmonyOS: ${msg}")
162
+ }
163
+ if (!feed.done) {
164
+ eprintln("[cj-tauri] http transport timeout (ohos): ${waited}ms ${url}")
165
+ throw CommandException("http: no response within ${waited}ms on HarmonyOS")
166
+ }
167
+ return (feed.status, feed.contentType, feed.bytes)
168
+ }
169
+
170
+ /** 非流式请求:与桌面侧同一契约(截断口径、Content-Type 取值都在这里对齐) */
171
+ @When[env == "ohos"]
172
+ public func httpTransportSend(method: String, url: String, host: String,
173
+ headers: HashMap<String, String>, body: String): HttpReply {
174
+ // host 只在建 TLS 时用得上(桌面侧写 SNI);鸿蒙把 TLS 交给系统,按 URL 里的域名自动协商
175
+ let _ = host
176
+ let (status, ctype, bytes) = ohosRequest(method, url, headers, body)
177
+ var truncated = false
178
+ var cut = bytes.size
179
+ if (cut > CJ_HTTP_MAX_BYTES) {
180
+ cut = CJ_HTTP_MAX_BYTES
181
+ truncated = true
182
+ }
183
+ return HttpReply(status, ctype, String.fromUtf8(bytes[0..cut]), truncated)
184
+ }
185
+
186
+ /**
187
+ * 流式请求:**鸿蒙侧如实拒绝**,不假装成功
188
+ *
189
+ * 系统栈的 `request` 一次回整包(`requestInStream` + `dataReceive` 是 ArkTS 侧 API 18 起的形态,
190
+ * 仓颉绑定上**没有验证过**,本机也没有鸿蒙工具链可核)——所以这里宁可当场报「不可用」,
191
+ * 也不返回一个永远读到 EOF 的假句柄:那种失败在页面上表现为「模型一句话都不回」,
192
+ * 没有凭证、极难排查(AGENTS §1 第 4 条:跑不起来就说跑不起来)。
193
+ * 命令本身照常注册、白名单照常判定——不可用的是**流式这一种取法**,不是整条出网链路。
194
+ */
195
+ @When[env == "ohos"]
196
+ public func httpTransportOpenStream(url: String, host: String,
197
+ headers: HashMap<String, String>, body: String): HttpResponseStream {
198
+ let _ = url
199
+ let _ = host
200
+ let _ = headers
201
+ let _ = body
202
+ throw CommandException(
203
+ "http:postStream is not available on HarmonyOS: the system network stack returns the whole body at once (no incremental chunks). Use http:post instead.")
204
+ }