mirror of https://github.com/Wox-launcher/Wox
Enhance Python host functionality
This commit is contained in:
parent
1af4b7f1b0
commit
a21dd8d509
|
@ -20,3 +20,5 @@ __pycache__/
|
|||
Release/
|
||||
__debug_bin*
|
||||
Wox/log/
|
||||
Wox.Plugin.Python/dist/
|
||||
Wox.Plugin.Python/wox_plugin.egg-info/
|
|
@ -1,22 +1,51 @@
|
|||
import asyncio
|
||||
import sys
|
||||
import uuid
|
||||
import os
|
||||
|
||||
import logger
|
||||
from host import start_websocket
|
||||
|
||||
if len(sys.argv) != 4:
|
||||
print('Usage: python python-host.pyz <port> <logDirectory>')
|
||||
print('Usage: python python-host.pyz <port> <logDirectory> <woxPid>')
|
||||
sys.exit(1)
|
||||
|
||||
port = int(sys.argv[1])
|
||||
log_directory = (sys.argv[2])
|
||||
log_directory = sys.argv[2]
|
||||
wox_pid = int(sys.argv[3])
|
||||
|
||||
trace_id = f"{uuid.uuid4()}"
|
||||
trace_id = str(uuid.uuid4())
|
||||
host_id = f"python-{uuid.uuid4()}"
|
||||
logger.update_log_directory(log_directory)
|
||||
logger.info(trace_id, "----------------------------------------")
|
||||
logger.info(trace_id, f"start python host: {host_id}")
|
||||
logger.info(trace_id, f"port: {port}")
|
||||
|
||||
asyncio.run(start_websocket(port))
|
||||
def check_wox_process():
|
||||
"""Check if Wox process is still alive"""
|
||||
try:
|
||||
os.kill(wox_pid, 0)
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
async def monitor_wox_process():
|
||||
"""Monitor Wox process and exit if it's not alive"""
|
||||
await logger.info(trace_id, "start monitor wox process")
|
||||
while True:
|
||||
if not check_wox_process():
|
||||
await logger.error(trace_id, "wox process is not alive, exit")
|
||||
sys.exit(1)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
async def main():
|
||||
"""Main function"""
|
||||
# Log startup information
|
||||
await logger.info(trace_id, "----------------------------------------")
|
||||
await logger.info(trace_id, f"start python host: {host_id}")
|
||||
await logger.info(trace_id, f"port: {port}")
|
||||
await logger.info(trace_id, f"wox pid: {wox_pid}")
|
||||
|
||||
# Start tasks
|
||||
monitor_task = asyncio.create_task(monitor_wox_process())
|
||||
websocket_task = asyncio.create_task(start_websocket(port))
|
||||
await asyncio.gather(monitor_task, websocket_task)
|
||||
|
||||
asyncio.run(main())
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
"""Constants used across the application"""
|
||||
|
||||
PLUGIN_JSONRPC_TYPE_REQUEST = "WOX_JSONRPC_REQUEST"
|
||||
PLUGIN_JSONRPC_TYPE_RESPONSE = "WOX_JSONRPC_RESPONSE"
|
||||
PLUGIN_JSONRPC_TYPE_SYSTEM_LOG = "WOX_JSONRPC_SYSTEM_LOG"
|
|
@ -1,23 +1,76 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import asyncio
|
||||
import pkgutil
|
||||
import json
|
||||
import uuid
|
||||
from typing import Dict, Any
|
||||
|
||||
import websockets
|
||||
from loguru import logger
|
||||
import logger
|
||||
from wox_plugin import Context, new_context_with_value
|
||||
from constants import PLUGIN_JSONRPC_TYPE_REQUEST, PLUGIN_JSONRPC_TYPE_RESPONSE
|
||||
from plugin_manager import waiting_for_response
|
||||
from jsonrpc import handle_request_from_wox
|
||||
|
||||
async def handle_message(ws: websockets.WebSocketServerProtocol, message: str):
|
||||
"""Handle incoming WebSocket message"""
|
||||
try:
|
||||
msg_data = json.loads(message)
|
||||
trace_id = msg_data.get("TraceId", str(uuid.uuid4()))
|
||||
ctx = new_context_with_value("traceId", trace_id)
|
||||
|
||||
async def handler(websocket):
|
||||
while True:
|
||||
message = await websocket.recv()
|
||||
logger.info(message)
|
||||
# my_module = importlib.import_module('os.path')
|
||||
if PLUGIN_JSONRPC_TYPE_RESPONSE in message:
|
||||
# Handle response from Wox
|
||||
if msg_data.get("Id") in waiting_for_response:
|
||||
deferred = waiting_for_response[msg_data["Id"]]
|
||||
if msg_data.get("Error"):
|
||||
deferred.reject(msg_data["Error"])
|
||||
else:
|
||||
deferred.resolve(msg_data.get("Result"))
|
||||
del waiting_for_response[msg_data["Id"]]
|
||||
elif PLUGIN_JSONRPC_TYPE_REQUEST in message:
|
||||
# Handle request from Wox
|
||||
try:
|
||||
result = await handle_request_from_wox(ctx, msg_data, ws)
|
||||
response = {
|
||||
"TraceId": trace_id,
|
||||
"Id": msg_data["Id"],
|
||||
"Method": msg_data["Method"],
|
||||
"Type": PLUGIN_JSONRPC_TYPE_RESPONSE,
|
||||
"Result": result
|
||||
}
|
||||
await ws.send(json.dumps(response))
|
||||
except Exception as e:
|
||||
error_response = {
|
||||
"TraceId": trace_id,
|
||||
"Id": msg_data["Id"],
|
||||
"Method": msg_data["Method"],
|
||||
"Type": PLUGIN_JSONRPC_TYPE_RESPONSE,
|
||||
"Error": str(e)
|
||||
}
|
||||
await logger.error(trace_id, f"handle request failed: {str(e)}")
|
||||
await ws.send(json.dumps(error_response))
|
||||
else:
|
||||
await logger.error(trace_id, f"unknown message type: {message}")
|
||||
except Exception as e:
|
||||
await logger.error(str(uuid.uuid4()), f"receive and handle msg error: {message}, err: {str(e)}")
|
||||
|
||||
async def handler(websocket: websockets.WebSocketServerProtocol):
|
||||
"""WebSocket connection handler"""
|
||||
logger.update_websocket(websocket)
|
||||
|
||||
try:
|
||||
async for message in websocket:
|
||||
await handle_message(websocket, message)
|
||||
except websockets.exceptions.ConnectionClosed:
|
||||
await logger.info(str(uuid.uuid4()), "connection closed")
|
||||
except Exception as e:
|
||||
await logger.error(str(uuid.uuid4()), f"connection error: {str(e)}")
|
||||
finally:
|
||||
logger.update_websocket(None)
|
||||
|
||||
async def start_websocket(websocket_port: int):
|
||||
"""Start WebSocket server"""
|
||||
await logger.info(str(uuid.uuid4()), "start websocket server")
|
||||
async with websockets.serve(handler, "", websocket_port):
|
||||
await asyncio.Future() # run forever
|
||||
|
||||
|
||||
def load_plugin():
|
||||
pkgutil.iter_modules(['plugins'])
|
||||
|
|
|
@ -0,0 +1,204 @@
|
|||
import json
|
||||
import importlib.util
|
||||
import sys
|
||||
from typing import Any, Dict, Optional
|
||||
import uuid
|
||||
import websockets
|
||||
import logger
|
||||
from wox_plugin import (
|
||||
Context,
|
||||
Plugin,
|
||||
Query,
|
||||
QueryType,
|
||||
Selection,
|
||||
QueryEnv,
|
||||
Result,
|
||||
new_context_with_value,
|
||||
PluginInitParams
|
||||
)
|
||||
from constants import PLUGIN_JSONRPC_TYPE_REQUEST, PLUGIN_JSONRPC_TYPE_RESPONSE
|
||||
from plugin_manager import plugin_instances, waiting_for_response
|
||||
from plugin_api import PluginAPI
|
||||
|
||||
async def handle_request_from_wox(ctx: Context, request: Dict[str, Any], ws: websockets.WebSocketServerProtocol) -> Any:
|
||||
"""Handle incoming request from Wox"""
|
||||
method = request.get("Method")
|
||||
plugin_name = request.get("PluginName")
|
||||
|
||||
await logger.info(ctx["Values"]["traceId"], f"invoke <{plugin_name}> method: {method}")
|
||||
|
||||
if method == "loadPlugin":
|
||||
return await load_plugin(ctx, request)
|
||||
elif method == "init":
|
||||
return await init_plugin(ctx, request, ws)
|
||||
elif method == "query":
|
||||
return await query(ctx, request)
|
||||
elif method == "action":
|
||||
return await action(ctx, request)
|
||||
elif method == "refresh":
|
||||
return await refresh(ctx, request)
|
||||
elif method == "unloadPlugin":
|
||||
return await unload_plugin(ctx, request)
|
||||
else:
|
||||
await logger.info(ctx["Values"]["traceId"], f"unknown method handler: {method}")
|
||||
raise Exception(f"unknown method handler: {method}")
|
||||
|
||||
async def load_plugin(ctx: Context, request: Dict[str, Any]) -> None:
|
||||
"""Load a plugin"""
|
||||
plugin_directory = request["Params"]["PluginDirectory"]
|
||||
entry = request["Params"]["Entry"]
|
||||
plugin_id = request["PluginId"]
|
||||
plugin_name = request["PluginName"]
|
||||
|
||||
try:
|
||||
# Add plugin directory to Python path
|
||||
if plugin_directory not in sys.path:
|
||||
sys.path.append(plugin_directory)
|
||||
|
||||
# Import the plugin module
|
||||
spec = importlib.util.spec_from_file_location("plugin", entry)
|
||||
if spec is None or spec.loader is None:
|
||||
raise ImportError(f"Could not load plugin from {entry}")
|
||||
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
if not hasattr(module, "plugin"):
|
||||
raise AttributeError("Plugin module does not have a 'plugin' attribute")
|
||||
|
||||
plugin_instances[plugin_id] = {
|
||||
"module": module,
|
||||
"plugin": module.plugin,
|
||||
"directory": plugin_directory,
|
||||
"entry": entry,
|
||||
"name": plugin_name,
|
||||
"api": None
|
||||
}
|
||||
|
||||
await logger.info(ctx["Values"]["traceId"], f"<{plugin_name}> load plugin successfully")
|
||||
except Exception as e:
|
||||
await logger.error(ctx["Values"]["traceId"], f"<{plugin_name}> load plugin failed: {str(e)}")
|
||||
raise e
|
||||
|
||||
async def init_plugin(ctx: Context, request: Dict[str, Any], ws: websockets.WebSocketServerProtocol) -> None:
|
||||
"""Initialize a plugin"""
|
||||
plugin_id = request["PluginId"]
|
||||
plugin = plugin_instances.get(plugin_id)
|
||||
if not plugin:
|
||||
raise Exception(f"plugin not found: {request['PluginName']}, forget to load plugin?")
|
||||
|
||||
try:
|
||||
# Create plugin API instance
|
||||
api = PluginAPI(ws, plugin_id, plugin["name"])
|
||||
plugin["api"] = api
|
||||
|
||||
# Call plugin's init method if it exists
|
||||
if hasattr(plugin["plugin"], "init"):
|
||||
init_params = PluginInitParams(API=api, PluginDirectory=plugin["directory"])
|
||||
await plugin["plugin"].init(ctx, init_params)
|
||||
|
||||
await logger.info(ctx["Values"]["traceId"], f"<{plugin['name']}> init plugin successfully")
|
||||
except Exception as e:
|
||||
await logger.error(ctx["Values"]["traceId"], f"<{plugin['name']}> init plugin failed: {str(e)}")
|
||||
raise e
|
||||
|
||||
async def query(ctx: Context, request: Dict[str, Any]) -> list:
|
||||
"""Handle query request"""
|
||||
plugin_id = request["PluginId"]
|
||||
plugin = plugin_instances.get(plugin_id)
|
||||
if not plugin:
|
||||
raise Exception(f"plugin not found: {request['PluginName']}, forget to load plugin?")
|
||||
|
||||
try:
|
||||
if not hasattr(plugin["plugin"], "query"):
|
||||
return []
|
||||
|
||||
query_params = Query(
|
||||
Type=QueryType(request["Params"]["Type"]),
|
||||
RawQuery=request["Params"]["RawQuery"],
|
||||
TriggerKeyword=request["Params"]["TriggerKeyword"],
|
||||
Command=request["Params"]["Command"],
|
||||
Search=request["Params"]["Search"],
|
||||
Selection=Selection(**json.loads(request["Params"]["Selection"])),
|
||||
Env=QueryEnv(**json.loads(request["Params"]["Env"]))
|
||||
)
|
||||
|
||||
results = await plugin["plugin"].query(ctx, query_params)
|
||||
|
||||
# Ensure each result has an ID
|
||||
if results:
|
||||
for result in results:
|
||||
if not result.Id:
|
||||
result.Id = str(uuid.uuid4())
|
||||
if result.Actions:
|
||||
for action in result.Actions:
|
||||
if not action.Id:
|
||||
action.Id = str(uuid.uuid4())
|
||||
|
||||
return [result.__dict__ for result in results] if results else []
|
||||
except Exception as e:
|
||||
await logger.error(ctx["Values"]["traceId"], f"<{plugin['name']}> query failed: {str(e)}")
|
||||
raise e
|
||||
|
||||
async def action(ctx: Context, request: Dict[str, Any]) -> Any:
|
||||
"""Handle action request"""
|
||||
plugin_id = request["PluginId"]
|
||||
plugin = plugin_instances.get(plugin_id)
|
||||
if not plugin:
|
||||
raise Exception(f"plugin not found: {request['PluginName']}, forget to load plugin?")
|
||||
|
||||
try:
|
||||
action_id = request["Params"]["ActionId"]
|
||||
context_data = request["Params"].get("ContextData")
|
||||
|
||||
# Find the action in the plugin's results
|
||||
if hasattr(plugin["plugin"], "handle_action"):
|
||||
return await plugin["plugin"].handle_action(action_id, context_data)
|
||||
|
||||
return None
|
||||
except Exception as e:
|
||||
await logger.error(ctx["Values"]["traceId"], f"<{plugin['name']}> action failed: {str(e)}")
|
||||
raise e
|
||||
|
||||
async def refresh(ctx: Context, request: Dict[str, Any]) -> Any:
|
||||
"""Handle refresh request"""
|
||||
plugin_id = request["PluginId"]
|
||||
plugin = plugin_instances.get(plugin_id)
|
||||
if not plugin:
|
||||
raise Exception(f"plugin not found: {request['PluginName']}, forget to load plugin?")
|
||||
|
||||
try:
|
||||
result_id = request["Params"]["ResultId"]
|
||||
|
||||
# Find the refresh callback in the plugin's results
|
||||
if hasattr(plugin["plugin"], "handle_refresh"):
|
||||
return await plugin["plugin"].handle_refresh(result_id)
|
||||
|
||||
return None
|
||||
except Exception as e:
|
||||
await logger.error(ctx["Values"]["traceId"], f"<{plugin['name']}> refresh failed: {str(e)}")
|
||||
raise e
|
||||
|
||||
async def unload_plugin(ctx: Context, request: Dict[str, Any]) -> None:
|
||||
"""Unload a plugin"""
|
||||
plugin_id = request["PluginId"]
|
||||
plugin = plugin_instances.get(plugin_id)
|
||||
if not plugin:
|
||||
raise Exception(f"plugin not found: {request['PluginName']}, forget to load plugin?")
|
||||
|
||||
try:
|
||||
# Call plugin's unload method if it exists
|
||||
if hasattr(plugin["plugin"], "unload"):
|
||||
await plugin["plugin"].unload()
|
||||
|
||||
# Remove plugin from instances
|
||||
del plugin_instances[plugin_id]
|
||||
|
||||
# Remove plugin directory from Python path
|
||||
if plugin["directory"] in sys.path:
|
||||
sys.path.remove(plugin["directory"])
|
||||
|
||||
await logger.info(ctx["Values"]["traceId"], f"<{plugin['name']}> unload plugin successfully")
|
||||
except Exception as e:
|
||||
await logger.error(ctx["Values"]["traceId"], f"<{plugin['name']}> unload plugin failed: {str(e)}")
|
||||
raise e
|
|
@ -1,22 +1,41 @@
|
|||
from loguru import logger as loguru_logger
|
||||
import json
|
||||
from typing import Optional
|
||||
import websockets
|
||||
from loguru import logger
|
||||
|
||||
PLUGIN_JSONRPC_TYPE_SYSTEM_LOG = "WOX_JSONRPC_SYSTEM_LOG"
|
||||
websocket: Optional[websockets.WebSocketServerProtocol] = None
|
||||
|
||||
def update_log_directory(log_directory: str) -> None:
|
||||
loguru_logger.remove()
|
||||
loguru_logger.add(f"{log_directory}/python.log", format="{time:YYYY-MM-DD HH:mm:ss.SSS} [{level}] {message}", rotation="100 MB", retention="3 days")
|
||||
def update_log_directory(log_directory: str):
|
||||
"""Update the log directory for the logger"""
|
||||
logger.remove()
|
||||
logger.add(f"{log_directory}/python.log", format="{time} {message}")
|
||||
|
||||
def update_websocket(ws: Optional[websockets.WebSocketServerProtocol]):
|
||||
"""Update the websocket connection for logging"""
|
||||
global websocket
|
||||
websocket = ws
|
||||
|
||||
def debug(trace_id: str, message: str) -> None:
|
||||
__inner_log(trace_id, message, "debug")
|
||||
async def log(trace_id: str, level: str, msg: str):
|
||||
"""Log a message to both file and websocket if available"""
|
||||
logger.log(level.upper(), f"{trace_id} [{level}] {msg}")
|
||||
|
||||
if websocket:
|
||||
try:
|
||||
await websocket.send(json.dumps({
|
||||
"Type": PLUGIN_JSONRPC_TYPE_SYSTEM_LOG,
|
||||
"TraceId": trace_id,
|
||||
"Level": level,
|
||||
"Message": msg
|
||||
}))
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send log message through websocket: {e}")
|
||||
|
||||
def info(trace_id: str, message: str) -> None:
|
||||
__inner_log(trace_id, message, "info")
|
||||
async def debug(trace_id: str, msg: str):
|
||||
await log(trace_id, "debug", msg)
|
||||
|
||||
async def info(trace_id: str, msg: str):
|
||||
await log(trace_id, "info", msg)
|
||||
|
||||
def error(trace_id: str, message: str) -> None:
|
||||
__inner_log(trace_id, message, "error")
|
||||
|
||||
|
||||
def __inner_log(trace_id: str, message: str, level: str) -> None:
|
||||
loguru_logger.log(__level=level, __message=f"{trace_id} {message}")
|
||||
async def error(trace_id: str, msg: str):
|
||||
await log(trace_id, "error", msg)
|
||||
|
|
|
@ -0,0 +1,144 @@
|
|||
import asyncio
|
||||
import json
|
||||
import uuid
|
||||
from typing import Any, Dict, Callable, Optional
|
||||
import websockets
|
||||
import logger
|
||||
from wox_plugin import (
|
||||
Context,
|
||||
PublicAPI,
|
||||
ChangeQueryParam,
|
||||
MetadataCommand,
|
||||
PluginSettingDefinitionItem,
|
||||
MapString,
|
||||
Conversation,
|
||||
ChatStreamFunc,
|
||||
)
|
||||
from jsonrpc import PLUGIN_JSONRPC_TYPE_REQUEST, waiting_for_response
|
||||
|
||||
class PluginAPI(PublicAPI):
|
||||
def __init__(self, ws: websockets.WebSocketServerProtocol, plugin_id: str, plugin_name: str):
|
||||
self.ws = ws
|
||||
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], PluginSettingDefinitionItem]] = {}
|
||||
self.deep_link_callbacks: Dict[str, Callable[[MapString], None]] = {}
|
||||
self.unload_callbacks: Dict[str, Callable[[], None]] = {}
|
||||
self.llm_stream_callbacks: Dict[str, ChatStreamFunc] = {}
|
||||
|
||||
async def invoke_method(self, ctx: Context, method: str, params: Dict[str, Any]) -> Any:
|
||||
"""Invoke a method on Wox"""
|
||||
request_id = str(uuid.uuid4())
|
||||
trace_id = ctx["Values"]["traceId"]
|
||||
|
||||
if method != "Log":
|
||||
await logger.info(trace_id, f"<{self.plugin_name}> start invoke method to Wox: {method}, id: {request_id}")
|
||||
|
||||
request = {
|
||||
"TraceId": trace_id,
|
||||
"Id": request_id,
|
||||
"Method": method,
|
||||
"Type": PLUGIN_JSONRPC_TYPE_REQUEST,
|
||||
"Params": params,
|
||||
"PluginId": self.plugin_id,
|
||||
"PluginName": self.plugin_name
|
||||
}
|
||||
|
||||
await self.ws.send(json.dumps(request))
|
||||
|
||||
# Create a Future to wait for the response
|
||||
future = asyncio.Future()
|
||||
waiting_for_response[request_id] = future
|
||||
|
||||
try:
|
||||
return await future
|
||||
except Exception as e:
|
||||
await logger.error(trace_id, f"invoke method failed: {str(e)}")
|
||||
raise e
|
||||
|
||||
async def change_query(self, ctx: Context, query: ChangeQueryParam) -> None:
|
||||
"""Change the query in Wox"""
|
||||
params = {
|
||||
"QueryType": query.QueryType,
|
||||
"QueryText": query.QueryText,
|
||||
"QuerySelection": query.QuerySelection.__dict__ if query.QuerySelection else None
|
||||
}
|
||||
await self.invoke_method(ctx, "ChangeQuery", params)
|
||||
|
||||
async def hide_app(self, ctx: Context) -> None:
|
||||
"""Hide the Wox window"""
|
||||
await self.invoke_method(ctx, "HideApp", {})
|
||||
|
||||
async def show_app(self, ctx: Context) -> None:
|
||||
"""Show the Wox window"""
|
||||
await self.invoke_method(ctx, "ShowApp", {})
|
||||
|
||||
async def notify(self, ctx: Context, message: str) -> None:
|
||||
"""Show a notification message"""
|
||||
await self.invoke_method(ctx, "Notify", {"message": message})
|
||||
|
||||
async def log(self, ctx: Context, level: str, msg: str) -> None:
|
||||
"""Write log"""
|
||||
await self.invoke_method(ctx, "Log", {
|
||||
"level": level,
|
||||
"message": msg
|
||||
})
|
||||
|
||||
async def get_translation(self, ctx: Context, key: str) -> str:
|
||||
"""Get a translation for a key"""
|
||||
result = await self.invoke_method(ctx, "GetTranslation", {"key": key})
|
||||
return str(result) if result is not None else key
|
||||
|
||||
async def get_setting(self, ctx: Context, key: str) -> str:
|
||||
"""Get a setting value"""
|
||||
result = await self.invoke_method(ctx, "GetSetting", {"key": key})
|
||||
return str(result) if result is not None else ""
|
||||
|
||||
async def save_setting(self, ctx: Context, key: str, value: str, is_platform_specific: bool) -> None:
|
||||
"""Save a setting value"""
|
||||
await self.invoke_method(ctx, "SaveSetting", {
|
||||
"key": key,
|
||||
"value": value,
|
||||
"isPlatformSpecific": is_platform_specific
|
||||
})
|
||||
|
||||
async def on_setting_changed(self, ctx: Context, callback: Callable[[str, str], None]) -> None:
|
||||
"""Register setting changed callback"""
|
||||
callback_id = str(uuid.uuid4())
|
||||
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], PluginSettingDefinitionItem]) -> None:
|
||||
"""Register dynamic setting callback"""
|
||||
callback_id = str(uuid.uuid4())
|
||||
self.get_dynamic_setting_callbacks[callback_id] = callback
|
||||
await self.invoke_method(ctx, "OnGetDynamicSetting", {"callbackId": callback_id})
|
||||
|
||||
async def on_deep_link(self, ctx: Context, callback: Callable[[MapString], None]) -> None:
|
||||
"""Register deep link callback"""
|
||||
callback_id = str(uuid.uuid4())
|
||||
self.deep_link_callbacks[callback_id] = callback
|
||||
await self.invoke_method(ctx, "OnDeepLink", {"callbackId": callback_id})
|
||||
|
||||
async def on_unload(self, ctx: Context, callback: Callable[[], None]) -> None:
|
||||
"""Register unload callback"""
|
||||
callback_id = str(uuid.uuid4())
|
||||
self.unload_callbacks[callback_id] = callback
|
||||
await self.invoke_method(ctx, "OnUnload", {"callbackId": callback_id})
|
||||
|
||||
async def register_query_commands(self, ctx: Context, commands: list[MetadataCommand]) -> None:
|
||||
"""Register query commands"""
|
||||
await self.invoke_method(ctx, "RegisterQueryCommands", {
|
||||
"commands": json.dumps([command.__dict__ for command in commands])
|
||||
})
|
||||
|
||||
async def llm_stream(self, ctx: Context, conversations: list[Conversation], callback: ChatStreamFunc) -> None:
|
||||
"""Chat using LLM"""
|
||||
callback_id = str(uuid.uuid4())
|
||||
self.llm_stream_callbacks[callback_id] = callback
|
||||
await self.invoke_method(ctx, "LLMStream", {
|
||||
"callbackId": callback_id,
|
||||
"conversations": json.dumps([conv.__dict__ for conv in conversations])
|
||||
})
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
"""Plugin manager for handling plugin instances and responses"""
|
||||
from typing import Dict, Any
|
||||
|
||||
# Global state
|
||||
plugin_instances: Dict[str, Dict[str, Any]] = {}
|
||||
waiting_for_response: Dict[str, Any] = {}
|
|
@ -1,2 +1,3 @@
|
|||
websockets==12.0
|
||||
loguru==0.7.2
|
||||
websockets==11.0.3
|
||||
wox-plugin==0.0.1
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
# Wox Plugin Python
|
||||
|
||||
This package provides type definitions for developing Wox plugins in Python.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install wox-plugin
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```python
|
||||
from wox_plugin import Plugin, Query, Result, Context, PluginInitParams
|
||||
|
||||
class MyPlugin(Plugin):
|
||||
async def init(self, ctx: Context, params: PluginInitParams) -> None:
|
||||
self.api = params.API
|
||||
|
||||
async def query(self, ctx: Context, query: Query) -> list[Result]:
|
||||
# Your plugin logic here
|
||||
return []
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
|
@ -1,188 +0,0 @@
|
|||
from typing import List, Dict, Any, Optional, Callable, Union
|
||||
from abc import ABC, abstractmethod
|
||||
from enum import Enum
|
||||
|
||||
class Platform(Enum):
|
||||
WINDOWS = "windows"
|
||||
DARWIN = "darwin"
|
||||
LINUX = "linux"
|
||||
|
||||
class SelectionType(Enum):
|
||||
TEXT = "text"
|
||||
FILE = "file"
|
||||
|
||||
class Selection:
|
||||
def __init__(self, type: SelectionType, text: Optional[str] = None, file_paths: Optional[List[str]] = None):
|
||||
self.Type = type
|
||||
self.Text = text
|
||||
self.FilePaths = file_paths
|
||||
|
||||
class QueryEnv:
|
||||
def __init__(self, active_window_title: str):
|
||||
self.ActiveWindowTitle = active_window_title
|
||||
|
||||
class Query:
|
||||
def __init__(self, type: str, raw_query: str, trigger_keyword: Optional[str], command: Optional[str], search: str, selection: Optional[Selection], env: QueryEnv):
|
||||
self.Type = type
|
||||
self.RawQuery = raw_query
|
||||
self.TriggerKeyword = trigger_keyword
|
||||
self.Command = command
|
||||
self.Search = search
|
||||
self.Selection = selection
|
||||
self.Env = env
|
||||
|
||||
def is_global_query(self) -> bool:
|
||||
return self.TriggerKeyword is None or self.TriggerKeyword == ""
|
||||
|
||||
class WoxImageType(Enum):
|
||||
ABSOLUTE = "absolute"
|
||||
RELATIVE = "relative"
|
||||
BASE64 = "base64"
|
||||
SVG = "svg"
|
||||
URL = "url"
|
||||
EMOJI = "emoji"
|
||||
LOTTIE = "lottie"
|
||||
|
||||
class WoxImage:
|
||||
def __init__(self, image_type: WoxImageType, image_data: str):
|
||||
self.ImageType = image_type
|
||||
self.ImageData = image_data
|
||||
|
||||
class WoxPreviewType(Enum):
|
||||
MARKDOWN = "markdown"
|
||||
TEXT = "text"
|
||||
IMAGE = "image"
|
||||
URL = "url"
|
||||
FILE = "file"
|
||||
|
||||
class WoxPreview:
|
||||
def __init__(self, preview_type: WoxPreviewType, preview_data: str, preview_properties: Dict[str, str]):
|
||||
self.PreviewType = preview_type
|
||||
self.PreviewData = preview_data
|
||||
self.PreviewProperties = preview_properties
|
||||
|
||||
class ResultTail:
|
||||
def __init__(self, type: str, text: Optional[str] = None, image: Optional[WoxImage] = None):
|
||||
self.Type = type
|
||||
self.Text = text
|
||||
self.Image = image
|
||||
|
||||
class ActionContext:
|
||||
def __init__(self, context_data: str):
|
||||
self.ContextData = context_data
|
||||
|
||||
class ResultAction:
|
||||
def __init__(self, id: Optional[str], name: str, icon: Optional[WoxImage], is_default: bool, prevent_hide_after_action: bool, action: Callable[[ActionContext], None], hotkey: Optional[str]):
|
||||
self.Id = id
|
||||
self.Name = name
|
||||
self.Icon = icon
|
||||
self.IsDefault = is_default
|
||||
self.PreventHideAfterAction = prevent_hide_after_action
|
||||
self.Action = action
|
||||
self.Hotkey = hotkey
|
||||
|
||||
class Result:
|
||||
def __init__(self, id: Optional[str], title: str, sub_title: Optional[str], icon: WoxImage, preview: Optional[WoxPreview], score: Optional[float], group: Optional[str], group_score: Optional[float], tails: Optional[List[ResultTail]], context_data: Optional[str], actions: Optional[List[ResultAction]], refresh_interval: Optional[int], on_refresh: Optional[Callable[['RefreshableResult'], 'RefreshableResult']]):
|
||||
self.Id = id
|
||||
self.Title = title
|
||||
self.SubTitle = sub_title
|
||||
self.Icon = icon
|
||||
self.Preview = preview
|
||||
self.Score = score
|
||||
self.Group = group
|
||||
self.GroupScore = group_score
|
||||
self.Tails = tails
|
||||
self.ContextData = context_data
|
||||
self.Actions = actions
|
||||
self.RefreshInterval = refresh_interval
|
||||
self.OnRefresh = on_refresh
|
||||
|
||||
class RefreshableResult:
|
||||
def __init__(self, title: str, sub_title: str, icon: WoxImage, preview: WoxPreview, context_data: str, refresh_interval: int):
|
||||
self.Title = title
|
||||
self.SubTitle = sub_title
|
||||
self.Icon = icon
|
||||
self.Preview = preview
|
||||
self.ContextData = context_data
|
||||
self.RefreshInterval = refresh_interval
|
||||
|
||||
class ChangeQueryParam:
|
||||
def __init__(self, query_type: str, query_text: Optional[str] = None, query_selection: Optional[Selection] = None):
|
||||
self.QueryType = query_type
|
||||
self.QueryText = query_text
|
||||
self.QuerySelection = query_selection
|
||||
|
||||
class Context:
|
||||
def __init__(self):
|
||||
self.Values = {}
|
||||
|
||||
def get(self, key: str) -> Optional[str]:
|
||||
return self.Values.get(key)
|
||||
|
||||
def set(self, key: str, value: str):
|
||||
self.Values[key] = value
|
||||
|
||||
def exists(self, key: str) -> bool:
|
||||
return key in self.Values
|
||||
|
||||
class PublicAPI:
|
||||
@staticmethod
|
||||
async def change_query(ctx: Context, query: ChangeQueryParam):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
async def hide_app(ctx: Context):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
async def show_app(ctx: Context):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
async def notify(ctx: Context, title: str, description: Optional[str] = None):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
async def log(ctx: Context, level: str, msg: str):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
async def get_translation(ctx: Context, key: str) -> str:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
async def get_setting(ctx: Context, key: str) -> str:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
async def save_setting(ctx: Context, key: str, value: str, is_platform_specific: bool):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
async def on_setting_changed(ctx: Context, callback: Callable[[str, str], None]):
|
||||
pass
|
||||
|
||||
class PluginInitParams:
|
||||
def __init__(self, api: PublicAPI, plugin_directory: str):
|
||||
self.API = api
|
||||
self.PluginDirectory = plugin_directory
|
||||
|
||||
class WoxPlugin(ABC):
|
||||
@abstractmethod
|
||||
async def init(self, ctx: Context, init_params: PluginInitParams):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def query(self, ctx: Context, query: Query) -> List[Result]:
|
||||
pass
|
||||
|
||||
def new_context() -> Context:
|
||||
return Context()
|
||||
|
||||
def new_context_with_value(key: str, value: str) -> Context:
|
||||
ctx = Context()
|
||||
ctx.set(key, value)
|
||||
return ctx
|
||||
|
||||
def new_base64_wox_image(image_data: str) -> WoxImage:
|
||||
return WoxImage(WoxImageType.BASE64, image_data)
|
|
@ -0,0 +1,94 @@
|
|||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
def run_command(command: str) -> int:
|
||||
"""Run command and return exit code"""
|
||||
print(f"\n>>> Running: {command}")
|
||||
return subprocess.call(command, shell=True)
|
||||
|
||||
def update_version(version_type: str) -> str:
|
||||
"""Update version number
|
||||
version_type: major, minor, or patch
|
||||
"""
|
||||
# Read setup.py
|
||||
setup_path = Path("setup.py")
|
||||
content = setup_path.read_text()
|
||||
|
||||
# Find current version
|
||||
version_match = re.search(r'version="(\d+)\.(\d+)\.(\d+)"', content)
|
||||
if not version_match:
|
||||
print("Error: Could not find version in setup.py")
|
||||
sys.exit(1)
|
||||
|
||||
major, minor, patch = map(int, version_match.groups())
|
||||
|
||||
# Update version number
|
||||
if version_type == "major":
|
||||
major += 1
|
||||
minor = 0
|
||||
patch = 0
|
||||
elif version_type == "minor":
|
||||
minor += 1
|
||||
patch = 0
|
||||
else: # patch
|
||||
patch += 1
|
||||
|
||||
new_version = f"{major}.{minor}.{patch}"
|
||||
|
||||
# Update setup.py
|
||||
new_content = re.sub(
|
||||
r'version="(\d+)\.(\d+)\.(\d+)"',
|
||||
f'version="{new_version}"',
|
||||
content
|
||||
)
|
||||
setup_path.write_text(new_content)
|
||||
|
||||
# Update __init__.py
|
||||
init_path = Path("wox_plugin/__init__.py")
|
||||
init_content = init_path.read_text()
|
||||
new_init_content = re.sub(
|
||||
r'__version__ = "(\d+)\.(\d+)\.(\d+)"',
|
||||
f'__version__ = "{new_version}"',
|
||||
init_content
|
||||
)
|
||||
init_path.write_text(new_init_content)
|
||||
|
||||
return new_version
|
||||
|
||||
def main():
|
||||
# Check command line arguments
|
||||
if len(sys.argv) != 2 or sys.argv[1] not in ["major", "minor", "patch"]:
|
||||
print("Usage: python publish.py [major|minor|patch]")
|
||||
sys.exit(1)
|
||||
|
||||
version_type = sys.argv[1]
|
||||
|
||||
# Clean previous build files
|
||||
if run_command("rm -rf dist/ build/ *.egg-info"):
|
||||
print("Error: Failed to clean old build files")
|
||||
sys.exit(1)
|
||||
|
||||
# Update version number
|
||||
new_version = update_version(version_type)
|
||||
print(f"Updated version to {new_version}")
|
||||
|
||||
# Build package
|
||||
if run_command("python -m build"):
|
||||
print("Error: Build failed")
|
||||
sys.exit(1)
|
||||
|
||||
# Upload to PyPI
|
||||
if run_command("python -m twine upload dist/*"):
|
||||
print("Error: Upload to PyPI failed")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"\nSuccessfully published version {new_version} to PyPI!")
|
||||
print("Package can be installed with:")
|
||||
print(f"pip install wox-plugin=={new_version}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -0,0 +1 @@
|
|||
python publish.py patch
|
|
@ -0,0 +1,34 @@
|
|||
from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="wox-plugin",
|
||||
version="0.0.1",
|
||||
description="All Python plugins for Wox should use types in this package",
|
||||
long_description=open("README.md").read(),
|
||||
long_description_content_type="text/markdown",
|
||||
author="Wox-launcher",
|
||||
author_email="",
|
||||
url="https://github.com/Wox-launcher/Wox",
|
||||
packages=find_packages(),
|
||||
install_requires=[
|
||||
"typing_extensions>=4.0.0; python_version < '3.8'"
|
||||
],
|
||||
python_requires=">=3.8",
|
||||
classifiers=[
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Operating System :: OS Independent",
|
||||
"Typing :: Typed",
|
||||
],
|
||||
keywords="wox launcher plugin types",
|
||||
project_urls={
|
||||
"Bug Reports": "https://github.com/Wox-launcher/Wox/issues",
|
||||
"Source": "https://github.com/Wox-launcher/Wox",
|
||||
},
|
||||
)
|
|
@ -0,0 +1,55 @@
|
|||
from .types import (
|
||||
# Basic types
|
||||
MapString,
|
||||
Platform,
|
||||
|
||||
# Context
|
||||
Context,
|
||||
new_context,
|
||||
new_context_with_value,
|
||||
|
||||
# Selection
|
||||
SelectionType,
|
||||
Selection,
|
||||
|
||||
# Query
|
||||
QueryType,
|
||||
Query,
|
||||
QueryEnv,
|
||||
|
||||
# Result
|
||||
WoxImageType,
|
||||
WoxImage,
|
||||
new_base64_wox_image,
|
||||
WoxPreviewType,
|
||||
WoxPreview,
|
||||
ResultTailType,
|
||||
ResultTail,
|
||||
ActionContext,
|
||||
ResultAction,
|
||||
Result,
|
||||
RefreshableResult,
|
||||
|
||||
# Plugin API
|
||||
ChangeQueryParam,
|
||||
|
||||
# AI
|
||||
ConversationRole,
|
||||
ChatStreamDataType,
|
||||
Conversation,
|
||||
ChatStreamFunc,
|
||||
|
||||
# Settings
|
||||
PluginSettingDefinitionType,
|
||||
PluginSettingValueStyle,
|
||||
PluginSettingDefinitionValue,
|
||||
PluginSettingDefinitionItem,
|
||||
MetadataCommand,
|
||||
|
||||
# Plugin Interface
|
||||
Plugin,
|
||||
PublicAPI,
|
||||
PluginInitParams,
|
||||
)
|
||||
|
||||
__version__ = "0.0.82"
|
|
@ -0,0 +1,261 @@
|
|||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Dict, List, Optional, Protocol, Union, Callable, Any, TypedDict, Literal
|
||||
import uuid
|
||||
|
||||
# Basic types
|
||||
MapString = Dict[str, str]
|
||||
Platform = Literal["windows", "darwin", "linux"]
|
||||
|
||||
# Context
|
||||
class Context(TypedDict):
|
||||
Values: Dict[str, str]
|
||||
|
||||
def new_context() -> Context:
|
||||
return {"Values": {"traceId": str(uuid.uuid4())}}
|
||||
|
||||
def new_context_with_value(key: str, value: str) -> Context:
|
||||
ctx = new_context()
|
||||
ctx["Values"][key] = value
|
||||
return ctx
|
||||
|
||||
# Selection
|
||||
class SelectionType(str, Enum):
|
||||
TEXT = "text"
|
||||
FILE = "file"
|
||||
|
||||
@dataclass
|
||||
class Selection:
|
||||
Type: SelectionType
|
||||
Text: Optional[str] = None
|
||||
FilePaths: Optional[List[str]] = None
|
||||
|
||||
# Query Environment
|
||||
@dataclass
|
||||
class QueryEnv:
|
||||
ActiveWindowTitle: str
|
||||
|
||||
# Query
|
||||
class QueryType(str, Enum):
|
||||
INPUT = "input"
|
||||
SELECTION = "selection"
|
||||
|
||||
@dataclass
|
||||
class Query:
|
||||
Type: QueryType
|
||||
RawQuery: str
|
||||
TriggerKeyword: Optional[str]
|
||||
Command: Optional[str]
|
||||
Search: str
|
||||
Selection: Selection
|
||||
Env: QueryEnv
|
||||
|
||||
def is_global_query(self) -> bool:
|
||||
return self.Type == QueryType.INPUT and not self.TriggerKeyword
|
||||
|
||||
# Result
|
||||
class WoxImageType(str, Enum):
|
||||
ABSOLUTE = "absolute"
|
||||
RELATIVE = "relative"
|
||||
BASE64 = "base64"
|
||||
SVG = "svg"
|
||||
URL = "url"
|
||||
EMOJI = "emoji"
|
||||
LOTTIE = "lottie"
|
||||
|
||||
@dataclass
|
||||
class WoxImage:
|
||||
ImageType: WoxImageType
|
||||
ImageData: str
|
||||
|
||||
def new_base64_wox_image(image_data: str) -> WoxImage:
|
||||
return WoxImage(ImageType=WoxImageType.BASE64, ImageData=image_data)
|
||||
|
||||
class WoxPreviewType(str, Enum):
|
||||
MARKDOWN = "markdown"
|
||||
TEXT = "text"
|
||||
IMAGE = "image"
|
||||
URL = "url"
|
||||
FILE = "file"
|
||||
|
||||
@dataclass
|
||||
class WoxPreview:
|
||||
PreviewType: WoxPreviewType
|
||||
PreviewData: str
|
||||
PreviewProperties: Dict[str, str]
|
||||
|
||||
class ResultTailType(str, Enum):
|
||||
TEXT = "text"
|
||||
IMAGE = "image"
|
||||
|
||||
@dataclass
|
||||
class ResultTail:
|
||||
Type: ResultTailType
|
||||
Text: Optional[str] = None
|
||||
Image: Optional[WoxImage] = None
|
||||
|
||||
@dataclass
|
||||
class ActionContext:
|
||||
ContextData: str
|
||||
|
||||
@dataclass
|
||||
class ResultAction:
|
||||
Id: Optional[str]
|
||||
Name: str
|
||||
Icon: Optional[WoxImage]
|
||||
IsDefault: Optional[bool]
|
||||
PreventHideAfterAction: Optional[bool]
|
||||
Action: Callable[[ActionContext], None]
|
||||
Hotkey: Optional[str]
|
||||
|
||||
@dataclass
|
||||
class Result:
|
||||
Id: Optional[str]
|
||||
Title: str
|
||||
SubTitle: Optional[str]
|
||||
Icon: WoxImage
|
||||
Preview: Optional[WoxPreview]
|
||||
Score: Optional[float]
|
||||
Group: Optional[str]
|
||||
GroupScore: Optional[float]
|
||||
Tails: Optional[List[ResultTail]]
|
||||
ContextData: Optional[str]
|
||||
Actions: Optional[List[ResultAction]]
|
||||
RefreshInterval: Optional[int]
|
||||
OnRefresh: Optional[Callable[["RefreshableResult"], "RefreshableResult"]]
|
||||
|
||||
@dataclass
|
||||
class RefreshableResult:
|
||||
Title: str
|
||||
SubTitle: str
|
||||
Icon: WoxImage
|
||||
Preview: WoxPreview
|
||||
Tails: List[ResultTail]
|
||||
ContextData: str
|
||||
RefreshInterval: int
|
||||
Actions: List[ResultAction]
|
||||
|
||||
# Plugin API
|
||||
@dataclass
|
||||
class ChangeQueryParam:
|
||||
QueryType: QueryType
|
||||
QueryText: Optional[str]
|
||||
QuerySelection: Optional[Selection]
|
||||
|
||||
# AI
|
||||
class ConversationRole(str, Enum):
|
||||
USER = "user"
|
||||
SYSTEM = "system"
|
||||
|
||||
class ChatStreamDataType(str, Enum):
|
||||
STREAMING = "streaming"
|
||||
FINISHED = "finished"
|
||||
ERROR = "error"
|
||||
|
||||
@dataclass
|
||||
class Conversation:
|
||||
Role: ConversationRole
|
||||
Text: str
|
||||
Timestamp: int
|
||||
|
||||
ChatStreamFunc = Callable[[ChatStreamDataType, str], None]
|
||||
|
||||
# Settings
|
||||
class PluginSettingDefinitionType(str, Enum):
|
||||
HEAD = "head"
|
||||
TEXTBOX = "textbox"
|
||||
CHECKBOX = "checkbox"
|
||||
SELECT = "select"
|
||||
LABEL = "label"
|
||||
NEWLINE = "newline"
|
||||
TABLE = "table"
|
||||
DYNAMIC = "dynamic"
|
||||
|
||||
@dataclass
|
||||
class PluginSettingValueStyle:
|
||||
PaddingLeft: int
|
||||
PaddingTop: int
|
||||
PaddingRight: int
|
||||
PaddingBottom: int
|
||||
Width: int
|
||||
LabelWidth: int
|
||||
|
||||
@dataclass
|
||||
class PluginSettingDefinitionValue:
|
||||
def get_key(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
def get_default_value(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
def translate(self, translator: Callable[[Context, str], str]) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
@dataclass
|
||||
class PluginSettingDefinitionItem:
|
||||
Type: PluginSettingDefinitionType
|
||||
Value: PluginSettingDefinitionValue
|
||||
DisabledInPlatforms: List[Platform]
|
||||
IsPlatformSpecific: bool
|
||||
|
||||
@dataclass
|
||||
class MetadataCommand:
|
||||
Command: str
|
||||
Description: str
|
||||
|
||||
# Plugin Interface
|
||||
class Plugin(Protocol):
|
||||
async def init(self, ctx: Context, init_params: "PluginInitParams") -> None:
|
||||
...
|
||||
|
||||
async def query(self, ctx: Context, query: Query) -> List[Result]:
|
||||
...
|
||||
|
||||
# Public API Interface
|
||||
class PublicAPI(Protocol):
|
||||
async def change_query(self, ctx: Context, query: ChangeQueryParam) -> None:
|
||||
...
|
||||
|
||||
async def hide_app(self, ctx: Context) -> None:
|
||||
...
|
||||
|
||||
async def show_app(self, ctx: Context) -> None:
|
||||
...
|
||||
|
||||
async def notify(self, ctx: Context, message: str) -> None:
|
||||
...
|
||||
|
||||
async def log(self, ctx: Context, level: str, msg: str) -> None:
|
||||
...
|
||||
|
||||
async def get_translation(self, ctx: Context, key: str) -> str:
|
||||
...
|
||||
|
||||
async def get_setting(self, ctx: Context, key: str) -> str:
|
||||
...
|
||||
|
||||
async def save_setting(self, ctx: Context, key: str, value: str, is_platform_specific: bool) -> None:
|
||||
...
|
||||
|
||||
async def on_setting_changed(self, ctx: Context, callback: Callable[[str, str], None]) -> None:
|
||||
...
|
||||
|
||||
async def on_get_dynamic_setting(self, ctx: Context, callback: Callable[[str], PluginSettingDefinitionItem]) -> None:
|
||||
...
|
||||
|
||||
async def on_deep_link(self, ctx: Context, callback: Callable[[MapString], None]) -> None:
|
||||
...
|
||||
|
||||
async def on_unload(self, ctx: Context, callback: Callable[[], None]) -> None:
|
||||
...
|
||||
|
||||
async def register_query_commands(self, ctx: Context, commands: List[MetadataCommand]) -> None:
|
||||
...
|
||||
|
||||
async def llm_stream(self, ctx: Context, conversations: List[Conversation], callback: ChatStreamFunc) -> None:
|
||||
...
|
||||
|
||||
@dataclass
|
||||
class PluginInitParams:
|
||||
API: PublicAPI
|
||||
PluginDirectory: str
|
|
@ -2,9 +2,14 @@ package host
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
"wox/plugin"
|
||||
"wox/util"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
@ -25,7 +30,61 @@ func (n *PythonHost) GetRuntime(ctx context.Context) plugin.Runtime {
|
|||
}
|
||||
|
||||
func (n *PythonHost) Start(ctx context.Context) error {
|
||||
return n.websocketHost.StartHost(ctx, "python", path.Join(util.GetLocation().GetHostDirectory(), "python-host.pyz"))
|
||||
return n.websocketHost.StartHost(ctx, n.findPythonPath(ctx), path.Join(util.GetLocation().GetHostDirectory(), "python-host.pyz"))
|
||||
}
|
||||
|
||||
func (n *PythonHost) findPythonPath(ctx context.Context) string {
|
||||
util.GetLogger().Debug(ctx, "start finding python path")
|
||||
|
||||
var possiblePythonPaths = []string{
|
||||
"/opt/homebrew/bin/python3",
|
||||
"/usr/local/bin/python3",
|
||||
"/usr/bin/python3",
|
||||
"/usr/local/python3",
|
||||
}
|
||||
|
||||
pyenvPaths, _ := homedir.Expand("~/.pyenv/versions")
|
||||
if util.IsDirExists(pyenvPaths) {
|
||||
versions, _ := util.ListDir(pyenvPaths)
|
||||
for _, v := range versions {
|
||||
possiblePythonPaths = append(possiblePythonPaths, path.Join(pyenvPaths, v, "bin", "python3"))
|
||||
}
|
||||
}
|
||||
|
||||
foundVersion, _ := semver.NewVersion("v0.0.1")
|
||||
foundPath := ""
|
||||
for _, p := range possiblePythonPaths {
|
||||
if util.IsFileExists(p) {
|
||||
versionOriginal, versionErr := util.ShellRunOutput(p, "--version")
|
||||
if versionErr != nil {
|
||||
util.GetLogger().Error(ctx, fmt.Sprintf("failed to get python version: %s, path=%s", versionErr, p))
|
||||
continue
|
||||
}
|
||||
// Python version output format is like "Python 3.9.0"
|
||||
version := strings.TrimSpace(string(versionOriginal))
|
||||
version = strings.TrimPrefix(version, "Python ")
|
||||
version = "v" + version
|
||||
installedVersion, err := semver.NewVersion(version)
|
||||
if err != nil {
|
||||
util.GetLogger().Error(ctx, fmt.Sprintf("failed to parse python version: %s, path=%s", err, p))
|
||||
continue
|
||||
}
|
||||
util.GetLogger().Debug(ctx, fmt.Sprintf("found python path: %s, version: %s", p, installedVersion.String()))
|
||||
|
||||
if installedVersion.GreaterThan(foundVersion) {
|
||||
foundPath = p
|
||||
foundVersion = installedVersion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if foundPath != "" {
|
||||
util.GetLogger().Info(ctx, fmt.Sprintf("finally use python path: %s, version: %s", foundPath, foundVersion.String()))
|
||||
return foundPath
|
||||
}
|
||||
|
||||
util.GetLogger().Info(ctx, "finally use default python3 from env path")
|
||||
return "python3"
|
||||
}
|
||||
|
||||
func (n *PythonHost) IsStarted(ctx context.Context) bool {
|
||||
|
|
|
@ -141,8 +141,7 @@ func (m *Manager) loadPlugins(ctx context.Context) error {
|
|||
}
|
||||
logger.Info(ctx, fmt.Sprintf("start loading user plugins, found %d user plugins", len(metaDataList)))
|
||||
|
||||
for _, h := range AllHosts {
|
||||
host := h
|
||||
for _, host := range AllHosts {
|
||||
util.Go(ctx, fmt.Sprintf("[%s] start host", host.GetRuntime(ctx)), func() {
|
||||
newCtx := util.NewTraceContext()
|
||||
hostErr := host.Start(newCtx)
|
||||
|
|
Loading…
Reference in New Issue