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.
- package/README.md +206 -139
- package/bin/cj-tauri.js +287 -287
- package/cjpm.toml +63 -52
- package/cli/cjpm.toml +22 -22
- package/cli/src/doctor.cj +308 -306
- package/cli/src/main.cj +129 -129
- package/cli/src/manifest.cj +643 -558
- package/cli/src/manifest_test.cj +87 -0
- package/cli/src/project.cj +770 -770
- package/cli/src/resolve.cj +348 -348
- package/cli/src/scaffold.cj +369 -369
- package/cli/src/sync.cj +638 -526
- package/cli/src/util.cj +168 -168
- package/cli/templates/app/README.md +51 -48
- package/cli/templates/app/assets/capabilities/default.json +7 -7
- package/cli/templates/app/assets/ui/index.html +120 -118
- package/cli/templates/app/cj-tauri.toml +32 -22
- package/cli/templates/app/cjpm.toml +34 -34
- package/cli/templates/app/src/main.cj +101 -90
- package/cli/templates/app-react/README.md +56 -56
- package/cli/templates/app-react/assets/capabilities/default.json +7 -7
- package/cli/templates/app-react/assets/ui/index.html +25 -25
- package/cli/templates/app-react/assets/ui/package.json +19 -19
- package/cli/templates/app-react/assets/ui/src/App.css +67 -67
- package/cli/templates/app-react/assets/ui/src/App.jsx +124 -124
- package/cli/templates/app-react/assets/ui/src/main.jsx +6 -6
- package/cli/templates/app-react/assets/ui/vite.config.js +24 -24
- package/cli/templates/app-react/cj-tauri.toml +33 -23
- package/cli/templates/app-react/cjpm.toml +34 -34
- package/cli/templates/app-react/src/main.cj +91 -91
- package/cli/templates/app-vue/README.md +56 -56
- package/cli/templates/app-vue/assets/capabilities/default.json +7 -7
- package/cli/templates/app-vue/assets/ui/index.html +25 -25
- package/cli/templates/app-vue/assets/ui/package.json +18 -18
- package/cli/templates/app-vue/assets/ui/src/App.vue +176 -176
- package/cli/templates/app-vue/assets/ui/src/main.js +4 -4
- package/cli/templates/app-vue/assets/ui/vite.config.js +24 -24
- package/cli/templates/app-vue/cj-tauri.toml +33 -23
- package/cli/templates/app-vue/cjpm.toml +34 -34
- package/cli/templates/app-vue/src/main.cj +91 -91
- package/cli/templates/ohos-skeleton/AppScope/app.json5 +13 -13
- package/cli/templates/ohos-skeleton/AppScope/resources/base/element/string.json +8 -8
- package/cli/templates/ohos-skeleton/AppScope/resources/base/media/layered_image.json +6 -6
- package/cli/templates/ohos-skeleton/README.md +84 -79
- package/cli/templates/ohos-skeleton/build-hap.sh +15 -0
- package/cli/templates/ohos-skeleton/build-profile.json5 +46 -46
- package/cli/templates/ohos-skeleton/entry/build-profile.json5 +26 -26
- package/cli/templates/ohos-skeleton/entry/cjpm.toml +51 -43
- package/cli/templates/ohos-skeleton/entry/hvigorfile.ts +6 -6
- package/cli/templates/ohos-skeleton/entry/oh-package.json5 +9 -9
- package/cli/templates/ohos-skeleton/entry/src/main/cangjie/ability_stage.cj +17 -17
- package/cli/templates/ohos-skeleton/entry/src/main/cangjie/app_host.cj +94 -94
- 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 -167
- package/cli/templates/ohos-skeleton/entry/src/main/cangjie/main_ability.cj +139 -139
- package/cli/templates/ohos-skeleton/entry/src/main/module.json5 +57 -49
- package/cli/templates/ohos-skeleton/entry/src/main/resources/base/element/color.json +8 -8
- package/cli/templates/ohos-skeleton/entry/src/main/resources/base/element/string.json +16 -16
- package/cli/templates/ohos-skeleton/entry/src/main/resources/base/media/layered_image.json +6 -6
- package/cli/templates/ohos-skeleton/entry/src/main/resources/base/profile/main_pages.json +2 -2
- package/cli/templates/ohos-skeleton/entry/src/main/resources/rawfile/capabilities.json +6 -6
- package/cli/templates/ohos-skeleton/entry/src/main/resources/rawfile/index.html +92 -92
- package/cli/templates/ohos-skeleton/hvigor/hvigor-config.json5 +5 -5
- package/cli/templates/ohos-skeleton/hvigorfile.ts +6 -6
- package/cli/templates/ohos-skeleton/oh-package.json5 +10 -10
- 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 +3745 -3248
- package/native/tests/gtk_threading_probe.c +143 -143
- package/native/tests/test_bridge_core.c +1339 -1228
- package/package.json +53 -53
- package/src/api_system.cj +67 -67
- package/src/app.cj +335 -969
- 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 -76
- package/src/arkweb_menu.cj +216 -0
- package/src/arkweb_script.cj +142 -142
- 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 -607
- package/src/host_arkweb.cj +924 -778
- package/src/host_webkit.cj +663 -570
- package/src/host_webview2.cj +663 -570
- package/src/hotkey.cj +66 -66
- package/src/ipc_hub.cj +401 -337
- package/src/ipc_message.cj +286 -286
- package/src/launch.cj +62 -62
- package/src/logging.cj +127 -127
- package/src/menu.cj +197 -197
- package/src/platform/platform_app_context.cj +77 -77
- package/src/platform/platform_assets.cj +33 -33
- package/src/platform/platform_discover.cj +176 -176
- package/src/platform/platform_env.cj +29 -29
- package/src/platform/platform_feature.cj +31 -31
- package/src/platform/platform_page_source.cj +87 -87
- package/src/platform/platform_target_info.cj +32 -32
- package/src/platform_host_provider.cj +87 -87
- package/src/plugin.cj +333 -333
- package/src/plugins/clipboard/plugin_clipboard.cj +152 -152
- 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 +158 -158
- package/src/plugins/notification/plugin_notification.cj +154 -154
- package/src/plugins/serial/plugin_serial.cj +458 -454
- package/src/plugins/serial/serial_bridge.cj +194 -194
- 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 +258 -258
- package/src/plugins/sqlite/plugin_sqlite.cj +705 -710
- package/src/plugins/sqlite/sqlite_ffi.cj +206 -211
- 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 +130 -127
- package/src/tests/capability_test.cj +243 -243
- package/src/tests/dialog_spec_test.cj +73 -0
- package/src/tests/fake_host.cj +201 -201
- package/src/tests/ipc_hub_test.cj +563 -487
- package/src/tests/logging_test.cj +62 -62
- package/src/tests/platform_contract_test.cj +264 -251
- 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 +605 -585
- 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/prebuilt/win32-x64/cj-tauri.exe +0 -0
- package/src/plugins/serial/serial_ohos_stub.cj +0 -61
- package/src/plugins/sqlite/sqlite_ohos_stub.cj +0 -91
package/src/ipc_message.cj
CHANGED
|
@@ -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
|
+
}
|