Threading contract
Which API may be called from which thread, and what the host guarantees.
| API | Allowed thread | Host guarantee |
|---|---|---|
initialize / shutdown / action_* | Any (often a worker thread) | The main thread is not assumed; initialize of an external plugin runs on a background thread |
Hotkey APIs such as register_plugin_hotkey | Any | Switches to the main thread internally before RegisterHotKey |
feed_direct_caption / feed_caption | Any | Queue / signal into the UI |
translate / translate_batch / speak / stop_speak | Any | Switches to a worker thread internally |
emit_event / handle_host_event | Any | Thread-safe |
subscribe_event / unsubscribe_* | Any | Locked internally |
show_toast / show_error / set_window_excluded_from_capture | Any | Signal into the UI |
Overlay APIs such as create_plugin_window | Any | Switches to the GUI thread internally and waits for the result |
pick_file / pick_files / pick_folder / pick_save_file | Any | Switches to the GUI thread internally, and blocks until the user closes the dialog |
The begin_progress family | Any | Locked; the pluginProgress signal goes into the UI |
| Config / KV / data directory | Any | The host serialises writes |
QML invokePluginAction | Main thread | Synchronous; use invokePluginActionAsync or run_in_background for time-consuming actions |
Hard prohibitions
- ❌ A plugin calling
ctypes.RegisterHotKeyitself — binding it to the wrong thread makes it fail silently. Always go through the host API. - ❌ Doing time-consuming network or inference work synchronously inside
initialize/initialize(it slows down and can even block the caller); useself.run_in_background(...). - ❌ Calling a long-running plugin action synchronously in QML anywhere outside
Component.onCompleted.
Recommended pattern
Always put time-consuming work in the background and send the result back to the UI with emit_event:
python
def action_ask(self, prompt="", **kwargs):
self.run_in_background(self._do_work, prompt)
def _do_work(self, prompt):
try:
result = self._call_api(prompt) # slow
except Exception as exc:
self.host.show_error(f"请求失败:{exc}", source=self.id)
self.emit_event("stream_done", {"full_text": "", "error": str(exc)})
return
self.emit_event("stream_done", {"full_text": result})Local-service plugins have one more point to watch: ensure_loaded() is allowed to block (the host already calls it from a background thread), but initialize() / read-only methods are not — see Local-service engine contract.
