openclaw-abacusai-auth 1.2.7 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/index.ts +322 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -298,6 +298,14 @@ If AbacusAI models are not available, ensure the plugin is installed:
|
|
|
298
298
|
openclaw plugins install openclaw-abacusai-auth
|
|
299
299
|
```
|
|
300
300
|
|
|
301
|
+
### Webchat no response (Zombie Proxy)
|
|
302
|
+
|
|
303
|
+
If you send messages in Webchat and receive no response, the Gateway may be attempting to communicate with a stale proxy port. This occurs when OpenClaw's internal `openclaw.json` registers `18862`, but a zombie plugin process from a previous run is holding `18862`, forcing the new plugin to dynamically increment its port to `18863` or higher. The Gateway sends requests to `18862` (the zombie proxy, which doesn't know how to route current requests), resulting in hanging chats.
|
|
304
|
+
|
|
305
|
+
**Solution (v1.2.8+)**:
|
|
306
|
+
The plugin now implements a self-healing `/__kill` HTTP endpoint. If the plugin detects `EADDRINUSE` during startup on `18862`, it aggressively kills the legacy zombie proxy holding the port, waits 1 second, and successfully binds to `18862`. You should no longer experience Webchat unresponsiveness due to port drifting.
|
|
307
|
+
If it ever hangs, simply restart your Gateway using standard `Ctrl+C` or `openclaw gateway stop` to ensure the hook securely cleans up the process.
|
|
308
|
+
|
|
301
309
|
---
|
|
302
310
|
|
|
303
311
|
## Getting an API Key
|
package/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync, appendFileSync } from "node:fs";
|
|
2
2
|
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
@@ -480,6 +480,8 @@ function normalizeMessagesForRouteLLM(messages: unknown[]): unknown[] {
|
|
|
480
480
|
return messages.map((msg) => {
|
|
481
481
|
if (!msg || typeof msg !== "object") return msg;
|
|
482
482
|
const m = msg as Record<string, unknown>;
|
|
483
|
+
|
|
484
|
+
// Handle assistant tool calls
|
|
483
485
|
if (m.role !== "assistant" || !Array.isArray(m.tool_calls)) return msg;
|
|
484
486
|
|
|
485
487
|
const normalized = { ...m };
|
|
@@ -510,6 +512,94 @@ function normalizeMessagesForRouteLLM(messages: unknown[]): unknown[] {
|
|
|
510
512
|
});
|
|
511
513
|
}
|
|
512
514
|
|
|
515
|
+
/**
|
|
516
|
+
* Flatten multi-turn conversation history for OpenAI models routed via RouteLLM.
|
|
517
|
+
*
|
|
518
|
+
* ROOT CAUSE: RouteLLM internally converts Chat Completions → OpenAI Responses
|
|
519
|
+
* API for newer OpenAI models (GPT-4o, GPT-5, o-series, etc.). Its converter
|
|
520
|
+
* incorrectly maps ALL message content blocks to `type: "input_text"`, but
|
|
521
|
+
* OpenAI's Responses API requires `type: "output_text"` for assistant/model
|
|
522
|
+
* output messages. This causes HTTP 400 on ANY multi-turn conversation.
|
|
523
|
+
*
|
|
524
|
+
* WORKAROUND: Embed the entire conversation history as plain text inside the
|
|
525
|
+
* system prompt, so the final payload contains ONLY `system` + `user` messages.
|
|
526
|
+
* With no `role: "assistant"` messages, RouteLLM's converter never produces the
|
|
527
|
+
* invalid `output_text` blocks. The model's current-turn tool definitions
|
|
528
|
+
* remain intact.
|
|
529
|
+
*
|
|
530
|
+
* This is only applied to OpenAI models; other providers (Claude, Gemini, etc.)
|
|
531
|
+
* pass through unchanged.
|
|
532
|
+
*/
|
|
533
|
+
function flattenHistoryForOpenAIModels(messages: unknown[], model: string): unknown[] {
|
|
534
|
+
// Only apply to OpenAI models that hit the Responses API path in RouteLLM
|
|
535
|
+
const isOpenAIModel = /^(gpt-|o1-|o3-|o4-|chatgpt-)/i.test(model);
|
|
536
|
+
if (!isOpenAIModel) return messages;
|
|
537
|
+
|
|
538
|
+
const msgs = messages as Array<Record<string, unknown>>;
|
|
539
|
+
|
|
540
|
+
// Separate system messages from conversation
|
|
541
|
+
const systemMsgs = msgs.filter((m) => m.role === "system");
|
|
542
|
+
const convMsgs = msgs.filter((m) => m.role !== "system");
|
|
543
|
+
|
|
544
|
+
// If there are no assistant messages, nothing to flatten
|
|
545
|
+
const hasAssistant = convMsgs.some((m) => m.role === "assistant");
|
|
546
|
+
if (!hasAssistant) return messages;
|
|
547
|
+
|
|
548
|
+
// Find the LAST user message – this becomes the sole user message
|
|
549
|
+
let lastUserIdx = -1;
|
|
550
|
+
for (let i = convMsgs.length - 1; i >= 0; i--) {
|
|
551
|
+
if (convMsgs[i].role === "user") {
|
|
552
|
+
lastUserIdx = i;
|
|
553
|
+
break;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
if (lastUserIdx < 0) return messages; // No user message, pass through
|
|
557
|
+
|
|
558
|
+
// Build textual history from all messages BEFORE the last user message
|
|
559
|
+
const historyParts: string[] = [];
|
|
560
|
+
for (let i = 0; i < lastUserIdx; i++) {
|
|
561
|
+
const m = convMsgs[i];
|
|
562
|
+
const role = m.role === "assistant" ? "Assistant" : m.role === "tool" ? "Tool-Result" : "User";
|
|
563
|
+
let content: string;
|
|
564
|
+
if (typeof m.content === "string") {
|
|
565
|
+
content = m.content;
|
|
566
|
+
} else if (m.content) {
|
|
567
|
+
content = JSON.stringify(m.content);
|
|
568
|
+
} else if (Array.isArray(m.tool_calls)) {
|
|
569
|
+
const calls = (m.tool_calls as Array<Record<string, unknown>>).map((tc) => {
|
|
570
|
+
const fn = tc.function as Record<string, unknown> | undefined;
|
|
571
|
+
return fn ? `${fn.name}(${fn.arguments || "{}"})` : JSON.stringify(tc);
|
|
572
|
+
});
|
|
573
|
+
content = `[Called tools: ${calls.join(", ")}]`;
|
|
574
|
+
} else {
|
|
575
|
+
content = "(empty)";
|
|
576
|
+
}
|
|
577
|
+
// Truncate very long messages
|
|
578
|
+
if (content.length > 3000) {
|
|
579
|
+
content = content.slice(0, 3000) + "... [truncated]";
|
|
580
|
+
}
|
|
581
|
+
historyParts.push(`${role}: ${content}`);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Build the flattened system prompt
|
|
585
|
+
const originalSystem = systemMsgs.map((m) =>
|
|
586
|
+
typeof m.content === "string" ? m.content : JSON.stringify(m.content ?? "")
|
|
587
|
+
).join("\n\n");
|
|
588
|
+
|
|
589
|
+
const historyBlock = historyParts.length > 0
|
|
590
|
+
? "\n\n<conversation_history>\n" + historyParts.join("\n\n") + "\n</conversation_history>"
|
|
591
|
+
: "";
|
|
592
|
+
|
|
593
|
+
const lastUserContent = typeof convMsgs[lastUserIdx].content === "string"
|
|
594
|
+
? convMsgs[lastUserIdx].content as string
|
|
595
|
+
: JSON.stringify(convMsgs[lastUserIdx].content ?? "");
|
|
596
|
+
|
|
597
|
+
return [
|
|
598
|
+
{ role: "system", content: originalSystem + historyBlock },
|
|
599
|
+
{ role: "user", content: lastUserContent },
|
|
600
|
+
];
|
|
601
|
+
}
|
|
602
|
+
|
|
513
603
|
/**
|
|
514
604
|
* Normalize a single tool_call from RouteLLM response to OpenAI standard format.
|
|
515
605
|
* RouteLLM may return tool_calls with flat `name`/`parameters` instead of nested `function`.
|
|
@@ -570,12 +660,64 @@ async function handleProxyRequest(req: IncomingMessage, res: ServerResponse) {
|
|
|
570
660
|
|
|
571
661
|
async function handleProxyRequestInner(req: IncomingMessage, res: ServerResponse) {
|
|
572
662
|
const path = req.url ?? "/";
|
|
573
|
-
|
|
663
|
+
|
|
664
|
+
if (path === "/__kill") {
|
|
665
|
+
console.log("[abacusai] Received /__kill command, stopping zombie proxy...");
|
|
666
|
+
sendJsonResponse(res, 200, { success: true });
|
|
667
|
+
// Execute stop proxy asynchronously after sending response
|
|
668
|
+
setTimeout(() => {
|
|
669
|
+
stopProxy().catch(() => process.exit(0));
|
|
670
|
+
}, 100);
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
let targetPath = path === "/" ? "" : path;
|
|
675
|
+
if (targetPath.startsWith("/v1/")) {
|
|
676
|
+
targetPath = targetPath.slice(3); // e.g., "/v1/models" -> "/models"
|
|
677
|
+
}
|
|
678
|
+
|
|
574
679
|
const headers: Record<string, string> = {
|
|
575
680
|
Authorization: `Bearer ${proxyApiKey}`,
|
|
576
681
|
"Content-Type": "application/json",
|
|
577
682
|
};
|
|
578
683
|
|
|
684
|
+
// Intercept /models requests to return standard OpenAI format
|
|
685
|
+
if (targetPath === "/models") {
|
|
686
|
+
try {
|
|
687
|
+
const { response: upstream, release } = await fetchWithSsrFGuard({
|
|
688
|
+
url: `${ROUTELLM_BASE}/models`,
|
|
689
|
+
init: { method: "GET", headers },
|
|
690
|
+
timeoutMs: 30_000,
|
|
691
|
+
});
|
|
692
|
+
const data = await upstream.text();
|
|
693
|
+
await release();
|
|
694
|
+
|
|
695
|
+
let json;
|
|
696
|
+
try {
|
|
697
|
+
json = JSON.parse(data);
|
|
698
|
+
} catch (e) {
|
|
699
|
+
sendJsonResponse(res, 500, { error: { message: "Invalid JSON from RouteLLM /models" } });
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Ensure it has { object: "list", data: [...] } where each model has { object: "model" }
|
|
704
|
+
const models = Array.isArray(json?.data) ? json.data : Array.isArray(json) ? json : [];
|
|
705
|
+
const normalizedModels = models.map((m: any) => ({
|
|
706
|
+
...m,
|
|
707
|
+
object: "model"
|
|
708
|
+
}));
|
|
709
|
+
|
|
710
|
+
sendJsonResponse(res, 200, {
|
|
711
|
+
object: "list",
|
|
712
|
+
data: normalizedModels
|
|
713
|
+
});
|
|
714
|
+
} catch (err: any) {
|
|
715
|
+
sendJsonResponse(res, 500, { error: { message: `Failed to fetch models: ${err.message}` } });
|
|
716
|
+
}
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const target = `${ROUTELLM_BASE}${targetPath}`;
|
|
579
721
|
let body: string | undefined;
|
|
580
722
|
if (req.method === "POST") {
|
|
581
723
|
const raw = await readBody(req);
|
|
@@ -583,13 +725,33 @@ async function handleProxyRequestInner(req: IncomingMessage, res: ServerResponse
|
|
|
583
725
|
// Normalize tools for RouteLLM: remove `strict` field, clean schemas
|
|
584
726
|
// (remove patternProperties, add additionalProperties: false, etc.)
|
|
585
727
|
|
|
728
|
+
// Log the request for debugging
|
|
729
|
+
try {
|
|
730
|
+
const logEntry = `[${new Date().toISOString()}] ${req.method} ${targetPath} model=${parsed.model || "?"}\n`;
|
|
731
|
+
appendFileSync("C:/tmp/proxy-requests.log", logEntry);
|
|
732
|
+
} catch (e) { /* ignore */ }
|
|
586
733
|
|
|
587
734
|
if (Array.isArray(parsed.tools)) {
|
|
588
735
|
parsed.tools = normalizeToolsForRouteLLM(parsed.tools);
|
|
589
736
|
}
|
|
590
737
|
// Normalize tool_calls in messages: add top-level name/parameters for RouteLLM
|
|
591
738
|
if (Array.isArray(parsed.messages)) {
|
|
739
|
+
// Log message roles BEFORE transformation
|
|
740
|
+
const rolesBefore = (parsed.messages as any[]).map((m: any) => m?.role || "?").join(",");
|
|
741
|
+
|
|
592
742
|
parsed.messages = normalizeMessagesForRouteLLM(parsed.messages);
|
|
743
|
+
// Flatten conversation history for OpenAI models to work around
|
|
744
|
+
// RouteLLM's buggy Chat Completions → Responses API converter
|
|
745
|
+
const modelName = typeof parsed.model === "string" ? parsed.model : "";
|
|
746
|
+
parsed.messages = flattenHistoryForOpenAIModels(parsed.messages as unknown[], modelName);
|
|
747
|
+
|
|
748
|
+
// Log message roles AFTER transformation
|
|
749
|
+
const rolesAfter = (parsed.messages as any[]).map((m: any) => m?.role || "?").join(",");
|
|
750
|
+
const hasAssistant = (parsed.messages as any[]).some((m: any) => m?.role === "assistant");
|
|
751
|
+
try {
|
|
752
|
+
appendFileSync("C:/tmp/proxy-requests.log",
|
|
753
|
+
` BEFORE: ${rolesBefore}\n AFTER: ${rolesAfter}\n hasAssistant=${hasAssistant} model=${modelName}\n`);
|
|
754
|
+
} catch (e) { /* ignore */ }
|
|
593
755
|
}
|
|
594
756
|
body = JSON.stringify(parsed);
|
|
595
757
|
}
|
|
@@ -703,33 +865,50 @@ function startProxy(apiKey: string): Promise<void> {
|
|
|
703
865
|
});
|
|
704
866
|
});
|
|
705
867
|
|
|
706
|
-
|
|
707
|
-
const tryListen = (port: number
|
|
868
|
+
let killAttempts = 0;
|
|
869
|
+
const tryListen = (port: number) => {
|
|
708
870
|
proxyServer!.listen(port, PROXY_HOST, () => {
|
|
709
871
|
proxyPort = port;
|
|
710
872
|
console.log(`[abacusai] proxy listening on http://${PROXY_HOST}:${proxyPort}`);
|
|
711
873
|
resolve();
|
|
712
874
|
});
|
|
713
875
|
proxyServer!.once("error", (err: NodeJS.ErrnoException) => {
|
|
714
|
-
if (err.code === "EADDRINUSE"
|
|
715
|
-
console.log(`[abacusai] port ${port} in use
|
|
876
|
+
if (err.code === "EADDRINUSE") {
|
|
877
|
+
console.log(`[abacusai] port ${port} in use. Attempting to kill zombie proxy...`);
|
|
878
|
+
killAttempts++;
|
|
879
|
+
if (killAttempts > 5) {
|
|
880
|
+
console.error("[abacusai] Could not kill zombie proxy after multiple attempts.");
|
|
881
|
+
reject(new Error("EADDRINUSE on port 18862 and cannot kill zombie proxy."));
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
716
884
|
proxyServer!.removeAllListeners("error");
|
|
717
|
-
|
|
885
|
+
|
|
886
|
+
// Try to kill the zombie proxy by sending it the /__kill command
|
|
887
|
+
const { request } = require("node:http");
|
|
888
|
+
const req = request(`http://${PROXY_HOST}:${port}/__kill`, { method: 'GET' }, (res: IncomingMessage) => {
|
|
889
|
+
res.resume();
|
|
890
|
+
});
|
|
891
|
+
req.on('error', () => { }); // Ignore network errors
|
|
892
|
+
req.end();
|
|
893
|
+
|
|
894
|
+
console.log(`[abacusai] Waiting 1s for port ${port} to free up...`);
|
|
895
|
+
setTimeout(() => {
|
|
896
|
+
// Create fresh proxyServer to avoid closed state issues
|
|
718
897
|
proxyServer = createServer((req, res) => {
|
|
719
898
|
handleProxyRequest(req, res).catch((e) => {
|
|
720
899
|
console.error("[abacusai] proxy error:", e);
|
|
721
900
|
sendJsonResponse(res, 500, { error: { message: String(e) } });
|
|
722
901
|
});
|
|
723
902
|
});
|
|
724
|
-
tryListen(port
|
|
725
|
-
});
|
|
903
|
+
tryListen(port);
|
|
904
|
+
}, 1000);
|
|
726
905
|
} else {
|
|
727
906
|
reject(err);
|
|
728
907
|
}
|
|
729
908
|
});
|
|
730
909
|
};
|
|
731
910
|
|
|
732
|
-
tryListen(PROXY_PORT_DEFAULT
|
|
911
|
+
tryListen(PROXY_PORT_DEFAULT);
|
|
733
912
|
});
|
|
734
913
|
}
|
|
735
914
|
|
|
@@ -854,7 +1033,85 @@ function updateBaseUrlInConfig(pluginApi: any): void {
|
|
|
854
1033
|
}
|
|
855
1034
|
|
|
856
1035
|
// ---------------------------------------------------------------------------
|
|
857
|
-
//
|
|
1036
|
+
// Dynamic Model Updater
|
|
1037
|
+
// ---------------------------------------------------------------------------
|
|
1038
|
+
|
|
1039
|
+
async function updateRouteLlmModels(pluginApi: any): Promise<string> {
|
|
1040
|
+
try {
|
|
1041
|
+
const res = await fetch("https://routellm.abacus.ai/v1/models");
|
|
1042
|
+
if (!res.ok) {
|
|
1043
|
+
throw new Error(`HTTP error! status: ${res.status}`);
|
|
1044
|
+
}
|
|
1045
|
+
const data = await res.json();
|
|
1046
|
+
if (!data.data || !Array.isArray(data.data)) {
|
|
1047
|
+
throw new Error("Invalid response format from RouteLLM");
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
const fetchedModelIds = data.data.map((m: any) => m.id);
|
|
1051
|
+
const newModels = fetchedModelIds.map(buildModelDefinition);
|
|
1052
|
+
|
|
1053
|
+
// 1. Update in-memory configuration
|
|
1054
|
+
if (pluginApi.config?.models?.providers?.abacusai) {
|
|
1055
|
+
pluginApi.config.models.providers.abacusai.models = newModels;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// 2. Persist to openclaw.json
|
|
1059
|
+
const stateDir = process.env.OPENCLAW_STATE_DIR || join(homedir(), ".openclaw");
|
|
1060
|
+
const configPath = join(stateDir, "openclaw.json");
|
|
1061
|
+
if (existsSync(configPath)) {
|
|
1062
|
+
const configStr = readFileSync(configPath, "utf8");
|
|
1063
|
+
const configObj = JSON.parse(configStr);
|
|
1064
|
+
let updatedConfig = false;
|
|
1065
|
+
|
|
1066
|
+
if (configObj.models?.providers?.abacusai) {
|
|
1067
|
+
configObj.models.providers.abacusai.models = newModels;
|
|
1068
|
+
updatedConfig = true;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// Sync with agents.defaults.models so they appear in chat dropdown immediately
|
|
1072
|
+
if (configObj.agents?.defaults) {
|
|
1073
|
+
if (!configObj.agents.defaults.models) {
|
|
1074
|
+
configObj.agents.defaults.models = {};
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
const prefixedFetchedIds = fetchedModelIds.map((id: string) => `abacusai/${id}`);
|
|
1078
|
+
|
|
1079
|
+
// 2a. Purge outdated models
|
|
1080
|
+
const existingAgentModels = Object.keys(configObj.agents.defaults.models);
|
|
1081
|
+
for (const modelId of existingAgentModels) {
|
|
1082
|
+
if (modelId.startsWith("abacusai/") && !prefixedFetchedIds.includes(modelId)) {
|
|
1083
|
+
delete configObj.agents.defaults.models[modelId];
|
|
1084
|
+
updatedConfig = true;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// 2b. Add new models
|
|
1089
|
+
for (const modelId of prefixedFetchedIds) {
|
|
1090
|
+
if (!configObj.agents.defaults.models[modelId]) {
|
|
1091
|
+
configObj.agents.defaults.models[modelId] = {};
|
|
1092
|
+
updatedConfig = true;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
if (updatedConfig) {
|
|
1098
|
+
writeFileSync(configPath, JSON.stringify(configObj, null, 2), "utf8");
|
|
1099
|
+
|
|
1100
|
+
// Tell OpenClaw to hot-reload the configuration so the model list reflects in the UI
|
|
1101
|
+
if (typeof (pluginApi as any).reloadConfig === "function") {
|
|
1102
|
+
(pluginApi as any).reloadConfig();
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
return `✅ Successfully fetched and updated ${newModels.length} models from AbacusAI RouteLLM.`;
|
|
1107
|
+
} catch (err: any) {
|
|
1108
|
+
console.error("[abacusai] Failed to update models:", err);
|
|
1109
|
+
return `❌ Failed to update models: ${err.message}`;
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// ---------------------------------------------------------------------------
|
|
1114
|
+
// Plugin Entry Point
|
|
858
1115
|
// ---------------------------------------------------------------------------
|
|
859
1116
|
|
|
860
1117
|
// Type definitions for plugin API
|
|
@@ -885,22 +1142,68 @@ const abacusaiPlugin = {
|
|
|
885
1142
|
config?: {
|
|
886
1143
|
models?: { providers?: { abacusai?: { compat?: { supportsStrictMode?: boolean } } } };
|
|
887
1144
|
};
|
|
1145
|
+
registerCommand?: (command: { name: string; description: string; handler: Function; execute?: Function }) => void;
|
|
888
1146
|
};
|
|
889
1147
|
|
|
890
1148
|
// ================================================================
|
|
891
1149
|
// Register gateway_stop hook for graceful proxy shutdown
|
|
892
1150
|
// ================================================================
|
|
893
1151
|
if (typeof pluginApi.registerHook === "function") {
|
|
894
|
-
pluginApi.registerHook(
|
|
895
|
-
"gateway_stop"
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
1152
|
+
pluginApi.registerHook("gateway_stop", async () => {
|
|
1153
|
+
console.log("[abacusai] gateway_stop hook triggered, stopping proxy gracefully...");
|
|
1154
|
+
await stopProxy();
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// ================================================================
|
|
1159
|
+
// Register chat command for updating models
|
|
1160
|
+
// ================================================================
|
|
1161
|
+
if (typeof pluginApi.registerCommand === "function") {
|
|
1162
|
+
try {
|
|
1163
|
+
pluginApi.registerCommand({
|
|
1164
|
+
name: "abacusai",
|
|
1165
|
+
description: "AbacusAI utilities (e.g. /abacusai pull-models)",
|
|
1166
|
+
// OpenClaw SDK typically accepts a handler function for the command
|
|
1167
|
+
handler: async (ctx: any) => {
|
|
1168
|
+
// Fallback string matching to capture args
|
|
1169
|
+
const contentStr = (ctx?.content || "").trim();
|
|
1170
|
+
const argsStr = ctx?.args ? ctx.args.join(" ") : contentStr;
|
|
1171
|
+
const isPull = argsStr === "pull-models" || argsStr === "" || argsStr.includes("pull-models");
|
|
1172
|
+
|
|
1173
|
+
// Dump api keys for debugging
|
|
1174
|
+
try {
|
|
1175
|
+
writeFileSync("C:/tmp/api-keys.txt", Object.keys(pluginApi).join(", "), "utf8");
|
|
1176
|
+
} catch (e) { }
|
|
1177
|
+
|
|
1178
|
+
if (isPull) {
|
|
1179
|
+
const res = await updateRouteLlmModels(pluginApi);
|
|
1180
|
+
try { appendFileSync("C:/tmp/abacus-update.log", res + "\n"); } catch (e) { }
|
|
1181
|
+
return { text: res };
|
|
1182
|
+
}
|
|
1183
|
+
return { text: `Unknown subcommand: ${argsStr}. Usage: /abacusai pull-models` };
|
|
1184
|
+
},
|
|
1185
|
+
// Just in case it's named 'execute' or 'run' in different SDK versions
|
|
1186
|
+
execute: async (ctx: any) => {
|
|
1187
|
+
const contentStr = (ctx?.content || "").trim();
|
|
1188
|
+
const argsStr = ctx?.args ? ctx.args.join(" ") : contentStr;
|
|
1189
|
+
const isPull = argsStr === "pull-models" || argsStr === "" || argsStr.includes("pull-models");
|
|
1190
|
+
|
|
1191
|
+
if (isPull) {
|
|
1192
|
+
const res = await updateRouteLlmModels(pluginApi);
|
|
1193
|
+
try { appendFileSync("C:/tmp/abacus-update.log", res + "\n"); } catch (e) { }
|
|
1194
|
+
return { text: res };
|
|
1195
|
+
}
|
|
1196
|
+
return { text: `Unknown subcommand: ${argsStr}. Usage: /abacusai pull-models` };
|
|
1197
|
+
}
|
|
1198
|
+
});
|
|
1199
|
+
} catch (e) {
|
|
1200
|
+
console.error("[abacusai] Error registering command:", e);
|
|
1201
|
+
}
|
|
901
1202
|
}
|
|
902
1203
|
|
|
903
|
-
//
|
|
1204
|
+
// ================================================================
|
|
1205
|
+
// Process signal handlers for fallback proxy shutdown
|
|
1206
|
+
// ================================================================
|
|
904
1207
|
const shutdownHandler = () => {
|
|
905
1208
|
stopProxy().then(() => process.exit(0));
|
|
906
1209
|
};
|