hermes-marketing-dashboard

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Hermes 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
    npm install -g pnpm
    or
    corepack 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 dev
The application will start at
http://localhost:3000
.
bash
git clone https://github.com/builderz-labs/marketing-dashboard.git
cd marketing-dashboard
pnpm install
pnpm env:bootstrap
pnpm dev
应用将在
http://localhost:3000
启动。

Configuration

配置

Required Environment Variables

必填环境变量

Create a
.env.local
file:
bash
undefined
创建
.env.local
文件:
bash
undefined

Authentication (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
undefined
DATABASE_URL=./state/hermes.db
undefined

OpenClaw Integration

OpenClaw集成

For AI agent integration with OpenClaw:
bash
undefined
如需与OpenClaw进行AI Agent集成:
bash
undefined

OpenClaw 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"}]'
undefined
HERMES_OPENCLAW_INSTANCES='[{"name":"prod","path":"/openclaw/prod"},{"name":"dev","path":"/openclaw/dev"}]'
undefined

Security Configuration

安全配置

bash
undefined
bash
undefined

Host 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
undefined
HERMES_ALLOW_POLICY_WRITE=false HERMES_ALLOW_CRON_WRITE=false HERMES_ALLOW_WORKSPACE_WRITE=false
undefined

Optional Analytics Integration

可选数据分析集成

bash
undefined
bash
undefined

Plausible 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
undefined
NEXT_PUBLIC_GA4_ID=G-XXXXXXXXXX
undefined

1Password Runtime Overlay (Optional)

1Password运行时覆盖(可选)

bash
HERMES_1PASSWORD_MODE=auto  # off|auto|required
HERMES_OP_ENV_FILE=/etc/hermes-dashboard/hermes-dashboard.op.env
bash
HERMES_1PASSWORD_MODE=auto  # off|auto|required
HERMES_OP_ENV_FILE=/etc/hermes-dashboard/hermes-dashboard.op.env

Key Commands

核心命令

Development

开发

bash
undefined
bash
undefined

Start 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
undefined
pnpm test:e2e
undefined

Database Management

数据库管理

bash
undefined
bash
undefined

Bootstrap environment and database

初始化环境和数据库

pnpm env:bootstrap
pnpm env:bootstrap

Reset database (warning: destructive)

重置数据库(警告:会清除所有数据)

pnpm db:reset
undefined
pnpm db:reset
undefined

Template Export

模板导出

bash
undefined
bash
undefined

Audit 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
undefined

Core 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
HERMES_OPENCLAW_HOME
points to valid directory:
bash
undefined
确保
HERMES_OPENCLAW_HOME
指向有效目录:
bash
undefined

Verify path

验证路径

ls $HERMES_OPENCLAW_HOME/agents
ls $HERMES_OPENCLAW_HOME/agents

Check permissions

检查权限

chmod -R u+r $HERMES_OPENCLAW_HOME/agents
undefined
chmod -R u+r $HERMES_OPENCLAW_HOME/agents
undefined

Session Cookie Not Persisting

会话Cookie无法持久化

For HTTPS deployments, ensure:
bash
AUTH_COOKIE_SECURE=true
For local HTTP development:
bash
AUTH_COOKIE_SECURE=false
对于HTTPS部署,请确保:
bash
AUTH_COOKIE_SECURE=true
对于本地HTTP开发:
bash
AUTH_COOKIE_SECURE=false

Host Lock Blocking Access

主机锁定阻止访问

If you need to access from network:
bash
undefined
如果需要从网络访问:
bash
undefined

Disable (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
undefined
HERMES_HOST_LOCK=localhost,192.168.1.100,mydomain.com
undefined

API Authentication Failures

API身份验证失败

Verify API key in request headers:
bash
curl -H "Authorization: Bearer $API_KEY" http://localhost:3000/api/crm/leads
Check 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

安全检查清单

  1. Change default credentials:
    bash
    AUTH_USER=your-admin-username
    AUTH_PASS=strong-password-min-10-chars
    API_KEY=cryptographically-secure-key
  2. Enable HTTPS cookie security:
    bash
    AUTH_COOKIE_SECURE=true
  3. Keep host lock enabled:
    bash
    HERMES_HOST_LOCK=yourdomain.com
  4. Keep writeback disabled unless required:
    bash
    HERMES_ALLOW_POLICY_WRITE=false
    HERMES_ALLOW_CRON_WRITE=false
    HERMES_ALLOW_WORKSPACE_WRITE=false
  1. 修改默认凭据:
    bash
    AUTH_USER=your-admin-username
    AUTH_PASS=strong-password-min-10-chars
    API_KEY=cryptographically-secure-key
  2. 启用HTTPS Cookie安全设置:
    bash
    AUTH_COOKIE_SECURE=true
  3. 保持主机锁定启用:
    bash
    HERMES_HOST_LOCK=yourdomain.com
  4. 保持写回功能禁用(除非明确需要):
    bash
    HERMES_ALLOW_POLICY_WRITE=false
    HERMES_ALLOW_CRON_WRITE=false
    HERMES_ALLOW_WORKSPACE_WRITE=false

Build and Deploy

构建与部署

bash
pnpm build
pnpm start
For 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 start
Docker部署示例:
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());
}