From 62fdd3b7c75b226bb920f11456ed8bd4a3542806 Mon Sep 17 00:00:00 2001 From: qianlifeng Date: Wed, 30 Jul 2025 21:22:37 +0800 Subject: [PATCH] feat(mru): Enhance nodejs/python plugin API with MRU restore functionality and dynamic setting improvements - Updated API interface to support dynamic settings returning PluginSettingDefinitionItem. - Implemented MRU restore callback in both Node.js and Python plugin hosts. - Added MRUData model for handling most recently used data. - Enhanced setting models to include new types and helper functions for creating settings. - Updated package dependencies for Node.js and Python plugins to the latest versions. --- wox.core/plugin/api.go | 5 +- wox.core/plugin/host/host_websocket.go | 18 +- wox.core/plugin/instance.go | 3 +- wox.core/ui/http.go | 8 +- wox.plugin.host.nodejs/package.json | 2 +- wox.plugin.host.nodejs/pnpm-lock.yaml | 8 +- wox.plugin.host.nodejs/src/jsonrpc.ts | 60 ++++- wox.plugin.host.nodejs/src/pluginAPI.ts | 11 +- wox.plugin.host.python/pyproject.toml | 2 +- .../src/wox_plugin_host/jsonrpc.py | 51 +++- .../src/wox_plugin_host/plugin_api.py | 16 +- wox.plugin.host.python/uv.lock | 8 +- wox.plugin.nodejs/package.json | 4 +- wox.plugin.nodejs/types/index.d.ts | 18 +- wox.plugin.python/pyproject.toml | 2 +- wox.plugin.python/src/wox_plugin/__init__.py | 27 ++ wox.plugin.python/src/wox_plugin/api.py | 17 +- .../src/wox_plugin/models/image.py | 12 + .../src/wox_plugin/models/mru.py | 42 +++ .../src/wox_plugin/models/setting.py | 242 ++++++++++++++++++ wox.plugin.python/uv.lock | 2 +- 21 files changed, 501 insertions(+), 57 deletions(-) create mode 100644 wox.plugin.python/src/wox_plugin/models/mru.py create mode 100644 wox.plugin.python/src/wox_plugin/models/setting.py diff --git a/wox.core/plugin/api.go b/wox.core/plugin/api.go index b34485b5..90c5b14b 100644 --- a/wox.core/plugin/api.go +++ b/wox.core/plugin/api.go @@ -10,6 +10,7 @@ import ( "wox/common" "wox/i18n" "wox/setting" + "wox/setting/definition" "wox/util" "github.com/samber/lo" @@ -34,7 +35,7 @@ type API interface { GetSetting(ctx context.Context, key string) string SaveSetting(ctx context.Context, key string, value string, isPlatformSpecific bool) OnSettingChanged(ctx context.Context, callback func(key string, value string)) - OnGetDynamicSetting(ctx context.Context, callback func(key string) string) + OnGetDynamicSetting(ctx context.Context, callback func(key string) definition.PluginSettingDefinitionItem) OnDeepLink(ctx context.Context, callback func(arguments map[string]string)) OnUnload(ctx context.Context, callback func()) OnMRURestore(ctx context.Context, callback func(mruData MRUData) (*QueryResult, error)) @@ -142,7 +143,7 @@ func (a *APIImpl) OnSettingChanged(ctx context.Context, callback func(key string a.pluginInstance.SettingChangeCallbacks = append(a.pluginInstance.SettingChangeCallbacks, callback) } -func (a *APIImpl) OnGetDynamicSetting(ctx context.Context, callback func(key string) string) { +func (a *APIImpl) OnGetDynamicSetting(ctx context.Context, callback func(key string) definition.PluginSettingDefinitionItem) { a.pluginInstance.DynamicSettingCallbacks = append(a.pluginInstance.DynamicSettingCallbacks, callback) } diff --git a/wox.core/plugin/host/host_websocket.go b/wox.core/plugin/host/host_websocket.go index 039deb1a..cff1443e 100644 --- a/wox.core/plugin/host/host_websocket.go +++ b/wox.core/plugin/host/host_websocket.go @@ -344,24 +344,19 @@ func (w *WebsocketHost) handleRequestFromPlugin(ctx context.Context, request Jso } metadata := pluginInstance.Metadata - pluginInstance.API.OnGetDynamicSetting(ctx, func(key string) string { + pluginInstance.API.OnGetDynamicSetting(ctx, func(key string) definition.PluginSettingDefinitionItem { result, err := w.invokeMethod(ctx, metadata, "onGetDynamicSetting", map[string]string{ "CallbackId": callbackId, "Key": key, }) if err != nil { util.GetLogger().Error(ctx, fmt.Sprintf("[%s] failed to get dynamic setting: %s", request.PluginName, err)) - settingJson, marshalErr := json.Marshal(definition.PluginSettingDefinitionItem{ + return definition.PluginSettingDefinitionItem{ Type: definition.PluginSettingDefinitionTypeLabel, Value: &definition.PluginSettingValueLabel{ Content: fmt.Sprintf("failed to get dynamic setting: %s", err), }, - }) - if marshalErr != nil { - util.GetLogger().Error(ctx, fmt.Sprintf("[%s] failed to marshal dynamic setting: %s", request.PluginName, marshalErr)) - return "" } - return string(settingJson) } // validate the result is a valid definition.PluginSettingDefinitionItem json string @@ -369,20 +364,15 @@ func (w *WebsocketHost) handleRequestFromPlugin(ctx context.Context, request Jso unmarshalErr := json.Unmarshal([]byte(result.(string)), &setting) if unmarshalErr != nil { util.GetLogger().Error(ctx, fmt.Sprintf("[%s] failed to unmarshal dynamic setting: %s", request.PluginName, unmarshalErr)) - settingJson, marshalErr := json.Marshal(definition.PluginSettingDefinitionItem{ + return definition.PluginSettingDefinitionItem{ Type: definition.PluginSettingDefinitionTypeLabel, Value: &definition.PluginSettingValueLabel{ Content: fmt.Sprintf("failed to unmarshal dynamic setting: %s", unmarshalErr), }, - }) - if marshalErr != nil { - util.GetLogger().Error(ctx, fmt.Sprintf("[%s] failed to marshal dynamic setting: %s", request.PluginName, marshalErr)) - return "" } - return string(settingJson) } - return result.(string) + return setting }) w.sendResponseToHost(ctx, request, "") case "OnDeepLink": diff --git a/wox.core/plugin/instance.go b/wox.core/plugin/instance.go index 1451dcf9..8c653ed4 100644 --- a/wox.core/plugin/instance.go +++ b/wox.core/plugin/instance.go @@ -2,6 +2,7 @@ package plugin import ( "wox/setting" + "wox/setting/definition" ) type Instance struct { @@ -15,7 +16,7 @@ type Instance struct { Host Host // plugin host to run this plugin Setting *setting.PluginSetting // setting for this plugin - DynamicSettingCallbacks []func(key string) string // dynamic setting callbacks + DynamicSettingCallbacks []func(key string) definition.PluginSettingDefinitionItem // dynamic setting callbacks SettingChangeCallbacks []func(key string, value string) DeepLinkCallbacks []func(arguments map[string]string) UnloadCallbacks []func() diff --git a/wox.core/ui/http.go b/wox.core/ui/http.go index 11987320..ae9cf727 100644 --- a/wox.core/ui/http.go +++ b/wox.core/ui/http.go @@ -202,13 +202,7 @@ func convertPluginDto(ctx context.Context, pluginDto dto.PluginDto, pluginInstan if settingDefinition.Type == definition.PluginSettingDefinitionTypeDynamic { replaced := false for _, callback := range pluginInstance.DynamicSettingCallbacks { - newSettingDefinitionJson := callback(settingDefinition.Value.GetKey()) - var newSettingDefinition definition.PluginSettingDefinitionItem - unmarshalErr := json.Unmarshal([]byte(newSettingDefinitionJson), &newSettingDefinition) - if unmarshalErr != nil { - logger.Error(ctx, fmt.Sprintf("failed to unmarshal dynamic setting: %s", unmarshalErr.Error())) - continue - } + newSettingDefinition := callback(settingDefinition.Value.GetKey()) if newSettingDefinition.Value != nil && newSettingDefinition.Type != definition.PluginSettingDefinitionTypeDynamic { logger.Debug(ctx, fmt.Sprintf("dynamic setting replaced: %s(%s) -> %s(%s)", settingDefinition.Value.GetKey(), settingDefinition.Type, newSettingDefinition.Value.GetKey(), newSettingDefinition.Type)) pluginDto.SettingDefinitions[i] = newSettingDefinition diff --git a/wox.plugin.host.nodejs/package.json b/wox.plugin.host.nodejs/package.json index 247cba9d..4ba62839 100644 --- a/wox.plugin.host.nodejs/package.json +++ b/wox.plugin.host.nodejs/package.json @@ -30,7 +30,7 @@ "typescript": "^5.2.2" }, "dependencies": { - "@wox-launcher/wox-plugin": "^0.0.82", + "@wox-launcher/wox-plugin": "^0.0.85", "dayjs": "^1.11.13", "promise-deferred": "^2.0.4", "winston": "^3.17.0", diff --git a/wox.plugin.host.nodejs/pnpm-lock.yaml b/wox.plugin.host.nodejs/pnpm-lock.yaml index 7681a0a4..0c78836d 100644 --- a/wox.plugin.host.nodejs/pnpm-lock.yaml +++ b/wox.plugin.host.nodejs/pnpm-lock.yaml @@ -6,8 +6,8 @@ settings: dependencies: '@wox-launcher/wox-plugin': - specifier: ^0.0.82 - version: 0.0.82 + specifier: ^0.0.85 + version: 0.0.85 dayjs: specifier: ^1.11.13 version: 1.11.13 @@ -1561,8 +1561,8 @@ packages: hasBin: true dev: true - /@wox-launcher/wox-plugin@0.0.82: - resolution: {integrity: sha512-BV2I/I7Bu2hperlqdJ2aqZd1j3ZZJk7bsWKChNVvctQ1D6mXoREEexgWYjF8cq11FNMKhMnj9nrRZZjccrD27g==} + /@wox-launcher/wox-plugin@0.0.85: + resolution: {integrity: sha512-1QbLcd/RvA0oNpfj4CkdpBjiqW8wV5XLwN6Ps1VJGZMrKCuR5S8n/cvMYeVM/IlqT6i8sswKcn014uwPa27ADA==} dev: false /JSONStream@1.3.5: diff --git a/wox.plugin.host.nodejs/src/jsonrpc.ts b/wox.plugin.host.nodejs/src/jsonrpc.ts index 367d439c..e4f86d55 100644 --- a/wox.plugin.host.nodejs/src/jsonrpc.ts +++ b/wox.plugin.host.nodejs/src/jsonrpc.ts @@ -1,7 +1,7 @@ import { logger } from "./logger" import path from "path" import { PluginAPI } from "./pluginAPI" -import { Context, MapString, Plugin, PluginInitParams, Query, QueryEnv, RefreshableResult, Result, ResultAction, Selection } from "@wox-launcher/wox-plugin" +import { Context, MapString, Plugin, PluginInitParams, Query, QueryEnv, RefreshableResult, Result, ResultAction, Selection, MRUData } from "@wox-launcher/wox-plugin" import { WebSocket } from "ws" import * as crypto from "crypto" import { AI } from "@wox-launcher/wox-plugin/types/ai" @@ -41,6 +41,8 @@ export async function handleRequestFromWox(ctx: Context, request: PluginJsonRpcR return onUnload(ctx, request) case "onLLMStream": return onLLMStream(ctx, request) + case "onMRURestore": + return onMRURestore(ctx, request) default: logger.info(ctx, `unknown method handler: ${request.Method}`) throw new Error(`unknown method handler: ${request.Method}`) @@ -107,7 +109,8 @@ async function initPlugin(ctx: Context, request: PluginJsonRpcRequest, ws: WebSo const init = getMethod(ctx, request, "init") const pluginApi = new PluginAPI(ws, request.PluginId, request.PluginName) plugin.API = pluginApi - return init(ctx, { API: pluginApi, PluginDirectory: request.Params.PluginDirectory } as PluginInitParams) + const initParams: PluginInitParams = { API: pluginApi, PluginDirectory: request.Params.PluginDirectory } + return init(ctx, initParams) } async function onPluginSettingChange(ctx: Context, request: PluginJsonRpcRequest) { @@ -253,7 +256,7 @@ async function action(ctx: Context, request: PluginJsonRpcRequest) { pluginAction({ ContextData: request.Params.ContextData }) - + return } @@ -298,13 +301,48 @@ async function refresh(ctx: Context, request: PluginJsonRpcRequest) { Tails: refreshedResult.Tails, ContextData: refreshedResult.ContextData, RefreshInterval: refreshedResult.RefreshInterval, - Actions: refreshedResult.Actions.map(action => ({ - Id: action.Id, - Name: action.Name, - Icon: action.Icon, - IsDefault: action.IsDefault, - PreventHideAfterAction: action.PreventHideAfterAction, - Hotkey: action.Hotkey, - } as ResultActionUI)) + Actions: refreshedResult.Actions.map( + action => + ({ + Id: action.Id, + Name: action.Name, + Icon: action.Icon, + IsDefault: action.IsDefault, + PreventHideAfterAction: action.PreventHideAfterAction, + Hotkey: action.Hotkey + }) as ResultActionUI + ) } as RefreshableResultWithResultId } + +async function onMRURestore(ctx: Context, request: PluginJsonRpcRequest): Promise { + const pluginInstance = pluginInstances.get(request.PluginId) + if (!pluginInstance) { + throw new Error(`plugin instance not found: ${request.PluginId}`) + } + + const callbackId = request.Params.callbackId + const mruDataRaw = JSON.parse(request.Params.mruData) + + // Convert raw data to MRUData type + const mruData: MRUData = { + PluginID: mruDataRaw.PluginID || "", + Title: mruDataRaw.Title || "", + SubTitle: mruDataRaw.SubTitle || "", + Icon: mruDataRaw.Icon || { ImageType: "absolute", ImageData: "" }, + ContextData: mruDataRaw.ContextData || "" + } + + const callback = pluginInstance.API.mruRestoreCallbacks.get(callbackId) + if (!callback) { + throw new Error(`MRU restore callback not found: ${callbackId}`) + } + + try { + const result = await callback(mruData) + return result + } catch (error) { + logger.error(ctx, `MRU restore callback error: ${error}`) + throw error + } +} diff --git a/wox.plugin.host.nodejs/src/pluginAPI.ts b/wox.plugin.host.nodejs/src/pluginAPI.ts index 29e67183..150b4f97 100644 --- a/wox.plugin.host.nodejs/src/pluginAPI.ts +++ b/wox.plugin.host.nodejs/src/pluginAPI.ts @@ -1,4 +1,4 @@ -import { ChangeQueryParam, Context, MapString, PublicAPI } from "@wox-launcher/wox-plugin" +import { ChangeQueryParam, Context, MapString, PublicAPI, Result } from "@wox-launcher/wox-plugin" import { WebSocket } from "ws" import * as crypto from "crypto" import { waitingForResponse } from "./index" @@ -6,6 +6,7 @@ import Deferred from "promise-deferred" import { logger } from "./logger" import { MetadataCommand, PluginSettingDefinitionItem } from "@wox-launcher/wox-plugin/types/setting" import { AI } from "@wox-launcher/wox-plugin/types/ai" +import { MRUData } from "@wox-launcher/wox-plugin" import { PluginJsonRpcTypeRequest } from "./jsonrpc" import { PluginJsonRpcRequest } from "./types" @@ -18,6 +19,7 @@ export class PluginAPI implements PublicAPI { deepLinkCallbacks: Map void> unloadCallbacks: Map Promise> llmStreamCallbacks: Map + mruRestoreCallbacks: Map Promise> constructor(ws: WebSocket, pluginId: string, pluginName: string) { this.ws = ws @@ -28,6 +30,7 @@ export class PluginAPI implements PublicAPI { this.deepLinkCallbacks = new Map void>() this.unloadCallbacks = new Map Promise>() this.llmStreamCallbacks = new Map() + this.mruRestoreCallbacks = new Map Promise>() } async invokeMethod(ctx: Context, method: string, params: { [key: string]: string }): Promise { @@ -124,4 +127,10 @@ export class PluginAPI implements PublicAPI { this.llmStreamCallbacks.set(callbackId, callback) await this.invokeMethod(ctx, "LLMStream", { callbackId, conversations: JSON.stringify(conversations) }) } + + async OnMRURestore(ctx: Context, callback: (mruData: MRUData) => Promise): Promise { + const callbackId = crypto.randomUUID() + this.mruRestoreCallbacks.set(callbackId, callback) + await this.invokeMethod(ctx, "OnMRURestore", { callbackId }) + } } diff --git a/wox.plugin.host.python/pyproject.toml b/wox.plugin.host.python/pyproject.toml index fb607163..6fd2da6a 100644 --- a/wox.plugin.host.python/pyproject.toml +++ b/wox.plugin.host.python/pyproject.toml @@ -6,7 +6,7 @@ readme = "README.md" requires-python = ">=3.10" license = "GPL-3.0" authors = [{ name = "Wox Team", email = "qianlifeng@gmail.com" }] -dependencies = ["loguru", "websockets", "wox-plugin==0.0.48"] +dependencies = ["loguru", "websockets", "wox-plugin==0.0.49"] [project.scripts] run = "wox_plugin_host.__main__:run" diff --git a/wox.plugin.host.python/src/wox_plugin_host/jsonrpc.py b/wox.plugin.host.python/src/wox_plugin_host/jsonrpc.py index 2cc09f86..9195a23b 100644 --- a/wox.plugin.host.python/src/wox_plugin_host/jsonrpc.py +++ b/wox.plugin.host.python/src/wox_plugin_host/jsonrpc.py @@ -1,5 +1,5 @@ import json -import importlib.util +import importlib from os import path import sys from typing import Any, Dict @@ -12,6 +12,7 @@ from wox_plugin import ( RefreshableResult, PluginInitParams, ActionContext, + MRUData, ) from .plugin_manager import plugin_instances, PluginInstance from .plugin_api import PluginAPI @@ -38,6 +39,8 @@ async def handle_request_from_wox(ctx: Context, request: Dict[str, Any], ws: web return await refresh(ctx, request) elif method == "unloadPlugin": return await unload_plugin(ctx, request) + elif method == "onMRURestore": + return await on_mru_restore(ctx, request) else: await logger.info(ctx.get_trace_id(), f"unknown method handler: {method}") raise Exception(f"unknown method handler: {method}") @@ -325,3 +328,49 @@ async def unload_plugin(ctx: Context, request: Dict[str, Any]) -> None: f"<{plugin_name}> unload plugin failed: {str(e)}\nStack trace:\n{error_stack}", ) raise e + + +async def on_mru_restore(ctx: Context, request: Dict[str, Any]) -> Any: + """Handle MRU restore callback""" + plugin_id = request.get("PluginId") + if not plugin_id: + raise Exception("PluginId is required") + + params = request.get("Params", {}) + callback_id = params.get("callbackId") + mru_data_dict = json.loads(params.get("mruData", "{}")) + + plugin_instance = plugin_instances.get(plugin_id) + if not plugin_instance: + raise Exception(f"plugin instance not found: {plugin_id}") + + if not plugin_instance.api: + raise Exception(f"plugin API not found: {plugin_id}") + + # Type cast to access implementation-specific attributes + from .plugin_api import PluginAPI + + api = plugin_instance.api + if not isinstance(api, PluginAPI): + raise Exception(f"Invalid API type for plugin: {plugin_id}") + + callback = api.mru_restore_callbacks.get(callback_id) + if not callback: + raise Exception(f"MRU restore callback not found: {callback_id}") + + try: + # Convert dict to MRUData object for type safety + mru_data = MRUData.from_dict(mru_data_dict) + + # Call the callback (may or may not be async) + result = callback(mru_data) + if hasattr(result, "__await__"): + result = await result # type: ignore + + # Convert Result object back to dict for JSON serialization + if result is not None: + return result.__dict__ + return None + except Exception as e: + await logger.error(ctx.get_trace_id(), f"MRU restore callback error: {str(e)}") + raise e diff --git a/wox.plugin.host.python/src/wox_plugin_host/plugin_api.py b/wox.plugin.host.python/src/wox_plugin_host/plugin_api.py index cdf9a2d3..f6a4a553 100644 --- a/wox.plugin.host.python/src/wox_plugin_host/plugin_api.py +++ b/wox.plugin.host.python/src/wox_plugin_host/plugin_api.py @@ -1,7 +1,7 @@ import asyncio import json import uuid -from typing import Any, Dict, Callable +from typing import Any, Dict, Callable, Optional import websockets from . import logger from wox_plugin import ( @@ -12,6 +12,9 @@ from wox_plugin import ( Conversation, AIModel, ChatStreamCallback, + MRUData, + Result, + PluginSettingDefinitionItem, ) from .constants import PLUGIN_JSONRPC_TYPE_REQUEST from .plugin_manager import waiting_for_response @@ -23,10 +26,11 @@ class PluginAPI(PublicAPI): self.plugin_id = plugin_id self.plugin_name = plugin_name self.setting_change_callbacks: Dict[str, Callable[[str, str], None]] = {} - self.get_dynamic_setting_callbacks: Dict[str, Callable[[str], str]] = {} + self.get_dynamic_setting_callbacks: Dict[str, Callable[[str], PluginSettingDefinitionItem]] = {} self.deep_link_callbacks: Dict[str, Callable[[Dict[str, str]], None]] = {} self.unload_callbacks: Dict[str, Callable[[], None]] = {} self.llm_stream_callbacks: Dict[str, ChatStreamCallback] = {} + self.mru_restore_callbacks: Dict[str, Callable[[MRUData], Optional[Result]]] = {} async def invoke_method(self, ctx: Context, method: str, params: Dict[str, Any]) -> Any: """Invoke a method on Wox""" @@ -110,7 +114,7 @@ class PluginAPI(PublicAPI): self.setting_change_callbacks[callback_id] = callback await self.invoke_method(ctx, "OnSettingChanged", {"callbackId": callback_id}) - async def on_get_dynamic_setting(self, ctx: Context, callback: Callable[[str], str]) -> None: + async def on_get_dynamic_setting(self, ctx: Context, callback: Callable[[str], PluginSettingDefinitionItem]) -> None: """Register dynamic setting callback""" callback_id = str(uuid.uuid4()) self.get_dynamic_setting_callbacks[callback_id] = callback @@ -154,3 +158,9 @@ class PluginAPI(PublicAPI): "conversations": json.dumps([conv.__dict__ for conv in conversations]), }, ) + + async def on_mru_restore(self, ctx: Context, callback: Callable[[MRUData], Optional[Result]]) -> None: + """Register MRU restore callback""" + callback_id = str(uuid.uuid4()) + self.mru_restore_callbacks[callback_id] = callback + await self.invoke_method(ctx, "OnMRURestore", {"callbackId": callback_id}) diff --git a/wox.plugin.host.python/uv.lock b/wox.plugin.host.python/uv.lock index cc701e99..60ad1b49 100644 --- a/wox.plugin.host.python/uv.lock +++ b/wox.plugin.host.python/uv.lock @@ -253,11 +253,11 @@ wheels = [ [[package]] name = "wox-plugin" -version = "0.0.48" +version = "0.0.49" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/59/e7/1a4f6701f653d7243d8ec35b29aad8e377760e03a61092e3950b53f2aadb/wox_plugin-0.0.48.tar.gz", hash = "sha256:132820a60dddc5d130c049ce302aba519b10e6a409e87453be393472f55153ba", size = 34994 } +sdist = { url = "https://files.pythonhosted.org/packages/6f/66/9961ded8a71981736334b2f95b70340903c233df6bdaf256bd6b2107255f/wox_plugin-0.0.49.tar.gz", hash = "sha256:734437d909d14f2b96509cf4d66e57731bff9fe6344dbfada4a93ce843f8eb44", size = 36975 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/68/0b131ae43f8a95fb2f8eb6f36c69606accb0c2729544ea92082256bd9957/wox_plugin-0.0.48-py3-none-any.whl", hash = "sha256:5a34cd5a59f8dbd218a45618ee00ce0c4c50e03d39f1dc237fcbd0ad34f013d5", size = 10386 }, + { url = "https://files.pythonhosted.org/packages/e6/53/dc3a17c6e9668b903fe5f4ff2d04c949f350cd3e556535c95c056b6d9491/wox_plugin-0.0.49-py3-none-any.whl", hash = "sha256:cd33ea38c5aecc576c2aefbe5427e04400ff13de997b1126f6d7d9e426edb446", size = 13125 }, ] [[package]] @@ -284,5 +284,5 @@ requires-dist = [ { name = "ruff", marker = "extra == 'dev'" }, { name = "shiv", marker = "extra == 'dev'" }, { name = "websockets" }, - { name = "wox-plugin", specifier = "==0.0.48" }, + { name = "wox-plugin", specifier = "==0.0.49" }, ] diff --git a/wox.plugin.nodejs/package.json b/wox.plugin.nodejs/package.json index 4a33fd32..2334546d 100644 --- a/wox.plugin.nodejs/package.json +++ b/wox.plugin.nodejs/package.json @@ -1,6 +1,6 @@ { "name": "@wox-launcher/wox-plugin", - "version": "0.0.83", + "version": "0.0.85", "description": "All nodejs plugin for Wox should use types in this package", "repository": { "type": "git", @@ -29,4 +29,4 @@ "typescript": "^5.4.5" }, "dependencies": {} -} +} \ No newline at end of file diff --git a/wox.plugin.nodejs/types/index.d.ts b/wox.plugin.nodejs/types/index.d.ts index 5630d00d..ca2bef84 100644 --- a/wox.plugin.nodejs/types/index.d.ts +++ b/wox.plugin.nodejs/types/index.d.ts @@ -152,6 +152,14 @@ export interface ActionContext { ContextData: string } +export interface MRUData { + PluginID: string + Title: string + SubTitle: string + Icon: WoxImage + ContextData: string +} + export interface PluginInitParams { API: PublicAPI PluginDirectory: string @@ -216,7 +224,7 @@ export interface PublicAPI { /** * Get dynamic setting definition */ - OnGetDynamicSetting: (ctx: Context, callback: (key: string) => string) => Promise + OnGetDynamicSetting: (ctx: Context, callback: (key: string) => PluginSettingDefinitionItem) => Promise /** * Register deep link callback @@ -237,6 +245,14 @@ export interface PublicAPI { * Chat using LLM */ LLMStream: (ctx: Context, conversations: AI.Conversation[], callback: AI.ChatStreamFunc) => Promise + + /** + * Register MRU restore callback + * @param ctx Context + * @param callback Callback function that takes MRUData and returns Result or null + * Return null if the MRU data is no longer valid + */ + OnMRURestore: (ctx: Context, callback: (mruData: MRUData) => Promise) => Promise } export type WoxImageType = "absolute" | "relative" | "base64" | "svg" | "url" | "emoji" | "lottie" diff --git a/wox.plugin.python/pyproject.toml b/wox.plugin.python/pyproject.toml index 5177d193..42db2533 100644 --- a/wox.plugin.python/pyproject.toml +++ b/wox.plugin.python/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "wox-plugin" -version = "0.0.48" +version = "0.0.49" description = "Python plugin SDK for Wox launcher" readme = "README.md" requires-python = ">=3.10" diff --git a/wox.plugin.python/src/wox_plugin/__init__.py b/wox.plugin.python/src/wox_plugin/__init__.py index a8a809fb..e4c56e7e 100644 --- a/wox.plugin.python/src/wox_plugin/__init__.py +++ b/wox.plugin.python/src/wox_plugin/__init__.py @@ -35,6 +35,19 @@ from .models.ai import ( ) from .models.image import WoxImage, WoxImageType from .models.preview import WoxPreview, WoxPreviewType, WoxPreviewScrollPosition +from .models.mru import MRUData, MRURestoreCallback +from .models.setting import ( + PluginSettingDefinitionItem, + PluginSettingDefinitionType, + PluginSettingDefinitionValue, + PluginSettingValueStyle, + PluginSettingValueTextBox, + PluginSettingValueCheckBox, + PluginSettingValueLabel, + create_textbox_setting, + create_checkbox_setting, + create_label_setting, +) __all__: List[str] = [ # Plugin @@ -84,4 +97,18 @@ __all__: List[str] = [ "WoxPreviewScrollPosition", # Result "ResultTailType", + # MRU + "MRUData", + "MRURestoreCallback", + # Settings + "PluginSettingDefinitionItem", + "PluginSettingDefinitionType", + "PluginSettingDefinitionValue", + "PluginSettingValueStyle", + "PluginSettingValueTextBox", + "PluginSettingValueCheckBox", + "PluginSettingValueLabel", + "create_textbox_setting", + "create_checkbox_setting", + "create_label_setting", ] diff --git a/wox.plugin.python/src/wox_plugin/api.py b/wox.plugin.python/src/wox_plugin/api.py index 0398cac1..151ba127 100644 --- a/wox.plugin.python/src/wox_plugin/api.py +++ b/wox.plugin.python/src/wox_plugin/api.py @@ -1,9 +1,12 @@ -from typing import Protocol, Callable, Dict, List +from typing import Protocol, Callable, Dict, List, Optional from .models.query import MetadataCommand from .models.context import Context from .models.query import ChangeQueryParam from .models.ai import AIModel, Conversation, ChatStreamCallback +from .models.mru import MRUData +from .models.result import Result +from .models.setting import PluginSettingDefinitionItem class PublicAPI(Protocol): @@ -45,7 +48,7 @@ class PublicAPI(Protocol): """Register setting change callback""" ... - async def on_get_dynamic_setting(self, ctx: Context, callback: Callable[[str], str]) -> None: + async def on_get_dynamic_setting(self, ctx: Context, callback: Callable[[str], PluginSettingDefinitionItem]) -> None: """Register dynamic setting callback""" ... @@ -81,3 +84,13 @@ class PublicAPI(Protocol): - data: str, the stream content """ ... + + async def on_mru_restore(self, ctx: Context, callback: Callable[[MRUData], Optional[Result]]) -> None: + """Register MRU restore callback + + Args: + ctx: Context + callback: Callback function that takes MRUData and returns Result or None + Return None if the MRU data is no longer valid + """ + ... diff --git a/wox.plugin.python/src/wox_plugin/models/image.py b/wox.plugin.python/src/wox_plugin/models/image.py index de4f9349..837bcba2 100644 --- a/wox.plugin.python/src/wox_plugin/models/image.py +++ b/wox.plugin.python/src/wox_plugin/models/image.py @@ -36,6 +36,11 @@ class WoxImage: def from_json(cls, json_str: str) -> "WoxImage": """Create from JSON string with camelCase naming""" data = json.loads(json_str) + return cls.from_dict(data) + + @classmethod + def from_dict(cls, data: dict) -> "WoxImage": + """Create from dictionary with camelCase naming""" if not data.get("ImageType"): data["ImageType"] = WoxImageType.ABSOLUTE @@ -44,6 +49,13 @@ class WoxImage: image_data=data.get("ImageData", ""), ) + def to_dict(self) -> dict: + """Convert to dictionary with camelCase naming""" + return { + "ImageData": self.image_data, + "ImageType": self.image_type, + } + @classmethod def new_base64(cls, data: str) -> "WoxImage": """Create a new base64 image""" diff --git a/wox.plugin.python/src/wox_plugin/models/mru.py b/wox.plugin.python/src/wox_plugin/models/mru.py new file mode 100644 index 00000000..810399bf --- /dev/null +++ b/wox.plugin.python/src/wox_plugin/models/mru.py @@ -0,0 +1,42 @@ +from dataclasses import dataclass +from typing import Optional, Callable, TYPE_CHECKING +from .image import WoxImage + +if TYPE_CHECKING: + from .result import Result + + +@dataclass +class MRUData: + """MRU (Most Recently Used) data structure""" + plugin_id: str + title: str + sub_title: str + icon: WoxImage + context_data: str + + @classmethod + def from_dict(cls, data: dict) -> 'MRUData': + """Create MRUData from dictionary""" + return cls( + plugin_id=data.get('PluginID', ''), + title=data.get('Title', ''), + sub_title=data.get('SubTitle', ''), + icon=WoxImage.from_dict(data.get('Icon', {})), + context_data=data.get('ContextData', '') + ) + + def to_dict(self) -> dict: + """Convert MRUData to dictionary""" + return { + 'PluginID': self.plugin_id, + 'Title': self.title, + 'SubTitle': self.sub_title, + 'Icon': self.icon.to_dict(), + 'ContextData': self.context_data + } + + +# Type alias for MRU restore callback +# Note: We use forward reference to avoid circular import +MRURestoreCallback = Callable[['MRUData'], Optional['Result']] diff --git a/wox.plugin.python/src/wox_plugin/models/setting.py b/wox.plugin.python/src/wox_plugin/models/setting.py new file mode 100644 index 00000000..2ad36c0e --- /dev/null +++ b/wox.plugin.python/src/wox_plugin/models/setting.py @@ -0,0 +1,242 @@ +from dataclasses import dataclass, field +from typing import Dict, Any, List +from enum import Enum +import json + + +class PluginSettingDefinitionType(str, Enum): + """Plugin setting definition type enum""" + HEAD = "head" + TEXTBOX = "textbox" + CHECKBOX = "checkbox" + SELECT = "select" + LABEL = "label" + NEWLINE = "newline" + TABLE = "table" + DYNAMIC = "dynamic" + + +@dataclass +class PluginSettingValueStyle: + """Style configuration for plugin settings""" + padding_left: int = field(default=0) + padding_top: int = field(default=0) + padding_right: int = field(default=0) + padding_bottom: int = field(default=0) + width: int = field(default=0) + label_width: int = field(default=0) + + def to_dict(self) -> Dict[str, Any]: + """Convert to dictionary with camelCase naming""" + return { + "PaddingLeft": self.padding_left, + "PaddingTop": self.padding_top, + "PaddingRight": self.padding_right, + "PaddingBottom": self.padding_bottom, + "Width": self.width, + "LabelWidth": self.label_width, + } + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> 'PluginSettingValueStyle': + """Create from dictionary with camelCase naming""" + return cls( + padding_left=data.get('PaddingLeft', 0), + padding_top=data.get('PaddingTop', 0), + padding_right=data.get('PaddingRight', 0), + padding_bottom=data.get('PaddingBottom', 0), + width=data.get('Width', 0), + label_width=data.get('LabelWidth', 0), + ) + + +@dataclass +class PluginSettingDefinitionValue: + """Base class for plugin setting values""" + key: str + default_value: str = field(default="") + + def get_key(self) -> str: + """Get the setting key""" + return self.key + + def get_default_value(self) -> str: + """Get the default value""" + return self.default_value + + def to_dict(self) -> Dict[str, Any]: + """Convert to dictionary""" + return { + "Key": self.key, + "DefaultValue": self.default_value, + } + + +@dataclass +class PluginSettingValueTextBox(PluginSettingDefinitionValue): + """Text box setting value""" + label: str = field(default="") + suffix: str = field(default="") + tooltip: str = field(default="") + max_lines: int = field(default=1) + style: PluginSettingValueStyle = field(default_factory=PluginSettingValueStyle) + + def to_dict(self) -> Dict[str, Any]: + """Convert to dictionary with camelCase naming""" + return { + "Key": self.key, + "Label": self.label, + "Suffix": self.suffix, + "DefaultValue": self.default_value, + "Tooltip": self.tooltip, + "MaxLines": self.max_lines, + "Style": self.style.to_dict(), + } + + +@dataclass +class PluginSettingValueCheckBox(PluginSettingDefinitionValue): + """Checkbox setting value""" + label: str = field(default="") + tooltip: str = field(default="") + style: PluginSettingValueStyle = field(default_factory=PluginSettingValueStyle) + + def to_dict(self) -> Dict[str, Any]: + """Convert to dictionary with camelCase naming""" + return { + "Key": self.key, + "Label": self.label, + "DefaultValue": self.default_value, + "Tooltip": self.tooltip, + "Style": self.style.to_dict(), + } + + +@dataclass +class PluginSettingValueLabel(PluginSettingDefinitionValue): + """Label setting value""" + content: str = field(default="") + tooltip: str = field(default="") + style: PluginSettingValueStyle = field(default_factory=PluginSettingValueStyle) + + def to_dict(self) -> Dict[str, Any]: + """Convert to dictionary with camelCase naming""" + return { + "Content": self.content, + "Tooltip": self.tooltip, + "Style": self.style.to_dict(), + } + + +@dataclass +class PluginSettingDefinitionItem: + """Plugin setting definition item""" + type: PluginSettingDefinitionType + value: PluginSettingDefinitionValue + disabled_in_platforms: List[str] = field(default_factory=list) + is_platform_specific: bool = field(default=False) + + def to_dict(self) -> Dict[str, Any]: + """Convert to dictionary with camelCase naming""" + return { + "Type": self.type, + "Value": self.value.to_dict(), + "DisabledInPlatforms": self.disabled_in_platforms, + "IsPlatformSpecific": self.is_platform_specific, + } + + def to_json(self) -> str: + """Convert to JSON string""" + return json.dumps(self.to_dict()) + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> 'PluginSettingDefinitionItem': + """Create from dictionary""" + setting_type = PluginSettingDefinitionType(data.get('Type', 'textbox')) + + # Create appropriate value object based on type + value_data = data.get('Value', {}) + value: PluginSettingDefinitionValue + if setting_type == PluginSettingDefinitionType.TEXTBOX: + value = PluginSettingValueTextBox( + key=value_data.get('Key', ''), + label=value_data.get('Label', ''), + suffix=value_data.get('Suffix', ''), + default_value=value_data.get('DefaultValue', ''), + tooltip=value_data.get('Tooltip', ''), + max_lines=value_data.get('MaxLines', 1), + style=PluginSettingValueStyle.from_dict(value_data.get('Style', {})) + ) + elif setting_type == PluginSettingDefinitionType.CHECKBOX: + value = PluginSettingValueCheckBox( + key=value_data.get('Key', ''), + label=value_data.get('Label', ''), + default_value=value_data.get('DefaultValue', ''), + tooltip=value_data.get('Tooltip', ''), + style=PluginSettingValueStyle.from_dict(value_data.get('Style', {})) + ) + elif setting_type == PluginSettingDefinitionType.LABEL: + value = PluginSettingValueLabel( + key=value_data.get('Key', ''), + content=value_data.get('Content', ''), + tooltip=value_data.get('Tooltip', ''), + style=PluginSettingValueStyle.from_dict(value_data.get('Style', {})) + ) + else: + # Default to basic value + value = PluginSettingDefinitionValue( + key=value_data.get('Key', ''), + default_value=value_data.get('DefaultValue', '') + ) + + return cls( + type=setting_type, + value=value, + disabled_in_platforms=data.get('DisabledInPlatforms', []), + is_platform_specific=data.get('IsPlatformSpecific', False) + ) + + @classmethod + def from_json(cls, json_str: str) -> 'PluginSettingDefinitionItem': + """Create from JSON string""" + data = json.loads(json_str) + return cls.from_dict(data) + + +# Helper functions for creating common setting types +def create_textbox_setting(key: str, label: str, default_value: str = "", tooltip: str = "") -> PluginSettingDefinitionItem: + """Create a textbox setting""" + return PluginSettingDefinitionItem( + type=PluginSettingDefinitionType.TEXTBOX, + value=PluginSettingValueTextBox( + key=key, + label=label, + default_value=default_value, + tooltip=tooltip + ) + ) + + +def create_checkbox_setting(key: str, label: str, default_value: str = "false", tooltip: str = "") -> PluginSettingDefinitionItem: + """Create a checkbox setting""" + return PluginSettingDefinitionItem( + type=PluginSettingDefinitionType.CHECKBOX, + value=PluginSettingValueCheckBox( + key=key, + label=label, + default_value=default_value, + tooltip=tooltip + ) + ) + + +def create_label_setting(content: str, tooltip: str = "") -> PluginSettingDefinitionItem: + """Create a label setting""" + return PluginSettingDefinitionItem( + type=PluginSettingDefinitionType.LABEL, + value=PluginSettingValueLabel( + key="", # Labels don't need keys + content=content, + tooltip=tooltip + ) + ) diff --git a/wox.plugin.python/uv.lock b/wox.plugin.python/uv.lock index 308dd1ce..b679f184 100644 --- a/wox.plugin.python/uv.lock +++ b/wox.plugin.python/uv.lock @@ -624,7 +624,7 @@ wheels = [ [[package]] name = "wox-plugin" -version = "0.0.48" +version = "0.0.49" source = { editable = "." } [package.optional-dependencies]