mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 20:00:47 +00:00
- Added Hermes tool to CLI tools and updated related components.
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
"use server";
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import os from "os";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
const PROVIDER_NAME = "9router";
|
||||
const API_KEY_ENV = "OPENAI_API_KEY";
|
||||
|
||||
const getHermesDir = () => path.join(os.homedir(), ".hermes");
|
||||
const getHermesConfigPath = () => path.join(getHermesDir(), "config.yaml");
|
||||
const getHermesEnvPath = () => path.join(getHermesDir(), ".env");
|
||||
|
||||
// Match top-level "model:" block (until next non-indented, non-empty line)
|
||||
const MODEL_BLOCK_RE = /^model:[ \t]*\r?\n((?:[ \t]+.*\r?\n?|[ \t]*\r?\n)*)/m;
|
||||
|
||||
const buildModelBlock = (model, baseUrl) =>
|
||||
`model:\n default: "${model}"\n provider: "custom"\n base_url: "${baseUrl}"\n`;
|
||||
|
||||
// Parse current model block back to fields (best-effort, simple key:value)
|
||||
const parseModelBlock = (yaml) => {
|
||||
const match = yaml.match(MODEL_BLOCK_RE);
|
||||
if (!match) return null;
|
||||
const body = match[1] || "";
|
||||
const get = (key) => {
|
||||
const m = body.match(new RegExp(`^[ \\t]+${key}:[ \\t]*["']?([^"'\\r\\n]+)["']?`, "m"));
|
||||
return m ? m[1].trim() : null;
|
||||
};
|
||||
return {
|
||||
default: get("default"),
|
||||
provider: get("provider"),
|
||||
base_url: get("base_url"),
|
||||
};
|
||||
};
|
||||
|
||||
const upsertModelBlock = (yaml, newBlock) => {
|
||||
if (MODEL_BLOCK_RE.test(yaml)) return yaml.replace(MODEL_BLOCK_RE, newBlock);
|
||||
return yaml.length > 0 ? `${newBlock}\n${yaml}` : newBlock;
|
||||
};
|
||||
|
||||
const removeModelBlock = (yaml) => yaml.replace(MODEL_BLOCK_RE, "").replace(/^\n+/, "");
|
||||
|
||||
// .env helpers — upsert/remove single KEY=VALUE line
|
||||
const upsertEnvVar = (envText, key, value) => {
|
||||
const re = new RegExp(`^${key}=.*$`, "m");
|
||||
const line = `${key}=${value}`;
|
||||
if (re.test(envText)) return envText.replace(re, line);
|
||||
return envText.length > 0 && !envText.endsWith("\n") ? `${envText}\n${line}\n` : `${envText}${line}\n`;
|
||||
};
|
||||
|
||||
const removeEnvVar = (envText, key) => {
|
||||
const re = new RegExp(`^${key}=.*\\r?\\n?`, "m");
|
||||
return envText.replace(re, "");
|
||||
};
|
||||
|
||||
const checkHermesInstalled = async () => {
|
||||
try {
|
||||
const isWindows = os.platform() === "win32";
|
||||
const command = isWindows ? "where hermes" : "which hermes";
|
||||
await execAsync(command, { windowsHide: true });
|
||||
return true;
|
||||
} catch {
|
||||
try {
|
||||
await fs.access(getHermesConfigPath());
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const readConfigYaml = async () => {
|
||||
try {
|
||||
return await fs.readFile(getHermesConfigPath(), "utf-8");
|
||||
} catch (error) {
|
||||
if (error.code === "ENOENT") return "";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const readEnvFile = async () => {
|
||||
try {
|
||||
return await fs.readFile(getHermesEnvPath(), "utf-8");
|
||||
} catch (error) {
|
||||
if (error.code === "ENOENT") return "";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Detect 9router by base_url containing localhost/127.0.0.1 or matching tunnel URL
|
||||
const has9RouterConfig = (modelCfg) => {
|
||||
if (!modelCfg?.base_url) return false;
|
||||
return modelCfg.provider === "custom" && /localhost|127\.0\.0\.1|0\.0\.0\.0/.test(modelCfg.base_url);
|
||||
};
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const installed = await checkHermesInstalled();
|
||||
if (!installed) {
|
||||
return NextResponse.json({ installed: false, settings: null, message: "Hermes Agent is not installed" });
|
||||
}
|
||||
const yaml = await readConfigYaml();
|
||||
const model = parseModelBlock(yaml);
|
||||
return NextResponse.json({
|
||||
installed: true,
|
||||
settings: { model },
|
||||
has9Router: has9RouterConfig(model),
|
||||
configPath: getHermesConfigPath(),
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("Error checking hermes settings:", error);
|
||||
return NextResponse.json({ error: "Failed to check hermes settings" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request) {
|
||||
try {
|
||||
const { baseUrl, apiKey, model } = await request.json();
|
||||
if (!baseUrl || !model) {
|
||||
return NextResponse.json({ error: "baseUrl and model are required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const dir = getHermesDir();
|
||||
await fs.mkdir(dir, { recursive: true });
|
||||
|
||||
const normalizedBaseUrl = baseUrl.endsWith("/v1") ? baseUrl : `${baseUrl}/v1`;
|
||||
|
||||
// Update config.yaml — replace/insert model: block, keep everything else
|
||||
const existingYaml = await readConfigYaml();
|
||||
const newYaml = upsertModelBlock(existingYaml, buildModelBlock(model, normalizedBaseUrl));
|
||||
await fs.writeFile(getHermesConfigPath(), newYaml);
|
||||
|
||||
// Update .env — upsert OPENAI_API_KEY only when caller provides one
|
||||
if (apiKey) {
|
||||
const existingEnv = await readEnvFile();
|
||||
const newEnv = upsertEnvVar(existingEnv, API_KEY_ENV, apiKey);
|
||||
await fs.writeFile(getHermesEnvPath(), newEnv);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: "Hermes settings applied successfully!",
|
||||
configPath: getHermesConfigPath(),
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("Error updating hermes settings:", error);
|
||||
return NextResponse.json({ error: "Failed to update hermes settings" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE() {
|
||||
try {
|
||||
const configPath = getHermesConfigPath();
|
||||
let yaml = "";
|
||||
try {
|
||||
yaml = await fs.readFile(configPath, "utf-8");
|
||||
} catch (error) {
|
||||
if (error.code === "ENOENT") {
|
||||
return NextResponse.json({ success: true, message: "No config file to reset" });
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
const newYaml = removeModelBlock(yaml);
|
||||
await fs.writeFile(configPath, newYaml);
|
||||
return NextResponse.json({ success: true, message: `${PROVIDER_NAME} model block removed` });
|
||||
} catch (error) {
|
||||
console.log("Error resetting hermes settings:", error);
|
||||
return NextResponse.json({ error: "Failed to reset hermes settings" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user