refactor(installer): replace bash installer logic with node-only implementation
This commit is contained in:
@@ -29,19 +29,23 @@ You can merge this snippet manually into your `openclaw.json`.
|
|||||||
|
|
||||||
## Installer script (with rollback)
|
## Installer script (with rollback)
|
||||||
|
|
||||||
For production-like install with automatic rollback on error:
|
For production-like install with automatic rollback on error (Node-only installer):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
node ./scripts/install-whispergate-openclaw.mjs --install
|
||||||
|
# or wrapper
|
||||||
./scripts/install-whispergate-openclaw.sh --install
|
./scripts/install-whispergate-openclaw.sh --install
|
||||||
```
|
```
|
||||||
|
|
||||||
Uninstall (revert all recorded config changes):
|
Uninstall (revert all recorded config changes):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
node ./scripts/install-whispergate-openclaw.mjs --uninstall
|
||||||
|
# or wrapper
|
||||||
./scripts/install-whispergate-openclaw.sh --uninstall
|
./scripts/install-whispergate-openclaw.sh --uninstall
|
||||||
# or specify a record explicitly
|
# or specify a record explicitly
|
||||||
# RECORD_FILE=~/.openclaw/whispergate-install-records/whispergate-YYYYmmddHHMMSS.json \
|
# RECORD_FILE=~/.openclaw/whispergate-install-records/whispergate-YYYYmmddHHMMSS.json \
|
||||||
# ./scripts/install-whispergate-openclaw.sh --uninstall
|
# node ./scripts/install-whispergate-openclaw.mjs --uninstall
|
||||||
```
|
```
|
||||||
|
|
||||||
Environment overrides:
|
Environment overrides:
|
||||||
|
|||||||
202
scripts/install-whispergate-openclaw.mjs
Executable file
202
scripts/install-whispergate-openclaw.mjs
Executable file
@@ -0,0 +1,202 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import os from "node:os";
|
||||||
|
import { execFileSync } from "node:child_process";
|
||||||
|
|
||||||
|
const modeArg = process.argv[2];
|
||||||
|
if (modeArg !== "--install" && modeArg !== "--uninstall") {
|
||||||
|
console.error("Usage: install-whispergate-openclaw.mjs --install | --uninstall");
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
const mode = modeArg === "--install" ? "install" : "uninstall";
|
||||||
|
|
||||||
|
const env = process.env;
|
||||||
|
const OPENCLAW_CONFIG_PATH = env.OPENCLAW_CONFIG_PATH || path.join(os.homedir(), ".openclaw", "openclaw.json");
|
||||||
|
const PLUGIN_PATH = env.PLUGIN_PATH || "/root/.openclaw/workspace-operator/WhisperGate/dist/whispergate";
|
||||||
|
const NO_REPLY_PROVIDER_ID = env.NO_REPLY_PROVIDER_ID || "whisper-gateway";
|
||||||
|
const NO_REPLY_MODEL_ID = env.NO_REPLY_MODEL_ID || "no-reply";
|
||||||
|
const NO_REPLY_BASE_URL = env.NO_REPLY_BASE_URL || "http://127.0.0.1:8787/v1";
|
||||||
|
const NO_REPLY_API_KEY = env.NO_REPLY_API_KEY || "wg-local-test-token";
|
||||||
|
const LIST_MODE = env.LIST_MODE || "human-list";
|
||||||
|
const HUMAN_LIST_JSON = env.HUMAN_LIST_JSON || '["561921120408698910","1474088632750047324"]';
|
||||||
|
const AGENT_LIST_JSON = env.AGENT_LIST_JSON || "[]";
|
||||||
|
const CHANNEL_POLICIES_FILE = (env.CHANNEL_POLICIES_FILE || "~/.openclaw/whispergate-channel-policies.json").replace(/^~(?=$|\/)/, os.homedir());
|
||||||
|
const CHANNEL_POLICIES_JSON = env.CHANNEL_POLICIES_JSON || "{}";
|
||||||
|
const END_SYMBOLS_JSON = env.END_SYMBOLS_JSON || '["🔚"]';
|
||||||
|
|
||||||
|
const STATE_DIR = (env.STATE_DIR || "~/.openclaw/whispergate-install-records").replace(/^~(?=$|\/)/, os.homedir());
|
||||||
|
const LATEST_RECORD_LINK = (env.LATEST_RECORD_LINK || "~/.openclaw/whispergate-install-record-latest.json").replace(/^~(?=$|\/)/, os.homedir());
|
||||||
|
|
||||||
|
const ts = new Date().toISOString().replace(/[-:TZ.]/g, "").slice(0, 14);
|
||||||
|
const BACKUP_PATH = `${OPENCLAW_CONFIG_PATH}.bak-whispergate-${mode}-${ts}`;
|
||||||
|
const RECORD_PATH = path.join(STATE_DIR, `whispergate-${ts}.json`);
|
||||||
|
|
||||||
|
const PATH_PLUGINS_LOAD = "plugins.load.paths";
|
||||||
|
const PATH_PLUGIN_ENTRY = "plugins.entries.whispergate";
|
||||||
|
const PROVIDER_PATH = `models.providers["${NO_REPLY_PROVIDER_ID}"]`;
|
||||||
|
|
||||||
|
function runOpenclaw(args, { allowFail = false } = {}) {
|
||||||
|
try {
|
||||||
|
return execFileSync("openclaw", args, { encoding: "utf8" }).trim();
|
||||||
|
} catch (e) {
|
||||||
|
if (allowFail) return null;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getJson(pathKey) {
|
||||||
|
const out = runOpenclaw(["config", "get", pathKey, "--json"], { allowFail: true });
|
||||||
|
if (out == null || out === "") return { exists: false };
|
||||||
|
return { exists: true, value: JSON.parse(out) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function setJson(pathKey, value) {
|
||||||
|
runOpenclaw(["config", "set", pathKey, JSON.stringify(value), "--json"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function unsetPath(pathKey) {
|
||||||
|
runOpenclaw(["config", "unset", pathKey], { allowFail: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeRecord(modeName, before, after) {
|
||||||
|
fs.mkdirSync(STATE_DIR, { recursive: true });
|
||||||
|
const rec = {
|
||||||
|
mode: modeName,
|
||||||
|
timestamp: ts,
|
||||||
|
openclawConfigPath: OPENCLAW_CONFIG_PATH,
|
||||||
|
backupPath: BACKUP_PATH,
|
||||||
|
paths: before,
|
||||||
|
applied: after,
|
||||||
|
};
|
||||||
|
fs.writeFileSync(RECORD_PATH, JSON.stringify(rec, null, 2));
|
||||||
|
fs.copyFileSync(RECORD_PATH, LATEST_RECORD_LINK);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readRecord(file) {
|
||||||
|
return JSON.parse(fs.readFileSync(file, "utf8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(OPENCLAW_CONFIG_PATH)) {
|
||||||
|
console.error(`[whispergate] config not found: ${OPENCLAW_CONFIG_PATH}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode === "install") {
|
||||||
|
fs.copyFileSync(OPENCLAW_CONFIG_PATH, BACKUP_PATH);
|
||||||
|
console.log(`[whispergate] backup: ${BACKUP_PATH}`);
|
||||||
|
|
||||||
|
if (!fs.existsSync(CHANNEL_POLICIES_FILE)) {
|
||||||
|
fs.mkdirSync(path.dirname(CHANNEL_POLICIES_FILE), { recursive: true });
|
||||||
|
fs.writeFileSync(CHANNEL_POLICIES_FILE, `${CHANNEL_POLICIES_JSON}\n`);
|
||||||
|
console.log(`[whispergate] initialized channel policies file: ${CHANNEL_POLICIES_FILE}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const before = {
|
||||||
|
[PATH_PLUGINS_LOAD]: getJson(PATH_PLUGINS_LOAD),
|
||||||
|
[PATH_PLUGIN_ENTRY]: getJson(PATH_PLUGIN_ENTRY),
|
||||||
|
[PROVIDER_PATH]: getJson(PROVIDER_PATH),
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pluginsNow = getJson("plugins").value || {};
|
||||||
|
const plugins = typeof pluginsNow === "object" ? pluginsNow : {};
|
||||||
|
plugins.load = plugins.load && typeof plugins.load === "object" ? plugins.load : {};
|
||||||
|
const paths = Array.isArray(plugins.load.paths) ? plugins.load.paths : [];
|
||||||
|
if (!paths.includes(PLUGIN_PATH)) paths.push(PLUGIN_PATH);
|
||||||
|
plugins.load.paths = paths;
|
||||||
|
plugins.entries = plugins.entries && typeof plugins.entries === "object" ? plugins.entries : {};
|
||||||
|
plugins.entries.whispergate = {
|
||||||
|
enabled: true,
|
||||||
|
config: {
|
||||||
|
enabled: true,
|
||||||
|
discordOnly: true,
|
||||||
|
listMode: LIST_MODE,
|
||||||
|
humanList: JSON.parse(HUMAN_LIST_JSON),
|
||||||
|
agentList: JSON.parse(AGENT_LIST_JSON),
|
||||||
|
channelPoliciesFile: CHANNEL_POLICIES_FILE,
|
||||||
|
endSymbols: JSON.parse(END_SYMBOLS_JSON),
|
||||||
|
noReplyProvider: NO_REPLY_PROVIDER_ID,
|
||||||
|
noReplyModel: NO_REPLY_MODEL_ID,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
setJson("plugins", plugins);
|
||||||
|
|
||||||
|
setJson(PROVIDER_PATH, {
|
||||||
|
baseUrl: NO_REPLY_BASE_URL,
|
||||||
|
apiKey: NO_REPLY_API_KEY,
|
||||||
|
api: "openai-completions",
|
||||||
|
models: [
|
||||||
|
{
|
||||||
|
id: NO_REPLY_MODEL_ID,
|
||||||
|
name: `${NO_REPLY_MODEL_ID} (Custom Provider)`,
|
||||||
|
reasoning: false,
|
||||||
|
input: ["text"],
|
||||||
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||||
|
contextWindow: 4096,
|
||||||
|
maxTokens: 4096,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const after = {
|
||||||
|
[PATH_PLUGINS_LOAD]: getJson(PATH_PLUGINS_LOAD),
|
||||||
|
[PATH_PLUGIN_ENTRY]: getJson(PATH_PLUGIN_ENTRY),
|
||||||
|
[PROVIDER_PATH]: getJson(PROVIDER_PATH),
|
||||||
|
};
|
||||||
|
writeRecord("install", before, after);
|
||||||
|
console.log("[whispergate] install ok");
|
||||||
|
console.log(`[whispergate] record: ${RECORD_PATH}`);
|
||||||
|
} catch (e) {
|
||||||
|
fs.copyFileSync(BACKUP_PATH, OPENCLAW_CONFIG_PATH);
|
||||||
|
console.error(`[whispergate] install failed; rollback complete: ${String(e)}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const recFile = env.RECORD_FILE || (fs.existsSync(LATEST_RECORD_LINK) ? LATEST_RECORD_LINK : "");
|
||||||
|
if (!recFile || !fs.existsSync(recFile)) {
|
||||||
|
console.error("[whispergate] no record found. set RECORD_FILE=<path> or install first.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.copyFileSync(OPENCLAW_CONFIG_PATH, BACKUP_PATH);
|
||||||
|
console.log(`[whispergate] backup before uninstall: ${BACKUP_PATH}`);
|
||||||
|
|
||||||
|
const rec = readRecord(recFile);
|
||||||
|
const before = rec.applied || {};
|
||||||
|
const target = rec.paths || {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pluginsNow = getJson("plugins").value || {};
|
||||||
|
const plugins = typeof pluginsNow === "object" ? pluginsNow : {};
|
||||||
|
plugins.load = plugins.load && typeof plugins.load === "object" ? plugins.load : {};
|
||||||
|
plugins.entries = plugins.entries && typeof plugins.entries === "object" ? plugins.entries : {};
|
||||||
|
|
||||||
|
if (target[PATH_PLUGINS_LOAD]?.exists) plugins.load.paths = target[PATH_PLUGINS_LOAD].value;
|
||||||
|
else delete plugins.load.paths;
|
||||||
|
|
||||||
|
if (target[PATH_PLUGIN_ENTRY]?.exists) plugins.entries.whispergate = target[PATH_PLUGIN_ENTRY].value;
|
||||||
|
else delete plugins.entries.whispergate;
|
||||||
|
|
||||||
|
setJson("plugins", plugins);
|
||||||
|
|
||||||
|
for (const k of Object.keys(target)) {
|
||||||
|
if (!k.startsWith("models.providers[")) continue;
|
||||||
|
if (target[k]?.exists) setJson(k, target[k].value);
|
||||||
|
else unsetPath(k);
|
||||||
|
}
|
||||||
|
|
||||||
|
const after = {
|
||||||
|
[PATH_PLUGINS_LOAD]: getJson(PATH_PLUGINS_LOAD),
|
||||||
|
[PATH_PLUGIN_ENTRY]: getJson(PATH_PLUGIN_ENTRY),
|
||||||
|
[PROVIDER_PATH]: getJson(PROVIDER_PATH),
|
||||||
|
};
|
||||||
|
writeRecord("uninstall", before, after);
|
||||||
|
console.log("[whispergate] uninstall ok");
|
||||||
|
console.log(`[whispergate] record: ${RECORD_PATH}`);
|
||||||
|
} catch (e) {
|
||||||
|
fs.copyFileSync(BACKUP_PATH, OPENCLAW_CONFIG_PATH);
|
||||||
|
console.error(`[whispergate] uninstall failed; rollback complete: ${String(e)}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,367 +1,4 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -Eeuo pipefail
|
set -euo pipefail
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
# WhisperGate installer/uninstaller for OpenClaw
|
exec node "$SCRIPT_DIR/install-whispergate-openclaw.mjs" "$@"
|
||||||
# Requirements:
|
|
||||||
# - all writes via `openclaw config set ... --json`
|
|
||||||
# - install supports rollback on failure
|
|
||||||
# - uninstall reverts ALL recorded changes
|
|
||||||
# - every install writes a change record
|
|
||||||
|
|
||||||
OPENCLAW_CONFIG_PATH="${OPENCLAW_CONFIG_PATH:-$HOME/.openclaw/openclaw.json}"
|
|
||||||
PLUGIN_PATH="${PLUGIN_PATH:-/root/.openclaw/workspace-operator/WhisperGate/dist/whispergate}"
|
|
||||||
NO_REPLY_PROVIDER_ID="${NO_REPLY_PROVIDER_ID:-whisper-gateway}"
|
|
||||||
NO_REPLY_MODEL_ID="${NO_REPLY_MODEL_ID:-no-reply}"
|
|
||||||
NO_REPLY_BASE_URL="${NO_REPLY_BASE_URL:-http://127.0.0.1:8787/v1}"
|
|
||||||
NO_REPLY_API_KEY="${NO_REPLY_API_KEY:-wg-local-test-token}"
|
|
||||||
LIST_MODE="${LIST_MODE:-human-list}"
|
|
||||||
HUMAN_LIST_JSON="${HUMAN_LIST_JSON:-[\"561921120408698910\",\"1474088632750047324\"]}"
|
|
||||||
AGENT_LIST_JSON="${AGENT_LIST_JSON:-[]}"
|
|
||||||
CHANNEL_POLICIES_FILE="${CHANNEL_POLICIES_FILE:-$HOME/.openclaw/whispergate-channel-policies.json}"
|
|
||||||
CHANNEL_POLICIES_JSON="${CHANNEL_POLICIES_JSON:-{}}"
|
|
||||||
END_SYMBOLS_JSON="${END_SYMBOLS_JSON:-[\"🔚\"]}"
|
|
||||||
|
|
||||||
STATE_DIR="${STATE_DIR:-$HOME/.openclaw/whispergate-install-records}"
|
|
||||||
LATEST_RECORD_LINK="${LATEST_RECORD_LINK:-$HOME/.openclaw/whispergate-install-record-latest.json}"
|
|
||||||
|
|
||||||
MODE=""
|
|
||||||
if [[ "${1:-}" == "--install" ]]; then
|
|
||||||
MODE="install"
|
|
||||||
elif [[ "${1:-}" == "--uninstall" ]]; then
|
|
||||||
MODE="uninstall"
|
|
||||||
else
|
|
||||||
echo "Usage: $0 --install | --uninstall"
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
TIMESTAMP="$(date +%Y%m%d%H%M%S)"
|
|
||||||
BACKUP_PATH="${OPENCLAW_CONFIG_PATH}.bak-whispergate-${MODE}-${TIMESTAMP}"
|
|
||||||
RECORD_PATH="${STATE_DIR}/whispergate-${TIMESTAMP}.json"
|
|
||||||
|
|
||||||
PROVIDER_PATH="models.providers[\"${NO_REPLY_PROVIDER_ID}\"]"
|
|
||||||
PATH_PLUGINS_LOAD="plugins.load.paths"
|
|
||||||
PATH_PLUGIN_ENTRY="plugins.entries.whispergate"
|
|
||||||
|
|
||||||
INSTALLED_OK=0
|
|
||||||
|
|
||||||
require_cmd() {
|
|
||||||
command -v "$1" >/dev/null 2>&1 || {
|
|
||||||
echo "[whispergate] missing command: $1" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
oc_get_json_or_missing() {
|
|
||||||
local path="$1"
|
|
||||||
if openclaw config get "$path" --json >/tmp/wg_get.json 2>/dev/null; then
|
|
||||||
printf '{"exists":true,"value":%s}\n' "$(cat /tmp/wg_get.json)"
|
|
||||||
else
|
|
||||||
printf '{"exists":false}'
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
oc_set_json() {
|
|
||||||
local path="$1"
|
|
||||||
local json="$2"
|
|
||||||
openclaw config set "$path" "$json" --json >/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
oc_unset() {
|
|
||||||
local path="$1"
|
|
||||||
openclaw config unset "$path" >/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
write_record() {
|
|
||||||
local mode="$1"
|
|
||||||
local prev_paths_json="$2"
|
|
||||||
local next_paths_json="$3"
|
|
||||||
mkdir -p "$STATE_DIR"
|
|
||||||
python3 - <<'PY'
|
|
||||||
import json, os
|
|
||||||
record={
|
|
||||||
'mode': os.environ['REC_MODE'],
|
|
||||||
'timestamp': os.environ['TIMESTAMP'],
|
|
||||||
'openclawConfigPath': os.environ['OPENCLAW_CONFIG_PATH'],
|
|
||||||
'backupPath': os.environ['BACKUP_PATH'],
|
|
||||||
'paths': json.loads(os.environ['PREV_PATHS_JSON']),
|
|
||||||
'applied': json.loads(os.environ['NEXT_PATHS_JSON']),
|
|
||||||
}
|
|
||||||
with open(os.environ['RECORD_PATH'],'w',encoding='utf-8') as f:
|
|
||||||
json.dump(record,f,ensure_ascii=False,indent=2)
|
|
||||||
PY
|
|
||||||
cp -f "$RECORD_PATH" "$LATEST_RECORD_LINK"
|
|
||||||
}
|
|
||||||
|
|
||||||
rollback_install() {
|
|
||||||
local ec=$?
|
|
||||||
if [[ $INSTALLED_OK -eq 1 ]]; then
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
echo "[whispergate] install failed (exit=$ec), rolling back..."
|
|
||||||
if [[ -f "$BACKUP_PATH" ]]; then
|
|
||||||
cp -f "$BACKUP_PATH" "$OPENCLAW_CONFIG_PATH"
|
|
||||||
echo "[whispergate] rollback complete"
|
|
||||||
else
|
|
||||||
echo "[whispergate] WARNING: backup missing; rollback skipped"
|
|
||||||
fi
|
|
||||||
exit "$ec"
|
|
||||||
}
|
|
||||||
|
|
||||||
run_install() {
|
|
||||||
trap rollback_install ERR
|
|
||||||
|
|
||||||
cp -f "$OPENCLAW_CONFIG_PATH" "$BACKUP_PATH"
|
|
||||||
echo "[whispergate] backup: $BACKUP_PATH"
|
|
||||||
|
|
||||||
# initialize standalone channel policies file if missing
|
|
||||||
CHANNEL_POLICIES_FILE_RESOLVED="$(CHANNEL_POLICIES_FILE="$CHANNEL_POLICIES_FILE" python3 - <<'PY'
|
|
||||||
import os
|
|
||||||
print(os.path.expanduser(os.environ['CHANNEL_POLICIES_FILE']))
|
|
||||||
PY
|
|
||||||
)"
|
|
||||||
if [[ ! -f "$CHANNEL_POLICIES_FILE_RESOLVED" ]]; then
|
|
||||||
mkdir -p "$(dirname "$CHANNEL_POLICIES_FILE_RESOLVED")"
|
|
||||||
printf '%s\n' "$CHANNEL_POLICIES_JSON" > "$CHANNEL_POLICIES_FILE_RESOLVED"
|
|
||||||
echo "[whispergate] initialized channel policies file: $CHANNEL_POLICIES_FILE_RESOLVED"
|
|
||||||
fi
|
|
||||||
|
|
||||||
local prev_paths_json
|
|
||||||
prev_paths_json="$(PATH_PLUGINS_LOAD="$PATH_PLUGINS_LOAD" PATH_PLUGIN_ENTRY="$PATH_PLUGIN_ENTRY" PROVIDER_PATH="$PROVIDER_PATH" python3 - <<'PY'
|
|
||||||
import json, os, subprocess
|
|
||||||
|
|
||||||
def get(path):
|
|
||||||
p=subprocess.run(['openclaw','config','get',path,'--json'],capture_output=True,text=True)
|
|
||||||
if p.returncode==0:
|
|
||||||
return {'exists':True,'value':json.loads(p.stdout)}
|
|
||||||
return {'exists':False}
|
|
||||||
|
|
||||||
payload={
|
|
||||||
os.environ['PATH_PLUGINS_LOAD']: get(os.environ['PATH_PLUGINS_LOAD']),
|
|
||||||
os.environ['PATH_PLUGIN_ENTRY']: get(os.environ['PATH_PLUGIN_ENTRY']),
|
|
||||||
os.environ['PROVIDER_PATH']: get(os.environ['PROVIDER_PATH']),
|
|
||||||
}
|
|
||||||
print(json.dumps(payload,ensure_ascii=False))
|
|
||||||
PY
|
|
||||||
)"
|
|
||||||
|
|
||||||
# 1+2) set plugins object in one write (avoid transient schema failure)
|
|
||||||
local current_plugins_json new_plugins_json
|
|
||||||
if openclaw config get plugins --json >/tmp/wg_plugins.json 2>/dev/null; then
|
|
||||||
current_plugins_json="$(cat /tmp/wg_plugins.json)"
|
|
||||||
else
|
|
||||||
current_plugins_json='{}'
|
|
||||||
fi
|
|
||||||
|
|
||||||
new_plugins_json="$(CURRENT_PLUGINS_JSON="$current_plugins_json" PLUGIN_PATH="$PLUGIN_PATH" LIST_MODE="$LIST_MODE" HUMAN_LIST_JSON="$HUMAN_LIST_JSON" AGENT_LIST_JSON="$AGENT_LIST_JSON" CHANNEL_POLICIES_FILE="$CHANNEL_POLICIES_FILE" END_SYMBOLS_JSON="$END_SYMBOLS_JSON" NO_REPLY_PROVIDER_ID="$NO_REPLY_PROVIDER_ID" NO_REPLY_MODEL_ID="$NO_REPLY_MODEL_ID" python3 - <<'PY'
|
|
||||||
import json, os
|
|
||||||
plugins=json.loads(os.environ['CURRENT_PLUGINS_JSON'])
|
|
||||||
if not isinstance(plugins,dict):
|
|
||||||
plugins={}
|
|
||||||
|
|
||||||
load=plugins.setdefault('load',{})
|
|
||||||
paths=load.get('paths')
|
|
||||||
if not isinstance(paths,list):
|
|
||||||
paths=[]
|
|
||||||
pp=os.environ['PLUGIN_PATH']
|
|
||||||
if pp not in paths:
|
|
||||||
paths.append(pp)
|
|
||||||
load['paths']=paths
|
|
||||||
|
|
||||||
entries=plugins.setdefault('entries',{})
|
|
||||||
entries['whispergate']={
|
|
||||||
'enabled': True,
|
|
||||||
'config': {
|
|
||||||
'enabled': True,
|
|
||||||
'discordOnly': True,
|
|
||||||
'listMode': os.environ['LIST_MODE'],
|
|
||||||
'humanList': json.loads(os.environ['HUMAN_LIST_JSON']),
|
|
||||||
'agentList': json.loads(os.environ['AGENT_LIST_JSON']),
|
|
||||||
'channelPoliciesFile': os.environ['CHANNEL_POLICIES_FILE'],
|
|
||||||
'endSymbols': json.loads(os.environ['END_SYMBOLS_JSON']),
|
|
||||||
'noReplyProvider': os.environ['NO_REPLY_PROVIDER_ID'],
|
|
||||||
'noReplyModel': os.environ['NO_REPLY_MODEL_ID'],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
print(json.dumps(plugins,ensure_ascii=False))
|
|
||||||
PY
|
|
||||||
)"
|
|
||||||
oc_set_json "plugins" "$new_plugins_json"
|
|
||||||
|
|
||||||
# 3) provider
|
|
||||||
local provider_json
|
|
||||||
provider_json="$(NO_REPLY_BASE_URL="$NO_REPLY_BASE_URL" NO_REPLY_API_KEY="$NO_REPLY_API_KEY" NO_REPLY_MODEL_ID="$NO_REPLY_MODEL_ID" python3 - <<'PY'
|
|
||||||
import json, os
|
|
||||||
provider={
|
|
||||||
'baseUrl': os.environ['NO_REPLY_BASE_URL'],
|
|
||||||
'apiKey': os.environ['NO_REPLY_API_KEY'],
|
|
||||||
'api': 'openai-completions',
|
|
||||||
'models': [{
|
|
||||||
'id': os.environ['NO_REPLY_MODEL_ID'],
|
|
||||||
'name': f"{os.environ['NO_REPLY_MODEL_ID']} (Custom Provider)",
|
|
||||||
'reasoning': False,
|
|
||||||
'input': ['text'],
|
|
||||||
'cost': {'input': 0, 'output': 0, 'cacheRead': 0, 'cacheWrite': 0},
|
|
||||||
'contextWindow': 4096,
|
|
||||||
'maxTokens': 4096,
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
print(json.dumps(provider,ensure_ascii=False))
|
|
||||||
PY
|
|
||||||
)"
|
|
||||||
oc_set_json "$PROVIDER_PATH" "$provider_json"
|
|
||||||
|
|
||||||
# validate writes
|
|
||||||
openclaw config get "$PATH_PLUGINS_LOAD" --json >/dev/null
|
|
||||||
openclaw config get "$PATH_PLUGIN_ENTRY" --json >/dev/null
|
|
||||||
openclaw config get "$PROVIDER_PATH" --json >/dev/null
|
|
||||||
|
|
||||||
local next_paths_json
|
|
||||||
next_paths_json="$(PATH_PLUGINS_LOAD="$PATH_PLUGINS_LOAD" PATH_PLUGIN_ENTRY="$PATH_PLUGIN_ENTRY" PROVIDER_PATH="$PROVIDER_PATH" python3 - <<'PY'
|
|
||||||
import json, os, subprocess
|
|
||||||
|
|
||||||
def get(path):
|
|
||||||
p=subprocess.run(['openclaw','config','get',path,'--json'],capture_output=True,text=True)
|
|
||||||
if p.returncode==0:
|
|
||||||
return {'exists':True,'value':json.loads(p.stdout)}
|
|
||||||
return {'exists':False}
|
|
||||||
|
|
||||||
payload={
|
|
||||||
os.environ['PATH_PLUGINS_LOAD']: get(os.environ['PATH_PLUGINS_LOAD']),
|
|
||||||
os.environ['PATH_PLUGIN_ENTRY']: get(os.environ['PATH_PLUGIN_ENTRY']),
|
|
||||||
os.environ['PROVIDER_PATH']: get(os.environ['PROVIDER_PATH']),
|
|
||||||
}
|
|
||||||
print(json.dumps(payload,ensure_ascii=False))
|
|
||||||
PY
|
|
||||||
)"
|
|
||||||
|
|
||||||
REC_MODE="install" PREV_PATHS_JSON="$prev_paths_json" NEXT_PATHS_JSON="$next_paths_json" TIMESTAMP="$TIMESTAMP" OPENCLAW_CONFIG_PATH="$OPENCLAW_CONFIG_PATH" BACKUP_PATH="$BACKUP_PATH" RECORD_PATH="$RECORD_PATH" write_record install "$prev_paths_json" "$next_paths_json"
|
|
||||||
|
|
||||||
INSTALLED_OK=1
|
|
||||||
trap - ERR
|
|
||||||
echo "[whispergate] install ok"
|
|
||||||
echo "[whispergate] record: $RECORD_PATH"
|
|
||||||
}
|
|
||||||
|
|
||||||
run_uninstall() {
|
|
||||||
local rec_file=""
|
|
||||||
if [[ -n "${RECORD_FILE:-}" ]]; then
|
|
||||||
rec_file="$RECORD_FILE"
|
|
||||||
elif [[ -f "$LATEST_RECORD_LINK" ]]; then
|
|
||||||
rec_file="$LATEST_RECORD_LINK"
|
|
||||||
else
|
|
||||||
echo "[whispergate] no record found. set RECORD_FILE=<path> or install first." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ ! -f "$rec_file" ]]; then
|
|
||||||
echo "[whispergate] record file not found: $rec_file" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
cp -f "$OPENCLAW_CONFIG_PATH" "$BACKUP_PATH"
|
|
||||||
echo "[whispergate] backup before uninstall: $BACKUP_PATH"
|
|
||||||
|
|
||||||
python3 - <<'PY'
|
|
||||||
import json, os, subprocess, sys
|
|
||||||
|
|
||||||
rec=json.load(open(os.environ['REC_FILE'],encoding='utf-8'))
|
|
||||||
paths=rec.get('paths',{})
|
|
||||||
|
|
||||||
PLUGINS_LOAD='plugins.load.paths'
|
|
||||||
PLUGINS_ENTRY='plugins.entries.whispergate'
|
|
||||||
PROVIDER_PATHS=[k for k in paths.keys() if k.startswith('models.providers[')]
|
|
||||||
|
|
||||||
# 1) restore plugins atomically to avoid transient schema failures
|
|
||||||
pcur=subprocess.run(['openclaw','config','get','plugins','--json'],capture_output=True,text=True)
|
|
||||||
plugins={}
|
|
||||||
if pcur.returncode==0:
|
|
||||||
plugins=json.loads(pcur.stdout)
|
|
||||||
if not isinstance(plugins,dict):
|
|
||||||
plugins={}
|
|
||||||
|
|
||||||
load=plugins.get('load') if isinstance(plugins.get('load'),dict) else {}
|
|
||||||
entries=plugins.get('entries') if isinstance(plugins.get('entries'),dict) else {}
|
|
||||||
|
|
||||||
# restore plugins.load.paths from record
|
|
||||||
info=paths.get(PLUGINS_LOAD,{'exists':False})
|
|
||||||
if info.get('exists'):
|
|
||||||
load['paths']=info.get('value')
|
|
||||||
else:
|
|
||||||
load.pop('paths',None)
|
|
||||||
|
|
||||||
# restore plugins.entries.whispergate from record
|
|
||||||
info=paths.get(PLUGINS_ENTRY,{'exists':False})
|
|
||||||
if info.get('exists'):
|
|
||||||
entries['whispergate']=info.get('value')
|
|
||||||
else:
|
|
||||||
entries.pop('whispergate',None)
|
|
||||||
|
|
||||||
plugins['load']=load
|
|
||||||
plugins['entries']=entries
|
|
||||||
|
|
||||||
pset=subprocess.run(['openclaw','config','set','plugins',json.dumps(plugins,ensure_ascii=False),'--json'],capture_output=True,text=True)
|
|
||||||
if pset.returncode!=0:
|
|
||||||
sys.stderr.write(pset.stderr or pset.stdout)
|
|
||||||
raise SystemExit(1)
|
|
||||||
|
|
||||||
# 2) restore provider paths (usually custom no-reply provider)
|
|
||||||
for path in PROVIDER_PATHS:
|
|
||||||
info=paths.get(path,{'exists':False})
|
|
||||||
if info.get('exists'):
|
|
||||||
val=json.dumps(info.get('value'),ensure_ascii=False)
|
|
||||||
p=subprocess.run(['openclaw','config','set',path,val,'--json'],capture_output=True,text=True)
|
|
||||||
if p.returncode!=0:
|
|
||||||
sys.stderr.write(p.stderr or p.stdout)
|
|
||||||
raise SystemExit(1)
|
|
||||||
else:
|
|
||||||
p=subprocess.run(['openclaw','config','unset',path],capture_output=True,text=True)
|
|
||||||
if p.returncode!=0:
|
|
||||||
txt=(p.stderr or p.stdout or '').lower()
|
|
||||||
if 'not found' not in txt and 'missing' not in txt and 'does not exist' not in txt:
|
|
||||||
sys.stderr.write(p.stderr or p.stdout)
|
|
||||||
raise SystemExit(1)
|
|
||||||
|
|
||||||
print('ok')
|
|
||||||
PY
|
|
||||||
|
|
||||||
local next_paths_json
|
|
||||||
next_paths_json="$(python3 - <<'PY'
|
|
||||||
import json, os, subprocess
|
|
||||||
paths=json.load(open(os.environ['REC_FILE'],encoding='utf-8')).get('paths',{}).keys()
|
|
||||||
def get(path):
|
|
||||||
p=subprocess.run(['openclaw','config','get',path,'--json'],capture_output=True,text=True)
|
|
||||||
if p.returncode==0:
|
|
||||||
return {'exists':True,'value':json.loads(p.stdout)}
|
|
||||||
return {'exists':False}
|
|
||||||
payload={k:get(k) for k in paths}
|
|
||||||
print(json.dumps(payload,ensure_ascii=False))
|
|
||||||
PY
|
|
||||||
)"
|
|
||||||
|
|
||||||
local prev_paths_json
|
|
||||||
prev_paths_json="$(python3 - <<'PY'
|
|
||||||
import json, os
|
|
||||||
print(json.dumps(json.load(open(os.environ['REC_FILE'],encoding='utf-8')).get('applied',{}),ensure_ascii=False))
|
|
||||||
PY
|
|
||||||
)"
|
|
||||||
|
|
||||||
REC_MODE="uninstall" PREV_PATHS_JSON="$prev_paths_json" NEXT_PATHS_JSON="$next_paths_json" TIMESTAMP="$TIMESTAMP" OPENCLAW_CONFIG_PATH="$OPENCLAW_CONFIG_PATH" BACKUP_PATH="$BACKUP_PATH" RECORD_PATH="$RECORD_PATH" write_record uninstall "$prev_paths_json" "$next_paths_json"
|
|
||||||
|
|
||||||
echo "[whispergate] uninstall ok"
|
|
||||||
echo "[whispergate] record: $RECORD_PATH"
|
|
||||||
}
|
|
||||||
|
|
||||||
main() {
|
|
||||||
require_cmd openclaw
|
|
||||||
require_cmd python3
|
|
||||||
[[ -f "$OPENCLAW_CONFIG_PATH" ]] || { echo "[whispergate] config not found: $OPENCLAW_CONFIG_PATH"; exit 1; }
|
|
||||||
|
|
||||||
if [[ "$MODE" == "install" ]]; then
|
|
||||||
run_install
|
|
||||||
else
|
|
||||||
REC_FILE="${RECORD_FILE:-$LATEST_RECORD_LINK}" run_uninstall
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
main
|
|
||||||
|
|||||||
Reference in New Issue
Block a user