cj-tauri 2.0.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/README.md +206 -139
  2. package/bin/cj-tauri.js +287 -287
  3. package/cjpm.toml +63 -52
  4. package/cli/cjpm.toml +22 -22
  5. package/cli/src/doctor.cj +308 -306
  6. package/cli/src/main.cj +129 -129
  7. package/cli/src/manifest.cj +643 -558
  8. package/cli/src/manifest_test.cj +87 -0
  9. package/cli/src/project.cj +770 -770
  10. package/cli/src/resolve.cj +348 -348
  11. package/cli/src/scaffold.cj +369 -369
  12. package/cli/src/sync.cj +638 -526
  13. package/cli/src/util.cj +168 -168
  14. package/cli/templates/app/README.md +51 -48
  15. package/cli/templates/app/assets/capabilities/default.json +7 -7
  16. package/cli/templates/app/assets/ui/index.html +120 -118
  17. package/cli/templates/app/cj-tauri.toml +32 -22
  18. package/cli/templates/app/cjpm.toml +34 -34
  19. package/cli/templates/app/src/main.cj +101 -90
  20. package/cli/templates/app-react/README.md +56 -56
  21. package/cli/templates/app-react/assets/capabilities/default.json +7 -7
  22. package/cli/templates/app-react/assets/ui/index.html +25 -25
  23. package/cli/templates/app-react/assets/ui/package.json +19 -19
  24. package/cli/templates/app-react/assets/ui/src/App.css +67 -67
  25. package/cli/templates/app-react/assets/ui/src/App.jsx +124 -124
  26. package/cli/templates/app-react/assets/ui/src/main.jsx +6 -6
  27. package/cli/templates/app-react/assets/ui/vite.config.js +24 -24
  28. package/cli/templates/app-react/cj-tauri.toml +33 -23
  29. package/cli/templates/app-react/cjpm.toml +34 -34
  30. package/cli/templates/app-react/src/main.cj +91 -91
  31. package/cli/templates/app-vue/README.md +56 -56
  32. package/cli/templates/app-vue/assets/capabilities/default.json +7 -7
  33. package/cli/templates/app-vue/assets/ui/index.html +25 -25
  34. package/cli/templates/app-vue/assets/ui/package.json +18 -18
  35. package/cli/templates/app-vue/assets/ui/src/App.vue +176 -176
  36. package/cli/templates/app-vue/assets/ui/src/main.js +4 -4
  37. package/cli/templates/app-vue/assets/ui/vite.config.js +24 -24
  38. package/cli/templates/app-vue/cj-tauri.toml +33 -23
  39. package/cli/templates/app-vue/cjpm.toml +34 -34
  40. package/cli/templates/app-vue/src/main.cj +91 -91
  41. package/cli/templates/ohos-skeleton/AppScope/app.json5 +13 -13
  42. package/cli/templates/ohos-skeleton/AppScope/resources/base/element/string.json +8 -8
  43. package/cli/templates/ohos-skeleton/AppScope/resources/base/media/layered_image.json +6 -6
  44. package/cli/templates/ohos-skeleton/README.md +84 -79
  45. package/cli/templates/ohos-skeleton/build-hap.sh +15 -0
  46. package/cli/templates/ohos-skeleton/build-profile.json5 +46 -46
  47. package/cli/templates/ohos-skeleton/entry/build-profile.json5 +26 -26
  48. package/cli/templates/ohos-skeleton/entry/cjpm.toml +51 -43
  49. package/cli/templates/ohos-skeleton/entry/hvigorfile.ts +6 -6
  50. package/cli/templates/ohos-skeleton/entry/oh-package.json5 +9 -9
  51. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/ability_stage.cj +17 -17
  52. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/app_host.cj +94 -94
  53. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/dialog_host.cj +217 -0
  54. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/index.cj +527 -167
  55. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/main_ability.cj +139 -139
  56. package/cli/templates/ohos-skeleton/entry/src/main/module.json5 +57 -49
  57. package/cli/templates/ohos-skeleton/entry/src/main/resources/base/element/color.json +8 -8
  58. package/cli/templates/ohos-skeleton/entry/src/main/resources/base/element/string.json +16 -16
  59. package/cli/templates/ohos-skeleton/entry/src/main/resources/base/media/layered_image.json +6 -6
  60. package/cli/templates/ohos-skeleton/entry/src/main/resources/base/profile/main_pages.json +2 -2
  61. package/cli/templates/ohos-skeleton/entry/src/main/resources/rawfile/capabilities.json +6 -6
  62. package/cli/templates/ohos-skeleton/entry/src/main/resources/rawfile/index.html +92 -92
  63. package/cli/templates/ohos-skeleton/hvigor/hvigor-config.json5 +5 -5
  64. package/cli/templates/ohos-skeleton/hvigorfile.ts +6 -6
  65. package/cli/templates/ohos-skeleton/oh-package.json5 +10 -10
  66. package/native/build_win.bat +62 -62
  67. package/native/core/bridge_core.c +1681 -1626
  68. package/native/core/bridge_core.h +675 -618
  69. package/native/core/bridge_js.h +108 -108
  70. package/native/platforms/harmonyos/README.md +115 -6
  71. package/native/platforms/linux/bridge_linux.c +2787 -2485
  72. package/native/platforms/windows/bridge_win.c +3745 -3248
  73. package/native/tests/gtk_threading_probe.c +143 -143
  74. package/native/tests/test_bridge_core.c +1339 -1228
  75. package/package.json +53 -53
  76. package/src/api_system.cj +67 -67
  77. package/src/app.cj +335 -969
  78. package/src/app_events.cj +166 -0
  79. package/src/app_host_ops.cj +287 -0
  80. package/src/app_run.cj +283 -0
  81. package/src/app_spec.cj +76 -76
  82. package/src/arkweb_menu.cj +216 -0
  83. package/src/arkweb_script.cj +142 -142
  84. package/src/capability.cj +409 -409
  85. package/src/clipboard_bridge.cj +178 -0
  86. package/src/dialog_spec.cj +79 -0
  87. package/src/dragdrop.cj +26 -26
  88. package/src/embedded_assets.cj +98 -98
  89. package/src/host.cj +752 -607
  90. package/src/host_arkweb.cj +924 -778
  91. package/src/host_webkit.cj +663 -570
  92. package/src/host_webview2.cj +663 -570
  93. package/src/hotkey.cj +66 -66
  94. package/src/ipc_hub.cj +401 -337
  95. package/src/ipc_message.cj +286 -286
  96. package/src/launch.cj +62 -62
  97. package/src/logging.cj +127 -127
  98. package/src/menu.cj +197 -197
  99. package/src/platform/platform_app_context.cj +77 -77
  100. package/src/platform/platform_assets.cj +33 -33
  101. package/src/platform/platform_discover.cj +176 -176
  102. package/src/platform/platform_env.cj +29 -29
  103. package/src/platform/platform_feature.cj +31 -31
  104. package/src/platform/platform_page_source.cj +87 -87
  105. package/src/platform/platform_target_info.cj +32 -32
  106. package/src/platform_host_provider.cj +87 -87
  107. package/src/plugin.cj +333 -333
  108. package/src/plugins/clipboard/plugin_clipboard.cj +152 -152
  109. package/src/plugins/dialog/plugin_dialog.cj +209 -209
  110. package/src/plugins/fs/plugin_fs.cj +105 -1
  111. package/src/plugins/http/http_backend.cj +131 -0
  112. package/src/plugins/http/http_desktop.cj +195 -0
  113. package/src/plugins/http/http_ohos.cj +204 -0
  114. package/src/plugins/http/plugin_http.cj +310 -459
  115. package/src/plugins/log/plugin_log.cj +105 -105
  116. package/src/plugins/menu/plugin_menu.cj +158 -158
  117. package/src/plugins/notification/plugin_notification.cj +154 -154
  118. package/src/plugins/serial/plugin_serial.cj +458 -454
  119. package/src/plugins/serial/serial_bridge.cj +194 -194
  120. package/src/plugins/serial/serial_manager_ohos.cj +92 -0
  121. package/src/plugins/serial/serial_ohos.cj +529 -0
  122. package/src/plugins/shell/plugin_shell.cj +258 -258
  123. package/src/plugins/sqlite/plugin_sqlite.cj +705 -710
  124. package/src/plugins/sqlite/sqlite_ffi.cj +206 -211
  125. package/src/plugins/ws/plugin_ws.cj +488 -470
  126. package/src/process_hidden.cj +174 -174
  127. package/src/scope.cj +391 -391
  128. package/src/shell_event.cj +69 -69
  129. package/src/tests/app_spec_test.cj +130 -127
  130. package/src/tests/capability_test.cj +243 -243
  131. package/src/tests/dialog_spec_test.cj +73 -0
  132. package/src/tests/fake_host.cj +201 -201
  133. package/src/tests/ipc_hub_test.cj +563 -487
  134. package/src/tests/logging_test.cj +62 -62
  135. package/src/tests/platform_contract_test.cj +264 -251
  136. package/src/tests/plugin_clipboard_test.cj +222 -222
  137. package/src/tests/plugin_dialog_test.cj +237 -237
  138. package/src/tests/plugin_fs_test.cj +207 -0
  139. package/src/tests/plugin_http_test.cj +124 -124
  140. package/src/tests/plugin_notification_test.cj +205 -205
  141. package/src/tests/plugin_serial_test.cj +48 -0
  142. package/src/tests/plugin_test.cj +605 -585
  143. package/src/tests/plugin_ws_test.cj +40 -40
  144. package/src/tests/process_hidden_test.cj +129 -129
  145. package/src/tests/scope_test.cj +350 -350
  146. package/src/tests/system_host_test.cj +99 -81
  147. package/src/tests/urlutil_test.cj +53 -53
  148. package/src/tests/version_test.cj +75 -75
  149. package/src/tests/window_registry_test.cj +123 -123
  150. package/src/tray.cj +90 -90
  151. package/src/urlutil.cj +159 -159
  152. package/src/version.cj +44 -44
  153. package/src/window.cj +51 -51
  154. package/src/window_registry.cj +116 -116
  155. package/prebuilt/win32-x64/cj-tauri.exe +0 -0
  156. package/src/plugins/serial/serial_ohos_stub.cj +0 -61
  157. package/src/plugins/sqlite/sqlite_ohos_stub.cj +0 -91
@@ -1,286 +1,286 @@
1
- package cjTauri
2
-
3
- import stdx.encoding.json.*
4
-
5
- /**
6
- * 窗口 label 的缺省值(对外契约,不能改)
7
- *
8
- * 与桥 JS 的 `windowLabel` 缺省、`emitToWindow(..., "main")` 是同一个约定:老前端与现有示例
9
- * 都写死 `"main"`,默认值一错,事件就静默收不到、回执就静默投不回原窗口(见架构文档 §6.4)。
10
- */
11
- public let DEFAULT_WINDOW_LABEL = "main"
12
-
13
- /**
14
- * IPC 消息模型(对标 Tauri IPC 协议)
15
- *
16
- * JS → 仓颉 (invoke):
17
- * {"type":"invoke","id":1,"cmd":"greet","args":{...},"window":"main"}
18
- * JS → 仓颉 (emit):
19
- * {"type":"emit","id":2,"event":"tick","payload":{...},"window":"main"}
20
- * `window` 是发起窗口的 label(批次 2 起由桥 JS 带上)。**缺省 / 非字符串 / 空串一律按 "main" 处理**:
21
- * 上行方向没有「广播」语义——一条回执必须能回到唯一一个窗口,否则前端那条 promise 永远 pending。
22
- * 仓颉 → JS (resolve/reject):
23
- * {"type":"resolve","id":1,"ok":true,"data":...}
24
- * {"type":"resolve","id":1,"ok":false,"error":"..."}
25
- * 仓颉 → JS (event):
26
- * {"type":"event","event":"tick","payload":{...},"window":"main"}
27
- * window 字段缺省 = 广播(所有窗口);定向推送时给接收窗口的 label,
28
- * 页面里由桥按自己的 label 过滤(多窗口落地前只有一个 "main")。
29
- * `emitLatest` 发出的报文多一个 "coalesce":true —— 页面侧同一帧内同名事件只投最后一次
30
- * payload(中间值丢弃),tick = 一帧。
31
- */
32
-
33
- public class InvokeRequest {
34
- public let id: Int64
35
- public let cmd: String
36
- public let args: JsonObject
37
- /** 发起窗口的 label(批次 2 起随报文来);回执按它回投给同一窗口 */
38
- public let window: String
39
-
40
- public init(id: Int64, cmd: String, args: JsonObject, window!: String = DEFAULT_WINDOW_LABEL) {
41
- this.id = id
42
- this.cmd = cmd
43
- this.args = args
44
- this.window = window
45
- }
46
-
47
- /**
48
- * 从 JS 侧 postMessage 的原始 JSON 解析 invoke 请求。
49
- * 解析失败返回 None(由调用方判定为非法消息)。
50
- */
51
- public static func parse(raw: String): Option<InvokeRequest> {
52
- try {
53
- let envOpt = parseEnvelope(raw)
54
- if (envOpt.isNone()) {
55
- return None
56
- }
57
- let env = envOpt.getOrThrow()
58
- if (env.msgType != "invoke") {
59
- return None
60
- }
61
- let obj = env.obj
62
- let idV = obj.get("id")
63
- let cmdV = obj.get("cmd")
64
- if (idV.isNone() || cmdV.isNone()) {
65
- return None
66
- }
67
- let id = idV.getOrThrow().asInt().getValue()
68
- let cmd = cmdV.getOrThrow().asString().getValue()
69
- var args = JsonObject()
70
- if (let Some(a) <- obj.get("args")) {
71
- match (a.kind()) {
72
- case JsonKind.JsObject => args = a.asObject()
73
- case _ => ()
74
- }
75
- }
76
- return Some(InvokeRequest(id, cmd, args, window: parseWindowLabel(obj)))
77
- } catch (e: Exception) {
78
- return None
79
- }
80
- }
81
- }
82
-
83
- /**
84
- * 前端 emit 事件(对标 Tauri v2 前端 `emit()` → `plugin:event|emit`)
85
- *
86
- * 语义与 Tauri 的**有意差异**(选了更保守的一条,理由与后续演进口径见 docs/IPC-通信机制.md §7):
87
- * 事件只投给**后端监听器**(`IpcHub.listenEvent` 注册的),不回投给任何页面。
88
- * Tauri 的 `emit` 是全局广播(含发起者自己),在单窗口下退化成「自己发自己收」,
89
- * 且多窗口寻址(报文里的 window 字段)还没落地——等窗口有了再把投递侧接到窗口注册表上,
90
- * 前端与报文格式都不用改。
91
- *
92
- * 带 id 是有意的:事件未授权 / 报文非法都要能把失败**回给前端**(reject 同一个 id),
93
- * 而不是只在 stderr 里悄悄丢(对照 Tauri 的 `emit()` 返回 Promise<void>)。
94
- */
95
- public class EmitRequest {
96
- public let id: Int64
97
- public let event: String
98
- public let payload: JsonValue
99
- /** 发起窗口的 label(批次 2 起随报文来);resolve 回执按它回投给同一窗口 */
100
- public let window: String
101
-
102
- public init(id: Int64, event: String, payload: JsonValue, window!: String = DEFAULT_WINDOW_LABEL) {
103
- this.id = id
104
- this.event = event
105
- this.payload = payload
106
- this.window = window
107
- }
108
-
109
- /**
110
- * 解析前端 emit 报文;payload 缺省为空对象,解析失败返回 None。
111
- */
112
- public static func parse(raw: String): Option<EmitRequest> {
113
- try {
114
- let envOpt = parseEnvelope(raw)
115
- if (envOpt.isNone()) {
116
- return None
117
- }
118
- let env = envOpt.getOrThrow()
119
- if (env.msgType != "emit") {
120
- return None
121
- }
122
- let obj = env.obj
123
- let idV = obj.get("id")
124
- let eventV = obj.get("event")
125
- if (idV.isNone() || eventV.isNone()) {
126
- return None
127
- }
128
- let id = idV.getOrThrow().asInt().getValue()
129
- let event = eventV.getOrThrow().asString().getValue()
130
- var payload: JsonValue = JsonObject()
131
- if (let Some(p) <- obj.get("payload")) {
132
- payload = p
133
- }
134
- return Some(EmitRequest(id, event, payload, window: parseWindowLabel(obj)))
135
- } catch (e: Exception) {
136
- return None
137
- }
138
- }
139
- }
140
-
141
- /**
142
- * 报文信封:顶层 JSON 对象 + 字符串 type 字段。
143
- * invoke / emit 两条解析路径共用——两份重复的预检迟早漂移,而「type 必须是字符串」这类
144
- * 判定正是非法报文与正常报文的分界。
145
- */
146
- class IpcEnvelope {
147
- let obj: JsonObject
148
- let msgType: String
149
-
150
- init(obj: JsonObject, msgType: String) {
151
- this.obj = obj
152
- this.msgType = msgType
153
- }
154
- }
155
-
156
- /**
157
- * 从报文里取发起窗口的 label:缺失 / 非字符串 / 空串一律回落到 `DEFAULT_WINDOW_LABEL`
158
- *
159
- * 为什么空串也回落:下行方向的空串是「广播」(谁在听谁收),上行方向没有这个语义——
160
- * 一条回执必须回到唯一一个窗口,否则前端那条 promise 永远 pending。
161
- */
162
- func parseWindowLabel(obj: JsonObject): String {
163
- if (let Some(w) <- obj.get("window")) {
164
- match (w.kind()) {
165
- case JsonKind.JsString =>
166
- let s = w.asString().getValue()
167
- if (s.size > 0) {
168
- return s
169
- }
170
- case _ => ()
171
- }
172
- }
173
- return DEFAULT_WINDOW_LABEL
174
- }
175
-
176
- /** 解析报文信封;非 JSON / 顶层不是对象 / type 缺失或不是字符串,一律 None */
177
- func parseEnvelope(raw: String): Option<IpcEnvelope> {
178
- try {
179
- let root = JsonValue.fromStr(raw)
180
- match (root.kind()) {
181
- case JsonKind.JsObject => ()
182
- case _ => return None
183
- }
184
- let obj = root.asObject()
185
- if (let Some(t) <- obj.get("type")) {
186
- match (t.kind()) {
187
- case JsonKind.JsString => return Some(IpcEnvelope(obj, t.asString().getValue()))
188
- case _ => return None
189
- }
190
- }
191
- return None
192
- } catch (e: Exception) {
193
- return None
194
- }
195
- }
196
-
197
- public class IpcMessage {
198
- /**
199
- * 构造 resolve 响应 JSON(ok=true)
200
- */
201
- public static func resolve(id: Int64, data: JsonValue): String {
202
- var obj = JsonObject()
203
- obj.put("type", JsonString("resolve"))
204
- obj.put("id", JsonInt(id))
205
- obj.put("ok", JsonBool(true))
206
- obj.put("data", data)
207
- return obj.toJsonString()
208
- }
209
-
210
- /**
211
- * 构造 reject 响应 JSON(ok=false)
212
- */
213
- public static func reject(id: Int64, error: String): String {
214
- var obj = JsonObject()
215
- obj.put("type", JsonString("resolve"))
216
- obj.put("id", JsonInt(id))
217
- obj.put("ok", JsonBool(false))
218
- obj.put("error", JsonString(error))
219
- return obj.toJsonString()
220
- }
221
-
222
- /**
223
- * 构造事件推送 JSON
224
- *
225
- * @param window 接收窗口的 label;空串 = 广播(报文里不带 window 字段,桥放行)。
226
- * 多窗口落地前只会是 "main",但字段现在就留在协议里,免得那次改造再动报文。
227
- */
228
- public static func event(event: String, payload: JsonValue, window: String): String {
229
- return eventJson(event, payload, window, false).toJsonString()
230
- }
231
-
232
- /**
233
- * 构造「只保最新」的事件推送 JSON(`IpcHub.emitLatest` 用)
234
- *
235
- * 报文只比 `event` 多一个 `coalesce: true`:页面侧桥把它落进「本帧槽位」,
236
- * 同一帧内同名事件只投最后一次 payload,中间值丢弃。要不要这种语义由**调用方显式选**——
237
- * 默认路径不做合并(静默丢数据比慢更难排查,Tauri v2 核心同样不合并)。
238
- * tick = 一帧,理由与对照见 docs/IPC-通信机制.md §6 第 5 项。
239
- */
240
- public static func eventLatest(event: String, payload: JsonValue, window: String): String {
241
- return eventJson(event, payload, window, true).toJsonString()
242
- }
243
-
244
- /** 事件报文的公共构造:`coalesce` 只在需要时落字段,旧报文形态逐字节不变 */
245
- private static func eventJson(event: String, payload: JsonValue, window: String, coalesce: Bool): JsonObject {
246
- var obj = JsonObject()
247
- obj.put("type", JsonString("event"))
248
- obj.put("protocol", JsonString(CJ_IPC_PROTOCOL))
249
- obj.put("event", JsonString(event))
250
- obj.put("payload", payload)
251
- if (coalesce) {
252
- obj.put("coalesce", JsonBool(true))
253
- }
254
- if (window.size > 0) {
255
- obj.put("window", JsonString(window))
256
- }
257
- return obj
258
- }
259
-
260
- /**
261
- * 尽力从报文里取出 id(只认顶层 int 字段),取下取不到返回 None。
262
- *
263
- * 用途是非法报文的回包:前端 `invoke` / `emit` 都带 id,只要 id 可读就应该回一条 reject,
264
- * 否则前端那条 promise 会永久 pending(比报错更难排查)。取不到 id 的(例如裸字符串、
265
- * 没有 id 的 JSON)没法对应到任何 promise,只能打 stderr。
266
- */
267
- public static func peekId(raw: String): Option<Int64> {
268
- try {
269
- let root = JsonValue.fromStr(raw)
270
- match (root.kind()) {
271
- case JsonKind.JsObject => ()
272
- case _ => return None
273
- }
274
- let idV = root.asObject().get("id")
275
- if (idV.isNone()) {
276
- return None
277
- }
278
- match (idV.getOrThrow().kind()) {
279
- case JsonKind.JsInt => return Some(idV.getOrThrow().asInt().getValue())
280
- case _ => return None
281
- }
282
- } catch (e: Exception) {
283
- return None
284
- }
285
- }
286
- }
1
+ package cjTauri
2
+
3
+ import stdx.encoding.json.*
4
+
5
+ /**
6
+ * 窗口 label 的缺省值(对外契约,不能改)
7
+ *
8
+ * 与桥 JS 的 `windowLabel` 缺省、`emitToWindow(..., "main")` 是同一个约定:老前端与现有示例
9
+ * 都写死 `"main"`,默认值一错,事件就静默收不到、回执就静默投不回原窗口(见架构文档 §6.4)。
10
+ */
11
+ public let DEFAULT_WINDOW_LABEL = "main"
12
+
13
+ /**
14
+ * IPC 消息模型(对标 Tauri IPC 协议)
15
+ *
16
+ * JS → 仓颉 (invoke):
17
+ * {"type":"invoke","id":1,"cmd":"greet","args":{...},"window":"main"}
18
+ * JS → 仓颉 (emit):
19
+ * {"type":"emit","id":2,"event":"tick","payload":{...},"window":"main"}
20
+ * `window` 是发起窗口的 label(批次 2 起由桥 JS 带上)。**缺省 / 非字符串 / 空串一律按 "main" 处理**:
21
+ * 上行方向没有「广播」语义——一条回执必须能回到唯一一个窗口,否则前端那条 promise 永远 pending。
22
+ * 仓颉 → JS (resolve/reject):
23
+ * {"type":"resolve","id":1,"ok":true,"data":...}
24
+ * {"type":"resolve","id":1,"ok":false,"error":"..."}
25
+ * 仓颉 → JS (event):
26
+ * {"type":"event","event":"tick","payload":{...},"window":"main"}
27
+ * window 字段缺省 = 广播(所有窗口);定向推送时给接收窗口的 label,
28
+ * 页面里由桥按自己的 label 过滤(多窗口落地前只有一个 "main")。
29
+ * `emitLatest` 发出的报文多一个 "coalesce":true —— 页面侧同一帧内同名事件只投最后一次
30
+ * payload(中间值丢弃),tick = 一帧。
31
+ */
32
+
33
+ public class InvokeRequest {
34
+ public let id: Int64
35
+ public let cmd: String
36
+ public let args: JsonObject
37
+ /** 发起窗口的 label(批次 2 起随报文来);回执按它回投给同一窗口 */
38
+ public let window: String
39
+
40
+ public init(id: Int64, cmd: String, args: JsonObject, window!: String = DEFAULT_WINDOW_LABEL) {
41
+ this.id = id
42
+ this.cmd = cmd
43
+ this.args = args
44
+ this.window = window
45
+ }
46
+
47
+ /**
48
+ * 从 JS 侧 postMessage 的原始 JSON 解析 invoke 请求。
49
+ * 解析失败返回 None(由调用方判定为非法消息)。
50
+ */
51
+ public static func parse(raw: String): Option<InvokeRequest> {
52
+ try {
53
+ let envOpt = parseEnvelope(raw)
54
+ if (envOpt.isNone()) {
55
+ return None
56
+ }
57
+ let env = envOpt.getOrThrow()
58
+ if (env.msgType != "invoke") {
59
+ return None
60
+ }
61
+ let obj = env.obj
62
+ let idV = obj.get("id")
63
+ let cmdV = obj.get("cmd")
64
+ if (idV.isNone() || cmdV.isNone()) {
65
+ return None
66
+ }
67
+ let id = idV.getOrThrow().asInt().getValue()
68
+ let cmd = cmdV.getOrThrow().asString().getValue()
69
+ var args = JsonObject()
70
+ if (let Some(a) <- obj.get("args")) {
71
+ match (a.kind()) {
72
+ case JsonKind.JsObject => args = a.asObject()
73
+ case _ => ()
74
+ }
75
+ }
76
+ return Some(InvokeRequest(id, cmd, args, window: parseWindowLabel(obj)))
77
+ } catch (e: Exception) {
78
+ return None
79
+ }
80
+ }
81
+ }
82
+
83
+ /**
84
+ * 前端 emit 事件(对标 Tauri v2 前端 `emit()` → `plugin:event|emit`)
85
+ *
86
+ * 语义与 Tauri 的**有意差异**(选了更保守的一条,理由与后续演进口径见 docs/IPC-通信机制.md §7):
87
+ * 事件只投给**后端监听器**(`IpcHub.listenEvent` 注册的),不回投给任何页面。
88
+ * Tauri 的 `emit` 是全局广播(含发起者自己),在单窗口下退化成「自己发自己收」,
89
+ * 且多窗口寻址(报文里的 window 字段)还没落地——等窗口有了再把投递侧接到窗口注册表上,
90
+ * 前端与报文格式都不用改。
91
+ *
92
+ * 带 id 是有意的:事件未授权 / 报文非法都要能把失败**回给前端**(reject 同一个 id),
93
+ * 而不是只在 stderr 里悄悄丢(对照 Tauri 的 `emit()` 返回 Promise<void>)。
94
+ */
95
+ public class EmitRequest {
96
+ public let id: Int64
97
+ public let event: String
98
+ public let payload: JsonValue
99
+ /** 发起窗口的 label(批次 2 起随报文来);resolve 回执按它回投给同一窗口 */
100
+ public let window: String
101
+
102
+ public init(id: Int64, event: String, payload: JsonValue, window!: String = DEFAULT_WINDOW_LABEL) {
103
+ this.id = id
104
+ this.event = event
105
+ this.payload = payload
106
+ this.window = window
107
+ }
108
+
109
+ /**
110
+ * 解析前端 emit 报文;payload 缺省为空对象,解析失败返回 None。
111
+ */
112
+ public static func parse(raw: String): Option<EmitRequest> {
113
+ try {
114
+ let envOpt = parseEnvelope(raw)
115
+ if (envOpt.isNone()) {
116
+ return None
117
+ }
118
+ let env = envOpt.getOrThrow()
119
+ if (env.msgType != "emit") {
120
+ return None
121
+ }
122
+ let obj = env.obj
123
+ let idV = obj.get("id")
124
+ let eventV = obj.get("event")
125
+ if (idV.isNone() || eventV.isNone()) {
126
+ return None
127
+ }
128
+ let id = idV.getOrThrow().asInt().getValue()
129
+ let event = eventV.getOrThrow().asString().getValue()
130
+ var payload: JsonValue = JsonObject()
131
+ if (let Some(p) <- obj.get("payload")) {
132
+ payload = p
133
+ }
134
+ return Some(EmitRequest(id, event, payload, window: parseWindowLabel(obj)))
135
+ } catch (e: Exception) {
136
+ return None
137
+ }
138
+ }
139
+ }
140
+
141
+ /**
142
+ * 报文信封:顶层 JSON 对象 + 字符串 type 字段。
143
+ * invoke / emit 两条解析路径共用——两份重复的预检迟早漂移,而「type 必须是字符串」这类
144
+ * 判定正是非法报文与正常报文的分界。
145
+ */
146
+ class IpcEnvelope {
147
+ let obj: JsonObject
148
+ let msgType: String
149
+
150
+ init(obj: JsonObject, msgType: String) {
151
+ this.obj = obj
152
+ this.msgType = msgType
153
+ }
154
+ }
155
+
156
+ /**
157
+ * 从报文里取发起窗口的 label:缺失 / 非字符串 / 空串一律回落到 `DEFAULT_WINDOW_LABEL`
158
+ *
159
+ * 为什么空串也回落:下行方向的空串是「广播」(谁在听谁收),上行方向没有这个语义——
160
+ * 一条回执必须回到唯一一个窗口,否则前端那条 promise 永远 pending。
161
+ */
162
+ func parseWindowLabel(obj: JsonObject): String {
163
+ if (let Some(w) <- obj.get("window")) {
164
+ match (w.kind()) {
165
+ case JsonKind.JsString =>
166
+ let s = w.asString().getValue()
167
+ if (s.size > 0) {
168
+ return s
169
+ }
170
+ case _ => ()
171
+ }
172
+ }
173
+ return DEFAULT_WINDOW_LABEL
174
+ }
175
+
176
+ /** 解析报文信封;非 JSON / 顶层不是对象 / type 缺失或不是字符串,一律 None */
177
+ func parseEnvelope(raw: String): Option<IpcEnvelope> {
178
+ try {
179
+ let root = JsonValue.fromStr(raw)
180
+ match (root.kind()) {
181
+ case JsonKind.JsObject => ()
182
+ case _ => return None
183
+ }
184
+ let obj = root.asObject()
185
+ if (let Some(t) <- obj.get("type")) {
186
+ match (t.kind()) {
187
+ case JsonKind.JsString => return Some(IpcEnvelope(obj, t.asString().getValue()))
188
+ case _ => return None
189
+ }
190
+ }
191
+ return None
192
+ } catch (e: Exception) {
193
+ return None
194
+ }
195
+ }
196
+
197
+ public class IpcMessage {
198
+ /**
199
+ * 构造 resolve 响应 JSON(ok=true)
200
+ */
201
+ public static func resolve(id: Int64, data: JsonValue): String {
202
+ var obj = JsonObject()
203
+ obj.put("type", JsonString("resolve"))
204
+ obj.put("id", JsonInt(id))
205
+ obj.put("ok", JsonBool(true))
206
+ obj.put("data", data)
207
+ return obj.toJsonString()
208
+ }
209
+
210
+ /**
211
+ * 构造 reject 响应 JSON(ok=false)
212
+ */
213
+ public static func reject(id: Int64, error: String): String {
214
+ var obj = JsonObject()
215
+ obj.put("type", JsonString("resolve"))
216
+ obj.put("id", JsonInt(id))
217
+ obj.put("ok", JsonBool(false))
218
+ obj.put("error", JsonString(error))
219
+ return obj.toJsonString()
220
+ }
221
+
222
+ /**
223
+ * 构造事件推送 JSON
224
+ *
225
+ * @param window 接收窗口的 label;空串 = 广播(报文里不带 window 字段,桥放行)。
226
+ * 多窗口落地前只会是 "main",但字段现在就留在协议里,免得那次改造再动报文。
227
+ */
228
+ public static func event(event: String, payload: JsonValue, window: String): String {
229
+ return eventJson(event, payload, window, false).toJsonString()
230
+ }
231
+
232
+ /**
233
+ * 构造「只保最新」的事件推送 JSON(`IpcHub.emitLatest` 用)
234
+ *
235
+ * 报文只比 `event` 多一个 `coalesce: true`:页面侧桥把它落进「本帧槽位」,
236
+ * 同一帧内同名事件只投最后一次 payload,中间值丢弃。要不要这种语义由**调用方显式选**——
237
+ * 默认路径不做合并(静默丢数据比慢更难排查,Tauri v2 核心同样不合并)。
238
+ * tick = 一帧,理由与对照见 docs/IPC-通信机制.md §6 第 5 项。
239
+ */
240
+ public static func eventLatest(event: String, payload: JsonValue, window: String): String {
241
+ return eventJson(event, payload, window, true).toJsonString()
242
+ }
243
+
244
+ /** 事件报文的公共构造:`coalesce` 只在需要时落字段,旧报文形态逐字节不变 */
245
+ private static func eventJson(event: String, payload: JsonValue, window: String, coalesce: Bool): JsonObject {
246
+ var obj = JsonObject()
247
+ obj.put("type", JsonString("event"))
248
+ obj.put("protocol", JsonString(CJ_IPC_PROTOCOL))
249
+ obj.put("event", JsonString(event))
250
+ obj.put("payload", payload)
251
+ if (coalesce) {
252
+ obj.put("coalesce", JsonBool(true))
253
+ }
254
+ if (window.size > 0) {
255
+ obj.put("window", JsonString(window))
256
+ }
257
+ return obj
258
+ }
259
+
260
+ /**
261
+ * 尽力从报文里取出 id(只认顶层 int 字段),取下取不到返回 None。
262
+ *
263
+ * 用途是非法报文的回包:前端 `invoke` / `emit` 都带 id,只要 id 可读就应该回一条 reject,
264
+ * 否则前端那条 promise 会永久 pending(比报错更难排查)。取不到 id 的(例如裸字符串、
265
+ * 没有 id 的 JSON)没法对应到任何 promise,只能打 stderr。
266
+ */
267
+ public static func peekId(raw: String): Option<Int64> {
268
+ try {
269
+ let root = JsonValue.fromStr(raw)
270
+ match (root.kind()) {
271
+ case JsonKind.JsObject => ()
272
+ case _ => return None
273
+ }
274
+ let idV = root.asObject().get("id")
275
+ if (idV.isNone()) {
276
+ return None
277
+ }
278
+ match (idV.getOrThrow().kind()) {
279
+ case JsonKind.JsInt => return Some(idV.getOrThrow().asInt().getValue())
280
+ case _ => return None
281
+ }
282
+ } catch (e: Exception) {
283
+ return None
284
+ }
285
+ }
286
+ }