mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-27 09:52:25 +00:00
434 lines
12 KiB
TypeScript
434 lines
12 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
import { loadConfig } from "../config/config.js";
|
|
import { hasConfiguredSecretInput } from "../config/types.secrets.js";
|
|
import { assertExplicitGatewayAuthModeWhenBothConfigured } from "../gateway/auth-mode-policy.js";
|
|
import {
|
|
buildGatewayConnectionDetails,
|
|
ensureExplicitGatewayAuth,
|
|
resolveExplicitGatewayAuth,
|
|
} from "../gateway/call.js";
|
|
import { GatewayClient } from "../gateway/client.js";
|
|
import { GATEWAY_CLIENT_CAPS } from "../gateway/protocol/client-info.js";
|
|
import {
|
|
type HelloOk,
|
|
PROTOCOL_VERSION,
|
|
type SessionsListParams,
|
|
type SessionsPatchResult,
|
|
type SessionsPatchParams,
|
|
} from "../gateway/protocol/index.js";
|
|
import { resolveConfiguredSecretInputString } from "../gateway/resolve-configured-secret-input-string.js";
|
|
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js";
|
|
import { VERSION } from "../version.js";
|
|
import type { ResponseUsageMode, SessionInfo, SessionScope } from "./tui-types.js";
|
|
|
|
export type GatewayConnectionOptions = {
|
|
url?: string;
|
|
token?: string;
|
|
password?: string;
|
|
};
|
|
|
|
export type ChatSendOptions = {
|
|
sessionKey: string;
|
|
message: string;
|
|
thinking?: string;
|
|
deliver?: boolean;
|
|
timeoutMs?: number;
|
|
runId?: string;
|
|
};
|
|
|
|
export type GatewayEvent = {
|
|
event: string;
|
|
payload?: unknown;
|
|
seq?: number;
|
|
};
|
|
|
|
type ResolvedGatewayConnection = {
|
|
url: string;
|
|
token?: string;
|
|
password?: string;
|
|
};
|
|
|
|
function trimToUndefined(value: unknown): string | undefined {
|
|
if (typeof value !== "string") {
|
|
return undefined;
|
|
}
|
|
const trimmed = value.trim();
|
|
return trimmed.length > 0 ? trimmed : undefined;
|
|
}
|
|
|
|
function throwGatewayAuthResolutionError(reason: string): never {
|
|
throw new Error(
|
|
[
|
|
reason,
|
|
"Fix: set OPENCLAW_GATEWAY_TOKEN/OPENCLAW_GATEWAY_PASSWORD, pass --token/--password,",
|
|
"or resolve the configured secret provider for this credential.",
|
|
].join("\n"),
|
|
);
|
|
}
|
|
|
|
export type GatewaySessionList = {
|
|
ts: number;
|
|
path: string;
|
|
count: number;
|
|
defaults?: {
|
|
model?: string | null;
|
|
modelProvider?: string | null;
|
|
contextTokens?: number | null;
|
|
};
|
|
sessions: Array<
|
|
Pick<
|
|
SessionInfo,
|
|
| "thinkingLevel"
|
|
| "fastMode"
|
|
| "verboseLevel"
|
|
| "reasoningLevel"
|
|
| "model"
|
|
| "contextTokens"
|
|
| "inputTokens"
|
|
| "outputTokens"
|
|
| "totalTokens"
|
|
| "modelProvider"
|
|
| "displayName"
|
|
> & {
|
|
key: string;
|
|
sessionId?: string;
|
|
updatedAt?: number | null;
|
|
fastMode?: boolean;
|
|
sendPolicy?: string;
|
|
responseUsage?: ResponseUsageMode;
|
|
label?: string;
|
|
provider?: string;
|
|
groupChannel?: string;
|
|
space?: string;
|
|
subject?: string;
|
|
chatType?: string;
|
|
lastProvider?: string;
|
|
lastTo?: string;
|
|
lastAccountId?: string;
|
|
derivedTitle?: string;
|
|
lastMessagePreview?: string;
|
|
}
|
|
>;
|
|
};
|
|
|
|
export type GatewayAgentsList = {
|
|
defaultId: string;
|
|
mainKey: string;
|
|
scope: SessionScope;
|
|
agents: Array<{
|
|
id: string;
|
|
name?: string;
|
|
}>;
|
|
};
|
|
|
|
export type GatewayModelChoice = {
|
|
id: string;
|
|
name: string;
|
|
provider: string;
|
|
contextWindow?: number;
|
|
reasoning?: boolean;
|
|
};
|
|
|
|
export class GatewayChatClient {
|
|
private client: GatewayClient;
|
|
private readyPromise: Promise<void>;
|
|
private resolveReady?: () => void;
|
|
readonly connection: { url: string; token?: string; password?: string };
|
|
hello?: HelloOk;
|
|
|
|
onEvent?: (evt: GatewayEvent) => void;
|
|
onConnected?: () => void;
|
|
onDisconnected?: (reason: string) => void;
|
|
onGap?: (info: { expected: number; received: number }) => void;
|
|
|
|
constructor(connection: ResolvedGatewayConnection) {
|
|
this.connection = connection;
|
|
|
|
this.readyPromise = new Promise((resolve) => {
|
|
this.resolveReady = resolve;
|
|
});
|
|
|
|
this.client = new GatewayClient({
|
|
url: connection.url,
|
|
token: connection.token,
|
|
password: connection.password,
|
|
clientName: GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT,
|
|
clientDisplayName: "openclaw-tui",
|
|
clientVersion: VERSION,
|
|
platform: process.platform,
|
|
mode: GATEWAY_CLIENT_MODES.UI,
|
|
caps: [GATEWAY_CLIENT_CAPS.TOOL_EVENTS],
|
|
instanceId: randomUUID(),
|
|
minProtocol: PROTOCOL_VERSION,
|
|
maxProtocol: PROTOCOL_VERSION,
|
|
onHelloOk: (hello) => {
|
|
this.hello = hello;
|
|
this.resolveReady?.();
|
|
this.onConnected?.();
|
|
},
|
|
onEvent: (evt) => {
|
|
this.onEvent?.({
|
|
event: evt.event,
|
|
payload: evt.payload,
|
|
seq: evt.seq,
|
|
});
|
|
},
|
|
onClose: (_code, reason) => {
|
|
// Reset so waitForReady() blocks again until the next successful reconnect.
|
|
this.readyPromise = new Promise((resolve) => {
|
|
this.resolveReady = resolve;
|
|
});
|
|
this.onDisconnected?.(reason);
|
|
},
|
|
onGap: (info) => {
|
|
this.onGap?.(info);
|
|
},
|
|
});
|
|
}
|
|
|
|
static async connect(opts: GatewayConnectionOptions): Promise<GatewayChatClient> {
|
|
const connection = await resolveGatewayConnection(opts);
|
|
return new GatewayChatClient(connection);
|
|
}
|
|
|
|
start() {
|
|
this.client.start();
|
|
}
|
|
|
|
stop() {
|
|
this.client.stop();
|
|
}
|
|
|
|
async waitForReady() {
|
|
await this.readyPromise;
|
|
}
|
|
|
|
async sendChat(opts: ChatSendOptions): Promise<{ runId: string }> {
|
|
const runId = opts.runId ?? randomUUID();
|
|
await this.client.request("chat.send", {
|
|
sessionKey: opts.sessionKey,
|
|
message: opts.message,
|
|
thinking: opts.thinking,
|
|
deliver: opts.deliver,
|
|
timeoutMs: opts.timeoutMs,
|
|
idempotencyKey: runId,
|
|
});
|
|
return { runId };
|
|
}
|
|
|
|
async abortChat(opts: { sessionKey: string; runId: string }) {
|
|
return await this.client.request<{ ok: boolean; aborted: boolean }>("chat.abort", {
|
|
sessionKey: opts.sessionKey,
|
|
runId: opts.runId,
|
|
});
|
|
}
|
|
|
|
async loadHistory(opts: { sessionKey: string; limit?: number }) {
|
|
return await this.client.request("chat.history", {
|
|
sessionKey: opts.sessionKey,
|
|
limit: opts.limit,
|
|
});
|
|
}
|
|
|
|
async listSessions(opts?: SessionsListParams) {
|
|
return await this.client.request<GatewaySessionList>("sessions.list", {
|
|
limit: opts?.limit,
|
|
activeMinutes: opts?.activeMinutes,
|
|
includeGlobal: opts?.includeGlobal,
|
|
includeUnknown: opts?.includeUnknown,
|
|
includeDerivedTitles: opts?.includeDerivedTitles,
|
|
includeLastMessage: opts?.includeLastMessage,
|
|
agentId: opts?.agentId,
|
|
});
|
|
}
|
|
|
|
async listAgents() {
|
|
return await this.client.request<GatewayAgentsList>("agents.list", {});
|
|
}
|
|
|
|
async patchSession(opts: SessionsPatchParams): Promise<SessionsPatchResult> {
|
|
return await this.client.request<SessionsPatchResult>("sessions.patch", opts);
|
|
}
|
|
|
|
async resetSession(key: string, reason?: "new" | "reset") {
|
|
return await this.client.request("sessions.reset", {
|
|
key,
|
|
...(reason ? { reason } : {}),
|
|
});
|
|
}
|
|
|
|
async getStatus() {
|
|
return await this.client.request("status");
|
|
}
|
|
|
|
async listModels(): Promise<GatewayModelChoice[]> {
|
|
const res = await this.client.request<{ models?: GatewayModelChoice[] }>("models.list");
|
|
return Array.isArray(res?.models) ? res.models : [];
|
|
}
|
|
}
|
|
|
|
export async function resolveGatewayConnection(
|
|
opts: GatewayConnectionOptions,
|
|
): Promise<ResolvedGatewayConnection> {
|
|
const config = loadConfig();
|
|
const env = process.env;
|
|
const gatewayAuthMode = config.gateway?.auth?.mode;
|
|
const isRemoteMode = config.gateway?.mode === "remote";
|
|
const remote = config.gateway?.remote;
|
|
const envToken = trimToUndefined(env.OPENCLAW_GATEWAY_TOKEN);
|
|
const envPassword = trimToUndefined(env.OPENCLAW_GATEWAY_PASSWORD);
|
|
|
|
const urlOverride =
|
|
typeof opts.url === "string" && opts.url.trim().length > 0 ? opts.url.trim() : undefined;
|
|
const explicitAuth = resolveExplicitGatewayAuth({ token: opts.token, password: opts.password });
|
|
ensureExplicitGatewayAuth({
|
|
urlOverride,
|
|
urlOverrideSource: "cli",
|
|
explicitAuth,
|
|
errorHint: "Fix: pass --token or --password when using --url.",
|
|
});
|
|
const url = buildGatewayConnectionDetails({
|
|
config,
|
|
...(urlOverride ? { url: urlOverride } : {}),
|
|
}).url;
|
|
|
|
if (urlOverride) {
|
|
return {
|
|
url,
|
|
token: explicitAuth.token,
|
|
password: explicitAuth.password,
|
|
};
|
|
}
|
|
|
|
if (isRemoteMode) {
|
|
const remoteToken = explicitAuth.token
|
|
? { value: explicitAuth.token }
|
|
: await resolveConfiguredSecretInputString({
|
|
value: remote?.token,
|
|
path: "gateway.remote.token",
|
|
env,
|
|
config,
|
|
});
|
|
const remotePassword =
|
|
explicitAuth.password || envPassword
|
|
? { value: explicitAuth.password ?? envPassword }
|
|
: await resolveConfiguredSecretInputString({
|
|
value: remote?.password,
|
|
path: "gateway.remote.password",
|
|
env,
|
|
config,
|
|
});
|
|
|
|
const token = explicitAuth.token ?? remoteToken.value;
|
|
const password = explicitAuth.password ?? envPassword ?? remotePassword.value;
|
|
if (!token && !password) {
|
|
throwGatewayAuthResolutionError(
|
|
remoteToken.unresolvedRefReason ??
|
|
remotePassword.unresolvedRefReason ??
|
|
"Missing gateway auth credentials.",
|
|
);
|
|
}
|
|
return { url, token, password };
|
|
}
|
|
|
|
if (gatewayAuthMode === "none" || gatewayAuthMode === "trusted-proxy") {
|
|
return {
|
|
url,
|
|
token: explicitAuth.token ?? envToken,
|
|
password: explicitAuth.password ?? envPassword,
|
|
};
|
|
}
|
|
|
|
try {
|
|
assertExplicitGatewayAuthModeWhenBothConfigured(config);
|
|
} catch (err) {
|
|
throwGatewayAuthResolutionError(err instanceof Error ? err.message : String(err));
|
|
}
|
|
|
|
const defaults = config.secrets?.defaults;
|
|
const hasConfiguredToken = hasConfiguredSecretInput(config.gateway?.auth?.token, defaults);
|
|
const hasConfiguredPassword = hasConfiguredSecretInput(config.gateway?.auth?.password, defaults);
|
|
if (gatewayAuthMode === "password") {
|
|
const localPassword =
|
|
explicitAuth.password || envPassword
|
|
? { value: explicitAuth.password ?? envPassword }
|
|
: await resolveConfiguredSecretInputString({
|
|
value: config.gateway?.auth?.password,
|
|
path: "gateway.auth.password",
|
|
env,
|
|
config,
|
|
});
|
|
const password = explicitAuth.password ?? envPassword ?? localPassword.value;
|
|
if (!password) {
|
|
throwGatewayAuthResolutionError(
|
|
localPassword.unresolvedRefReason ?? "Missing gateway auth password.",
|
|
);
|
|
}
|
|
return {
|
|
url,
|
|
token: explicitAuth.token ?? envToken,
|
|
password,
|
|
};
|
|
}
|
|
|
|
const resolveToken = async () => {
|
|
const localToken = explicitAuth.token
|
|
? { value: explicitAuth.token }
|
|
: await resolveConfiguredSecretInputString({
|
|
value: config.gateway?.auth?.token,
|
|
path: "gateway.auth.token",
|
|
env,
|
|
config,
|
|
});
|
|
const token = explicitAuth.token ?? localToken.value ?? envToken;
|
|
if (!token) {
|
|
throwGatewayAuthResolutionError(
|
|
localToken.unresolvedRefReason ?? "Missing gateway auth token.",
|
|
);
|
|
}
|
|
return token;
|
|
};
|
|
|
|
if (gatewayAuthMode === "token") {
|
|
const token = await resolveToken();
|
|
return {
|
|
url,
|
|
token,
|
|
password: explicitAuth.password ?? envPassword,
|
|
};
|
|
}
|
|
|
|
const passwordCandidate = explicitAuth.password ?? envPassword;
|
|
const shouldUsePassword =
|
|
Boolean(passwordCandidate) || (hasConfiguredPassword && !hasConfiguredToken);
|
|
|
|
if (shouldUsePassword) {
|
|
const localPassword = passwordCandidate
|
|
? { value: passwordCandidate }
|
|
: await resolveConfiguredSecretInputString({
|
|
value: config.gateway?.auth?.password,
|
|
path: "gateway.auth.password",
|
|
env,
|
|
config,
|
|
});
|
|
const password = explicitAuth.password ?? localPassword.value ?? envPassword;
|
|
if (!password) {
|
|
throwGatewayAuthResolutionError(
|
|
localPassword.unresolvedRefReason ?? "Missing gateway auth password.",
|
|
);
|
|
}
|
|
return {
|
|
url,
|
|
token: explicitAuth.token ?? envToken,
|
|
password,
|
|
};
|
|
}
|
|
|
|
const token = await resolveToken();
|
|
return {
|
|
url,
|
|
token,
|
|
password: explicitAuth.password ?? envPassword,
|
|
};
|
|
}
|