mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 09:28:14 +00:00
fix(clawhubadapter): manager CLI improvements, pysimulator updates, sidecar fixes
This commit is contained in:
@ -16,6 +16,79 @@ function readJSON(file) {
|
||||
try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch (e) { return null; }
|
||||
}
|
||||
|
||||
// ---- Enhanced OC Plugin Detection ----
|
||||
|
||||
function resolvePackageEntry(dir, pkg) {
|
||||
const candidates = [];
|
||||
|
||||
if (pkg) {
|
||||
if (pkg.main) candidates.push(path.resolve(dir, pkg.main));
|
||||
if (pkg.exports) {
|
||||
const exp = pkg.exports;
|
||||
if (typeof exp === 'string') candidates.push(path.resolve(dir, exp));
|
||||
if (exp['.']) {
|
||||
const dot = exp['.'];
|
||||
if (typeof dot === 'string') candidates.push(path.resolve(dir, dot));
|
||||
if (dot.require) candidates.push(path.resolve(dir, dot.require));
|
||||
if (dot.default) candidates.push(path.resolve(dir, dot.default));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!pkg || !pkg.main) {
|
||||
for (const name of ['index.js', 'main.js', 'src/index.js', 'lib/index.js']) {
|
||||
candidates.push(path.join(dir, name));
|
||||
}
|
||||
}
|
||||
|
||||
for (const cp of candidates) {
|
||||
if (fs.existsSync(cp)) return cp;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function ensureOCManifest(dir, name) {
|
||||
const manifestPath = path.join(dir, 'openclaw.plugin.json');
|
||||
if (fs.existsSync(manifestPath)) return;
|
||||
|
||||
const pkg = readJSON(path.join(dir, 'package.json'));
|
||||
const entry = resolvePackageEntry(dir, pkg);
|
||||
const relEntry = entry ? path.relative(dir, entry) : 'index.js';
|
||||
|
||||
const manifest = {
|
||||
name: name,
|
||||
version: (pkg && pkg.version) || '1.0.0',
|
||||
entry: relEntry,
|
||||
description: (pkg && pkg.description) || 'OpenClaw plugin (auto-detected)'
|
||||
};
|
||||
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
||||
process.stderr.write(`[manager] created synthetic manifest: ${manifestPath}\n`);
|
||||
}
|
||||
|
||||
function tryDetectOCPackage(pkgDir, pkgName) {
|
||||
const pkg = readJSON(path.join(pkgDir, 'package.json'));
|
||||
if (!pkg) return null;
|
||||
|
||||
const entryPath = resolvePackageEntry(pkgDir, pkg);
|
||||
if (!entryPath) return null;
|
||||
|
||||
try {
|
||||
delete require.cache[require.resolve(entryPath)];
|
||||
const mod = require(entryPath);
|
||||
const entry = mod.default || mod;
|
||||
if (entry && typeof entry === 'object' && typeof entry.register === 'function') {
|
||||
const name = pkgName || (pkg.openclaw ? (pkg.openclaw.name || pkg.name) : pkg.name) || path.basename(pkgDir);
|
||||
ensureOCManifest(pkgDir, name);
|
||||
process.stderr.write(`[manager] enhanced detection found OC plugin: ${name} (via require)\n`);
|
||||
return { dir: pkgDir, name };
|
||||
}
|
||||
} catch (e) {
|
||||
process.stderr.write(`[manager] require detect failed for ${pkgDir}: ${e.message}\n`);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ---- Plugin registry ----
|
||||
const loadedPlugins = {}; // name -> { entry, tools: [{name, execute, ...}] }
|
||||
const allTools = []; // flat list of all tools across all plugins
|
||||
@ -180,6 +253,27 @@ function installNPMPackage(spec, skillsDir) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundPluginDir) {
|
||||
process.stderr.write(`[manager] standard scan failed, trying enhanced detection...\n`);
|
||||
for (const entry of entries) {
|
||||
if (foundPluginDir) break;
|
||||
const dir = path.join(nm, entry);
|
||||
if (!fs.statSync(dir).isDirectory()) continue;
|
||||
|
||||
if (entry.startsWith('@')) {
|
||||
for (const sub of fs.readdirSync(dir)) {
|
||||
if (foundPluginDir) break;
|
||||
const subDir = path.join(dir, sub);
|
||||
const detected = tryDetectOCPackage(subDir, entry + '/' + sub);
|
||||
if (detected) { foundPluginDir = detected.dir; foundName = detected.name; }
|
||||
}
|
||||
} else {
|
||||
const detected = tryDetectOCPackage(dir, entry);
|
||||
if (detected) { foundPluginDir = detected.dir; foundName = detected.name; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundPluginDir) {
|
||||
fs.rmSync(installDir, { recursive: true, force: true });
|
||||
return { error: `no OC plugin found in installed package "${spec}"` };
|
||||
@ -207,29 +301,195 @@ function cpSync(src, dst) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- OpenClaw CLI compatibility ----
|
||||
|
||||
function showQR(text) {
|
||||
try {
|
||||
const qrcode = require('qrcode');
|
||||
qrcode.generate(text, { small: true }, (qr) => process.stdout.write(qr + '\n'));
|
||||
} catch {
|
||||
process.stdout.write(`QR: ${text}\n`);
|
||||
process.stdout.write('(install qrcode package for QR display: npm install qrcode)\n');
|
||||
}
|
||||
}
|
||||
|
||||
async function runCLI(skillsDir, cliArgs) {
|
||||
const cmd = cliArgs[0] || '';
|
||||
|
||||
switch (cmd) {
|
||||
case 'plugin:install': {
|
||||
const spec = cliArgs[1];
|
||||
if (!spec) throw new Error('Usage: openclaw plugin:install <npm:package|clawhub:name|path>');
|
||||
const result = installNPMPackage(spec, skillsDir);
|
||||
if (result.error) throw new Error(result.error);
|
||||
console.log(`Installed: ${result.name}`);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'plugin:uninstall': {
|
||||
const name = cliArgs[1];
|
||||
if (!name) throw new Error('Usage: openclaw plugin:uninstall <name>');
|
||||
const targetDir = path.join(skillsDir, name);
|
||||
if (!fs.existsSync(targetDir)) throw new Error(`Plugin not found: ${name}`);
|
||||
fs.rmSync(targetDir, { recursive: true, force: true });
|
||||
console.log(`Uninstalled: ${name}`);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'plugin:list': {
|
||||
if (!fs.existsSync(skillsDir)) { console.log('(no plugins)'); break; }
|
||||
let count = 0;
|
||||
for (const entry of fs.readdirSync(skillsDir)) {
|
||||
if (entry.startsWith('.')) continue;
|
||||
const pluginDir = path.join(skillsDir, entry);
|
||||
if (!fs.statSync(pluginDir).isDirectory()) continue;
|
||||
const pkg = readJSON(path.join(pluginDir, 'package.json'));
|
||||
const manifest = readJSON(path.join(pluginDir, 'openclaw.plugin.json'));
|
||||
if (pkg || manifest) {
|
||||
const version = pkg?.version || manifest?.version || '?';
|
||||
const desc = pkg?.description || manifest?.description || '';
|
||||
console.log(` ${entry} v${version}${desc ? ' — ' + desc : ''}`);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
if (count === 0) console.log('(no OpenClaw plugins)');
|
||||
break;
|
||||
}
|
||||
|
||||
case 'auth:login':
|
||||
case 'auth:qrcode': {
|
||||
const url = cliArgs[1] || 'openclaw://auth';
|
||||
console.log('Scan the QR code to log in:');
|
||||
showQR(url);
|
||||
console.log('\nOr open this URL:');
|
||||
console.log(` ${url}`);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'auth:status': {
|
||||
console.log('Auth status: not implemented (running in HomeAgent mode)');
|
||||
break;
|
||||
}
|
||||
|
||||
case 'config:get': {
|
||||
const key = cliArgs[1];
|
||||
if (!key) throw new Error('Usage: openclaw config:get <key>');
|
||||
// TODO: read from HomeAgent config system when bridged
|
||||
console.log(`(not available in CLI mode: ${key})`);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'config:set': {
|
||||
const key = cliArgs[1];
|
||||
const value = cliArgs[2];
|
||||
if (!key || value === undefined) throw new Error('Usage: openclaw config:set <key> <value>');
|
||||
// TODO: write to HomeAgent config system when bridged
|
||||
console.log(`(not available in CLI mode: ${key}=${value})`);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'config:list':
|
||||
console.log('(not available in CLI mode)');
|
||||
break;
|
||||
|
||||
case 'env': {
|
||||
const info = {
|
||||
homeAgent: true,
|
||||
openclawVersion: 'compatible',
|
||||
platform: process.platform,
|
||||
nodeVersion: process.version,
|
||||
skillsDir,
|
||||
};
|
||||
console.log(JSON.stringify(info, null, 2));
|
||||
break;
|
||||
}
|
||||
|
||||
case '--version':
|
||||
case 'version':
|
||||
console.log('HomeAgent OpenClaw Adapter 1.0.0 (openclaw-compatible)');
|
||||
break;
|
||||
|
||||
case 'help':
|
||||
case '--help':
|
||||
console.log(`Usage: openclaw <command> [args]
|
||||
|
||||
Commands:
|
||||
plugin:install <spec> Install a plugin (npm:xxx, clawhub:xxx, or path)
|
||||
plugin:uninstall <name> Uninstall a plugin
|
||||
plugin:list List installed plugins
|
||||
auth:login [url] Show QR code for login/binding
|
||||
auth:status Check authentication status
|
||||
config:get <key> Get config value
|
||||
config:set <key> <val> Set config value
|
||||
config:list List all config
|
||||
env Show runtime environment info
|
||||
--version Show version
|
||||
help Show this help`);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown command: ${cmd}\nRun 'openclaw help' for usage.`);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Main ----
|
||||
const args = process.argv.slice(2);
|
||||
if (args.length < 1) {
|
||||
process.stderr.write('[manager] usage: node main.js <skills-dir>\n');
|
||||
process.stderr.write('[manager] usage: node main.js <skills-dir> [openclaw-command...]\n');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const skillsDir = path.resolve(args[0]);
|
||||
|
||||
// CLI mode: if additional args provided, run as openclaw CLI command and exit
|
||||
if (args.length > 1) {
|
||||
const cliArgs = args.slice(1);
|
||||
runCLI(skillsDir, cliArgs).then(() => process.exit(0)).catch(e => {
|
||||
process.stderr.write(`Error: ${e.message}\n`);
|
||||
process.exit(1);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Server mode: persist skills dir reference for CLI wrapper
|
||||
try {
|
||||
const simDir = path.dirname(process.argv[1]);
|
||||
fs.writeFileSync(path.join(simDir, '.skillsdir'), skillsDir);
|
||||
} catch (e) {
|
||||
process.stderr.write(`[manager] warning: could not write .skillsdir: ${e.message}\n`);
|
||||
}
|
||||
|
||||
// Add our bin directory to PATH so subprocesses can find 'openclaw' CLI
|
||||
try {
|
||||
const binDir = path.join(path.dirname(process.argv[1]), 'bin');
|
||||
if (fs.existsSync(binDir)) {
|
||||
const PATH = process.env.PATH || '';
|
||||
if (!PATH.includes(binDir)) {
|
||||
process.env.PATH = binDir + path.delimiter + PATH;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
process.stderr.write(`[manager] warning: could not update PATH: ${e.message}\n`);
|
||||
}
|
||||
|
||||
// Load existing plugins on startup
|
||||
process.stderr.write(`[manager] scanning: ${skillsDir}\n`);
|
||||
if (fs.existsSync(skillsDir)) {
|
||||
for (const entry of fs.readdirSync(skillsDir)) {
|
||||
if (entry.startsWith('.')) continue; // skip hidden
|
||||
if (entry.startsWith('.')) continue;
|
||||
const pluginDir = path.join(skillsDir, entry);
|
||||
if (!fs.statSync(pluginDir).isDirectory()) continue;
|
||||
if (fs.existsSync(path.join(pluginDir, 'main.js')) || fs.existsSync(path.join(pluginDir, 'main.py'))) {
|
||||
process.stderr.write(`[manager] skip non-OC plugin: ${entry} (main.js/main.py)\n`);
|
||||
continue;
|
||||
}
|
||||
if (fs.existsSync(path.join(pluginDir, 'openclaw.plugin.json')) ||
|
||||
(fs.existsSync(path.join(pluginDir, 'package.json')) && readJSON(path.join(pluginDir, 'package.json'))?.openclaw)) {
|
||||
loadPlugin(pluginDir, entry);
|
||||
} else {
|
||||
const detected = tryDetectOCPackage(pluginDir, entry);
|
||||
if (detected) {
|
||||
loadPlugin(detected.dir, detected.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -301,6 +561,66 @@ rl.on('line', async (line) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (method === 'plugins/detect') {
|
||||
const pluginDir = req.params?.dir;
|
||||
const name = req.params?.name || (pluginDir ? path.basename(pluginDir) : '');
|
||||
if (!pluginDir) { sendError(id, -32602, 'dir required'); return; }
|
||||
|
||||
const resolvedDir = path.resolve(pluginDir);
|
||||
if (!fs.existsSync(resolvedDir)) {
|
||||
sendError(id, -32601, `directory not found: ${resolvedDir}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (loadedPlugins[name]) {
|
||||
writeJSON({ jsonrpc: '2.0', id, result: { name, tools: loadedPlugins[name].tools.map(t => t.name), type: 'loaded' } });
|
||||
return;
|
||||
}
|
||||
|
||||
// Install npm dependencies if package.json exists with deps
|
||||
const pkgPath = path.join(resolvedDir, 'package.json');
|
||||
if (fs.existsSync(pkgPath)) {
|
||||
const pkg = readJSON(pkgPath);
|
||||
if (pkg) {
|
||||
const hasDeps = (pkg.dependencies && Object.keys(pkg.dependencies).length > 0) ||
|
||||
(pkg.devDependencies && Object.keys(pkg.devDependencies).length > 0);
|
||||
if (hasDeps) {
|
||||
try {
|
||||
execSync(`npm install --no-save --prefix "${resolvedDir}"`, {
|
||||
cwd: resolvedDir, stdio: ['pipe', 'pipe', 'pipe'],
|
||||
timeout: 120000, env: { ...process.env, NODE_PATH: path.join(resolvedDir, 'node_modules') }
|
||||
});
|
||||
process.stderr.write(`[manager] installed dependencies for ${name}\n`);
|
||||
} catch (e) {
|
||||
process.stderr.write(`[manager] npm install failed for ${name}: ${e.message}\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let detected = null;
|
||||
if (fs.existsSync(path.join(resolvedDir, 'openclaw.plugin.json')) ||
|
||||
(fs.existsSync(path.join(resolvedDir, 'package.json')) && readJSON(path.join(resolvedDir, 'package.json'))?.openclaw)) {
|
||||
detected = { dir: resolvedDir, name };
|
||||
} else {
|
||||
detected = tryDetectOCPackage(resolvedDir, name);
|
||||
}
|
||||
|
||||
if (!detected) {
|
||||
sendError(id, -32601, `no OC plugin detected in: ${resolvedDir}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const ok = loadPlugin(detected.dir, detected.name);
|
||||
if (!ok) {
|
||||
sendError(id, -32603, `failed to load detected plugin: ${detected.name}`);
|
||||
return;
|
||||
}
|
||||
|
||||
writeJSON({ jsonrpc: '2.0', id, result: { name: detected.name, tools: loadedPlugins[detected.name].tools.map(t => t.name), type: detected === Object(detected) && detected.dir === resolvedDir ? 'detected' : 'standard' } });
|
||||
return;
|
||||
}
|
||||
|
||||
if (method === 'tools/list') {
|
||||
const tools = allTools.map(t => ({
|
||||
name: t.name,
|
||||
|
||||
22
internal/plugins/clawhubadapter/manager/openclaw_cli.js
Normal file
22
internal/plugins/clawhubadapter/manager/openclaw_cli.js
Normal file
@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env node
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { fork } = require('child_process');
|
||||
|
||||
const binDir = __dirname;
|
||||
const skillsdirPath = path.join(binDir, '..', '.skillsdir');
|
||||
|
||||
if (!fs.existsSync(skillsdirPath)) {
|
||||
process.stderr.write('Error: OpenClaw manager not running (no .skillsdir found)\n');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const skillsDir = fs.readFileSync(skillsdirPath, 'utf8').trim();
|
||||
const managerPath = path.join(binDir, '..', 'manager.js');
|
||||
|
||||
const proc = fork(managerPath, [skillsDir, ...process.argv.slice(2)], {
|
||||
stdio: 'inherit',
|
||||
env: { ...process.env, OPENCLAW_CLI: '1' },
|
||||
});
|
||||
|
||||
proc.on('exit', (code) => process.exit(code));
|
||||
@ -30,6 +30,9 @@ var managerSrc string
|
||||
//go:embed pysimulator/main.py
|
||||
var pySimulatorSrc string
|
||||
|
||||
//go:embed manager/openclaw_cli.js
|
||||
var openclawCliSrc string
|
||||
|
||||
var SkillsDir string
|
||||
var SimulatorDir string
|
||||
|
||||
@ -217,6 +220,16 @@ func (p *Plugin) launchManager(s *sdk.PluginSDK) error {
|
||||
return fmt.Errorf("write manager: %w", err)
|
||||
}
|
||||
|
||||
// Write openclaw CLI wrapper so plugins can exec 'openclaw plugin:install' etc.
|
||||
cliBinDir := filepath.Join(p.simulatorDir, "bin")
|
||||
if err := os.MkdirAll(cliBinDir, 0755); err != nil {
|
||||
return fmt.Errorf("create cli bin dir: %w", err)
|
||||
}
|
||||
openclawPath := filepath.Join(cliBinDir, "openclaw")
|
||||
if err := os.WriteFile(openclawPath, []byte(openclawCliSrc), 0755); err != nil {
|
||||
return fmt.Errorf("write openclaw CLI: %w", err)
|
||||
}
|
||||
|
||||
// Ensure skills dir exists for the manager to scan
|
||||
os.MkdirAll(p.skillsDir, 0755)
|
||||
|
||||
@ -323,6 +336,30 @@ func (p *Plugin) installFromClawHub(spec string) (interface{}, error) {
|
||||
}
|
||||
|
||||
if err := p.reloadPlugin(name); err != nil {
|
||||
// Fallback: try manager's enhanced detection
|
||||
p.mu.Lock()
|
||||
mgr := p.manager
|
||||
p.mu.Unlock()
|
||||
|
||||
if mgr != nil {
|
||||
data, mgrErr := mgr.call("plugins/detect", map[string]interface{}{
|
||||
"dir": extractDir,
|
||||
"name": name,
|
||||
})
|
||||
if mgrErr == nil {
|
||||
var result struct {
|
||||
Name string `json:"name"`
|
||||
Tools []string `json:"tools"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
if json.Unmarshal(data, &result) == nil {
|
||||
return map[string]interface{}{
|
||||
"content": fmt.Sprintf("已从 ClawHub 安装插件: %s\n 工具: %s", result.Name, strings.Join(result.Tools, ", ")),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errorResult(fmt.Sprintf("loaded but with warning: %v", err)), nil
|
||||
}
|
||||
|
||||
|
||||
@ -14,11 +14,14 @@ Plugin main.py should define:
|
||||
def register(api):
|
||||
api.register_tool(name, description, parameters, execute_fn)
|
||||
"""
|
||||
import asyncio
|
||||
import importlib.util
|
||||
import inspect
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def resolve_entry(plugin_dir):
|
||||
"""Resolve the plugin entry point."""
|
||||
main_py = os.path.join(plugin_dir, 'main.py')
|
||||
@ -45,7 +48,6 @@ class PluginAPI:
|
||||
def register_tool(self, name, description='', parameters=None, execute=None):
|
||||
if parameters is None:
|
||||
parameters = {'type': 'object', 'properties': {}}
|
||||
# Notify Go side
|
||||
self._notify('register', {
|
||||
'type': 'tool',
|
||||
'data': {
|
||||
@ -79,8 +81,261 @@ class PluginAPI:
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
# ---- Legacy Python Plugin Detection ----
|
||||
|
||||
def _resolve_env_vars(plugin_dir):
|
||||
"""Read env var requirements from skill.json and resolve from OS env."""
|
||||
skill_json = os.path.join(plugin_dir, 'skill.json')
|
||||
if not os.path.isfile(skill_json):
|
||||
return {}
|
||||
try:
|
||||
with open(skill_json, 'r') as f:
|
||||
meta = json.load(f)
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
env_map = {}
|
||||
for key in meta.get('required_env_vars', []):
|
||||
val = os.environ.get(key) or os.environ.get(key.lower())
|
||||
if val:
|
||||
env_map[key.lower()] = val
|
||||
for key in meta.get('optional_env_vars', []):
|
||||
val = os.environ.get(key) or os.environ.get(key.lower())
|
||||
if val:
|
||||
env_map[key.lower()] = val
|
||||
|
||||
return env_map
|
||||
|
||||
|
||||
def _collect_sub_skills(instance):
|
||||
"""Discover sub-module names from a class instance by finding attributes with execute methods.
|
||||
|
||||
Prefer instance __dict__ attributes over properties to avoid triggering side effects.
|
||||
"""
|
||||
skills = []
|
||||
seen = set()
|
||||
|
||||
# First look at instance __dict__ (safe, no property triggers)
|
||||
for attr_name in list(instance.__dict__.keys()):
|
||||
if attr_name.startswith('_'):
|
||||
continue
|
||||
attr = instance.__dict__[attr_name]
|
||||
seen.add(attr_name)
|
||||
if hasattr(attr, 'execute') and callable(attr.execute):
|
||||
try:
|
||||
sig = inspect.signature(attr.execute)
|
||||
params = list(sig.parameters.keys())
|
||||
if 'action' in params and len(params) >= 2:
|
||||
skills.append(attr_name)
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
|
||||
# Also check class-level non-property attributes
|
||||
for attr_name in dir(type(instance)):
|
||||
if attr_name.startswith('_') or attr_name in seen:
|
||||
continue
|
||||
try:
|
||||
attr = getattr(type(instance), attr_name, None)
|
||||
except Exception:
|
||||
continue
|
||||
if isinstance(attr, property):
|
||||
continue # skip properties (may raise on access)
|
||||
try:
|
||||
attr = getattr(instance, attr_name, None)
|
||||
except Exception:
|
||||
continue
|
||||
if attr is None:
|
||||
continue
|
||||
seen.add(attr_name)
|
||||
if hasattr(attr, 'execute') and callable(attr.execute):
|
||||
try:
|
||||
sig = inspect.signature(attr.execute)
|
||||
params = list(sig.parameters.keys())
|
||||
if 'action' in params and len(params) >= 2:
|
||||
skills.append(attr_name)
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
|
||||
return skills
|
||||
|
||||
|
||||
def _readable_class_name(cls):
|
||||
"""Convert PascalCase to snake_case for readable names."""
|
||||
import re
|
||||
name = cls.__name__
|
||||
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
|
||||
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
|
||||
|
||||
|
||||
def _make_async_execute(instance, skill_name):
|
||||
"""Wrap an async sub-module execute into a sync callable for JSON-RPC."""
|
||||
def execute(args):
|
||||
action = args.pop('action', '') if isinstance(args, dict) else ''
|
||||
result = asyncio.run(instance.execute(skill_name=skill_name, action=action, **args))
|
||||
return result
|
||||
return execute
|
||||
|
||||
|
||||
def _make_legacy_execute(instance, attr_name):
|
||||
"""Wrap an async legacy execute into a sync callable for JSON-RPC."""
|
||||
def execute(args):
|
||||
action = args.pop('action', '') if isinstance(args, dict) else ''
|
||||
handler = getattr(instance, attr_name)
|
||||
result = asyncio.run(handler.execute(action=action, **args))
|
||||
return result
|
||||
return execute
|
||||
|
||||
|
||||
def detect_legacy_plugin(module, plugin_dir, plugin_name):
|
||||
"""Auto-detect legacy Python plugin patterns and register as OC tools.
|
||||
|
||||
Returns a PluginAPI with discovered tools, or None if no pattern matches.
|
||||
"""
|
||||
api = PluginAPI(plugin_dir)
|
||||
|
||||
# Pattern 1: Module has a main class with execute(skill_name, action, **kwargs)
|
||||
# Look for any class that has an 'execute' method accepting 'skill_name'
|
||||
main_class = None
|
||||
main_class_name = None
|
||||
|
||||
for name in dir(module):
|
||||
obj = getattr(module, name, None)
|
||||
if not isinstance(obj, type):
|
||||
continue
|
||||
if not hasattr(obj, 'execute') or not callable(getattr(obj, 'execute', None)):
|
||||
continue
|
||||
try:
|
||||
sig = inspect.signature(obj.execute)
|
||||
params = list(sig.parameters.keys())
|
||||
if 'skill_name' in params:
|
||||
main_class = obj
|
||||
main_class_name = name
|
||||
break
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
|
||||
if main_class is None:
|
||||
return None
|
||||
|
||||
# Resolve env vars for constructor
|
||||
env_args = _resolve_env_vars(plugin_dir)
|
||||
try:
|
||||
sig = inspect.signature(main_class.__init__)
|
||||
init_params = list(sig.parameters.keys())[1:] # skip self
|
||||
kwargs = {}
|
||||
for p in init_params:
|
||||
lower_p = p.lower()
|
||||
if lower_p in env_args:
|
||||
kwargs[p] = env_args[lower_p]
|
||||
sys.stderr.write(f'[pysimulator] legacy: instantiating {main_class_name} with {kwargs}\n')
|
||||
instance = main_class(**kwargs)
|
||||
except Exception as e:
|
||||
sys.stderr.write(f'[pysimulator] legacy: cannot instantiate {main_class_name}: {e}\n')
|
||||
return None
|
||||
|
||||
# Discover sub-modules
|
||||
sub_skills = _collect_sub_skills(instance)
|
||||
sys.stderr.write(f'[pysimulator] legacy: discovered sub-skills: {sub_skills}\n')
|
||||
|
||||
discovered_actions = {}
|
||||
for attr_name in sub_skills:
|
||||
handler = getattr(instance, attr_name)
|
||||
# Build parameter schema from the execute signature
|
||||
try:
|
||||
sig = inspect.signature(handler.execute)
|
||||
param_props = {}
|
||||
for p_name, p_sig in list(sig.parameters.items())[1:]: # skip 'self'
|
||||
if p_name == 'action':
|
||||
param_props['action'] = {
|
||||
'type': 'string',
|
||||
'description': f'Action to perform in {attr_name}',
|
||||
}
|
||||
continue
|
||||
if p_sig.annotation != inspect.Parameter.empty:
|
||||
p_type = 'string'
|
||||
if p_sig.annotation is int:
|
||||
p_type = 'number'
|
||||
elif p_sig.annotation is bool:
|
||||
p_type = 'boolean'
|
||||
elif p_sig.annotation is float:
|
||||
p_type = 'number'
|
||||
else:
|
||||
anno_str = str(p_sig.annotation)
|
||||
if 'int' in anno_str:
|
||||
p_type = 'number'
|
||||
elif 'bool' in anno_str:
|
||||
p_type = 'boolean'
|
||||
elif 'float' in anno_str:
|
||||
p_type = 'number'
|
||||
param_props[p_name] = {
|
||||
'type': p_type,
|
||||
'description': f'Parameter {p_name}',
|
||||
}
|
||||
else:
|
||||
param_props[p_name] = {
|
||||
'type': 'string',
|
||||
'description': f'Parameter {p_name}',
|
||||
}
|
||||
|
||||
required = ['action']
|
||||
schema = {
|
||||
'type': 'object',
|
||||
'properties': param_props,
|
||||
'required': required,
|
||||
}
|
||||
|
||||
tool_name = attr_name
|
||||
doc = handler.execute.__doc__ or f'{attr_name} operations'
|
||||
first_line = doc.strip().split('\n')[0] if doc else f'{attr_name} module'
|
||||
|
||||
discovered_actions[attr_name] = (tool_name, first_line, schema)
|
||||
except (ValueError, TypeError) as e:
|
||||
sys.stderr.write(f'[pysimulator] legacy: skip {attr_name}: {e}\n')
|
||||
continue
|
||||
|
||||
if discovered_actions:
|
||||
# Register each sub-skill as a separate OC tool
|
||||
for attr_name, (tool_name, desc, schema) in discovered_actions.items():
|
||||
handler = getattr(instance, attr_name)
|
||||
api.register_tool(
|
||||
name=tool_name,
|
||||
description=f'{plugin_name}: {desc}',
|
||||
parameters=schema,
|
||||
execute=_make_legacy_execute(instance, attr_name),
|
||||
)
|
||||
sys.stderr.write(f'[pysimulator] legacy registered tool: {tool_name}\n')
|
||||
else:
|
||||
# Fallback: single tool wrapping the whole plugin
|
||||
try:
|
||||
sig = inspect.signature(main_class.execute)
|
||||
skill_params = [p for p in list(sig.parameters.keys())[1:]] # skip self
|
||||
param_props = {}
|
||||
for p in skill_params:
|
||||
param_props[p] = {'type': 'string', 'description': p}
|
||||
|
||||
api.register_tool(
|
||||
name=plugin_name,
|
||||
description=f'{plugin_name}: unified operations',
|
||||
parameters={
|
||||
'type': 'object',
|
||||
'properties': param_props,
|
||||
'required': skill_params,
|
||||
},
|
||||
execute=_make_async_execute(instance, None),
|
||||
)
|
||||
sys.stderr.write(f'[pysimulator] legacy registered fallback tool: {plugin_name}\n')
|
||||
except Exception as e:
|
||||
sys.stderr.write(f'[pysimulator] legacy: fallback failed: {e}\n')
|
||||
return None
|
||||
|
||||
return api
|
||||
|
||||
|
||||
def load_plugin(plugin_dir):
|
||||
"""Load the plugin from main.py and call its register()."""
|
||||
"""Load the plugin from main.py.
|
||||
|
||||
Tries OC-style register(api) first, then auto-detects legacy patterns.
|
||||
"""
|
||||
entry_path = resolve_entry(plugin_dir)
|
||||
if entry_path is None:
|
||||
sys.stderr.write(f'[pysimulator] entry not found in {plugin_dir}\n')
|
||||
@ -91,13 +346,22 @@ def load_plugin(plugin_dir):
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
if not hasattr(module, 'register'):
|
||||
sys.stderr.write(f'[pysimulator] {entry_path} must define a register(api) function\n')
|
||||
sys.exit(1)
|
||||
plugin_name = os.path.basename(plugin_dir)
|
||||
|
||||
api = PluginAPI(plugin_dir)
|
||||
module.register(api)
|
||||
return api
|
||||
# OC-style: register(api)
|
||||
if hasattr(module, 'register'):
|
||||
api = PluginAPI(plugin_dir)
|
||||
module.register(api)
|
||||
return api
|
||||
|
||||
# Legacy auto-detection
|
||||
sys.stderr.write(f'[pysimulator] no register() function, trying legacy detection for {plugin_name}...\n')
|
||||
api = detect_legacy_plugin(module, plugin_dir, plugin_name)
|
||||
if api is not None:
|
||||
return api
|
||||
|
||||
sys.stderr.write(f'[pysimulator] {entry_path} must define a register(api) function\n')
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@ -9,6 +9,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
@ -162,6 +163,22 @@ func launchProcess(bin, arg, dir, name string) (*sidecarProcess, error) {
|
||||
cmd.Dir = dir
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
// Add openclaw CLI bin dir to PATH so subprocesses can exec 'openclaw' command
|
||||
if SimulatorDir != "" {
|
||||
binDir := filepath.Join(SimulatorDir, "bin")
|
||||
if info, err := os.Stat(binDir); err == nil && info.IsDir() {
|
||||
env := os.Environ()
|
||||
binDirPath := binDir + string(os.PathListSeparator)
|
||||
for i, e := range env {
|
||||
if strings.HasPrefix(e, "PATH=") {
|
||||
env[i] = "PATH=" + binDirPath + e[5:]
|
||||
break
|
||||
}
|
||||
}
|
||||
cmd.Env = env
|
||||
}
|
||||
}
|
||||
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("stdin pipe: %w", err)
|
||||
|
||||
Reference in New Issue
Block a user