Files
HomeAgent/internal/plugins/clawhubadapter/manager/main.js
JianFeeeee f256098c9f refactor: 全面改名为 HologramAgent + 提示词技术内容下沉到 skill/工具描述 + 修 21 处乱码
## ① 全面改名 HomeAgent → HologramAgent

仓库已改名,但代码与文档里仍有大量旧名。本次按"用户能看到的优先"全量替换:

- 用户可见:登录页标题/页脚、默认人格第一句、CLI 状态输出、启动日志、
  plugin_install 的工具描述、AI 图片插件的 User-Agent
- 文档与脚本:README、assets/docs、docs/zh、deploy/*.sh、.github/workflows
- 鸿蒙工程:cmd/ohos/HomeAgent → cmd/ohos/HologramAgent(git mv)
  目录内的 module/app 描述同步;**证书路径原样保留**
  (default_HomeAgent_xxx.cer/p7b/p12 是签名材料的实际文件名,改了会找不到证书)
- 插件元数据:plugin.json 的 author

★ 必须保持原样的两处(改了会坏):
  · legacySeededPromptSignature = "你是 HomeAgent 的看板娘「小宅」…"
    —— 那是识别历史播种人格卡的**原文锚点**,改了就识别不出旧数据
  · 公开 SDK 的 ProxyAuthHomeAgent = "homeagent"
    —— 那是 SDK 契约里的值,属独立仓库,本仓不改

## ② 修 21 处乱码(U+FFFD)

这些是历史编辑留下的编码破坏,全在注释里,例如:
  「赌注重下���遍」→「赌注重下一遍」
  「「不��滤」」  →「「不过滤」」
  「**待��退场**」→「**待退场**」

★ 保留 providers/chineseclip/tokenizer.go 里的 2 处:那是**故意**用
  U+FFFD 演示"非法 UTF-8 字节被归一成 U+FFFD",不是乱码。

## ③ 提示词技术内容下沉(用户:技术性内容应该收敛到 skill 与 -help 中)

实测系统提示词 2309 字,其中两段几乎全是**模型无法据以行动的实现细节**:

  【工具执行顺序】1272 字(ParallelSafe 声明、批内降级、batchRunnable 判据…)
  【三层记忆】   1069 字(L1/L2/L3 编号、消费式删除、向量空间、指纹…)

收敛后 **2309 → 1723 字(-25%)**,保留的全是"模型据此做什么不同的事":

  工具顺序:默认并行 / 不要依赖先后 / 同通道保序 / 写类工具可能串行
  三层记忆:时序里能看最近 / doc_query 把历史事件按原始时间戳插回时序 /
           关系用 memory_recall / 成文知识用 knowledge_search

判断口径写进判据:新加一段提示词前先问"模型会据此做什么不同的事",
答不上来就该进 skill 或工具描述。

## 判据

prompt_memory_test.go 新增 2 条:
- TestSystemPromptHasNoImplementationJargon
  黑名单:ParallelSafe / batchRunnable / 消费式 / DenseCosine / fingerprint /
  向量空间 / 批内降级 / 自激
- TestSystemPromptStaysCompact:上限 2000 字(当前 1723)

变异:把 "ParallelSafe 声明、批内降级" 加回工具顺序段 ⇒ 第一条立刻红。

## 验证

- go build ./... / go vet ./... 干净
- go test ./... -count=1:58 包 0 失败
- make check-client-versions:客户端与内核版本一致
2026-10-22 20:49:44 +08:00

1064 lines
41 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
// ---- Utility ----
// stdout 仅承载 JSON-RPC;插件/manager 的 console 输出一律走 stderr,避免污染协议流
const rawLog = console.log.bind(console);
console.log = (...args) => process.stderr.write(args.map(String).join(' ') + '\n');
function writeJSON(obj) {
process.stdout.write(JSON.stringify(obj) + '\n');
}
function sendError(id, code, message) {
writeJSON({ jsonrpc: '2.0', id, error: { code, message } });
}
function notify(method, params) {
writeJSON({ jsonrpc: '2.0', method, params });
}
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
const allProviders = {}; // type -> { name, instance } across all plugins
const registeredChannels = {}; // name -> { pluginName, channelPlugin, output, send, type, runtime, deliverers, accounts, status }
// ---- OpenClaw channelRuntime mock ----
// 真实通道插件依赖 channelRuntime 完成:入站转发(dispatchReplyWithBufferedBlockDispatcher)、
// 出站回调(dispatcherOptions.deliver,插件自带的发送实现)、poll 轮询与 call 透传。
function makeChannelRuntime(chName, ch) {
return {
id: chName,
// OC 通用通道轮询输入:Go 端经 channel/send 注入的消息放入 ch.pollQueue,
// 插件每次 chatPolls/getPolls 取走(poll 语义:取走即消费,不重复投递)。
chatPolls: async (opts) => {
const accountId = (opts && opts.accountId) || 'default';
const limit = (opts && opts.limit) || 20;
const q = ch.pollQueues.get(accountId) || [];
const msgs = q.splice(0, limit).map((m) => ({ ...m }));
return { msgs };
},
getPolls: async (opts) => {
const accountId = (opts && opts.accountId) || 'default';
const limit = (opts && opts.limit) || 20;
const q = ch.pollQueues.get(accountId) || [];
const msgs = q.splice(0, limit).map((m) => ({ ...m }));
return { msgs };
},
// 插件经 runtime 直接调用服务器 API:无目标服务器,转发 Go 端作日志/降级
call: async (method, args) => {
notify('channel_output', { channel: chName, type: 'call', method, args });
return { ok: true };
},
reply: {
// 入站消息 + 出站 deliver 绑定。ctx 来自真实插件(Body/From/SessionKey/AccountId...)
dispatchReplyWithBufferedBlockDispatcher: async (opts) => {
const ctx = (opts && opts.ctx) || {};
const dopts = (opts && opts.dispatcherOptions) || {};
const accountId = ctx.AccountId || ctx.accountId || 'default';
ch.deliverers.set(accountId, {
deliver: dopts.deliver,
typingCallbacks: dopts.typingCallbacks || {},
});
notify('channel_input', {
channel: chName,
payload: {
content: ctx.BodyForAgent || ctx.Body || ctx.RawBody || '',
from: ctx.From || ctx.SenderId || '',
sessionKey: ctx.SessionKey || '',
accountId,
messageSid: ctx.MessageSid || '',
chatType: ctx.ChatType || 'direct',
raw: ctx,
},
});
const buffer = [];
return {
sendNow: async (items) => {
for (const item of (items || [])) await deliverItem(ch, accountId, item);
},
addToBuffer: async (item) => { buffer.push(item); },
sendBuffer: async () => {
for (const item of buffer) await deliverItem(ch, accountId, item);
buffer.length = 0;
},
closeBuffer: async () => {},
};
},
},
};
}
async function deliverItem(ch, accountId, item) {
const d = ch.deliverers.get(accountId);
if (!d || typeof d.deliver !== 'function') return;
const payload = {
text: (item && item.text) || '',
mediaUrls: (item && (item.mediaUrls || (item.mediaUrl ? [item.mediaUrl] : []))) || [],
};
if (d.typingCallbacks && typeof d.typingCallbacks.onReplyStart === 'function') {
try { await d.typingCallbacks.onReplyStart(payload); } catch {}
}
await d.deliver(payload);
if (d.typingCallbacks && typeof d.typingCallbacks.onCleanup === 'function') {
try { await d.typingCallbacks.onCleanup(payload); } catch {}
}
}
// ---- OpenClaw gateway 生命周期桥 ----
// 通道插件的心跳/收消息/状态上报都挂在 gateway.startAccount 上;startAccount 会永久挂起
// (await new Promise(()=>{})),配合 OC health-monitor 判定账号存活,必须 fire-and-forget。
async function startChannels(name) {
const ch = registeredChannels[name];
if (!ch || !ch.channelPlugin) return;
const gateway = ch.channelPlugin.gateway;
if (!gateway || typeof gateway.startAccount !== 'function') return;
ch.runtime = ch.runtime || makeChannelRuntime(name, ch);
ch.accounts = ch.accounts || {};
ch.status = ch.status || { running: false, connected: false };
const chCfg = ch.channelPlugin.config || {};
const passCfg = ch.pluginConfig || chCfg;
let accountIds = [];
try {
if (typeof chCfg.listAccountIds === 'function') {
// OC 规范签名: listAccountIds(cfg)
const ids = chCfg.listAccountIds(passCfg);
accountIds = (ids && ids.then ? await ids : (Array.isArray(ids) ? ids : []));
}
} catch (e) {
process.stderr.write(`[manager] ${name}: listAccountIds failed: ${e.message}\n`);
}
if (!Array.isArray(accountIds) || accountIds.length === 0) accountIds = ['default'];
for (const accountId of accountIds) {
let account = null;
try {
if (typeof chCfg.resolveAccount === 'function') {
// OC 规范签名: resolveAccount(cfg, accountId)
account = chCfg.resolveAccount(passCfg, accountId);
if (account && account.then) account = await account;
}
} catch (e) {
process.stderr.write(`[manager] ${name}: resolveAccount(${accountId}) failed: ${e.message}\n`);
}
if (!account) account = { accountId };
if (ch.accounts[accountId]) continue;
ch.accounts[accountId] = { started: true };
const getStatus = () => ({ ...ch.status, accountId });
const setStatus = (patch) => {
ch.status = { ...ch.status, ...(patch || {}) };
notify('channel_status', { channel: name, status: { ...ch.status, accountId } });
};
const ctx = {
account,
cfg: ch.pluginConfig || chCfg,
channelRuntime: ch.runtime,
getStatus,
setStatus,
};
gateway.startAccount(ctx).catch((err) => {
process.stderr.write(`[manager] ${name}: startAccount(${accountId}) crashed: ${err.message}\n`);
ch.status = { ...ch.status, running: false, connected: false, error: err.message };
notify('channel_status', { channel: name, status: { ...ch.status, accountId } });
});
}
}
function stopChannels(name) {
const ch = registeredChannels[name];
if (!ch || !ch.channelPlugin || !ch.accounts) return;
const gateway = ch.channelPlugin.gateway;
if (!gateway || typeof gateway.stopAccount !== 'function') return;
for (const accountId of Object.keys(ch.accounts)) {
const account = { accountId };
gateway.stopAccount({ account, channelRuntime: ch.runtime, cfg: ch.pluginConfig || {} }).catch((err) => {
process.stderr.write(`[manager] ${name}: stopAccount(${accountId}) failed: ${err.message}\n`);
});
}
ch.accounts = {};
}
function registerPluginTools(name, tools, api) {
for (const t of tools) {
if (t && t.name) {
t._plugin = name;
allTools.push(t);
notify('register', { type: 'tool', data: { name: t.name, description: t.description, parameters: t.parameters, plugin: name } });
}
}
loadedPlugins[name] = { tools, api };
}
function loadPlugin(pluginDir, name) {
// Resolve entry
const pkg = readJSON(path.join(pluginDir, 'package.json'));
let entryPath = null;
if (pkg && pkg.openclaw) {
let raw = pkg.openclaw.runtimeExtensions || pkg.openclaw.extensions;
if (typeof raw === 'string') raw = [raw];
if (Array.isArray(raw)) {
for (const ext of raw) {
let ep = path.resolve(pluginDir, ext);
if (ep.endsWith('.ts')) { const js = ep.replace(/\.ts$/, '.js'); if (fs.existsSync(js)) { entryPath = js; break; } }
if (fs.existsSync(ep)) { entryPath = ep; break; }
}
}
}
if (!entryPath) {
const manifest = readJSON(path.join(pluginDir, 'openclaw.plugin.json'));
if (manifest) {
const ep = manifest.entry || manifest.main || 'index.js';
entryPath = path.join(pluginDir, ep);
}
}
if (!entryPath) entryPath = path.join(pluginDir, 'index.js');
if (!fs.existsSync(entryPath)) {
process.stderr.write(`[manager] entry not found for ${name}: ${entryPath}\n`);
return false;
}
let pluginEntry;
try { pluginEntry = require(entryPath); } catch (e) {
process.stderr.write(`[manager] load ${name}: ${e.message}\n`);
return false;
}
const entry = pluginEntry.default || pluginEntry;
if (typeof entry !== 'object' || typeof entry.register !== 'function') {
process.stderr.write(`[manager] ${name}: entry must export {register(api)}\n`);
return false;
}
const registeredTools = [];
const api = {
id: name,
name,
version: (pkg && pkg.version) || '1.0.0',
description: (pkg && pkg.description) || '',
source: pluginDir,
rootDir: pluginDir,
config: {},
pluginConfig: {},
registrationMode: 'full',
logger: { debug: () => {}, info: () => {}, warn: () => {}, error: (...args) => process.stderr.write(`[${name}] ${args.join(' ')}\n`) },
resolvePath: (p) => path.resolve(pluginDir, p),
registerTool: (def, opts) => {
if (typeof def === 'function') {
const toolCtx = { id: name, cwd: pluginDir, env: process.env, allow: ['*'] };
const result = def(toolCtx);
const tools = Array.isArray(result) ? result : [result];
for (const t of tools) { if (t && typeof t.execute === 'function') registeredTools.push(t); }
return;
}
if (!def || !def.name) return;
registeredTools.push({ name: def.name, label: def.label || def.name, description: def.description || '', parameters: def.parameters || { type: 'object', properties: {} }, execute: typeof def.execute === 'function' ? def.execute : undefined });
},
registerProvider: (p) => {
if (p && p.id) allProviders['llm'] = { name: p.id, instance: p };
notify('register', { type: 'provider', data: { name: p?.id || p?.name, plugin: name } });
},
registerChannel: (ch) => {
let chName = ch.name;
let chType = ch.type || 'text';
const chPlugin = ch.plugin;
// OpenClaw ChannelPlugin 格式: { plugin: { id, gateway: {startAccount, stopAccount}, config: {...}, ... } }
if (chPlugin && typeof chPlugin === 'object') {
chName = chName || chPlugin.id || chPlugin.meta?.id || name + '-channel';
chType = chType || (chPlugin.capabilities?.media ? 'io' : 'text');
registeredChannels[chName] = {
pluginName: name, channelPlugin: chPlugin, type: chType,
deliverers: new Map(), accounts: {}, status: {},
pollQueues: new Map(),
};
// gateway 生命周期桥:fire-and-forget,绝不阻塞 registerChannel
startChannels(chName).catch((e) =>
process.stderr.write(`[manager] ${name}: startChannels failed: ${e.message}\n`));
} else {
// 简单格式: { name, type, output }
registeredChannels[chName] = { pluginName: name, output: ch.output || ch.send, type: chType };
}
notify('register', { type: 'channel', data: { name: chName, type: chType, id: chPlugin?.id, plugin: name } });
},
submitInput: (msg) => {
notify('channel_input', { channel: name, payload: msg });
},
registerHook: (hook) => notify('register', { type: 'hook', data: { name: hook.name, event: hook.event } }),
registerHttpRoute: (route) => notify('register', { type: 'http_route', data: { path: route.path, method: route.method } }),
registerCommand: (cmd) => notify('register', { type: 'command', data: { name: cmd.name, description: cmd.description } }),
registerService: (svc) => notify('register', { type: 'service', data: { name: svc.name } }),
registerImageGenerationProvider: (p) => {
if (p) allProviders['image_generation'] = { name: p.name, instance: p };
notify('register', { type: 'image_generation_provider', data: { name: p?.name, plugin: name } });
},
registerWebFetchProvider: (p) => {
if (p) allProviders['web_fetch'] = { name: p.name, instance: p };
notify('register', { type: 'web_fetch_provider', data: { name: p?.name, plugin: name } });
},
registerWebSearchProvider: (p) => {
if (p) allProviders['web_search'] = { name: p.name, instance: p };
notify('register', { type: 'web_search_provider', data: { name: p?.name, plugin: name } });
},
registerSpeechProvider: (p) => {
if (p) allProviders['speech'] = { name: p.name, instance: p };
notify('register', { type: 'speech_provider', data: { name: p?.name, plugin: name } });
},
registerRealtimeTranscriptionProvider: (p) => {
if (p) allProviders['realtime_transcription'] = { name: p.name, instance: p };
notify('register', { type: 'realtime_transcription_provider', data: { name: p?.name, plugin: name } });
},
registerRealtimeVoiceProvider: (p) => {
if (p) allProviders['realtime_voice'] = { name: p.name, instance: p };
notify('register', { type: 'realtime_voice_provider', data: { name: p?.name, plugin: name } });
},
registerMediaUnderstandingProvider: (p) => {
if (p) allProviders['media_understanding'] = { name: p.name, instance: p };
notify('register', { type: 'media_understanding_provider', data: { name: p?.name, plugin: name } });
},
registerMusicGenerationProvider: (p) => {
if (p) allProviders['music_generation'] = { name: p.name, instance: p };
notify('register', { type: 'music_generation_provider', data: { name: p?.name, plugin: name } });
},
registerVideoGenerationProvider: (p) => {
if (p) allProviders['video_generation'] = { name: p.name, instance: p };
notify('register', { type: 'video_generation_provider', data: { name: p?.name, plugin: name } });
},
registerEmbeddingProvider: (p) => {
if (p) allProviders['embedding'] = { name: p.name, instance: p };
notify('register', { type: 'embedding_provider', data: { name: p?.name, plugin: name } });
},
registerMemoryEmbeddingProvider: (p) => {
if (p) allProviders['memory_embedding'] = { name: p.name, instance: p };
notify('register', { type: 'memory_embedding_provider', data: { name: p?.name, plugin: name } });
},
start: (cb) => {},
stop: (cb) => {},
};
entry.register(api);
registerPluginTools(name, registeredTools, api);
process.stderr.write(`[manager] loaded plugin: ${name} (${registeredTools.length} tools)\n`);
return true;
}
// ---- Install npm package ----
function installNPMPackage(spec, skillsDir) {
// Strip npm: prefix if present
if (spec.startsWith('npm:')) spec = spec.slice(4);
process.stderr.write(`[manager] installing: ${spec}\n`);
const installDir = path.join(skillsDir, '.npm_install_' + Date.now());
fs.mkdirSync(installDir, { recursive: true });
try {
execSync(`npm install ${spec} --no-save --prefix "${installDir}"`, {
cwd: installDir, stdio: ['pipe', 'pipe', 'pipe'],
timeout: 120000, env: { ...process.env, NODE_PATH: path.join(installDir, 'node_modules') }
});
} catch (e) {
fs.rmSync(installDir, { recursive: true, force: true });
return { error: e.stderr ? e.stderr.toString() : e.message };
}
const nm = path.join(installDir, 'node_modules');
if (!fs.existsSync(nm)) {
fs.rmSync(installDir, { recursive: true, force: true });
return { error: 'node_modules not created' };
}
let foundPluginDir = null;
let foundName = null;
const entries = fs.readdirSync(nm);
for (const entry of entries) {
const dir = path.join(nm, entry);
if (!fs.statSync(dir).isDirectory()) continue;
if (entry.startsWith('@')) {
const subs = fs.readdirSync(dir);
for (const sub of subs) {
const subDir = path.join(dir, sub);
if (fs.existsSync(path.join(subDir, 'openclaw.plugin.json')) ||
(fs.existsSync(path.join(subDir, 'package.json')) && readJSON(path.join(subDir, 'package.json'))?.openclaw)) {
foundPluginDir = subDir;
foundName = entry + '/' + sub;
}
}
continue;
}
if (fs.existsSync(path.join(dir, 'openclaw.plugin.json')) ||
(fs.existsSync(path.join(dir, 'package.json')) && readJSON(path.join(dir, 'package.json'))?.openclaw)) {
foundPluginDir = dir;
foundName = entry;
}
}
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}"` };
}
// Copy to skills dir
const targetDir = path.join(skillsDir, foundName);
if (fs.existsSync(targetDir)) fs.rmSync(targetDir, { recursive: true, force: true });
cpSync(foundPluginDir, targetDir);
fs.rmSync(installDir, { recursive: true, force: true });
return { name: foundName, dir: targetDir };
}
function cpSync(src, dst) {
fs.mkdirSync(dst, { recursive: true });
for (const entry of fs.readdirSync(src)) {
const s = path.join(src, entry);
const d = path.join(dst, entry);
if (fs.statSync(s).isDirectory()) {
cpSync(s, d);
} else {
fs.copyFileSync(s, d);
}
}
}
// ---- 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}`);
// Write install result for CLI wrapper to pick up
const simDir = path.dirname(process.argv[1]);
fs.writeFileSync(path.join(simDir, '.install-result'), JSON.stringify({ name: 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 HologramAgent mode)');
break;
}
case 'config:get': {
const key = cliArgs[1];
if (!key) throw new Error('Usage: openclaw config:get <key>');
// TODO: read from HologramAgent 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 HologramAgent 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('HologramAgent 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> [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;
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'))) {
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);
}
}
}
}
// ---- JSON-RPC ----
const readline = require('readline');
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: false });
rl.on('line', async (line) => {
let req;
try { req = JSON.parse(line); } catch { sendError(null, -32700, 'Parse error'); return; }
const id = req.id;
const method = req.method;
if (method === 'ping') {
writeJSON({ jsonrpc: '2.0', id, result: { status: 'ok', plugins: Object.keys(loadedPlugins).length } });
return;
}
// 向通道注入输入:Go 端外部输入(webui 会话/其他插件)→ pollQueue → 插件 chatPolls 轮询取走
if (method === 'channel/send') {
const chName = req.params?.channel;
const payload = req.params?.payload || {};
const accountId = (req.params && req.params.accountId) || payload.accountId || 'default';
const ch = registeredChannels[chName];
if (!ch) { sendError(id, -32601, `channel not found: ${chName}`); return; }
ch.pollQueues.set(accountId, ch.pollQueues.get(accountId) || []);
ch.pollQueues.get(accountId).push({
sessionKey: payload.sessionKey || `${chName}:${accountId}:${payload.from || 'poll'}`,
from: payload.from || '',
text: payload.content || payload.text || '',
type: payload.type || 'text',
timestamp: Date.now(),
});
writeJSON({ jsonrpc: '2.0', id, result: { status: 'queued', channel: chName, accountId } });
return;
}
// 停止通道账号(stopAccount:停止心跳/轮询)。channel 可省略(停该插件全部通道)
if (method === 'channel/stop') {
const chName = req.params?.channel;
const accountId = req.params?.accountId;
if (chName) {
const ch = registeredChannels[chName];
if (!ch) { sendError(id, -32601, `channel not found: ${chName}`); return; }
if (accountId) {
const gateway = ch.channelPlugin && ch.channelPlugin.gateway;
if (gateway && typeof gateway.stopAccount === 'function') {
gateway.stopAccount({ account: { accountId }, channelRuntime: ch.runtime, cfg: ch.pluginConfig || {} }).catch((e) =>
process.stderr.write(`[manager] ${chName}: stopAccount(${accountId}) failed: ${e.message}\n`));
}
if (ch.accounts) delete ch.accounts[accountId];
} else {
stopChannels(chName);
}
writeJSON({ jsonrpc: '2.0', id, result: { status: 'stopped', channel: chName, accountId: accountId || 'all' } });
} else {
for (const name of Object.keys(registeredChannels)) stopChannels(name);
writeJSON({ jsonrpc: '2.0', id, result: { status: 'stopped', channel: 'all' } });
}
return;
}
// 启动通道账号(gateway.startAccount fire-and-forget)
if (method === 'channel/start') {
const chName = req.params?.channel;
const ch = registeredChannels[chName];
if (!ch) { sendError(id, -32601, `channel not found: ${chName}`); return; }
ch.accounts = {};
startChannels(chName).catch((e) => process.stderr.write(`[manager] ${chName}: startChannels failed: ${e.message}\n`));
writeJSON({ jsonrpc: '2.0', id, result: { status: 'started', channel: chName } });
return;
}
if (method === 'plugins/list') {
const list = Object.entries(loadedPlugins).map(([name, p]) => ({
name,
tools: p.tools.map(t => ({ name: t.name, description: t.description })),
}));
writeJSON({ jsonrpc: '2.0', id, result: { plugins: list } });
return;
}
if (method === 'plugins/channels') {
const list = Object.entries(registeredChannels).map(([name, ch]) => ({
name,
plugin: ch.pluginName,
type: ch.type || 'text',
status: ch.status || {},
accounts: ch.accounts ? Object.keys(ch.accounts) : [],
}));
writeJSON({ jsonrpc: '2.0', id, result: { channels: list } });
return;
}
if (method === 'plugins/install') {
const pkg = req.params?.package;
if (!pkg) { sendError(id, -32602, 'package required'); return; }
const result = installNPMPackage(pkg, skillsDir);
if (result.error) {
sendError(id, -32603, result.error);
return;
}
// Load the newly installed plugin
const ok = loadPlugin(result.dir, result.name);
if (!ok) {
sendError(id, -32603, `failed to load installed plugin: ${result.name}`);
return;
}
writeJSON({ jsonrpc: '2.0', id, result: { name: result.name, tools: loadedPlugins[result.name].tools.map(t => t.name) } });
return;
}
if (method === 'plugins/uninstall') {
const name = req.params?.name;
if (!name) { sendError(id, -32602, 'name required'); return; }
if (!loadedPlugins[name]) { sendError(id, -32601, `plugin not found: ${name}`); return; }
// 先优雅停靠通道账号(stopAccount 停止心跳/轮询),再卸载
for (const chName of Object.keys(registeredChannels)) {
if (registeredChannels[chName].pluginName === name) {
stopChannels(chName);
}
}
// Remove tools
const idxs = [];
for (let i = allTools.length - 1; i >= 0; i--) {
if (allTools[i]._plugin === name) allTools.splice(i, 1);
}
delete loadedPlugins[name];
// Remove directory
const pluginDir = path.join(skillsDir, name);
if (fs.existsSync(pluginDir)) fs.rmSync(pluginDir, { recursive: true, force: true });
writeJSON({ jsonrpc: '2.0', id, result: { status: 'uninstalled', name } });
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 === 'plugins/load') {
const dir = req.params?.dir;
const pluginName = req.params?.name || (dir ? path.basename(dir) : '');
if (!dir) { sendError(id, -32602, 'dir required'); return; }
const resolvedDir = path.resolve(dir);
if (!fs.existsSync(resolvedDir)) { sendError(id, -32601, `directory not found: ${resolvedDir}`); return; }
if (loadedPlugins[pluginName]) {
writeJSON({ jsonrpc: '2.0', id, result: { name: pluginName, tools: loadedPlugins[pluginName].tools.map(t => t.name), type: 'already_loaded' } });
return;
}
const ok = loadPlugin(resolvedDir, pluginName);
if (!ok) { sendError(id, -32603, `failed to load plugin: ${pluginName}`); return; }
writeJSON({ jsonrpc: '2.0', id, result: { name: pluginName, tools: loadedPlugins[pluginName].tools.map(t => t.name), type: 'loaded' } });
return;
}
if (method === 'tools/list') {
const tools = allTools.map(t => ({
name: t.name,
description: t.description || '',
inputSchema: t.parameters || { type: 'object', properties: {} },
}));
writeJSON({ jsonrpc: '2.0', id, result: { tools } });
return;
}
if (method === 'tools/call') {
const params = req.params || {};
const toolName = params.name;
const args = params.arguments || {};
// 通道输出路由:toolName 匹配已注册通道名时,调通道的输出 handler
const ch = registeredChannels[toolName];
if (ch) {
try {
const channelPlugin = ch.channelPlugin;
let metaObj = {};
try { metaObj = typeof args.meta === 'string' ? JSON.parse(args.meta) : (args.meta || {}); } catch {}
const accountId = metaObj.accountId || 'default';
if (channelPlugin && channelPlugin.outbound) {
// 旧格式 outbound 直发
const to = metaObj.user_id || metaObj.to || metaObj.group_id || '';
const ctx = { to, text: args.payload || '', mediaUrl: metaObj.mediaUrl || '', cfg: {}, accountId };
let result;
if (ctx.mediaUrl && channelPlugin.outbound.sendMedia) {
result = await channelPlugin.outbound.sendMedia(ctx);
} else if (channelPlugin.outbound.sendText) {
result = await channelPlugin.outbound.sendText(ctx);
} else {
throw new Error(`channel ${toolName} has no sendText/sendMedia handler`);
}
writeJSON({ jsonrpc: '2.0', id, result: { status: 'sent', result } });
} else if (typeof ch.output === 'function') {
const result = await ch.output(args.payload, args.type, args.meta);
writeJSON({ jsonrpc: '2.0', id, result: { status: 'sent', result } });
} else if (typeof ch.send === 'function') {
const result = await ch.send(args.payload, args.meta);
writeJSON({ jsonrpc: '2.0', id, result: { status: 'sent', result } });
} else if (ch.deliverers && ch.deliverers.has(accountId)) {
// OpenClaw ChannelPlugin 事件式发送:回复交给插件在入站时挂载的 deliver(插件自带发送实现)
await deliverItem(ch, accountId, {
text: args.payload || '',
mediaUrls: metaObj.mediaUrl ? [metaObj.mediaUrl] : [],
});
writeJSON({ jsonrpc: '2.0', id, result: { status: 'sent', via: 'channelRuntime.deliver' } });
} else if (ch.runtime) {
// 通道已启动但尚未收到入站消息(deliver 未建立):降级为 channel_output 事件
notify('channel_output', {
channel: toolName, type: 'message',
text: args.payload || '', mediaUrl: metaObj.mediaUrl || '', to: metaObj.user_id || '',
});
writeJSON({ jsonrpc: '2.0', id, result: { status: 'queued', via: 'channel_output' } });
} else {
sendError(id, -32601, `channel ${toolName} has no output handler`);
}
} catch (e) { sendError(id, -32603, e.message); }
return;
}
const tool = allTools.find(t => t.name === toolName);
if (!tool) { sendError(id, -32601, `Tool not found: ${toolName}`); return; }
if (typeof tool.execute !== 'function') { sendError(id, -32603, `Tool ${toolName} has no execute`); return; }
try {
const result = await tool.execute('mgr-call-1', args, undefined, undefined);
if (result && typeof result === 'object' && Array.isArray(result.content)) {
writeJSON({ jsonrpc: '2.0', id, result });
} else {
const text = typeof result === 'string' ? result : JSON.stringify(result);
writeJSON({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text }] } });
}
} catch (e) { sendError(id, -32603, e.message); }
return;
}
if (method === 'provider/call') {
const { type, action, args } = req.params || {};
if (!type) { sendError(id, -32602, 'type required'); return; }
const provider = allProviders[type];
if (!provider) { sendError(id, -32601, `Provider not found: ${type}`); return; }
const methodName = action || 'execute';
if (typeof provider.instance[methodName] !== 'function') {
sendError(id, -32603, `Provider ${type} has no method ${methodName}`);
return;
}
try {
const result = await provider.instance[methodName](args);
if (result && typeof result === 'object' && Array.isArray(result.content)) {
writeJSON({ jsonrpc: '2.0', id, result });
} else {
const text = typeof result === 'string' ? result : JSON.stringify(result);
writeJSON({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text }] } });
}
} catch (e) { sendError(id, -32603, e.message); }
return;
}
sendError(id, -32601, `Method not found: ${method}`);
});
// ---- 优雅停靠:进程退出前逐个 stopAccount(插件停止心跳/轮询) ----
let shuttingDown = false;
async function shutdown() {
if (shuttingDown) return;
shuttingDown = true;
for (const name of Object.keys(registeredChannels)) stopChannels(name);
setTimeout(() => process.exit(0), 2000);
}
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);