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/
|
Release/
|
||||||
__debug_bin*
|
__debug_bin*
|
||||||
Wox/log/
|
Wox/log/
|
||||||
|
Wox.Plugin.Python/dist/
|
||||||
|
Wox.Plugin.Python/wox_plugin.egg-info/
|
|
@ -1,22 +1,51 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import sys
|
import sys
|
||||||
import uuid
|
import uuid
|
||||||
|
import os
|
||||||
|
|
||||||
import logger
|
import logger
|
||||||
from host import start_websocket
|
from host import start_websocket
|
||||||
|
|
||||||
if len(sys.argv) != 4:
|
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)
|
sys.exit(1)
|
||||||
|
|
||||||
port = int(sys.argv[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()}"
|
host_id = f"python-{uuid.uuid4()}"
|
||||||
logger.update_log_directory(log_directory)
|
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
|
#!/usr/bin/env python
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import pkgutil
|
import json
|
||||||
|
import uuid
|
||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
import websockets
|
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):
|
if PLUGIN_JSONRPC_TYPE_RESPONSE in message:
|
||||||
while True:
|
# Handle response from Wox
|
||||||
message = await websocket.recv()
|
if msg_data.get("Id") in waiting_for_response:
|
||||||
logger.info(message)
|
deferred = waiting_for_response[msg_data["Id"]]
|
||||||
# my_module = importlib.import_module('os.path')
|
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):
|
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):
|
async with websockets.serve(handler, "", websocket_port):
|
||||||
await asyncio.Future() # run forever
|
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:
|
def update_log_directory(log_directory: str):
|
||||||
loguru_logger.remove()
|
"""Update the log directory for the logger"""
|
||||||
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")
|
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:
|
async def log(trace_id: str, level: str, msg: str):
|
||||||
__inner_log(trace_id, message, "debug")
|
"""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}")
|
||||||
|
|
||||||
|
async def debug(trace_id: str, msg: str):
|
||||||
|
await log(trace_id, "debug", msg)
|
||||||
|
|
||||||
def info(trace_id: str, message: str) -> None:
|
async def info(trace_id: str, msg: str):
|
||||||
__inner_log(trace_id, message, "info")
|
await log(trace_id, "info", msg)
|
||||||
|
|
||||||
|
async def error(trace_id: str, msg: str):
|
||||||
def error(trace_id: str, message: str) -> None:
|
await log(trace_id, "error", msg)
|
||||||
__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}")
|
|
||||||
|
|
|
@ -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
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"path"
|
"path"
|
||||||
|
"strings"
|
||||||
"wox/plugin"
|
"wox/plugin"
|
||||||
"wox/util"
|
"wox/util"
|
||||||
|
|
||||||
|
"github.com/Masterminds/semver/v3"
|
||||||
|
"github.com/mitchellh/go-homedir"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -25,7 +30,61 @@ func (n *PythonHost) GetRuntime(ctx context.Context) plugin.Runtime {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *PythonHost) Start(ctx context.Context) error {
|
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 {
|
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)))
|
logger.Info(ctx, fmt.Sprintf("start loading user plugins, found %d user plugins", len(metaDataList)))
|
||||||
|
|
||||||
for _, h := range AllHosts {
|
for _, host := range AllHosts {
|
||||||
host := h
|
|
||||||
util.Go(ctx, fmt.Sprintf("[%s] start host", host.GetRuntime(ctx)), func() {
|
util.Go(ctx, fmt.Sprintf("[%s] start host", host.GetRuntime(ctx)), func() {
|
||||||
newCtx := util.NewTraceContext()
|
newCtx := util.NewTraceContext()
|
||||||
hostErr := host.Start(newCtx)
|
hostErr := host.Start(newCtx)
|
||||||
|
|
Loading…
Reference in New Issue