cj-tauri 0.8.0 → 1.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 (124) hide show
  1. package/README.md +3 -3
  2. package/cjpm.toml +52 -25
  3. package/cli/cjpm.toml +1 -1
  4. package/cli/src/doctor.cj +304 -0
  5. package/cli/src/main.cj +33 -7
  6. package/cli/src/project.cj +59 -1
  7. package/cli/src/resolve.cj +348 -304
  8. package/cli/src/scaffold.cj +139 -22
  9. package/cli/templates/ohos-pc/AppScope/app.json5 +13 -0
  10. package/cli/templates/ohos-pc/AppScope/resources/base/element/string.json +8 -0
  11. package/cli/templates/ohos-pc/AppScope/resources/base/media/background.png +0 -0
  12. package/cli/templates/ohos-pc/AppScope/resources/base/media/foreground.png +0 -0
  13. package/cli/templates/ohos-pc/AppScope/resources/base/media/layered_image.json +6 -0
  14. package/cli/templates/ohos-pc/README.md +79 -0
  15. package/cli/templates/ohos-pc/build-hap.sh +190 -0
  16. package/cli/templates/ohos-pc/build-profile.json5 +46 -0
  17. package/cli/templates/ohos-pc/entry/build-profile.json5 +26 -0
  18. package/cli/templates/ohos-pc/entry/cjpm.toml +43 -0
  19. package/cli/templates/ohos-pc/entry/hvigorfile.ts +6 -0
  20. package/cli/templates/ohos-pc/entry/oh-package.json5 +9 -0
  21. package/cli/templates/ohos-pc/entry/src/main/cangjie/ability_stage.cj +17 -0
  22. package/cli/templates/ohos-pc/entry/src/main/cangjie/app_host.cj +94 -0
  23. package/cli/templates/ohos-pc/entry/src/main/cangjie/index.cj +167 -0
  24. package/cli/templates/ohos-pc/entry/src/main/cangjie/main_ability.cj +139 -0
  25. package/cli/templates/ohos-pc/entry/src/main/module.json5 +49 -0
  26. package/cli/templates/ohos-pc/entry/src/main/resources/base/element/color.json +8 -0
  27. package/cli/templates/ohos-pc/entry/src/main/resources/base/element/string.json +16 -0
  28. package/cli/templates/ohos-pc/entry/src/main/resources/base/media/background.png +0 -0
  29. package/cli/templates/ohos-pc/entry/src/main/resources/base/media/foreground.png +0 -0
  30. package/cli/templates/ohos-pc/entry/src/main/resources/base/media/layered_image.json +6 -0
  31. package/cli/templates/ohos-pc/entry/src/main/resources/base/media/startIcon.png +0 -0
  32. package/cli/templates/ohos-pc/entry/src/main/resources/base/profile/main_pages.json +2 -0
  33. package/cli/templates/ohos-pc/entry/src/main/resources/rawfile/capabilities.json +6 -0
  34. package/cli/templates/ohos-pc/entry/src/main/resources/rawfile/index.html +92 -0
  35. package/cli/templates/ohos-pc/hvigor/hvigor-config.json5 +5 -0
  36. package/cli/templates/ohos-pc/hvigorfile.ts +6 -0
  37. package/cli/templates/ohos-pc/oh-package.json5 +10 -0
  38. package/native/build_linux.sh +30 -2
  39. package/native/build_win.bat +62 -61
  40. package/native/core/bridge_core.c +1626 -0
  41. package/native/{bridge_core.h → core/bridge_core.h} +246 -6
  42. package/native/platforms/harmonyos/README.md +106 -0
  43. package/native/platforms/linux/bridge_linux.c +2485 -0
  44. package/native/platforms/windows/bridge_win.c +3248 -0
  45. package/native/tests/test_bridge_core.c +528 -1
  46. package/package.json +12 -3
  47. package/prebuilt/win32-x64/cj-tauri.exe +0 -0
  48. package/src/api_system.cj +10 -2
  49. package/src/app.cj +248 -85
  50. package/src/arkweb_proxy.cj +56 -0
  51. package/src/arkweb_script.cj +154 -0
  52. package/src/capability.cj +140 -0
  53. package/src/clipboard_bridge.cj +55 -0
  54. package/src/dragdrop.cj +26 -0
  55. package/src/embedded_assets.cj +98 -0
  56. package/src/hexutil.cj +53 -0
  57. package/src/host.cj +197 -26
  58. package/src/host_arkweb.cj +740 -0
  59. package/src/host_factory.cj +48 -0
  60. package/src/host_webkit.cj +202 -26
  61. package/src/host_webview2.cj +176 -3
  62. package/src/hotkey.cj +66 -0
  63. package/src/ipc_hub.cj +49 -0
  64. package/src/ipc_message.cj +2 -1
  65. package/src/jsutil.cj +33 -0
  66. package/src/log_bridge.cj +107 -0
  67. package/src/logging.cj +127 -0
  68. package/src/menu.cj +197 -196
  69. package/src/plugin.cj +40 -0
  70. package/src/plugins/clipboard/plugin_clipboard.cj +144 -0
  71. package/src/{plugin_dialog.cj → plugins/dialog/plugin_dialog.cj} +74 -17
  72. package/src/plugins/fs/plugin_fs.cj +194 -0
  73. package/src/plugins/http/plugin_http.cj +459 -0
  74. package/src/plugins/log/plugin_log.cj +105 -0
  75. package/src/{plugin_menu.cj → plugins/menu/plugin_menu.cj} +4 -2
  76. package/src/plugins/mqtt/mqtt_codec.cj +661 -0
  77. package/src/plugins/mqtt/plugin_mqtt.cj +1446 -0
  78. package/src/plugins/notification/plugin_notification.cj +146 -0
  79. package/src/plugins/plugins.cj +5 -0
  80. package/src/plugins/redis/plugin_redis.cj +2409 -0
  81. package/src/plugins/redis/redis_codec.cj +556 -0
  82. package/src/{plugin_serial.cj → plugins/serial/plugin_serial.cj} +442 -489
  83. package/src/{serial_bridge.cj → plugins/serial/serial_bridge.cj} +185 -184
  84. package/src/{plugin_shell.cj → plugins/shell/plugin_shell.cj} +230 -228
  85. package/src/plugins/sqlite/plugin_sqlite.cj +697 -0
  86. package/src/plugins/sqlite/sqlite_ffi.cj +206 -0
  87. package/src/plugins/store/plugin_store.cj +287 -0
  88. package/src/plugins/ws/plugin_ws.cj +470 -0
  89. package/src/process_hidden.cj +174 -0
  90. package/src/scope.cj +391 -0
  91. package/src/shell_event.cj +69 -68
  92. package/src/tests/capability_test.cj +53 -0
  93. package/src/tests/fake_host.cj +85 -20
  94. package/src/tests/ipc_hub_test.cj +13 -0
  95. package/src/tests/jsutil_test.cj +62 -0
  96. package/src/tests/logging_test.cj +62 -0
  97. package/src/tests/mqtt_codec_test.cj +405 -0
  98. package/src/tests/plugin_clipboard_test.cj +222 -0
  99. package/src/tests/plugin_dialog_test.cj +98 -4
  100. package/src/tests/plugin_http_test.cj +124 -0
  101. package/src/tests/plugin_menu_test.cj +1 -0
  102. package/src/tests/plugin_notification_test.cj +205 -0
  103. package/src/tests/plugin_serial_test.cj +269 -267
  104. package/src/tests/plugin_shell_test.cj +189 -188
  105. package/src/tests/plugin_sqlite_test.cj +230 -0
  106. package/src/tests/plugin_store_test.cj +260 -0
  107. package/src/tests/plugin_test.cj +542 -524
  108. package/src/tests/plugin_ws_test.cj +40 -0
  109. package/src/tests/process_hidden_test.cj +129 -0
  110. package/src/tests/redis_codec_test.cj +396 -0
  111. package/src/tests/scope_test.cj +350 -0
  112. package/src/tests/shell_event_test.cj +0 -1
  113. package/src/tests/system_host_test.cj +81 -79
  114. package/src/tests/urlutil_test.cj +53 -0
  115. package/src/tests/version_test.cj +75 -65
  116. package/src/tray.cj +90 -0
  117. package/src/urlutil.cj +159 -0
  118. package/src/version.cj +23 -2
  119. package/src/window.cj +51 -42
  120. package/native/bridge_core.c +0 -797
  121. package/native/bridge_linux.c +0 -1268
  122. package/native/bridge_win.c +0 -1633
  123. package/src/plugin_fs.cj +0 -118
  124. /package/native/{bridge_js.h → core/bridge_js.h} +0 -0
@@ -79,6 +79,10 @@ typedef void (*cj_on_shell_fn)(struct cj_host *host, const char *json);
79
79
  #define CJ_CAP_MENU 1
80
80
  #define CJ_CAP_TRAY 2
81
81
  #define CJ_CAP_DRAG_DROP 4
82
+ #define CJ_CAP_NOTIFICATION 8 /* 平台通知可用(Linux = freedesktop Notifications)*/
83
+ #define CJ_CAP_HOTKEY 16 /* 全局快捷键可用(Linux = X11 grab;Wayland 不置位)*/
84
+ #define CJ_CAP_SUBTITLE 32 /* 窗口顶部那一行能显示副标题(Linux = 与菜单栏同排的 label;
85
+ Windows 的系统菜单没有自绘区,未实现、不置位)*/
82
86
 
83
87
  /* ===== 回投队列节点(core 拥有;UTF-8 存储,宽字符转换由平台在 cj_plat_post_js 里做)===== */
84
88
  typedef struct js_node {
@@ -88,7 +92,7 @@ typedef struct js_node {
88
92
 
89
93
  /* ===== 原生对话框请求(core 分配 / 释放;平台只填 ok 与 path)===== */
90
94
  typedef struct dlg_req {
91
- int kind; /* 0 打开文件 1 保存文件 2 info 3 warning 4 error 5 confirm */
95
+ int kind; /* 0 打开文件 1 保存文件 2 info 3 warning 4 error 5 confirm 6 选择目录 */
92
96
  char *title;
93
97
  char *message;
94
98
  char *filter; /* "描述|模式|描述|模式";空串 = 不过滤 */
@@ -119,6 +123,15 @@ typedef struct cj_host {
119
123
  int dlg_ready; /* 结果就绪(与 dlg_lock 配套的谓词)*/
120
124
  dlg_req *dlg_cur; /* 进行中的请求;窗口先关时由 cj_core_window_destroyed 作废 */
121
125
 
126
+ /* --- 剪贴板单槽(同一时刻只允许一次读写;窗口销毁时作废等待方)---
127
+ 读结果经 cb_text 交回(core malloc,调用方 cj_bridge_clipboard_free 释放),
128
+ cb_ok=0 表示失败/作废(cb_text 为 NULL)。 */
129
+ cj_sync *clip_lock;
130
+ int clip_ready;
131
+ int clip_ok;
132
+ char *clip_text; /* 读操作的结果(UI 线程 malloc,等待方负责释放)*/
133
+ int clip_busy;
134
+
122
135
  /* --- 窗口配置:由仓颉侧在 cj_bridge_start 之前经 cj_bridge_set_window 注入 --- */
123
136
  char *win_title;
124
137
  int win_w;
@@ -126,6 +139,15 @@ typedef struct cj_host {
126
139
  char *win_icon; /* NULL = 系统默认图标 */
127
140
  int devtools; /* 开发者工具开关 */
128
141
 
142
+ /* 窗口初始可见性(cj_bridge_set_window_visible 存入):1 = 显示(默认,老应用行为不变)、
143
+ 0 = 窗口建好即隐藏。**只解决「建窗那一刻该不该显示」**——运行期改可见性由平台直接投
144
+ UI 线程执行,不需要它。 */
145
+ int win_visible;
146
+
147
+ /* 最小化到托盘(cj_bridge_set_minimize_to_tray 存入):1 = 平台把「最小化」改写成「隐藏」。
148
+ 真到最小化那一刻平台还会复核托盘是否真装上了(没有就如实降级为普通最小化 + 一行 stderr)。 */
149
+ int minimize_to_tray;
150
+
129
151
  /* --- 预执行脚本(cj_bridge_add_init_script 追加,WebView 创建时逐条注入到
130
152
  document-start;插件的 jsShim 走这条通道,注册顺序排在 BRIDGE_JS 之后)--- */
131
153
  char **init_scripts;
@@ -139,6 +161,27 @@ typedef struct cj_host {
139
161
  /* --- 菜单线路文本(cj_bridge_set_menu 存入;平台建窗时读它,运行期改则由平台重建)--- */
140
162
  char *pending_menu_text;
141
163
 
164
+ /* --- 拖放策略(cj_bridge_set_drag_drop 存入:enabled/files 两个开关)。
165
+ enabled=0 平台不注册 drop target(默认关——现有应用零改动);
166
+ 平台建 view 时读它决定要不要挂 drag dest,运行期再改由平台重挂 --- */
167
+ int pending_drop_enabled;
168
+ int pending_drop_files;
169
+
170
+ /* --- 全局快捷键线路(cj_bridge_set_global_hotkeys 存入,空串 = 一个都没注册)。
171
+ 格式一行一条:`<accel>:<id>`(accel 如 Ctrl+Alt+C,id 是应用给的标识)。
172
+ 平台负责解析 accel 并 grab;触发时经 cj_core_hotkey_pressed 交回 core。
173
+ 运行期再调 = 整表替换(先 ungrab 再按新表 grab) --- */
174
+ char *pending_hotkeys;
175
+
176
+ /* --- 托盘线路(cj_bridge_set_tray 存入,空串 = 移除托盘)。
177
+ 格式(与 src/tray.cj 的 TrayModel.toWire() 逐字对应):
178
+ 第 1 行 `icon\t<路径>`(空路径 = 主题默认图标)
179
+ 第 2 行 `tooltip\t<文本>`(可空)
180
+ 其余行 = 菜单项,与菜单线路同格式(kind\tid\tlabel\tflags\taccel,无前导制表符 = 平铺一层)
181
+ 平台建宿主线程时读它装配;运行期再调 = 整体重建。菜单项被点经
182
+ cj_core_tray_clicked 交回 core({"kind":"tray","id":…},与菜单点击同走 on_shell) --- */
183
+ char *pending_tray;
184
+
142
185
  /* --- 能力位(cj_plat_init 时由 cj_plat_host_capabilities 填,此后只读)--- */
143
186
  int capabilities;
144
187
 
@@ -153,7 +196,7 @@ typedef struct cj_host {
153
196
  } cj_host;
154
197
 
155
198
  /* =========================================================================
156
- * 平台原语清单(每个平台实现一份,共 21 个)
199
+ * 平台原语清单(每个平台实现一份,共 24 个)
157
200
  * ========================================================================= */
158
201
 
159
202
  /* ---- 同步(Linux: pthread_mutex + pthread_cond / Windows: CS + CONDITION_VARIABLE)---- */
@@ -180,6 +223,12 @@ void cj_plat_wake_ui(cj_host *h);/* 让 UI 线程尽快跑一次 cj_core_flush_j
180
223
  void cj_plat_post_js(cj_host *h, const char *script_utf8); /* UI 线程执行一段 JS(含失败日志)*/
181
224
  int cj_plat_is_ui_thread(cj_host *h); /* 1 = 调用方已站在 UI 线程上 */
182
225
  void cj_plat_post_dialog(cj_host *h, dlg_req *req); /* 投到 UI 线程 → cj_core_dialog_tick */
226
+
227
+ /* 通用「投到 UI 线程执行」原语(剪贴板等后续宿主能力共用;对话框那套是它之前的专用通道):
228
+ 在 UI 线程上回调 fn(ud)(一次),投不出去返回 0(调用方自行作废等待)。
229
+ 平台实现:Linux = g_idle_add;Windows = PostMessageW 新消息,UI 线程取 lParam 调 fn。 */
230
+ typedef void (*cj_ui_fn)(void *ud);
231
+ int cj_plat_post_ui(cj_host *h, cj_ui_fn fn, void *ud);
183
232
  int cj_plat_run_dialog(cj_host *h, dlg_req *req); /* UI 线程直接弹原生对话框,写 req */
184
233
  void cj_plat_open_devtools(cj_host *h);
185
234
  void cj_plat_load_url(cj_host *h, const char *url);
@@ -190,7 +239,33 @@ void cj_plat_reload(cj_host *h);
190
239
  调用时机两头都要支持:**未就绪**时只记下(平台建窗时读 h->pending_menu_text);**已就绪**时由平台
191
240
  投到 UI 线程重建菜单。cj_plat_menu_item_state 只改运行期状态(可用 / 勾选),要快、可能频繁。 */
192
241
  void cj_plat_set_menu(cj_host *h, const char *wire);
242
+
243
+ /* 平台原语:把副标题写到窗口顶部那一行(UI 线程调用)。返回 0 = 已提交,
244
+ -3 = 宿主未就绪,-4 = 本平台没这块(Windows 的系统菜单没有自绘区)。 */
245
+ int cj_plat_set_titlebar_subtitle(cj_host *h, const char *text);
193
246
  void cj_plat_menu_item_state(cj_host *h, const char *id, int enabled, int checked);
247
+ void cj_plat_set_drag_drop(cj_host *h, int enabled, int files);
248
+
249
+ /* 全局快捷键(RFC-003 第 5 期首个落地项):wire 一行一条 `<accel>:<id>`(见 pending_hotkeys 注释)。
250
+ 调用时机两头都要支持:未就绪时只记下(平台建宿主线程后读 h->pending_hotkeys),
251
+ 已就绪时整表替换(先 ungrab 旧表再 grab 新表)。平台不支持(Wayland / Windows 桩)时
252
+ 打一行 stderr 如实说明,不算错误。 */
253
+ void cj_plat_set_global_hotkeys(cj_host *h, const char *wire);
254
+
255
+ /* 托盘(RFC-002 §7.C):wire 见 pending_tray 注释,空串 = 移除托盘。
256
+ 未就绪时只记下(宿主线程装配时读 h->pending_tray);已就绪时投 UI 线程重建。
257
+ 平台不支持 / 依赖没编进来时打一行 stderr 如实说明(可选编译,RFC-002 §6.3)。 */
258
+ void cj_plat_set_tray(cj_host *h, const char *wire);
259
+
260
+ /* 窗口可见性(RFC-003 第 5 期余项:最小化到托盘的还原路径)。
261
+ 未就绪时平台**什么都不做**——core 已把值写进 h->win_visible,平台建窗时读它定稿;
262
+ 已就绪时投 UI 线程执行(Linux = gtk_widget_show/hide + gtk_window_present,Windows = ShowWindow)。 */
263
+ void cj_plat_set_window_visible(cj_host *h, int visible);
264
+
265
+ /* 最小化到托盘策略(须在建窗前调):平台把窗口过程 / 窗口状态信号里的「最小化」改写成「隐藏」。
266
+ 平台在**真要最小化那一刻**复核托盘是否真的装上了(能力位 / 应用有没有 setTray):
267
+ 没装就降级为普通最小化并打一行 stderr——窗口藏起来却没有还原入口是比「不支持」更糟的结果。 */
268
+ void cj_plat_set_minimize_to_tray(cj_host *h, int enabled);
194
269
 
195
270
  /* 宿主能力位(CJ_CAP_* 的按位或):纯静态事实,cj_plat_init 里由平台给出。 */
196
271
  int cj_plat_host_capabilities(cj_host *h);
@@ -221,10 +296,10 @@ long long cj_plat_serial_open(const char *path, const cj_serial_cfg *cfg, char *
221
296
  int cj_plat_serial_read(long long h, unsigned char *buf, int len, int timeout_ms, char *err, int err_len);
222
297
  int cj_plat_serial_write(long long h, const unsigned char *buf, int len, int timeout_ms,
223
298
  char *err, int err_len);
224
- int cj_plat_serial_close(long long h, char *err, int err_len);
225
-
226
- /* 枚举本机串口:一行一个名字写进 out(\n 分隔),返回条数;-1 参数非法,-4 未实现。
227
- Linux 侧尚未实现(-4)——serial:list 在 Linux 仍走仓颉侧的 /dev 目录扫描。 */
299
+ int cj_plat_serial_close(long long h, char *err, int err_len);
300
+
301
+ /* 枚举本机串口:一行一个名字写进 out(\n 分隔),返回条数;-1 参数非法,-4 未实现。
302
+ Linux 侧尚未实现(-4)——serial:list 在 Linux 仍走仓颉侧的 /dev 目录扫描。 */
228
303
  int cj_plat_serial_list(char *out, int out_len);
229
304
 
230
305
  /* 设备路径白名单(纯函数、平台无关):只放行常见串口设备节点 / 伪终端 / udev 稳定链接,
@@ -240,6 +315,8 @@ void cj_core_set_ready(cj_host *h); /* 窗口 + WebView 就绪(平台在
240
315
  void cj_core_flush_js(cj_host *h); /* UI 线程:取队列拼批回投(idle / WM_CJT_FLUSH 里调)*/
241
316
  void cj_core_dialog_tick(cj_host *h, dlg_req *req); /* UI 线程:弹对话框 → 回调 → 唤醒等待方 */
242
317
  void cj_core_dialog_abort(cj_host *h); /* 作废进行中的对话框并唤醒等待方(ok=0,幂等)*/
318
+ int cj_core_clipboard_tick(cj_host *h, int is_write, char *text); /* UI 线程:执行剪贴板调用 → 唤醒等待方(返回 1=成功)*/
319
+ void cj_core_clipboard_abort(cj_host *h); /* 作废进行中的剪贴板请求并唤醒等待方(幂等)*/
243
320
  void cj_core_window_destroyed(cj_host *h);/* 窗口销毁:置标志 + 通知 onDestroy(一次)+ 作废对话框 */
244
321
 
245
322
  /* ---- 菜单线路格式的共享解析(两平台翻译层共用,避免各写一遍前缀/分割逻辑)----
@@ -260,6 +337,24 @@ void cj_core_menu_walk(const char *wire, cj_menu_row_fn fn, void *ctx);
260
337
  /* 平台把「菜单项被点了」交回 core:core 记日志 + 经 h->on_menu 回调仓颉侧(带宿主身份)。 */
261
338
  void cj_core_menu_clicked(cj_host *h, const char *id, int enabled, int checked);
262
339
 
340
+ /* 全局快捷键触发(平台在 grab 的事件回调里调):id 是应用注册时给的标识。
341
+ core 记日志 + 以 shell 报文 {"kind":"hotkey","id":"…"} 经 on_shell 回调仓颉侧(首参宿主句柄,
342
+ 仓颉侧按句柄路由——快捷键事件与菜单点击同走 on_shell 通道,不新增回调类型)。 */
343
+ void cj_core_hotkey_pressed(cj_host *h, const char *id);
344
+
345
+ /* 托盘菜单项被点(平台在 appindicator 的 activate 回调里调):id 是 TrayModel 里应用给的标识。
346
+ core 记日志 + 以 shell 报文 {"kind":"tray","id":"…"} 经 on_shell 回调仓颉侧(首参宿主句柄,
347
+ 按句柄路由;前端事件名 tray:click 由仓颉侧 shellEventNameFor 映射——RFC-002 §5.5 既有约定)。 */
348
+ void cj_core_tray_clicked(cj_host *h, const char *id);
349
+
350
+ /* 拖放落地(平台在 drag-data-received 里调):uri_list 是 text/uri-list 原文
351
+ (\r\n 或 \n 分隔,可含 file:// URI 与百分比编码)。
352
+ core 负责解析:跳过注释行/空行、file:// 解码成路径(%XX 十六进制解码)、
353
+ 非 file:// 的 URI 原样保留(v1 载荷只有文件,页面自己决定要不要)。
354
+ 结果以 shell 报文 {"kind":"drop","paths":[…]} 经 on_shell 回投(首参宿主句柄,
355
+ 仓颉侧按句柄路由)。paths 上限 32 个、单个路径上限 4096 字节(超限丢弃并记日志)。 */
356
+ void cj_core_drop_files(cj_host *h, const char *uri_list);
357
+
263
358
  /* =========================================================================
264
359
  * 仓颉 → C 桥:导出(全部实现在 bridge_core.c,两平台同一份)
265
360
  * ========================================================================= */
@@ -328,6 +423,33 @@ CJ_BRIDGE_API void cj_bridge_set_menu(cj_host *h, const char *wire);
328
423
  /* 运行期改单个菜单项的状态(按 id 找;找不到只记日志不报错)。 */
329
424
  CJ_BRIDGE_API void cj_bridge_set_menu_item_state(cj_host *h, const char *id, int enabled, int checked);
330
425
 
426
+ /* 拖放策略(RFC-002 §5):enabled=0 平台不注册 drop target(默认关)。
427
+ 两头都能用:未就绪时记 pending(建 view 时读),就绪后平台投 UI 线程重挂 drag dest。 */
428
+ CJ_BRIDGE_API void cj_bridge_set_drag_drop(cj_host *h, int enabled, int files);
429
+
430
+ /* 窗口顶部副标题:把一行 UTF-8 文本画在原生菜单栏**同一行的右侧**(应用据此省掉
431
+ 自己的页面顶栏,标题 / 保存状态 / 字数都能挂在原生那一行上)。允许运行期高频更新,
432
+ 实现必须自己投递到 UI 线程(Linux 走 g_idle_add,同 set_menu)。
433
+ 返回 0 = 已受理,-1 = 参数非法(含超过 1024 字节),-3 = 宿主未就绪,
434
+ -4 = 本平台未实现(应用据 CJ_CAP_SUBTITLE 决定要不要自己画顶栏)。 */
435
+ CJ_BRIDGE_API int cj_bridge_set_titlebar_subtitle(cj_host *h, const char *text);
436
+
437
+ /* 全局快捷键:wire 一行一条 `<accel>:<id>`(空串 = 全部注销)。
438
+ 两头都能用:未就绪时记 pending(宿主线程起来后读),就绪后整表替换(ungrab 旧表再 grab 新表)。 */
439
+ CJ_BRIDGE_API void cj_bridge_set_global_hotkeys(cj_host *h, const char *wire);
440
+
441
+ /* 托盘(RFC-002 §7.C):wire 见 pending_tray 注释(icon/tooltip 两行 + 菜单项),空串 = 移除。
442
+ 两头都能用:未就绪时记 pending(宿主线程装配时读),就绪后平台投 UI 线程重建。 */
443
+ CJ_BRIDGE_API void cj_bridge_set_tray(cj_host *h, const char *wire);
444
+
445
+ /* 窗口可见性(对标 Tauri 的 window.show()/hide()):visible=1 显示并提到前台、0 隐藏。
446
+ 两头都能用:未 start 时只记下(平台建窗时按 h->win_visible 定稿),start 之后投 UI 线程执行。 */
447
+ CJ_BRIDGE_API void cj_bridge_set_window_visible(cj_host *h, int visible);
448
+
449
+ /* 最小化到托盘策略(须在 cj_bridge_start 之前调):1 = 平台把「最小化」改写成「隐藏」。
450
+ 平台在真要最小化那一刻复核托盘是否装上,没装则降级为普通最小化 + 一行 stderr。 */
451
+ CJ_BRIDGE_API void cj_bridge_set_minimize_to_tray(cj_host *h, int enabled);
452
+
331
453
  /* shell 事件回调(菜单点击等;载荷见 cj_on_shell_fn 的注释,首参为宿主句柄)。 */
332
454
  CJ_BRIDGE_API void cj_bridge_set_shell_callback(cj_host *h, cj_on_shell_fn cb);
333
455
 
@@ -375,4 +497,122 @@ CJ_BRIDGE_API int cj_bridge_serial_list(char *out, int out_len);
375
497
  每次串口导出调用都会先清空它,失败时写入;返回的指针在该线程下次调用前有效。 */
376
498
  CJ_BRIDGE_API const char *cj_bridge_serial_last_error(void);
377
499
 
500
+ /* =========================================================================
501
+ * 剪贴板(plugin_clipboard):宿主能力——GTK 的 clipboard 依赖 gtk_init 后的环境,
502
+ * 且 GTK 规定剪贴板调用必须在 GTK 线程上,所以**带 cj_host**(借它的 UI 线程分流),
503
+ * 与对话框同一套「已在 UI 线程就直接做、否则投递 + 有界等待」的样板。
504
+ * 返回码口径:0 = 成功,-1 = 参数非法,-3 = 宿主未就绪 / 平台失败,-4 = 本平台未实现。
505
+ * ========================================================================= */
506
+
507
+ /* 写文本:text 须为合法 UTF-8,长度上限 8 MB(core 校验)。0 = 成功。 */
508
+ CJ_BRIDGE_API int cj_bridge_clipboard_write_text(cj_host *h, const char *text);
509
+
510
+ /* 读文本:返回 malloc 的 UTF-8 字符串(可能为 NULL = 剪贴板为空 / 不是文本)。
511
+ 用 cj_bridge_clipboard_free 释放,**不许**在仓颉侧 free。 */
512
+ CJ_BRIDGE_API char *cj_bridge_clipboard_read_text(cj_host *h);
513
+
514
+ /* 释放 read_text 返回的缓冲(NULL 安全,幂等)。 */
515
+ CJ_BRIDGE_API void cj_bridge_clipboard_free(char *s);
516
+
517
+ /* 平台原语:真正的剪贴板读写。err 口径同其余原语(-4 未实现由平台自己报)。 */
518
+ int cj_plat_clipboard_write_text(cj_host *h, const char *text, char *err, int err_len);
519
+ char *cj_plat_clipboard_read_text(cj_host *h, char *err, int err_len);
520
+
521
+ /* =========================================================================
522
+ * 系统通知(plugin_notification):与剪贴板同一类宿主能力,但**发后即忘**——
523
+ * 通知提交后显示与否不回传(freedesktop 协议本就不给同步回执),core 校验 + 投
524
+ * UI 线程即返回,成败只在 stderr 留行。返回码:0 = 已提交,-1 = 参数非法,
525
+ * -3 = 宿主未就绪 / 平台失败,-4 = 本平台未实现。
526
+ * ========================================================================= */
527
+
528
+ /* 发一条通知:title 非空且 ≤256 字节,body ≤8 KiB(core 校验)。0 = 已提交。 */
529
+ CJ_BRIDGE_API int cj_bridge_notification_notify(cj_host *h, const char *title, const char *body);
530
+
531
+ /* 平台原语:真正发通知(UI 线程调用)。 */
532
+ int cj_plat_notification_notify(cj_host *h, const char *title, const char *body,
533
+ char *err, int err_len);
534
+
535
+ /* =========================================================================
536
+ * 日志落盘(RFC-003 第 5 期):把 stderr 接管到文件,给「没有控制台」的产物留凭证
537
+ *
538
+ * 为什么在 fd 层接管:单文件产物是 Windows GUI 子系统,双击时 stderr 无处可去;
539
+ * 而诊断输出分布在 core、平台实现与仓颉侧上百处。解 `freopen` 一次,**所有** stderr
540
+ * 写者自动落进同一个 FILE*(顺序天然一致),两端调用点一处都不用改。stdout 一律不碰
541
+ * (`CJ_TAURI_MANIFEST` 的插件清单走 stdout)。
542
+ * ========================================================================= */
543
+
544
+ /* 开启日志落盘:path 非空且可写时返回 1,否则返回 0 并在**还在的** stderr 上说明原因。
545
+ max_kb ≤ 0 表示不轮转(交给外部 logrotate);keep = 保留几份档案(log.1 … log.keep),
546
+ ≤0 表示轮转时直接丢弃旧内容。**只能在第一条日志之前调**——已开启后重复调用返回 0
547
+ (stderr 已经被接管,换不了目标;想换就重启应用换参数)。 */
548
+ CJ_BRIDGE_API int cj_bridge_set_log_file(const char *path, int max_kb, int keep);
549
+
550
+ /* 回读生效中的日志路径;未开启返回空串。供 system:host 如实上报。 */
551
+ CJ_BRIDGE_API const char *cj_bridge_log_file(void);
552
+
553
+ /* 追加一行调用方提供的日志(同样走轮转;用于 log 插件把前端报错并进同一份文件)。
554
+ 返回 0 = 已写入(或未开启但已落 stderr),-1 = 参数非法。 */
555
+ CJ_BRIDGE_API int cj_bridge_log_write(const char *line);
556
+
557
+ /* core 内的诊断输出统一走它:**未开启落盘时逐字节等同 `fprintf(stderr, …)`**
558
+ (现有 E2E needle 与 `[cj-bridge]` 前缀断言因此一条都不受影响);开启后才加
559
+ `[YYYY-MM-DD HH:MM:SS] ` 前缀并按大小轮转。 */
560
+ void cj_core_logf(const char *fmt, ...)
561
+ #if defined(__GNUC__)
562
+ __attribute__((format(printf, 1, 2)))
563
+ #endif
564
+ ;
565
+ #define cj_logf(...) cj_core_logf(__VA_ARGS__)
566
+
567
+ /* =========================================================================
568
+ * 静默子进程:后台启动控制台程序,与宿主 / 窗口无关(不带 cj_host)
569
+ *
570
+ * 为什么必须下沉到 C 桥:cj-tauri 应用都是 GUI 子系统(Windows 的 `--subsystem=windows`),
571
+ * 父进程**没有控制台**;此时拉起一个控制台子系统的子进程,Windows 会给它**单独配一个控制台
572
+ * 窗口**——用户看到的就是一个黑窗口。仓颉的 `std.process.launch` 没有创建标志可传,
573
+ * 所以这条只能在桥里做:Windows 走 `CreateProcessW` + `DETACHED_PROCESS`(连隐藏 console 都不建),
574
+ * Linux 走 `fork` + `execv` + `setsid`(脱开终端,Ctrl-C 不连带打死)。
575
+ *
576
+ * 三个导出两平台同名同签名(AGENTS §2):上层因此没有平台分叉的调用点。
577
+ * ========================================================================= */
578
+
579
+ /* 静默启动:exe 是程序路径(UTF-8),args 是参数串(UTF-8,**以 0x1F 分隔**,空串 = 无参数)。
580
+ 引号 / 空格由平台按各自规则转义,调用方**不要**自己加引号。
581
+ 返回 pid(> 0);失败返回 0,原因由平台写进 stderr(core 会再补一行带 exe 的上下文)。 */
582
+ CJ_BRIDGE_API long long cj_bridge_spawn_hidden(const char *exe, const char *args);
583
+
584
+ /* 终止该 pid(幂等:进程已经不在了也返回 1)。返回 1 = 已结束,0 = 无权 / 失败。 */
585
+ CJ_BRIDGE_API int cj_bridge_kill_pid(long long pid);
586
+
587
+ /* 该 pid 是否还活着:1 = 活着,0 = 已退出或查不到(**含没权限查**——按「不在」上报,
588
+ 比谎报活着好:调用方据它决定要不要重新拉起)。 */
589
+ CJ_BRIDGE_API int cj_bridge_pid_alive(long long pid);
590
+
591
+ /* 把产物内嵌的资源释放到 dir(单文件包用:scripts/pack-win-single.sh 把随包的程序与依赖 DLL
592
+ 作为 RT_RCDATA 资源嵌进 exe,资源名就是文件名)。
593
+ 返回值:> 0 = 已就绪的文件数(本次释放的 + 本来就在的),0 = **产物里没有内嵌资源**
594
+ (普通构建、非单文件包都是这个),-1 = 参数为空或写盘失败(原因在 stderr)。
595
+ 幂等:目标文件已存在且大小与资源一致就跳过——每次启动调它不会重写一份几十 MB 的资源。
596
+ 目录由调用方给(桥不认识应用名):典型是 %LOCALAPPDATA%\应用名\runtime。 */
597
+ CJ_BRIDGE_API int cj_bridge_extract_assets(const char *dir);
598
+
599
+ /* 按**绝对路径**把一个动态库加载进本进程(单文件包专用:随包释放出来的 OpenSSL 这类
600
+ 「运行期按名字 LoadLibrary」的组件,不预加载就永远找不到)。
601
+ 为什么需要它(2026-10-21 实测,另一台机器):仓颉运行时的 TLS 加载器按**裸名**找
602
+ `libssl-3-x64.dll` / `libcrypto-3-x64.dll`,SDK 与 stdx 都不带;开发机上它恰好命中 PATH 里的
603
+ Git 自带那份,于是「本机全通、别人双击就挂」。症状极具误导性:http 照常 200(cjh 的
604
+ /api/health 通),**只有 ws 握手失败**——`ws: connect failed: Can not load openssl library or
605
+ function RAND_bytes.`(RFC6455 升级要算随机 nonce,明文 ws:// 也逃不掉)。
606
+ 做法是进程启动早期先按绝对路径 LoadLibrary/dlopen 一次:之后同类裸名加载会命中已加载模块,
607
+ 不必改系统 PATH、也不必把 DLL 摆到 exe 旁边(单文件承诺不破)。
608
+ 返回 1 = 已加载(含本来就在的),0 = 失败(原因在 stderr)。 */
609
+ CJ_BRIDGE_API int cj_bridge_preload_library(const char *path);
610
+
611
+ /* 平台原语(err 口径同其余原语:写不进就留空,core 会打「平台没有给出原因」)。 */
612
+ long long cj_plat_spawn_hidden(const char *exe, const char *args, char *err, int err_len);
613
+ int cj_plat_kill_pid(long long pid);
614
+ int cj_plat_pid_alive(long long pid);
615
+ int cj_plat_extract_assets(const char *dir, char *err, int err_len);
616
+ int cj_plat_preload_library(const char *path, char *err, int err_len);
617
+
378
618
  #endif /* CJ_BRIDGE_CORE_H */
@@ -0,0 +1,106 @@
1
+ # HarmonyOS ArkWeb 平台适配
2
+
3
+ 状态(2026-10-19):**宿主已在仓颉侧落地并在真机跑通完整闭环;C 桥未落地,宿主能力位仍全 0**。
4
+
5
+ 判据分三层,别混为一谈:
6
+
7
+ - **宿主基元已落地并真机验证**——建窗标签、页面装载(内联 HTML / URL)、document-start 桥脚本注入、
8
+ 报文接送(`post`)、结果回投(`runJavaScript` 推送)、页面事件回投、退出标志。
9
+ 真机闭环的判据是**页面自己发起**的一次往返:`system:version` 回到页面拿到
10
+ `{"name":"cj-tauri","version":"1.0.0","protocol":"1","cangjie":"1.0.5","platform":"ohos"}`,
11
+ 应用自注册命令(`probe:log`)同链路返回 `"logged"`,同轮 hilog `SIGSEGV` 计数 0。
12
+ - **C 桥(`cj_core_*`)/ NAPI 层未落地**——鸿蒙侧不走 C 桥:宿主直接由仓颉侧 `src/host_arkweb.cj`
13
+ 实现,`@When[env == "ohos"]` 一平台一份。按 AGENTS §2,新增宿主能力仍是「先扩 `WebViewHost` 接口、
14
+ 再改各平台实现」,但不要求鸿蒙侧补同名 `cj_bridge_*` 导出(无 C 桥平台:核心 FFI 一律
15
+ `@When` 门禁 + 如实降级,见 §4)。
16
+ - **宿主能力位全 0**——菜单、托盘、全局快捷键、拖放、原生对话框、剪贴板、通知、标题栏副标题
17
+ 一律如实拒绝(`hostCapabilities()` 返回 0)。按 AGENTS §1 门禁 4:**没有实机证据的能力位
18
+ 不许上报**,所以它们不会因为「代码里写了」就变成 1。
19
+
20
+ ⚠️ **两条与桌面平台不同的硬约束**(真机踩过,改动前务必看):
21
+
22
+ 1. **`@Component` / `Web` / `javaScriptProxy` 属应用侧**,框架包不得引用 ArkUI——
23
+ 框架包作为独立编译单元引用 `kit.ArkUI` 会让鸿蒙包初始化链跳坏地址、启动即 SIGSEGV。
24
+ 框架只导出 ArkUI-free 的桥接函数(`src/arkweb_proxy.cj`)。
25
+ 2. **`javaScriptProxy` 的返回值传不回页面**(实测恒 `null`)。回投必须**由原生推**:
26
+ 页面发 `{"type":"pull"}` 心跳,应用侧 proxy 回调(UI 线程)取 `takePendingScripts()`
27
+ 用 `controller.runJavaScript` 推送。详见 `docs/鸿蒙ArkWeb接入踩坑.md` §8.1。
28
+
29
+ ## 形态:宿主在仓颉侧,不是 ArkTS + NAPI
30
+
31
+ 本目录最初的设计假设是「ArkTS 托管 Web 组件 + NAPI 模块回投」,那条路**没有采用**。
32
+ 实测结论:仓颉的 ArkUI 绑定(`kit.ArkUI` / `ohos.arkui.component.common`)能直接声明
33
+ `Web(src: controller:)` 组件、拿到 `WebviewController`,因此整个宿主可以写在仓颉里,
34
+ 与桌面两平台共用同一套 `WebViewHost` 接口(`src/host.cj`)与同一层 IPC / 能力校验。
35
+
36
+ 所以 `native/platforms/harmonyos/` **暂时没有 C 实现,也不需要**:
37
+
38
+ - framework 核心包在鸿蒙侧已无任何未解析的 `cj_bridge_*` 符号——所有核心 FFI 都带了
39
+ `@When[env != "ohos"]` 门禁并在鸿蒙侧如实降级(`src/log_bridge.cj` 的日志落盘、
40
+ `src/clipboard_bridge.cj` 的剪贴板/通知)。判据是一条命令:
41
+ `llvm-nm -D --undefined-only <模块.so> | grep cj_bridge` 必须 **0 行**。
42
+ - 剩下的插件级 FFI(`src/plugins/serial/serial_bridge.cj`、`src/plugins/sqlite/sqlite_ffi.cj`)
43
+ 属于各自插件包,应用不引用就不链接;真要在鸿蒙用它们时,那是**插件**的事,不是本平台的桥。
44
+
45
+ 什么时候才真的需要这座桥:某个能力**在仓颉 ArkUI 绑定里没有对应 API**、只能靠 NAPI 才能做到。
46
+ 到那时按 AGENTS §2 补——导出名与桌面同名同签名(`cj_bridge_*`)、回调首参必须带
47
+ `struct cj_host *`、不要造新的静态单槽。
48
+
49
+ ## 实现分布
50
+
51
+ | 文件 | 职责 |
52
+ | --- | --- |
53
+ | `src/host_arkweb.cj` | `ArkWebHost`(实现 `WebViewHost` 全部 31 个成员)+ `ArkWebRuntime`(运行时登记、沙箱目录、退出标志) |
54
+ | `src/arkweb_proxy.cj` | ArkUI-free 的两个代理函数(`arkWebProxyPost` / `arkWebProxyPoll`),由**应用侧** `@Component` 塞进 `javaScriptProxy` |
55
+ | `src/arkweb_script.cj` | 前端桥门面:`ARKWEB_BRIDGE_JS` + label 注入;回投靠页面 `setInterval` 发 `{"type":"pull"}` 心跳、原生推回去(**不是**返回值通道) |
56
+ | ~~`src/arkweb_view.cj`~~ | **已移出框架**:框架包一旦 `import kit.ArkUI.*`,鸿蒙上启动即 SIGSEGV(判据见踩坑文档 §9.2)。`@Component` / `Web` 只能由应用模块持有 |
57
+ | `src/host.cj` | 平台分叉点:`@When[env == "ohos"]` 下 `createHost` 取应用注入的宿主工厂 |
58
+
59
+ ## 平台判据:只能用 `env`,不能用 `os` / `arch`
60
+
61
+ - 鸿蒙目标的 `os` **报 `Linux`**(实测),与桌面 Linux 撞车,分不开「GTK 宿主」和「ArkWeb 宿主」。
62
+ - `arch` 也不行:桌面 aarch64 Linux 同样是 `aarch64`,会把有 C 桥的桌面平台一起门禁掉
63
+ (日志落盘能力会凭空消失)。
64
+ - 可靠判据是 `env`:`aarch64-linux-ohos` → `ohos`,桌面 → `gnu`。所以框架里所有鸿蒙门禁
65
+ 一律写 `@When[env == "ohos"]` / `@When[env != "ohos"]`。
66
+ - `system:version` 的 `platform` 字段在鸿蒙上如实返回 `ohos`(`src/version.cj`)。
67
+
68
+ ## 与桌面宿主的行为差异(已知且有意)
69
+
70
+ 1. **窗口配置不生效**:窗口归 Ability 的 `WindowStage`,`setWindowConfig` 只留一行 stderr。
71
+ 能力位里没有窗口控制位。
72
+ 2. **内联 HTML 落沙箱文件**:ArkWeb 的 `WebviewController` **没有 `loadData`**,所以
73
+ `run(html)` 先把页面写进应用沙箱再以 `file://` 装载;沙箱目录由
74
+ `ArkWebRuntime.configure(filesDir)` 提供,未配置时**明确拒绝**而不是白屏。
75
+ 3. **没有 UI 定时器**:ArkUI 绑定不暴露 UI 线程定时器,也没有 `UIContext`。页面到原生的
76
+ 回投改成「页面侧 `setInterval` 调同步 `javaScriptProxy` 的 `poll`」,页面求值在页面
77
+ 自己的线程上执行——避免把宿主 UI 线程当消息泵。
78
+ 4. **`quit()` 只置标志**:窗口归 Ability,退出得应用自己 `terminateSelf()`;
79
+ 框架不假装关掉了窗口。
80
+ 5. **宿主句柄没有跨平台相等语义**:`hostHandleEquals` 在鸿蒙侧返回 false 并留一行 stderr。
81
+ 多窗口路由在鸿蒙上靠 window label,不靠句柄——**绝不伪造相等**(伪造会让两个窗口串台)。
82
+
83
+ ## 验收出口(未完成的都在这里)
84
+
85
+ 真机(平板 / 2-in-1;手机需单独声明)上要拿到的证据,按 AGENTS §1:
86
+
87
+ - [x] HAP 能构建(`entry-default-signed.hap`)
88
+ - [x] 应用启动到 ArkWeb 页面,hilog 里没有 panic / fatal(同轮 `SIGSEGV` 计数 0)
89
+ - [x] 前端 `invoke('system:version')` 往返成功,返回 `platform=ohos`
90
+ - [x] 多窗口下宿主事件按 label 路由、不串台
91
+ - [x] `llvm-nm -D --undefined-only <模块.so> | grep cj_bridge` 为 0 行
92
+
93
+ 前四项与最后一项是 2026-10-19 两轮真机闭环的取证结果(过程见
94
+ `docs/鸿蒙ArkWeb接入踩坑.md` §8.1 / §8.2)。多窗口那条的判据(同一台真机、同一份 HAP):
95
+
96
+ - 两个 `@Component` 实例各带自己的 label(`main` / `second`),上层宿主登记里出现两行
97
+ `host pageUrl label=main url="…/cj-tauri-main.html"` 与 `…cj-tauri-second.html`
98
+ ——**页面文件按 label 分开**,两窗看的不是同一份 HTML;
99
+ - 两窗各自 `page begin label=main` / `label=second`,两窗各完成一次 `probe:hello`;
100
+ - 跨窗**定向**投递双向成立:`probe:hello window=main -> probe:ping target=second` 之后是
101
+ `probe:log window=second <- {"kind":"ping-recv","me":"second","from":"main"}`,反向同理;
102
+ 每条报文里 `window=`(原生侧归属)与 `me=`(页面自认的 label)**逐条恒等**——这就是「不串台」;
103
+ - 同轮 `SIGSEGV` / `panic` / `fatal` 计数 0,`command not allowed|registered|rejected` 计数 0。
104
+
105
+ 宿主能力位(菜单 / 托盘 / 快捷键 / 拖放 / 对话框 / 剪贴板 / 通知)**仍一项都没有证据**,
106
+ 所以 `hostCapabilities()` 保持返回 0——别因为「代码里写了降级」就把它读成验收过了。