168 lines
4.5 KiB
TypeScript
168 lines
4.5 KiB
TypeScript
type ChatCompletionResponse = {
|
|
choices?: Array<{
|
|
message?: {
|
|
content?: string;
|
|
};
|
|
}>;
|
|
};
|
|
|
|
type OpenClawAuthMode = 'bearer' | 'basic' | 'none';
|
|
|
|
type OpenClawConfig = {
|
|
baseUrl?: string;
|
|
apiKey?: string;
|
|
model: string;
|
|
authMode: OpenClawAuthMode;
|
|
basicAuthUsername?: string;
|
|
basicAuthPassword?: string;
|
|
apiKeyHeader?: string;
|
|
};
|
|
|
|
function envValue(name: string) {
|
|
const value = process.env[name];
|
|
if (!value) {
|
|
return undefined;
|
|
}
|
|
|
|
const trimmed = value.trim();
|
|
return trimmed.length > 0 ? trimmed : undefined;
|
|
}
|
|
|
|
const DEFAULT_MODEL = 'zeroclaw';
|
|
const DEFAULT_AUTH_MODE: OpenClawAuthMode = 'bearer';
|
|
|
|
function parseAuthMode(value: string | undefined): OpenClawAuthMode {
|
|
const normalized = value?.trim().toLowerCase();
|
|
if (normalized === 'basic' || normalized === 'none') {
|
|
return normalized;
|
|
}
|
|
|
|
return DEFAULT_AUTH_MODE;
|
|
}
|
|
|
|
function hasSupportedProtocol(url: string) {
|
|
try {
|
|
const parsed = new URL(url);
|
|
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function buildCompletionsUrl(baseUrl: string) {
|
|
if (!hasSupportedProtocol(baseUrl)) {
|
|
return undefined;
|
|
}
|
|
|
|
const withSlash = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
|
|
return new URL('v1/chat/completions', withSlash).toString();
|
|
}
|
|
|
|
function hasRequiredAuth(config: OpenClawConfig) {
|
|
if (config.authMode === 'none') {
|
|
return true;
|
|
}
|
|
|
|
if (config.authMode === 'basic') {
|
|
return Boolean(config.basicAuthUsername && config.basicAuthPassword);
|
|
}
|
|
|
|
return Boolean(config.apiKey);
|
|
}
|
|
|
|
function buildAuthHeaders(config: OpenClawConfig) {
|
|
const headers: Record<string, string> = {
|
|
'Content-Type': 'application/json'
|
|
};
|
|
|
|
if (config.authMode === 'basic' && config.basicAuthUsername && config.basicAuthPassword) {
|
|
const credentials = Buffer
|
|
.from(`${config.basicAuthUsername}:${config.basicAuthPassword}`, 'utf8')
|
|
.toString('base64');
|
|
headers.Authorization = `Basic ${credentials}`;
|
|
} else if (config.authMode === 'bearer' && config.apiKey) {
|
|
headers.Authorization = `Bearer ${config.apiKey}`;
|
|
}
|
|
|
|
if (config.apiKey && config.apiKeyHeader) {
|
|
headers[config.apiKeyHeader] = config.apiKey;
|
|
}
|
|
|
|
return headers;
|
|
}
|
|
|
|
function fallbackResponse(prompt: string) {
|
|
const clipped = prompt.split('\n').slice(0, 6).join(' ').slice(0, 260);
|
|
|
|
return [
|
|
'OpenClaw fallback mode is active (configuration is missing or invalid).',
|
|
'Thesis: Portfolio remains analyzable with local heuristics until live model access is configured.',
|
|
'Risk scan: Concentration and filing sentiment should be monitored after each sync cycle.',
|
|
`Context digest: ${clipped}`
|
|
].join('\n\n');
|
|
}
|
|
|
|
export function getOpenClawConfig() {
|
|
return {
|
|
baseUrl: envValue('OPENCLAW_BASE_URL'),
|
|
apiKey: envValue('OPENCLAW_API_KEY'),
|
|
model: envValue('OPENCLAW_MODEL') ?? DEFAULT_MODEL,
|
|
authMode: parseAuthMode(envValue('OPENCLAW_AUTH_MODE')),
|
|
basicAuthUsername: envValue('OPENCLAW_BASIC_AUTH_USERNAME'),
|
|
basicAuthPassword: envValue('OPENCLAW_BASIC_AUTH_PASSWORD'),
|
|
apiKeyHeader: envValue('OPENCLAW_API_KEY_HEADER')
|
|
} satisfies OpenClawConfig;
|
|
}
|
|
|
|
export function isOpenClawConfigured() {
|
|
const config = getOpenClawConfig();
|
|
return Boolean(config.baseUrl && hasSupportedProtocol(config.baseUrl) && hasRequiredAuth(config));
|
|
}
|
|
|
|
export async function runOpenClawAnalysis(prompt: string, systemPrompt?: string) {
|
|
const config = getOpenClawConfig();
|
|
const endpoint = config.baseUrl ? buildCompletionsUrl(config.baseUrl) : undefined;
|
|
|
|
if (!endpoint || !hasRequiredAuth(config)) {
|
|
return {
|
|
provider: 'local-fallback',
|
|
model: config.model,
|
|
text: fallbackResponse(prompt)
|
|
};
|
|
}
|
|
|
|
const response = await fetch(endpoint, {
|
|
method: 'POST',
|
|
headers: buildAuthHeaders(config),
|
|
body: JSON.stringify({
|
|
model: config.model,
|
|
temperature: 0.2,
|
|
messages: [
|
|
systemPrompt
|
|
? { role: 'system', content: systemPrompt }
|
|
: null,
|
|
{ role: 'user', content: prompt }
|
|
].filter(Boolean)
|
|
}),
|
|
cache: 'no-store'
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const body = await response.text();
|
|
throw new Error(`OpenClaw request failed (${response.status}): ${body.slice(0, 220)}`);
|
|
}
|
|
|
|
const payload = await response.json() as ChatCompletionResponse;
|
|
const text = payload.choices?.[0]?.message?.content?.trim();
|
|
|
|
if (!text) {
|
|
throw new Error('OpenClaw returned an empty response');
|
|
}
|
|
|
|
return {
|
|
provider: 'openclaw',
|
|
model: config.model,
|
|
text
|
|
};
|
|
}
|