hermes-marketing-dashboard
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseHermes Marketing Dashboard
Hermes营销仪表盘
Skill by ara.so — Marketing Skills collection.
Hermes Dashboard is an open-source marketing operations control center designed for AI agent teams. It provides CRM, outreach sequencing, content operations, analytics, and automation workflows in a single Next.js application powered by OpenClaw integration and local SQLite storage.
由ara.so开发的Skill——营销技能合集。
Hermes仪表盘是一款面向AI Agent团队的开源营销运营控制中心。它在一个基于Next.js的应用中集成了CRM、客户触达序列编排、内容运营、数据分析和自动化工作流,依托OpenClaw集成和本地SQLite存储实现。
Installation
安装
Prerequisites
前置条件
- Node.js 18+
- pnpm (required) - install with or
npm install -g pnpmcorepack enable - OpenClaw CLI (optional but recommended for agent integration)
- Node.js 18+
- pnpm(必填)- 通过 或
npm install -g pnpm安装corepack enable - OpenClaw CLI(可选,但推荐用于Agent集成)
Quick Start
快速开始
bash
git clone https://github.com/builderz-labs/marketing-dashboard.git
cd marketing-dashboard
pnpm install
pnpm env:bootstrap
pnpm devThe application will start at .
http://localhost:3000bash
git clone https://github.com/builderz-labs/marketing-dashboard.git
cd marketing-dashboard
pnpm install
pnpm env:bootstrap
pnpm dev应用将在 启动。
http://localhost:3000Configuration
配置
Required Environment Variables
必填环境变量
Create a file:
.env.localbash
undefined创建 文件:
.env.localbash
undefinedAuthentication (required)
身份验证(必填)
AUTH_USER=admin
AUTH_PASS=your-secure-password-min-10-chars
API_KEY=your-api-key-for-programmatic-access
AUTH_USER=admin
AUTH_PASS=your-secure-password-min-10-chars
API_KEY=your-api-key-for-programmatic-access
Cookie security (false for HTTP local, true for HTTPS production)
Cookie安全设置(本地HTTP环境设为false,生产HTTPS环境设为true)
AUTH_COOKIE_SECURE=false
AUTH_COOKIE_SECURE=false
Database (auto-created in ./state)
数据库(自动创建于./state目录)
DATABASE_URL=./state/hermes.db
undefinedDATABASE_URL=./state/hermes.db
undefinedOpenClaw Integration
OpenClaw集成
For AI agent integration with OpenClaw:
bash
undefined如需与OpenClaw进行AI Agent集成:
bash
undefinedOpenClaw home directory
OpenClaw主目录
HERMES_OPENCLAW_HOME=/path/to/openclaw
HERMES_OPENCLAW_HOME=/path/to/openclaw
Default instance name
默认实例名称
HERMES_DEFAULT_INSTANCE=main
HERMES_DEFAULT_INSTANCE=main
Multi-instance support (optional JSON array)
多实例支持(可选JSON数组)
HERMES_OPENCLAW_INSTANCES='[{"name":"prod","path":"/openclaw/prod"},{"name":"dev","path":"/openclaw/dev"}]'
undefinedHERMES_OPENCLAW_INSTANCES='[{"name":"prod","path":"/openclaw/prod"},{"name":"dev","path":"/openclaw/dev"}]'
undefinedSecurity Configuration
安全配置
bash
undefinedbash
undefinedHost access lock (default: local-only)
主机访问限制(默认:仅本地)
HERMES_HOST_LOCK=local # or 'off' or 'host1,host2'
HERMES_HOST_LOCK=local # 或设为'off'或'host1,host2'
Writeback protection (keep false unless explicitly needed)
写回保护(除非明确需要,否则保持false)
HERMES_ALLOW_POLICY_WRITE=false
HERMES_ALLOW_CRON_WRITE=false
HERMES_ALLOW_WORKSPACE_WRITE=false
undefinedHERMES_ALLOW_POLICY_WRITE=false
HERMES_ALLOW_CRON_WRITE=false
HERMES_ALLOW_WORKSPACE_WRITE=false
undefinedOptional Analytics Integration
可选数据分析集成
bash
undefinedbash
undefinedPlausible Analytics
Plausible Analytics
NEXT_PUBLIC_PLAUSIBLE_DOMAIN=yourdomain.com
PLAUSIBLE_API_KEY=your-plausible-api-key
NEXT_PUBLIC_PLAUSIBLE_DOMAIN=yourdomain.com
PLAUSIBLE_API_KEY=your-plausible-api-key
Google Analytics 4
Google Analytics 4
NEXT_PUBLIC_GA4_ID=G-XXXXXXXXXX
undefinedNEXT_PUBLIC_GA4_ID=G-XXXXXXXXXX
undefined1Password Runtime Overlay (Optional)
1Password运行时覆盖(可选)
bash
HERMES_1PASSWORD_MODE=auto # off|auto|required
HERMES_OP_ENV_FILE=/etc/hermes-dashboard/hermes-dashboard.op.envbash
HERMES_1PASSWORD_MODE=auto # off|auto|required
HERMES_OP_ENV_FILE=/etc/hermes-dashboard/hermes-dashboard.op.envKey Commands
核心命令
Development
开发
bash
undefinedbash
undefinedStart development server
启动开发服务器
pnpm dev
pnpm dev
Build for production
构建生产版本
pnpm build
pnpm build
Start production server
启动生产服务器
pnpm start
pnpm start
Type checking
类型检查
pnpm typecheck
pnpm typecheck
Linting
代码检查
pnpm lint
pnpm lint
Run tests
运行测试
pnpm test
pnpm test
Run end-to-end tests
运行端到端测试
pnpm test:e2e
undefinedpnpm test:e2e
undefinedDatabase Management
数据库管理
bash
undefinedbash
undefinedBootstrap environment and database
初始化环境和数据库
pnpm env:bootstrap
pnpm env:bootstrap
Reset database (warning: destructive)
重置数据库(警告:会清除所有数据)
pnpm db:reset
undefinedpnpm db:reset
undefinedTemplate Export
模板导出
bash
undefinedbash
undefinedAudit template for sensitive data
审计模板中的敏感数据
./scripts/template-audit.sh
./scripts/template-audit.sh
Export clean template
导出清理后的模板
./scripts/template-export.sh /path/to/output
undefined./scripts/template-export.sh /path/to/output
undefinedCore API Patterns
核心API模式
CRM Lead Management
CRM线索管理
typescript
// app/api/crm/leads/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { getDatabase } from '@/lib/db';
export async function GET(request: NextRequest) {
const db = getDatabase();
const leads = db.prepare(`
SELECT id, email, name, source, status, created_at
FROM crm_leads
WHERE status = ?
ORDER BY created_at DESC
`).all('active');
return NextResponse.json({ leads });
}
export async function POST(request: NextRequest) {
const db = getDatabase();
const { email, name, source, metadata } = await request.json();
const result = db.prepare(`
INSERT INTO crm_leads (email, name, source, metadata, status)
VALUES (?, ?, ?, ?, 'new')
`).run(email, name, source, JSON.stringify(metadata));
return NextResponse.json({ id: result.lastInsertRowid }, { status: 201 });
}typescript
// app/api/crm/leads/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { getDatabase } from '@/lib/db';
export async function GET(request: NextRequest) {
const db = getDatabase();
const leads = db.prepare(`
SELECT id, email, name, source, status, created_at
FROM crm_leads
WHERE status = ?
ORDER BY created_at DESC
`).all('active');
return NextResponse.json({ leads });
}
export async function POST(request: NextRequest) {
const db = getDatabase();
const { email, name, source, metadata } = await request.json();
const result = db.prepare(`
INSERT INTO crm_leads (email, name, source, metadata, status)
VALUES (?, ?, ?, ?, 'new')
`).run(email, name, source, JSON.stringify(metadata));
return NextResponse.json({ id: result.lastInsertRowid }, { status: 201 });
}Outreach Sequencing
客户触达序列编排
typescript
// lib/outreach/sequence.ts
import { getDatabase } from '@/lib/db';
export interface OutreachSequence {
id: number;
name: string;
steps: OutreachStep[];
status: 'active' | 'paused' | 'archived';
}
export interface OutreachStep {
order: number;
delay_hours: number;
template_id: string;
channel: 'email' | 'linkedin' | 'twitter';
}
export function createSequence(
name: string,
steps: OutreachStep[]
): number {
const db = getDatabase();
const result = db.prepare(`
INSERT INTO outreach_sequences (name, steps, status)
VALUES (?, ?, 'active')
`).run(name, JSON.stringify(steps));
return result.lastInsertRowid as number;
}
export function enrollInSequence(
leadId: number,
sequenceId: number
): void {
const db = getDatabase();
db.prepare(`
INSERT INTO outreach_enrollments (lead_id, sequence_id, current_step, status)
VALUES (?, ?, 0, 'active')
`).run(leadId, sequenceId);
}
export function pauseSequence(sequenceId: number): void {
const db = getDatabase();
db.prepare(`
UPDATE outreach_sequences
SET status = 'paused'
WHERE id = ?
`).run(sequenceId);
}typescript
// lib/outreach/sequence.ts
import { getDatabase } from '@/lib/db';
export interface OutreachSequence {
id: number;
name: string;
steps: OutreachStep[];
status: 'active' | 'paused' | 'archived';
}
export interface OutreachStep {
order: number;
delay_hours: number;
template_id: string;
channel: 'email' | 'linkedin' | 'twitter';
}
export function createSequence(
name: string,
steps: OutreachStep[]
): number {
const db = getDatabase();
const result = db.prepare(`
INSERT INTO outreach_sequences (name, steps, status)
VALUES (?, ?, 'active')
`).run(name, JSON.stringify(steps));
return result.lastInsertRowid as number;
}
export function enrollInSequence(
leadId: number,
sequenceId: number
): void {
const db = getDatabase();
db.prepare(`
INSERT INTO outreach_enrollments (lead_id, sequence_id, current_step, status)
VALUES (?, ?, 0, 'active')
`).run(leadId, sequenceId);
}
export function pauseSequence(sequenceId: number): void {
const db = getDatabase();
db.prepare(`
UPDATE outreach_sequences
SET status = 'paused'
WHERE id = ?
`).run(sequenceId);
}Content Operations
内容运营
typescript
// lib/content/calendar.ts
import { getDatabase } from '@/lib/db';
export interface ContentItem {
id: number;
title: string;
type: 'blog' | 'social' | 'email' | 'video';
status: 'draft' | 'scheduled' | 'published';
scheduled_at?: Date;
published_at?: Date;
metadata: Record<string, any>;
}
export function getContentCalendar(
startDate: Date,
endDate: Date
): ContentItem[] {
const db = getDatabase();
const items = db.prepare(`
SELECT id, title, type, status, scheduled_at, published_at, metadata
FROM content_items
WHERE scheduled_at BETWEEN ? AND ?
ORDER BY scheduled_at ASC
`).all(startDate.toISOString(), endDate.toISOString());
return items.map(item => ({
...item,
metadata: JSON.parse(item.metadata as string),
scheduled_at: item.scheduled_at ? new Date(item.scheduled_at) : undefined,
published_at: item.published_at ? new Date(item.published_at) : undefined,
}));
}
export function createContentItem(item: Omit<ContentItem, 'id'>): number {
const db = getDatabase();
const result = db.prepare(`
INSERT INTO content_items (title, type, status, scheduled_at, metadata)
VALUES (?, ?, ?, ?, ?)
`).run(
item.title,
item.type,
item.status,
item.scheduled_at?.toISOString(),
JSON.stringify(item.metadata)
);
return result.lastInsertRowid as number;
}typescript
// lib/content/calendar.ts
import { getDatabase } from '@/lib/db';
export interface ContentItem {
id: number;
title: string;
type: 'blog' | 'social' | 'email' | 'video';
status: 'draft' | 'scheduled' | 'published';
scheduled_at?: Date;
published_at?: Date;
metadata: Record<string, any>;
}
export function getContentCalendar(
startDate: Date,
endDate: Date
): ContentItem[] {
const db = getDatabase();
const items = db.prepare(`
SELECT id, title, type, status, scheduled_at, published_at, metadata
FROM content_items
WHERE scheduled_at BETWEEN ? AND ?
ORDER BY scheduled_at ASC
`).all(startDate.toISOString(), endDate.toISOString());
return items.map(item => ({
...item,
metadata: JSON.parse(item.metadata as string),
scheduled_at: item.scheduled_at ? new Date(item.scheduled_at) : undefined,
published_at: item.published_at ? new Date(item.published_at) : undefined,
}));
}
export function createContentItem(item: Omit<ContentItem, 'id'>): number {
const db = getDatabase();
const result = db.prepare(`
INSERT INTO content_items (title, type, status, scheduled_at, metadata)
VALUES (?, ?, ?, ?, ?)
`).run(
item.title,
item.type,
item.status,
item.scheduled_at?.toISOString(),
JSON.stringify(item.metadata)
);
return result.lastInsertRowid as number;
}OpenClaw Agent Integration
OpenClaw Agent集成
typescript
// lib/openclaw/agents.ts
import { readdir, readFile } from 'fs/promises';
import { join } from 'path';
export interface OpenClawAgent {
name: string;
description: string;
type: 'agent' | 'squad';
capabilities: string[];
config: Record<string, any>;
}
export async function discoverAgents(
openclawHome: string
): Promise<OpenClawAgent[]> {
const agentsDir = join(openclawHome, 'agents');
const agents: OpenClawAgent[] = [];
try {
const entries = await readdir(agentsDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
const configPath = join(agentsDir, entry.name, 'agent.json');
try {
const configData = await readFile(configPath, 'utf-8');
const config = JSON.parse(configData);
agents.push({
name: entry.name,
description: config.description || '',
type: config.type || 'agent',
capabilities: config.capabilities || [],
config,
});
} catch (err) {
console.warn(`Could not load agent config for ${entry.name}`);
}
}
}
} catch (err) {
console.error('Failed to discover agents:', err);
}
return agents;
}typescript
// lib/openclaw/agents.ts
import { readdir, readFile } from 'fs/promises';
import { join } from 'path';
export interface OpenClawAgent {
name: string;
description: string;
type: 'agent' | 'squad';
capabilities: string[];
config: Record<string, any>;
}
export async function discoverAgents(
openclawHome: string
): Promise<OpenClawAgent[]> {
const agentsDir = join(openclawHome, 'agents');
const agents: OpenClawAgent[] = [];
try {
const entries = await readdir(agentsDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
const configPath = join(agentsDir, entry.name, 'agent.json');
try {
const configData = await readFile(configPath, 'utf-8');
const config = JSON.parse(configData);
agents.push({
name: entry.name,
description: config.description || '',
type: config.type || 'agent',
capabilities: config.capabilities || [],
config,
});
} catch (err) {
console.warn(`Could not load agent config for ${entry.name}`);
}
}
}
} catch (err) {
console.error('Failed to discover agents:', err);
}
return agents;
}Cron Job Management
Cron任务管理
typescript
// lib/cron/scheduler.ts
import { getDatabase } from '@/lib/db';
export interface CronJob {
id: number;
name: string;
schedule: string; // cron expression, 'every 1h', or 'at 09:00'
agent: string;
task: string;
enabled: boolean;
last_run?: Date;
}
export function createCronJob(job: Omit<CronJob, 'id'>): number {
const db = getDatabase();
const result = db.prepare(`
INSERT INTO cron_jobs (name, schedule, agent, task, enabled)
VALUES (?, ?, ?, ?, ?)
`).run(job.name, job.schedule, job.agent, job.task, job.enabled ? 1 : 0);
return result.lastInsertRowid as number;
}
export function getCronJobs(): CronJob[] {
const db = getDatabase();
const jobs = db.prepare(`
SELECT id, name, schedule, agent, task, enabled, last_run
FROM cron_jobs
ORDER BY name ASC
`).all();
return jobs.map(job => ({
...job,
enabled: Boolean(job.enabled),
last_run: job.last_run ? new Date(job.last_run) : undefined,
}));
}
export function updateLastRun(jobId: number): void {
const db = getDatabase();
db.prepare(`
UPDATE cron_jobs
SET last_run = ?
WHERE id = ?
`).run(new Date().toISOString(), jobId);
}typescript
// lib/cron/scheduler.ts
import { getDatabase } from '@/lib/db';
export interface CronJob {
id: number;
name: string;
schedule: string; // cron表达式, 'every 1h', 或 'at 09:00'
agent: string;
task: string;
enabled: boolean;
last_run?: Date;
}
export function createCronJob(job: Omit<CronJob, 'id'>): number {
const db = getDatabase();
const result = db.prepare(`
INSERT INTO cron_jobs (name, schedule, agent, task, enabled)
VALUES (?, ?, ?, ?, ?)
`).run(job.name, job.schedule, job.agent, job.task, job.enabled ? 1 : 0);
return result.lastInsertRowid as number;
}
export function getCronJobs(): CronJob[] {
const db = getDatabase();
const jobs = db.prepare(`
SELECT id, name, schedule, agent, task, enabled, last_run
FROM cron_jobs
ORDER BY name ASC
`).all();
return jobs.map(job => ({
...job,
enabled: Boolean(job.enabled),
last_run: job.last_run ? new Date(job.last_run) : undefined,
}));
}
export function updateLastRun(jobId: number): void {
const db = getDatabase();
db.prepare(`
UPDATE cron_jobs
SET last_run = ?
WHERE id = ?
`).run(new Date().toISOString(), jobId);
}Authentication Patterns
身份验证模式
Session-Based Auth
基于会话的身份验证
typescript
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const session = request.cookies.get('hermes-session');
if (!session && request.nextUrl.pathname.startsWith('/api/')) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
return NextResponse.next();
}
export const config = {
matcher: '/api/:path*',
};typescript
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const session = request.cookies.get('hermes-session');
if (!session && request.nextUrl.pathname.startsWith('/api/')) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
return NextResponse.next();
}
export const config = {
matcher: '/api/:path*',
};API Key Auth
API密钥身份验证
typescript
// lib/auth/api-key.ts
import { NextRequest } from 'next/server';
export function validateApiKey(request: NextRequest): boolean {
const authHeader = request.headers.get('authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return false;
}
const token = authHeader.substring(7);
return token === process.env.API_KEY;
}
// Usage in API route
export async function GET(request: NextRequest) {
if (!validateApiKey(request)) {
return NextResponse.json({ error: 'Invalid API key' }, { status: 401 });
}
// Continue with request...
}typescript
// lib/auth/api-key.ts
import { NextRequest } from 'next/server';
export function validateApiKey(request: NextRequest): boolean {
const authHeader = request.headers.get('authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return false;
}
const token = authHeader.substring(7);
return token === process.env.API_KEY;
}
// 在API路由中的用法
export async function GET(request: NextRequest) {
if (!validateApiKey(request)) {
return NextResponse.json({ error: 'Invalid API key' }, { status: 401 });
}
// 继续处理请求...
}Analytics Integration
数据分析集成
Track Events
事件追踪
typescript
// lib/analytics/events.ts
import { getDatabase } from '@/lib/db';
export interface AnalyticsEvent {
event_type: string;
properties: Record<string, any>;
user_id?: string;
session_id?: string;
}
export function trackEvent(event: AnalyticsEvent): void {
const db = getDatabase();
db.prepare(`
INSERT INTO analytics_events (event_type, properties, user_id, session_id, timestamp)
VALUES (?, ?, ?, ?, ?)
`).run(
event.event_type,
JSON.stringify(event.properties),
event.user_id,
event.session_id,
new Date().toISOString()
);
}
export function getKPIs(startDate: Date, endDate: Date) {
const db = getDatabase();
return {
leads: db.prepare(`
SELECT COUNT(*) as count FROM crm_leads
WHERE created_at BETWEEN ? AND ?
`).get(startDate.toISOString(), endDate.toISOString()),
outreach_sent: db.prepare(`
SELECT COUNT(*) as count FROM outreach_messages
WHERE sent_at BETWEEN ? AND ?
`).get(startDate.toISOString(), endDate.toISOString()),
content_published: db.prepare(`
SELECT COUNT(*) as count FROM content_items
WHERE published_at BETWEEN ? AND ?
`).get(startDate.toISOString(), endDate.toISOString()),
};
}typescript
// lib/analytics/events.ts
import { getDatabase } from '@/lib/db';
export interface AnalyticsEvent {
event_type: string;
properties: Record<string, any>;
user_id?: string;
session_id?: string;
}
export function trackEvent(event: AnalyticsEvent): void {
const db = getDatabase();
db.prepare(`
INSERT INTO analytics_events (event_type, properties, user_id, session_id, timestamp)
VALUES (?, ?, ?, ?, ?)
`).run(
event.event_type,
JSON.stringify(event.properties),
event.user_id,
event.session_id,
new Date().toISOString()
);
}
export function getKPIs(startDate: Date, endDate: Date) {
const db = getDatabase();
return {
leads: db.prepare(`
SELECT COUNT(*) as count FROM crm_leads
WHERE created_at BETWEEN ? AND ?
`).get(startDate.toISOString(), endDate.toISOString()),
outreach_sent: db.prepare(`
SELECT COUNT(*) as count FROM outreach_messages
WHERE sent_at BETWEEN ? AND ?
`).get(startDate.toISOString(), endDate.toISOString()),
content_published: db.prepare(`
SELECT COUNT(*) as count FROM content_items
WHERE published_at BETWEEN ? AND ?
`).get(startDate.toISOString(), endDate.toISOString()),
};
}Component Patterns
组件模式
Dashboard Widget
仪表盘组件
typescript
// components/dashboard/LeadsFunnel.tsx
'use client';
import { useEffect, useState } from 'react';
interface FunnelData {
stage: string;
count: number;
}
export function LeadsFunnel() {
const [data, setData] = useState<FunnelData[]>([]);
useEffect(() => {
fetch('/api/crm/funnel')
.then(res => res.json())
.then(setData);
}, []);
return (
<div className="funnel-widget">
<h3>Pipeline Funnel</h3>
{data.map(stage => (
<div key={stage.stage} className="funnel-stage">
<span>{stage.stage}</span>
<span>{stage.count}</span>
</div>
))}
</div>
);
}typescript
// components/dashboard/LeadsFunnel.tsx
'use client';
import { useEffect, useState } from 'react';
interface FunnelData {
stage: string;
count: number;
}
export function LeadsFunnel() {
const [data, setData] = useState<FunnelData[]>([]);
useEffect(() => {
fetch('/api/crm/funnel')
.then(res => res.json())
.then(setData);
}, []);
return (
<div className="funnel-widget">
<h3>销售漏斗</h3>
{data.map(stage => (
<div key={stage.stage} className="funnel-stage">
<span>{stage.stage}</span>
<span>{stage.count}</span>
</div>
))}
</div>
);
}Troubleshooting
故障排查
Database Lock Errors
数据库锁定错误
SQLite database locks can occur with concurrent writes:
typescript
// lib/db.ts
import Database from 'better-sqlite3';
let db: Database.Database | null = null;
export function getDatabase(): Database.Database {
if (!db) {
db = new Database(process.env.DATABASE_URL || './state/hermes.db');
db.pragma('journal_mode = WAL'); // Write-Ahead Logging for better concurrency
db.pragma('busy_timeout = 5000'); // 5 second timeout
}
return db;
}并发写入时可能会出现SQLite数据库锁定问题:
typescript
// lib/db.ts
import Database from 'better-sqlite3';
let db: Database.Database | null = null;
export function getDatabase(): Database.Database {
if (!db) {
db = new Database(process.env.DATABASE_URL || './state/hermes.db');
db.pragma('journal_mode = WAL'); // 启用预写日志以提升并发性能
db.pragma('busy_timeout = 5000'); // 设置5秒超时
}
return db;
}OpenClaw Agent Discovery Fails
OpenClaw Agent发现失败
Ensure points to valid directory:
HERMES_OPENCLAW_HOMEbash
undefined确保指向有效目录:
HERMES_OPENCLAW_HOMEbash
undefinedVerify path
验证路径
ls $HERMES_OPENCLAW_HOME/agents
ls $HERMES_OPENCLAW_HOME/agents
Check permissions
检查权限
chmod -R u+r $HERMES_OPENCLAW_HOME/agents
undefinedchmod -R u+r $HERMES_OPENCLAW_HOME/agents
undefinedSession Cookie Not Persisting
会话Cookie无法持久化
For HTTPS deployments, ensure:
bash
AUTH_COOKIE_SECURE=trueFor local HTTP development:
bash
AUTH_COOKIE_SECURE=false对于HTTPS部署,请确保:
bash
AUTH_COOKIE_SECURE=true对于本地HTTP开发:
bash
AUTH_COOKIE_SECURE=falseHost Lock Blocking Access
主机锁定阻止访问
If you need to access from network:
bash
undefined如果需要从网络访问:
bash
undefinedDisable (not recommended for production)
禁用(不推荐用于生产环境)
HERMES_HOST_LOCK=off
HERMES_HOST_LOCK=off
Allow specific hosts
允许特定主机
HERMES_HOST_LOCK=localhost,192.168.1.100,mydomain.com
undefinedHERMES_HOST_LOCK=localhost,192.168.1.100,mydomain.com
undefinedAPI Authentication Failures
API身份验证失败
Verify API key in request headers:
bash
curl -H "Authorization: Bearer $API_KEY" http://localhost:3000/api/crm/leadsCheck environment variable is loaded:
typescript
// In API route
if (!process.env.API_KEY) {
console.error('API_KEY not configured');
}验证请求头中的API密钥:
bash
curl -H "Authorization: Bearer $API_KEY" http://localhost:3000/api/crm/leads检查环境变量是否已加载:
typescript
// 在API路由中
if (!process.env.API_KEY) {
console.error('API_KEY未配置');
}Production Deployment
生产部署
Security Checklist
安全检查清单
-
Change default credentials:bash
AUTH_USER=your-admin-username AUTH_PASS=strong-password-min-10-chars API_KEY=cryptographically-secure-key -
Enable HTTPS cookie security:bash
AUTH_COOKIE_SECURE=true -
Keep host lock enabled:bash
HERMES_HOST_LOCK=yourdomain.com -
Keep writeback disabled unless required:bash
HERMES_ALLOW_POLICY_WRITE=false HERMES_ALLOW_CRON_WRITE=false HERMES_ALLOW_WORKSPACE_WRITE=false
-
修改默认凭据:bash
AUTH_USER=your-admin-username AUTH_PASS=strong-password-min-10-chars API_KEY=cryptographically-secure-key -
启用HTTPS Cookie安全设置:bash
AUTH_COOKIE_SECURE=true -
保持主机锁定启用:bash
HERMES_HOST_LOCK=yourdomain.com -
保持写回功能禁用(除非明确需要):bash
HERMES_ALLOW_POLICY_WRITE=false HERMES_ALLOW_CRON_WRITE=false HERMES_ALLOW_WORKSPACE_WRITE=false
Build and Deploy
构建与部署
bash
pnpm build
pnpm startFor Docker deployment:
dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
EXPOSE 3000
CMD ["pnpm", "start"]bash
pnpm build
pnpm startDocker部署示例:
dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
EXPOSE 3000
CMD ["pnpm", "start"]Common Patterns
常见模式
Multi-Instance OpenClaw Setup
多实例OpenClaw配置
typescript
// lib/openclaw/instances.ts
export function getInstances(): Array<{name: string, path: string}> {
const instancesEnv = process.env.HERMES_OPENCLAW_INSTANCES;
if (instancesEnv) {
return JSON.parse(instancesEnv);
}
return [{
name: process.env.HERMES_DEFAULT_INSTANCE || 'main',
path: process.env.HERMES_OPENCLAW_HOME || './openclaw'
}];
}typescript
// lib/openclaw/instances.ts
export function getInstances(): Array<{name: string, path: string}> {
const instancesEnv = process.env.HERMES_OPENCLAW_INSTANCES;
if (instancesEnv) {
return JSON.parse(instancesEnv);
}
return [{
name: process.env.HERMES_DEFAULT_INSTANCE || 'main',
path: process.env.HERMES_OPENCLAW_HOME || './openclaw'
}];
}Audit Logging
审计日志
typescript
// lib/audit/logger.ts
import { getDatabase } from '@/lib/db';
export function logAuditEvent(
action: string,
userId: string,
metadata: Record<string, any>
): void {
const db = getDatabase();
db.prepare(`
INSERT INTO audit_log (action, user_id, metadata, timestamp)
VALUES (?, ?, ?, ?)
`).run(action, userId, JSON.stringify(metadata), new Date().toISOString());
}typescript
// lib/audit/logger.ts
import { getDatabase } from '@/lib/db';
export function logAuditEvent(
action: string,
userId: string,
metadata: Record<string, any>
): void {
const db = getDatabase();
db.prepare(`
INSERT INTO audit_log (action, user_id, metadata, timestamp)
VALUES (?, ?, ?, ?)
`).run(action, userId, JSON.stringify(metadata), new Date().toISOString());
}