Support Nginx-authenticated OpenClaw and fix workflow retry start context
This commit is contained in:
20
README.md
20
README.md
@@ -88,6 +88,12 @@ BETTER_AUTH_TRUSTED_ORIGINS=https://fiscal.b11studio.xyz
|
|||||||
OPENCLAW_BASE_URL=http://localhost:4000
|
OPENCLAW_BASE_URL=http://localhost:4000
|
||||||
OPENCLAW_API_KEY=your_key
|
OPENCLAW_API_KEY=your_key
|
||||||
OPENCLAW_MODEL=zeroclaw
|
OPENCLAW_MODEL=zeroclaw
|
||||||
|
OPENCLAW_AUTH_MODE=bearer
|
||||||
|
# for OPENCLAW_AUTH_MODE=basic
|
||||||
|
# OPENCLAW_BASIC_AUTH_USERNAME=your_nginx_user
|
||||||
|
# OPENCLAW_BASIC_AUTH_PASSWORD=your_nginx_password
|
||||||
|
# optional: forward API key in a custom header when using basic/none auth
|
||||||
|
# OPENCLAW_API_KEY_HEADER=X-OpenClaw-API-Key
|
||||||
SEC_USER_AGENT=Fiscal Clone <support@fiscal.local>
|
SEC_USER_AGENT=Fiscal Clone <support@fiscal.local>
|
||||||
|
|
||||||
WORKFLOW_TARGET_WORLD=local
|
WORKFLOW_TARGET_WORLD=local
|
||||||
@@ -95,7 +101,19 @@ WORKFLOW_LOCAL_DATA_DIR=.workflow-data
|
|||||||
WORKFLOW_LOCAL_QUEUE_CONCURRENCY=100
|
WORKFLOW_LOCAL_QUEUE_CONCURRENCY=100
|
||||||
```
|
```
|
||||||
|
|
||||||
If OpenClaw is unset, the app uses local fallback analysis so task workflows still run.
|
If OpenClaw is unset or invalidly configured, the app uses local fallback analysis so task workflows still run.
|
||||||
|
|
||||||
|
For OpenClaw behind Nginx Basic Auth, use:
|
||||||
|
|
||||||
|
```env
|
||||||
|
OPENCLAW_BASE_URL=https://your-nginx-host
|
||||||
|
OPENCLAW_AUTH_MODE=basic
|
||||||
|
OPENCLAW_BASIC_AUTH_USERNAME=your_nginx_user
|
||||||
|
OPENCLAW_BASIC_AUTH_PASSWORD=your_nginx_password
|
||||||
|
# optional if upstream still needs an API key in a non-Authorization header
|
||||||
|
OPENCLAW_API_KEY=your_key
|
||||||
|
OPENCLAW_API_KEY_HEADER=X-OpenClaw-API-Key
|
||||||
|
```
|
||||||
|
|
||||||
## API surface
|
## API surface
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export async function runTaskWorkflow(taskId: string) {
|
|||||||
|
|
||||||
if (nextState.shouldRetry) {
|
if (nextState.shouldRetry) {
|
||||||
await sleep('1200ms');
|
await sleep('1200ms');
|
||||||
await start(runTaskWorkflow, [task.id]);
|
await restartTaskWorkflowStep(task.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,3 +52,8 @@ async function markTaskFailureStep(taskId: string, reason: string) {
|
|||||||
'use step';
|
'use step';
|
||||||
return await markTaskFailure(taskId, reason);
|
return await markTaskFailure(taskId, reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function restartTaskWorkflowStep(taskId: string) {
|
||||||
|
'use step';
|
||||||
|
await start(runTaskWorkflow, [taskId]);
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ services:
|
|||||||
OPENCLAW_BASE_URL: ${OPENCLAW_BASE_URL:-}
|
OPENCLAW_BASE_URL: ${OPENCLAW_BASE_URL:-}
|
||||||
OPENCLAW_API_KEY: ${OPENCLAW_API_KEY:-}
|
OPENCLAW_API_KEY: ${OPENCLAW_API_KEY:-}
|
||||||
OPENCLAW_MODEL: ${OPENCLAW_MODEL:-zeroclaw}
|
OPENCLAW_MODEL: ${OPENCLAW_MODEL:-zeroclaw}
|
||||||
|
OPENCLAW_AUTH_MODE: ${OPENCLAW_AUTH_MODE:-bearer}
|
||||||
|
OPENCLAW_BASIC_AUTH_USERNAME: ${OPENCLAW_BASIC_AUTH_USERNAME:-}
|
||||||
|
OPENCLAW_BASIC_AUTH_PASSWORD: ${OPENCLAW_BASIC_AUTH_PASSWORD:-}
|
||||||
|
OPENCLAW_API_KEY_HEADER: ${OPENCLAW_API_KEY_HEADER:-}
|
||||||
SEC_USER_AGENT: ${SEC_USER_AGENT:-Fiscal Clone <support@fiscal.local>}
|
SEC_USER_AGENT: ${SEC_USER_AGENT:-Fiscal Clone <support@fiscal.local>}
|
||||||
WORKFLOW_TARGET_WORLD: local
|
WORKFLOW_TARGET_WORLD: local
|
||||||
WORKFLOW_LOCAL_DATA_DIR: ${WORKFLOW_LOCAL_DATA_DIR:-/app/.workflow-data}
|
WORKFLOW_LOCAL_DATA_DIR: ${WORKFLOW_LOCAL_DATA_DIR:-/app/.workflow-data}
|
||||||
|
|||||||
@@ -6,6 +6,18 @@ type ChatCompletionResponse = {
|
|||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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) {
|
function envValue(name: string) {
|
||||||
const value = process.env[name];
|
const value = process.env[name];
|
||||||
if (!value) {
|
if (!value) {
|
||||||
@@ -17,12 +29,73 @@ function envValue(name: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_MODEL = 'zeroclaw';
|
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) {
|
function fallbackResponse(prompt: string) {
|
||||||
const clipped = prompt.split('\n').slice(0, 6).join(' ').slice(0, 260);
|
const clipped = prompt.split('\n').slice(0, 6).join(' ').slice(0, 260);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'OpenClaw fallback mode is active (missing OPENCLAW_BASE_URL or OPENCLAW_API_KEY).',
|
'OpenClaw fallback mode is active (configuration is missing or invalid).',
|
||||||
'Thesis: Portfolio remains analyzable with local heuristics until live model access is configured.',
|
'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.',
|
'Risk scan: Concentration and filing sentiment should be monitored after each sync cycle.',
|
||||||
`Context digest: ${clipped}`
|
`Context digest: ${clipped}`
|
||||||
@@ -33,19 +106,24 @@ export function getOpenClawConfig() {
|
|||||||
return {
|
return {
|
||||||
baseUrl: envValue('OPENCLAW_BASE_URL'),
|
baseUrl: envValue('OPENCLAW_BASE_URL'),
|
||||||
apiKey: envValue('OPENCLAW_API_KEY'),
|
apiKey: envValue('OPENCLAW_API_KEY'),
|
||||||
model: envValue('OPENCLAW_MODEL') ?? DEFAULT_MODEL
|
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() {
|
export function isOpenClawConfigured() {
|
||||||
const config = getOpenClawConfig();
|
const config = getOpenClawConfig();
|
||||||
return Boolean(config.baseUrl && config.apiKey);
|
return Boolean(config.baseUrl && hasSupportedProtocol(config.baseUrl) && hasRequiredAuth(config));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runOpenClawAnalysis(prompt: string, systemPrompt?: string) {
|
export async function runOpenClawAnalysis(prompt: string, systemPrompt?: string) {
|
||||||
const config = getOpenClawConfig();
|
const config = getOpenClawConfig();
|
||||||
|
const endpoint = config.baseUrl ? buildCompletionsUrl(config.baseUrl) : undefined;
|
||||||
|
|
||||||
if (!config.baseUrl || !config.apiKey) {
|
if (!endpoint || !hasRequiredAuth(config)) {
|
||||||
return {
|
return {
|
||||||
provider: 'local-fallback',
|
provider: 'local-fallback',
|
||||||
model: config.model,
|
model: config.model,
|
||||||
@@ -53,12 +131,9 @@ export async function runOpenClawAnalysis(prompt: string, systemPrompt?: string)
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(`${config.baseUrl}/v1/chat/completions`, {
|
const response = await fetch(endpoint, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: buildAuthHeaders(config),
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Authorization: `Bearer ${config.apiKey}`
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model: config.model,
|
model: config.model,
|
||||||
temperature: 0.2,
|
temperature: 0.2,
|
||||||
|
|||||||
Reference in New Issue
Block a user