mirror of
https://github.com/iamlukethedev/Claw3D.git
synced 2026-07-30 11:12:32 +00:00
fixed - onboarding and hermes calls
This commit is contained in:
@@ -285,6 +285,141 @@ function hermesPost(path, body) {
|
||||
});
|
||||
}
|
||||
|
||||
function hermesGet(path) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const urlStr = HERMES_API_URL + path;
|
||||
let url;
|
||||
try { url = new URL(urlStr); } catch { reject(new Error(`Invalid URL: ${urlStr}`)); return; }
|
||||
const transport = url.protocol === "https:" ? https : http;
|
||||
const headers = {};
|
||||
if (HERMES_API_KEY) headers["Authorization"] = `Bearer ${HERMES_API_KEY}`;
|
||||
const req = transport.request(
|
||||
{ hostname: url.hostname, port: url.port ? parseInt(url.port, 10) : (url.protocol === "https:" ? 443 : 80),
|
||||
path: url.pathname + (url.search || ""), method: "GET", headers },
|
||||
resolve
|
||||
);
|
||||
req.on("error", reject);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
async function readJsonBody(res) {
|
||||
const chunks = [];
|
||||
for await (const chunk of res) chunks.push(Buffer.from(chunk));
|
||||
const raw = Buffer.concat(chunks).toString("utf8");
|
||||
if (!raw.trim()) return {};
|
||||
return JSON.parse(raw);
|
||||
}
|
||||
|
||||
function extractOpenAiStyleError(payload, fallbackMessage) {
|
||||
if (payload && typeof payload === "object") {
|
||||
const message =
|
||||
typeof payload?.error?.message === "string"
|
||||
? payload.error.message.trim()
|
||||
: "";
|
||||
if (message) return message;
|
||||
}
|
||||
return fallbackMessage;
|
||||
}
|
||||
|
||||
let cachedHermesModels = null;
|
||||
let cachedHermesModelsAt = 0;
|
||||
|
||||
async function fetchHermesModels() {
|
||||
const now = Date.now();
|
||||
if (cachedHermesModels && now - cachedHermesModelsAt < 30_000) {
|
||||
return cachedHermesModels;
|
||||
}
|
||||
const res = await hermesGet("/v1/models");
|
||||
if (res.statusCode >= 400) {
|
||||
res.resume();
|
||||
throw new Error(`Hermes models API HTTP ${res.statusCode}`);
|
||||
}
|
||||
const payload = await readJsonBody(res);
|
||||
const models = Array.isArray(payload?.data)
|
||||
? payload.data
|
||||
.map((entry) => (typeof entry?.id === "string" ? entry.id.trim() : ""))
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
cachedHermesModels = models;
|
||||
cachedHermesModelsAt = now;
|
||||
return models;
|
||||
}
|
||||
|
||||
async function resolveHermesModel(requestedModel) {
|
||||
const trimmed = typeof requestedModel === "string" ? requestedModel.trim() : "";
|
||||
const normalized = trimmed.includes("/") ? trimmed.split("/").pop().trim() : trimmed;
|
||||
try {
|
||||
const models = await fetchHermesModels();
|
||||
if (models.length === 0) {
|
||||
return normalized || trimmed || HERMES_MODEL;
|
||||
}
|
||||
const candidates = [trimmed, normalized, HERMES_MODEL]
|
||||
.map((value) => (typeof value === "string" ? value.trim() : ""))
|
||||
.filter(Boolean);
|
||||
for (const candidate of candidates) {
|
||||
const exact = models.find((modelId) => modelId === candidate);
|
||||
if (exact) return exact;
|
||||
}
|
||||
for (const candidate of candidates) {
|
||||
const suffix = models.find((modelId) => modelId.endsWith(`/${candidate}`));
|
||||
if (suffix) return suffix;
|
||||
}
|
||||
return models[0];
|
||||
} catch {
|
||||
return normalized || trimmed || HERMES_MODEL;
|
||||
}
|
||||
}
|
||||
|
||||
async function completeOneTurn(messages, model, tools) {
|
||||
const resolvedModel = await resolveHermesModel(model);
|
||||
const body = { model: resolvedModel, messages, stream: false };
|
||||
if (tools && tools.length > 0) {
|
||||
body.tools = tools;
|
||||
body.tool_choice = "auto";
|
||||
}
|
||||
const res = await hermesPost("/v1/chat/completions", body);
|
||||
const payload = await readJsonBody(res);
|
||||
if (res.statusCode >= 400) {
|
||||
throw new Error(
|
||||
extractOpenAiStyleError(payload, `Hermes API HTTP ${res.statusCode}`)
|
||||
);
|
||||
}
|
||||
const choice = Array.isArray(payload?.choices) ? payload.choices[0] : null;
|
||||
const message = choice?.message || {};
|
||||
const textContent =
|
||||
typeof message?.content === "string"
|
||||
? message.content
|
||||
: Array.isArray(message?.content)
|
||||
? message.content
|
||||
.map((part) => (typeof part?.text === "string" ? part.text : ""))
|
||||
.join("")
|
||||
: "";
|
||||
const finishReason =
|
||||
typeof choice?.finish_reason === "string" && choice.finish_reason
|
||||
? choice.finish_reason
|
||||
: "stop";
|
||||
const toolCalls = Array.isArray(message?.tool_calls)
|
||||
? message.tool_calls.map((tc) => {
|
||||
let args = {};
|
||||
const rawArgs = tc?.function?.arguments;
|
||||
if (typeof rawArgs === "string" && rawArgs.trim()) {
|
||||
try {
|
||||
args = JSON.parse(rawArgs);
|
||||
} catch {
|
||||
args = { _raw: rawArgs };
|
||||
}
|
||||
}
|
||||
return {
|
||||
id: typeof tc?.id === "string" ? tc.id : randomId(),
|
||||
name: typeof tc?.function?.name === "string" ? tc.function.name : "",
|
||||
args,
|
||||
};
|
||||
})
|
||||
: [];
|
||||
return { textContent, toolCalls, finishReason, resolvedModel };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SSE streaming — handles both text deltas and tool calls
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -297,6 +432,8 @@ async function streamOneTurn(messages, model, tools, onTextDelta, abortCheck) {
|
||||
const body = { model, messages, stream: true };
|
||||
if (tools && tools.length > 0) { body.tools = tools; body.tool_choice = "auto"; }
|
||||
|
||||
const resolvedModel = await resolveHermesModel(model);
|
||||
body.model = resolvedModel;
|
||||
const res = await hermesPost("/v1/chat/completions", body);
|
||||
if (res.statusCode >= 400) {
|
||||
res.resume();
|
||||
@@ -355,6 +492,15 @@ async function streamOneTurn(messages, model, tools, onTextDelta, abortCheck) {
|
||||
return { id: tc.id, name: tc.name, args };
|
||||
});
|
||||
|
||||
if (!textContent.trim() && toolCalls.length === 0 && finishReason === "stop") {
|
||||
const fallback = await completeOneTurn(messages, resolvedModel, tools);
|
||||
return {
|
||||
textContent: fallback.textContent,
|
||||
toolCalls: fallback.toolCalls,
|
||||
finishReason: fallback.finishReason,
|
||||
};
|
||||
}
|
||||
|
||||
return { textContent, toolCalls, finishReason };
|
||||
}
|
||||
|
||||
@@ -743,14 +889,15 @@ async function handleMethod(method, params, id, sendEvent) {
|
||||
const key = typeof p.key === "string" ? p.key : MAIN_SESSION_KEY;
|
||||
const current = sessionSettings.get(key) || {};
|
||||
const next = { ...current };
|
||||
if (p.model !== undefined) next.model = p.model;
|
||||
if (p.model !== undefined) next.model = typeof p.model === "string" ? p.model.trim() : p.model;
|
||||
if (p.thinkingLevel !== undefined) next.thinkingLevel = p.thinkingLevel;
|
||||
if (p.execHost !== undefined) next.execHost = p.execHost;
|
||||
if (p.execSecurity !== undefined) next.execSecurity = p.execSecurity;
|
||||
if (p.execAsk !== undefined) next.execAsk = p.execAsk;
|
||||
sessionSettings.set(key, next);
|
||||
const resolvedModel = await resolveHermesModel(next.model || HERMES_MODEL);
|
||||
return resOk(id, { ok: true, key, entry: { thinkingLevel: next.thinkingLevel },
|
||||
resolved: { model: next.model || HERMES_MODEL, modelProvider: "hermes" } });
|
||||
resolved: { model: resolvedModel, modelProvider: "hermes" } });
|
||||
}
|
||||
|
||||
case "sessions.reset": {
|
||||
@@ -896,7 +1043,20 @@ async function handleMethod(method, params, id, sendEvent) {
|
||||
return resOk(id, { skills: [] });
|
||||
|
||||
case "models.list":
|
||||
return resOk(id, { models: [{ id: HERMES_MODEL, name: HERMES_AGENT_NAME }] });
|
||||
try {
|
||||
const models = await fetchHermesModels();
|
||||
return resOk(id, {
|
||||
models: (models.length > 0 ? models : [HERMES_MODEL]).map((modelId) => ({
|
||||
id: modelId,
|
||||
name: modelId,
|
||||
})),
|
||||
});
|
||||
} catch {
|
||||
return resOk(id, { models: [{ id: HERMES_MODEL, name: HERMES_MODEL }] });
|
||||
}
|
||||
|
||||
case "tasks.list":
|
||||
return resOk(id, { tasks: [] });
|
||||
|
||||
// --- Cron jobs ----------------------------------------------------------
|
||||
|
||||
@@ -1019,6 +1179,7 @@ function startAdapter() {
|
||||
"agents.files.get","agents.files.set",
|
||||
"exec.approvals.get","exec.approvals.set","exec.approval.resolve",
|
||||
"wake","skills.status","models.list",
|
||||
"tasks.list",
|
||||
"cron.list","cron.add","cron.remove","cron.patch","cron.run"],
|
||||
events: ["chat","presence","heartbeat","cron"] },
|
||||
snapshot: { health: { agents: allAgents, defaultAgentId: AGENT_ID },
|
||||
|
||||
@@ -194,7 +194,7 @@ export function CompanyBuilderModal({
|
||||
</div>
|
||||
<h2 className="mt-1 text-lg font-semibold">Design an AI company from one prompt</h2>
|
||||
<p className="mt-1 text-sm text-white/55">
|
||||
Uses your connected OpenClaw runtime
|
||||
Uses your connected runtime
|
||||
{plannerAgentName ? ` via ${plannerAgentName}.` : "."}
|
||||
</p>
|
||||
</div>
|
||||
@@ -329,7 +329,7 @@ export function CompanyBuilderModal({
|
||||
Company Actions
|
||||
</p>
|
||||
<p className="mt-1 text-[11px] text-white/45">
|
||||
Generate the org, then create it in OpenClaw.
|
||||
Generate the org, then create it in your connected runtime.
|
||||
</p>
|
||||
</div>
|
||||
{replacesExistingAgents ? (
|
||||
@@ -341,7 +341,7 @@ export function CompanyBuilderModal({
|
||||
) : null}
|
||||
{!canUseAi ? (
|
||||
<p className="text-xs text-amber-200/80">
|
||||
Connect to OpenClaw and keep at least one available planning agent in the fleet
|
||||
Connect to a runtime and keep at least one available planning agent in the fleet
|
||||
to use AI suggestions.
|
||||
</p>
|
||||
) : null}
|
||||
@@ -416,7 +416,7 @@ export function CompanyBuilderModal({
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-white">Org structure</p>
|
||||
<p className="text-xs text-white/55">
|
||||
Edit the team before creating agents in OpenClaw.
|
||||
Edit the team before creating agents in your connected runtime.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
@@ -704,7 +704,7 @@ export function CompanyBuilderModal({
|
||||
{statusLine?.trim() || "Working on your company."}
|
||||
</p>
|
||||
<p className="mt-2 text-xs leading-5 text-white/55">
|
||||
Claw3D is using your OpenClaw runtime right now. Please wait until this finishes.
|
||||
Claw3D is using your connected runtime right now. Please wait until this finishes.
|
||||
</p>
|
||||
<div className="mt-5 flex gap-2">
|
||||
{Array.from({ length: 4 }, (_, index) => (
|
||||
@@ -897,7 +897,8 @@ export function CompanyBuilderModal({
|
||||
What should the company do?
|
||||
</p>
|
||||
<p className="mt-2 text-sm text-white/55">
|
||||
As soon as you submit this, OpenClaw will improve the brief automatically.
|
||||
As soon as you submit this, Claw3D will improve the brief using your connected
|
||||
runtime.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
@@ -923,9 +924,12 @@ export function CompanyBuilderModal({
|
||||
disabled={busy}
|
||||
/>
|
||||
<div className="mt-5 flex items-center justify-between gap-3">
|
||||
<p className="text-xs text-white/45">
|
||||
The improved brief becomes the main editable input for generation.
|
||||
</p>
|
||||
<div>
|
||||
<p className="text-xs text-white/45">
|
||||
The improved brief becomes the main editable input for generation.
|
||||
</p>
|
||||
{error ? <p className="mt-2 text-xs text-red-200">{error}</p> : null}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-2 rounded-md bg-amber-500 px-4 py-2 text-sm font-semibold text-[#1a1206] transition hover:bg-amber-400 disabled:cursor-not-allowed disabled:opacity-40"
|
||||
|
||||
@@ -311,7 +311,7 @@ const normalizeRole = (value: ParsedCompanyRole, index: number): CompanyBuilderR
|
||||
export const buildImproveCompanyBriefPrompt = (businessDescription: string) =>
|
||||
[
|
||||
"You are helping a user describe the company they want to build inside Claw3D.",
|
||||
"Rewrite their brief so another OpenClaw agent can generate a clean org structure from it.",
|
||||
"Rewrite their brief so another connected runtime agent can generate a clean org structure from it.",
|
||||
"Keep the answer short, concrete, and useful.",
|
||||
"Return markdown with these sections only:",
|
||||
"## Company",
|
||||
|
||||
@@ -1580,7 +1580,7 @@ export function OfficeScreen({
|
||||
const runCompanyBuilderAiTask = useCallback(
|
||||
async (prompt: string, statusText: string) => {
|
||||
if (status !== "connected") {
|
||||
throw new Error("Connect to OpenClaw before using the company builder.");
|
||||
throw new Error("Connect to a runtime before using the company builder.");
|
||||
}
|
||||
const livePlannerAgent = resolveCompanyPlanningAgent({
|
||||
agents: stateRef.current.agents,
|
||||
@@ -1608,7 +1608,7 @@ export function OfficeScreen({
|
||||
try {
|
||||
const improvedBrief = await runCompanyBuilderAiTask(
|
||||
buildImproveCompanyBriefPrompt(brief),
|
||||
"Improving your company brief with OpenClaw.",
|
||||
"Improving your company brief with the connected runtime.",
|
||||
);
|
||||
setCompanyBuilderInput((current) => ({
|
||||
...current,
|
||||
@@ -1635,7 +1635,7 @@ export function OfficeScreen({
|
||||
try {
|
||||
const response = await runCompanyBuilderAiTask(
|
||||
buildGenerateCompanyPlanPrompt(brief),
|
||||
"Generating your AI company structure with OpenClaw.",
|
||||
"Generating your AI company structure with the connected runtime.",
|
||||
);
|
||||
const parsedPlan = parseCompanyPlanFromAssistantText(response);
|
||||
const nextInput: CompanyBuilderInput = {
|
||||
@@ -1678,7 +1678,7 @@ export function OfficeScreen({
|
||||
const handleCreateCompanyFromPlan = useCallback(
|
||||
async (params: { input: CompanyBuilderInput; plan: CompanyBuilderPlan }) => {
|
||||
if (status !== "connected") {
|
||||
const message = "Connect to OpenClaw before creating the company.";
|
||||
const message = "Connect to a runtime before creating the company.";
|
||||
setCompanyBuilderError(message);
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ export const CompanyStep = ({
|
||||
{
|
||||
icon: Sparkles,
|
||||
title: "Improve the brief",
|
||||
description: "Use your connected OpenClaw runtime to sharpen the company prompt.",
|
||||
description: "Use your connected runtime to sharpen the company prompt.",
|
||||
},
|
||||
{
|
||||
icon: Users,
|
||||
@@ -45,7 +45,7 @@ export const CompanyStep = ({
|
||||
{
|
||||
icon: Wand2,
|
||||
title: "Create everything",
|
||||
description: "Write agent files and create the team directly in OpenClaw.",
|
||||
description: "Write agent files and create the team directly in the connected runtime.",
|
||||
},
|
||||
].map(({ icon: Icon, title, description }) => (
|
||||
<div
|
||||
@@ -73,7 +73,7 @@ export const CompanyStep = ({
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-md border border-amber-500/20 bg-amber-500/10 px-4 py-3 text-xs text-amber-100/80">
|
||||
Connect to OpenClaw and keep at least one planning agent available to generate the
|
||||
Connect to a runtime and keep at least one planning agent available to generate the
|
||||
company with AI.
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -62,7 +62,7 @@ export const CompleteStep = ({
|
||||
</p>
|
||||
<p className="max-w-sm text-sm text-white/60">
|
||||
{companyCreated
|
||||
? `${companyName?.trim() || "Your company"} is ready. Your new team has been created in OpenClaw and placed into the office.`
|
||||
? `${companyName?.trim() || "Your company"} is ready. Your new team has been created in the connected runtime and placed into the office.`
|
||||
: "Your gateway is connected and your agents are ready. Step inside and explore the 3D workspace where your AI team operates."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -32,11 +32,11 @@ export const WelcomeStep = () => (
|
||||
<p className="text-sm leading-relaxed text-white/80">
|
||||
Claw3D turns your AI automation into a{" "}
|
||||
<span className="font-medium text-white">visual workplace</span> — an
|
||||
office where your OpenClaw agents collaborate, code, test, and execute
|
||||
office where your AI agents collaborate, code, test, and execute
|
||||
tasks in a shared 3D environment.
|
||||
</p>
|
||||
<p className="text-sm text-white/60">
|
||||
This wizard will help you connect to your OpenClaw gateway and get
|
||||
This wizard will help you connect to your runtime gateway and get
|
||||
started in about two minutes.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -49,7 +49,7 @@ export const ONBOARDING_STEPS: OnboardingStep[] = [
|
||||
{
|
||||
id: "connect",
|
||||
title: "Connect Your Gateway",
|
||||
description: "Link to your OpenClaw instance",
|
||||
description: "Link to your runtime instance",
|
||||
skippable: false,
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user