open-design-ai-prototyping

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Open Design AI Prototyping

Open Design AI 原型设计

Skill by ara.so — Design Skills collection.
Open Design is a local-first, open-source alternative to Claude Design that transforms coding agents (Claude Code, Cursor, Codex, Gemini CLI, etc.) into design engines. It provides 31 composable skills, 129 brand-grade design systems, and a sandboxed preview environment with HTML/PDF/PPTX/MP4 export capabilities.
ara.so 提供的技能——设计技能合集。
Open Design是Claude Design的本地优先开源替代方案,可将编码Agent(Claude Code、Cursor、Codex、Gemini CLI等)转化为设计引擎。它提供31种可组合技能、129套品牌级设计系统,以及支持HTML/PDF/PPTX/MP4导出功能的沙箱预览环境。

What It Does

功能特性

  • Agent-Native Design: Detects 16 coding agent CLIs on your PATH and uses them as the design execution engine
  • Skill-Driven Workflow: 31 built-in skills across web prototypes, decks, dashboards, mobile apps, marketing materials, and more
  • Design Systems Library: 129 pre-built design systems (Linear, Stripe, Vercel, Airbnb, Tesla, Notion, etc.)
  • Multi-Format Export: HTML, PDF, PPTX, MP4, ZIP, Markdown from a single artifact
  • Local-First: SQLite persistence, on-disk project folders, no cloud lock-in
  • BYOK Support: OpenAI/Anthropic/Azure/Google-compatible API proxy when no CLI is available
  • Agent原生设计:检测PATH中的16种编码Agent CLI,并将其用作设计执行引擎
  • 技能驱动工作流:内置31种技能,覆盖网页原型、演示文稿、仪表盘、移动应用、营销物料等场景
  • 设计系统库:129套预构建设计系统(Linear、Stripe、Vercel、Airbnb、Tesla、Notion等)
  • 多格式导出:从单一工件导出HTML、PDF、PPTX、MP4、ZIP、Markdown格式文件
  • 本地优先:采用SQLite持久化存储,项目文件夹本地存储,无云锁定
  • BYOK支持:当无CLI可用时,支持OpenAI/Anthropic/Azure/Google兼容的API代理

Installation

安装

Quick Start (Local Development)

快速开始(本地开发)

bash
undefined
bash
undefined

Clone the repository

克隆仓库

Install dependencies

安装依赖

pnpm install
pnpm install

Start daemon + web interface

启动守护进程 + Web界面

pnpm tools-dev

This boots:
- Daemon on `http://localhost:3001`
- Web UI on `http://localhost:3000`
- Auto-detects coding agents on your `PATH`
pnpm tools-dev

此命令将启动:
- 守护进程:`http://localhost:3001`
- Web界面:`http://localhost:3000`
- 自动检测PATH中的编码Agent

Desktop App

桌面应用

Download pre-built installers from open-design.ai:
  • macOS (Apple Silicon):
    .dmg
  • Windows (x64):
    .exe
open-design.ai 下载预构建安装包:
  • macOS(Apple Silicon):
    .dmg
  • Windows(x64):
    .exe

Vercel Deployment (Web Layer Only)

Vercel部署(仅Web层)

bash
undefined
bash
undefined

Deploy web interface (daemon runs separately)

部署Web界面(守护进程需单独运行)

vercel deploy
vercel deploy

Set environment variables in Vercel dashboard:

在Vercel控制台设置环境变量:

- DAEMON_URL=your-daemon-endpoint

- DAEMON_URL=你的守护进程端点

- ANTHROPIC_API_KEY (optional, for BYOK)

- ANTHROPIC_API_KEY(可选,用于BYOK)

- OPENAI_API_KEY (optional, for BYOK)

- OPENAI_API_KEY(可选,用于BYOK)

undefined
undefined

Project Structure

项目结构

open-design/
├── apps/
│   ├── daemon/          # Core agent orchestration service
│   │   ├── src/
│   │   │   ├── prompts/ # Discovery, directions, critique prompts
│   │   │   ├── agents/  # CLI adapters (claude-code, cursor, etc.)
│   │   │   └── routes/  # API endpoints
│   │   └── package.json
│   ├── web/             # Next.js frontend
│   │   ├── app/
│   │   ├── components/
│   │   └── lib/
│   └── desktop/         # Electron wrapper (optional)
├── skills/              # 31 built-in skills
│   ├── web-prototype/
│   ├── guizang-ppt/     # Magazine-style decks
│   ├── saas-landing/
│   └── ...
├── design-systems/      # 129 design systems
│   ├── linear/
│   ├── stripe/
│   └── ...
└── prompt-templates/    # Media generation gallery (93 prompts)
open-design/
├── apps/
│   ├── daemon/          # 核心Agent编排服务
│   │   ├── src/
│   │   │   ├── prompts/ # 发现、指示、评审提示词
│   │   │   ├── agents/  # CLI适配器(claude-code、cursor等)
│   │   │   └── routes/  # API端点
│   │   └── package.json
│   ├── web/             # Next.js前端
│   │   ├── app/
│   │   ├── components/
│   │   └── lib/
│   └── desktop/         # Electron包装器(可选)
├── skills/              # 31种内置技能
│   ├── web-prototype/
│   ├── guizang-ppt/     # 杂志风格演示文稿
│   ├── saas-landing/
│   └── ...
├── design-systems/      # 129套设计系统
│   ├── linear/
│   ├── stripe/
│   └── ...
└── prompt-templates/    # 媒体生成模板库(93个提示词)

Key Commands

核心命令

Tools CLI

Tools CLI

bash
undefined
bash
undefined

Start all services

启动所有服务

pnpm tools-dev
pnpm tools-dev

Check system status

检查系统状态

pnpm tools-dev status
pnpm tools-dev status

View daemon logs

查看守护进程日志

pnpm tools-dev logs
pnpm tools-dev logs

Inspect desktop (if Electron running)

检查桌面应用状态(若Electron运行)

pnpm tools-dev inspect desktop screenshot
pnpm tools-dev inspect desktop screenshot

Stop all services

停止所有服务

pnpm tools-dev stop
pnpm tools-dev stop

Health check

健康检查

pnpm tools-dev check
undefined
pnpm tools-dev check
undefined

Development

开发相关

bash
undefined
bash
undefined

Run daemon only

仅启动守护进程

cd apps/daemon pnpm dev
cd apps/daemon pnpm dev

Run web only

仅启动Web服务

cd apps/web pnpm dev
cd apps/web pnpm dev

Build for production

生产构建

pnpm build
pnpm build

Run tests

运行测试

pnpm test
undefined
pnpm test
undefined

Configuration

配置

Agent Detection

Agent检测

The daemon auto-detects CLIs on your
PATH
:
typescript
// Supported agents (auto-detected)
const AGENTS = [
  'claude-code',      // Claude Code
  'codex',            // Codex CLI
  'devin',            // Devin for Terminal
  'cursor-agent',     // Cursor Agent
  'gemini',           // Gemini CLI
  'opencode',         // OpenCode
  'qwen-code',        // Qwen Code
  'qoder',            // Qoder CLI
  'gh-copilot',       // GitHub Copilot CLI
  'hermes',           // Hermes (ACP)
  'kimi',             // Kimi CLI (ACP)
  'pi',               // Pi (RPC)
  'kiro',             // Kiro CLI (ACP)
  'kilo',             // Kilo (ACP)
  'mistral-vibe',     // Mistral Vibe CLI
  'deepseek-tui'      // DeepSeek TUI
];
守护进程会自动检测PATH中的CLI:
typescript
// 支持的Agent(自动检测)
const AGENTS = [
  'claude-code',      // Claude Code
  'codex',            // Codex CLI
  'devin',            // Devin for Terminal
  'cursor-agent',     // Cursor Agent
  'gemini',           // Gemini CLI
  'opencode',         // OpenCode
  'qwen-code',        // Qwen Code
  'qoder',            // Qoder CLI
  'gh-copilot',       // GitHub Copilot CLI
  'hermes',           // Hermes (ACP)
  'kimi',             // Kimi CLI (ACP)
  'pi',               // Pi (RPC)
  'kiro',             // Kiro CLI (ACP)
  'kilo',             // Kilo (ACP)
  'mistral-vibe',     // Mistral Vibe CLI
  'deepseek-tui'      // DeepSeek TUI
];

BYOK Configuration (No CLI)

BYOK配置(无CLI时)

When no agent CLI is detected, configure API proxy:
bash
undefined
当未检测到Agent CLI时,配置API代理:
bash
undefined

Environment variables

环境变量

ANTHROPIC_API_KEY=your_key_here OPENAI_API_KEY=your_key_here AZURE_OPENAI_KEY=your_key_here AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com GOOGLE_API_KEY=your_key_here
undefined
ANTHROPIC_API_KEY=你的密钥 OPENAI_API_KEY=你的密钥 AZURE_OPENAI_KEY=你的密钥 AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com GOOGLE_API_KEY=你的密钥
undefined

Database

数据库

SQLite configuration (
.od/app.sqlite
):
typescript
// apps/daemon/src/db/schema.ts
export const projects = sqliteTable('projects', {
  id: text('id').primaryKey(),
  name: text('name').notNull(),
  skill: text('skill').notNull(),
  designSystem: text('design_system'),
  direction: text('direction'),
  createdAt: integer('created_at', { mode: 'timestamp' }),
  updatedAt: integer('updated_at', { mode: 'timestamp' })
});

export const conversations = sqliteTable('conversations', {
  id: text('id').primaryKey(),
  projectId: text('project_id').notNull().references(() => projects.id),
  messages: text('messages', { mode: 'json' }),
  artifacts: text('artifacts', { mode: 'json' })
});
SQLite配置(
.od/app.sqlite
):
typescript
// apps/daemon/src/db/schema.ts
export const projects = sqliteTable('projects', {
  id: text('id').primaryKey(),
  name: text('name').notNull(),
  skill: text('skill').notNull(),
  designSystem: text('design_system'),
  direction: text('direction'),
  createdAt: integer('created_at', { mode: 'timestamp' }),
  updatedAt: integer('updated_at', { mode: 'timestamp' })
});

export const conversations = sqliteTable('conversations', {
  id: text('id').primaryKey(),
  projectId: text('project_id').notNull().references(() => projects.id),
  messages: text('messages', { mode: 'json' }),
  artifacts: text('artifacts', { mode: 'json' })
});

API Reference

API参考

Daemon Endpoints

守护进程端点

Start Agent Session

启动Agent会话

typescript
// POST /api/agent/start
const response = await fetch('http://localhost:3001/api/agent/start', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    projectId: 'proj_123',
    skill: 'web-prototype',
    designSystem: 'linear',
    direction: 'modern-minimal',
    prompt: 'Create a SaaS dashboard with user analytics'
  })
});

const { sessionId, status } = await response.json();
typescript
// POST /api/agent/start
const response = await fetch('http://localhost:3001/api/agent/start', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    projectId: 'proj_123',
    skill: 'web-prototype',
    designSystem: 'linear',
    direction: 'modern-minimal',
    prompt: 'Create a SaaS dashboard with user analytics'
  })
});

const { sessionId, status } = await response.json();

Stream Agent Response

流式Agent响应

typescript
// GET /api/agent/stream/:sessionId (SSE)
const eventSource = new EventSource(
  `http://localhost:3001/api/agent/stream/${sessionId}`
);

eventSource.addEventListener('message', (event) => {
  const data = JSON.parse(event.data);
  console.log(data); // { type: 'delta', content: '...' }
});

eventSource.addEventListener('artifact', (event) => {
  const artifact = JSON.parse(event.data);
  console.log(artifact); // { type: 'html', content: '...', title: '...' }
});

eventSource.addEventListener('tool_call', (event) => {
  const tool = JSON.parse(event.data);
  console.log(tool); // { name: 'Write', args: { path: '...', content: '...' } }
});
typescript
// GET /api/agent/stream/:sessionId (SSE)
const eventSource = new EventSource(
  `http://localhost:3001/api/agent/stream/${sessionId}`
);

eventSource.addEventListener('message', (event) => {
  const data = JSON.parse(event.data);
  console.log(data); // { type: 'delta', content: '...' }
});

eventSource.addEventListener('artifact', (event) => {
  const artifact = JSON.parse(event.data);
  console.log(artifact); // { type: 'html', content: '...', title: '...' }
});

eventSource.addEventListener('tool_call', (event) => {
  const tool = JSON.parse(event.data);
  console.log(tool); // { name: 'Write', args: { path: '...', content: '...' } }
});

Export Artifacts

导出工件

typescript
// POST /api/export
const response = await fetch('http://localhost:3001/api/export', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    projectId: 'proj_123',
    format: 'pdf', // 'html' | 'pdf' | 'pptx' | 'zip' | 'mp4' | 'markdown'
    artifactId: 'art_456'
  })
});

const blob = await response.blob();
// Save or download the exported file
typescript
// POST /api/export
const response = await fetch('http://localhost:3001/api/export', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    projectId: 'proj_123',
    format: 'pdf', // 'html' | 'pdf' | 'pptx' | 'zip' | 'mp4' | 'markdown'
    artifactId: 'art_456'
  })
});

const blob = await response.blob();
// 保存或下载导出文件

Import Claude Design Export

导入Claude Design导出内容

typescript
// POST /api/import/claude-design
const formData = new FormData();
formData.append('file', claudeDesignZip);

const response = await fetch('http://localhost:3001/api/import/claude-design', {
  method: 'POST',
  body: formData
});

const { projectId, conversationId } = await response.json();
typescript
// POST /api/import/claude-design
const formData = new FormData();
formData.append('file', claudeDesignZip);

const response = await fetch('http://localhost:3001/api/import/claude-design', {
  method: 'POST',
  body: formData
});

const { projectId, conversationId } = await response.json();

BYOK Proxy Endpoints

BYOK代理端点

typescript
// POST /api/proxy/anthropic/stream
const response = await fetch('http://localhost:3001/api/proxy/anthropic/stream', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${process.env.ANTHROPIC_API_KEY}`
  },
  body: JSON.stringify({
    model: 'claude-3-5-sonnet-20241022',
    messages: [{ role: 'user', content: 'Design a landing page' }],
    max_tokens: 4096
  })
});

// SSE stream normalized to Open Design chat protocol
typescript
// POST /api/proxy/anthropic/stream
const response = await fetch('http://localhost:3001/api/proxy/anthropic/stream', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${process.env.ANTHROPIC_API_KEY}`
  },
  body: JSON.stringify({
    model: 'claude-3-5-sonnet-20241022',
    messages: [{ role: 'user', content: 'Design a landing page' }],
    max_tokens: 4096
  })
});

// SSE流已标准化为Open Design聊天协议

Working Code Examples

代码示例

Creating a Web Prototype with Design System

使用设计系统创建网页原型

typescript
// apps/web/lib/create-prototype.ts
import { useAgent } from '@/hooks/use-agent';

export function usePrototypeCreation() {
  const { startSession, streamResponse } = useAgent();

  async function createPrototype(prompt: string) {
    const session = await startSession({
      skill: 'web-prototype',
      designSystem: 'stripe', // Use Stripe design system
      direction: 'modern-minimal',
      prompt: `
        ${prompt}
        
        Requirements:
        - Use Stripe's color palette and typography
        - Include responsive navigation
        - Add interactive components
        - Follow accessibility best practices
      `
    });

    for await (const chunk of streamResponse(session.id)) {
      if (chunk.type === 'artifact') {
        // Artifact ready for sandboxed preview
        renderInIframe(chunk.content);
      }
      if (chunk.type === 'tool_call' && chunk.name === 'Write') {
        // Agent writing to project folder
        console.log(`Writing: ${chunk.args.path}`);
      }
    }
  }

  return { createPrototype };
}
typescript
// apps/web/lib/create-prototype.ts
import { useAgent } from '@/hooks/use-agent';

export function usePrototypeCreation() {
  const { startSession, streamResponse } = useAgent();

  async function createPrototype(prompt: string) {
    const session = await startSession({
      skill: 'web-prototype',
      designSystem: 'stripe', // 使用Stripe设计系统
      direction: 'modern-minimal',
      prompt: `
        ${prompt}
        
        需求:
        - 使用Stripe的配色方案和排版
        - 包含响应式导航
        - 添加交互式组件
        - 遵循无障碍最佳实践
      `
    });

    for await (const chunk of streamResponse(session.id)) {
      if (chunk.type === 'artifact') {
        // 工件已准备好用于沙箱预览
        renderInIframe(chunk.content);
      }
      if (chunk.type === 'tool_call' && chunk.name === 'Write') {
        // Agent正在写入项目文件夹
        console.log(`写入: ${chunk.args.path}`);
      }
    }
  }

  return { createPrototype };
}

Using Skills Programmatically

程序化使用技能

typescript
// apps/daemon/src/skills/loader.ts
import { loadSkill } from './skills-registry';

async function executeSkillWithAgent(
  skillName: string,
  userPrompt: string,
  agentCli: string
) {
  const skill = await loadSkill(skillName);
  
  // Combine skill prompt + user prompt
  const fullPrompt = `
${skill.systemPrompt}
typescript
// apps/daemon/src/skills/loader.ts
import { loadSkill } from './skills-registry';

async function executeSkillWithAgent(
  skillName: string,
  userPrompt: string,
  agentCli: string
) {
  const skill = await loadSkill(skillName);
  
  // 组合技能提示词 + 用户提示词
  const fullPrompt = `
${skill.systemPrompt}

User Request

用户请求

${userPrompt}
${userPrompt}

Design System

设计系统

${await loadDesignSystem('linear')}
${await loadDesignSystem('linear')}

Visual Direction

视觉方向

${await loadDirection('modern-minimal')}
${await loadDirection('modern-minimal')}

Pre-flight Checklist

预检查清单

${skill.checklist.join('\n')} `;
// Spawn agent CLI const process = spawn(agentCli, ['--prompt-file', 'prompt.txt'], { cwd: projectPath, env: { ...process.env } });
// Stream response process.stdout.on('data', (chunk) => { const artifact = parseArtifact(chunk.toString()); if (artifact) { emit('artifact', artifact); } }); }
undefined
${skill.checklist.join('\n')} `;
// 启动Agent CLI const process = spawn(agentCli, ['--prompt-file', 'prompt.txt'], { cwd: projectPath, env: { ...process.env } });
// 流式响应 process.stdout.on('data', (chunk) => { const artifact = parseArtifact(chunk.toString()); if (artifact) { emit('artifact', artifact); } }); }
undefined

Custom Skill Definition

自定义技能定义

typescript
// skills/custom-portfolio/skill.json
{
  "name": "custom-portfolio",
  "displayName": "Portfolio Website",
  "scenario": "personal",
  "description": "Personal portfolio with project showcase",
  "mode": "prototype",
  "template": "portfolio-base",
  "checklist": [
    "Hero section with name and tagline",
    "Project grid with hover states",
    "About section with bio",
    "Contact form with validation",
    "Responsive mobile layout"
  ],
  "systemPrompt": "You are building a personal portfolio...",
  "palette": ["oklch(0.95 0.02 200)", "oklch(0.2 0.05 250)"],
  "fonts": {
    "heading": "Inter",
    "body": "Inter"
  }
}
typescript
// skills/custom-portfolio/skill.json
{
  "name": "custom-portfolio",
  "displayName": "Portfolio Website",
  "scenario": "personal",
  "description": "Personal portfolio with project showcase",
  "mode": "prototype",
  "template": "portfolio-base",
  "checklist": [
    "Hero section with name and tagline",
    "Project grid with hover states",
    "About section with bio",
    "Contact form with validation",
    "Responsive mobile layout"
  ],
  "systemPrompt": "You are building a personal portfolio...",
  "palette": ["oklch(0.95 0.02 200)", "oklch(0.2 0.05 250)"],
  "fonts": {
    "heading": "Inter",
    "body": "Inter"
  }
}

Export Pipeline

导出流水线

typescript
// apps/daemon/src/export/pdf.ts
import puppeteer from 'puppeteer';

export async function exportToPDF(
  htmlContent: string,
  options: { format?: 'A4' | 'Letter' }
) {
  const browser = await puppeteer.launch({ headless: true });
  const page = await browser.newPage();
  
  await page.setContent(htmlContent, {
    waitUntil: 'networkidle0'
  });
  
  const pdf = await page.pdf({
    format: options.format || 'A4',
    printBackground: true,
    margin: { top: '20px', bottom: '20px' }
  });
  
  await browser.close();
  return pdf;
}
typescript
// apps/daemon/src/export/pdf.ts
import puppeteer from 'puppeteer';

export async function exportToPDF(
  htmlContent: string,
  options: { format?: 'A4' | 'Letter' }
) {
  const browser = await puppeteer.launch({ headless: true });
  const page = await browser.newPage();
  
  await page.setContent(htmlContent, {
    waitUntil: 'networkidle0'
  });
  
  const pdf = await page.pdf({
    format: options.format || 'A4',
    printBackground: true,
    margin: { top: '20px', bottom: '20px' }
  });
  
  await browser.close();
  return pdf;
}

Media Generation (Image)

媒体生成(图片)

typescript
// apps/daemon/src/media/image.ts
export async function generateImage(prompt: string) {
  const response = await fetch('https://api.openai.com/v1/images/generations', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      model: 'dall-e-3',
      prompt: prompt,
      size: '1792x1024',
      quality: 'hd',
      n: 1
    })
  });

  const { data } = await response.json();
  return data[0].url; // Download and save to project workspace
}
typescript
// apps/daemon/src/media/image.ts
export async function generateImage(prompt: string) {
  const response = await fetch('https://api.openai.com/v1/images/generations', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      model: 'dall-e-3',
      prompt: prompt,
      size: '1792x1024',
      quality: 'hd',
      n: 1
    })
  });

  const { data } = await response.json();
  return data[0].url; // 下载并保存到项目工作区
}

Common Patterns

通用模式

Interactive Question Form Before Generation

生成前的交互式问题表单

typescript
// apps/web/components/question-form.tsx
export function QuestionForm({ skill, onSubmit }) {
  const questions = skill.discoveryQuestions || [
    { id: 'purpose', text: 'What is the main purpose?' },
    { id: 'audience', text: 'Who is the target audience?' },
    { id: 'tone', text: 'What tone should it have?' }
  ];

  const [answers, setAnswers] = useState({});

  function handleSubmit() {
    const enrichedPrompt = `
User Request: ${originalPrompt}

Discovery Answers:
${Object.entries(answers).map(([k, v]) => `- ${k}: ${v}`).join('\n')}
`;
    onSubmit(enrichedPrompt);
  }

  return <form>...</form>;
}
typescript
// apps/web/components/question-form.tsx
export function QuestionForm({ skill, onSubmit }) {
  const questions = skill.discoveryQuestions || [
    { id: 'purpose', text: '主要用途是什么?' },
    { id: 'audience', text: '目标受众是谁?' },
    { id: 'tone', text: '应该采用什么风格?' }
  ];

  const [answers, setAnswers] = useState({});

  function handleSubmit() {
    const enrichedPrompt = `
用户请求: ${originalPrompt}

发现问题答案:
${Object.entries(answers).map(([k, v]) => `- ${k}: ${v}`).join('\n')}
`;
    onSubmit(enrichedPrompt);
  }

  return <form>...</form>;
}

Five-Dimensional Self-Critique

五维自我评审

typescript
// apps/daemon/src/prompts/critique.ts
export const CRITIQUE_DIMENSIONS = [
  {
    name: 'Visual Hierarchy',
    criteria: 'Clear focal point, logical reading flow, proper emphasis'
  },
  {
    name: 'Brand Consistency',
    criteria: 'Design system palette used, fonts match spec, no arbitrary colors'
  },
  {
    name: 'Responsive Design',
    criteria: 'Mobile breakpoints defined, touch targets 44px+, no horizontal scroll'
  },
  {
    name: 'Accessibility',
    criteria: 'WCAG AA contrast, semantic HTML, keyboard navigation'
  },
  {
    name: 'Polish',
    criteria: 'No placeholder content, real copy, production-ready assets'
  }
];

export function buildCritiquePrompt(artifact: string) {
  return `
typescript
// apps/daemon/src/prompts/critique.ts
export const CRITIQUE_DIMENSIONS = [
  {
    name: 'Visual Hierarchy',
    criteria: '清晰的焦点、合理的阅读流程、恰当的重点突出'
  },
  {
    name: 'Brand Consistency',
    criteria: '使用设计系统配色、字体符合规范、无随意颜色'
  },
  {
    name: 'Responsive Design',
    criteria: '定义移动端断点、触摸目标≥44px、无横向滚动'
  },
  {
    name: 'Accessibility',
    criteria: '符合WCAG AA对比度、语义化HTML、键盘可导航'
  },
  {
    name: 'Polish',
    criteria: '无占位内容、真实文案、生产就绪资源'
  }
];

export function buildCritiquePrompt(artifact: string) {
  return `

Self-Critique

自我评审

Review your output against these dimensions:
${CRITIQUE_DIMENSIONS.map(d => `
根据以下维度评审你的输出:
${CRITIQUE_DIMENSIONS.map(d => `

${d.name}

${d.name}

${d.criteria} Score (1-5): _____ Issues: _____ `).join('\n')}
If any dimension scores below 4, revise before emitting final artifact. `; }
undefined
${d.criteria} 评分(1-5): _____ 问题: _____ `).join('\n')}
若任何维度评分低于4分,在输出最终工件前进行修订。 `; }
undefined

Sandboxed Iframe Rendering

沙箱化Iframe渲染

typescript
// apps/web/components/artifact-preview.tsx
import { useEffect, useRef } from 'react';

export function ArtifactPreview({ html }: { html: string }) {
  const iframeRef = useRef<HTMLIFrameElement>(null);

  useEffect(() => {
    const iframe = iframeRef.current;
    if (!iframe) return;

    const doc = iframe.contentDocument;
    if (!doc) return;

    doc.open();
    doc.write(`
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
  </style>
</head>
<body>
  ${html}
</body>
</html>
    `);
    doc.close();
  }, [html]);

  return (
    <iframe
      ref={iframeRef}
      sandbox="allow-scripts allow-same-origin"
      style={{ width: '100%', height: '100%', border: 'none' }}
    />
  );
}
typescript
// apps/web/components/artifact-preview.tsx
import { useEffect, useRef } from 'react';

export function ArtifactPreview({ html }: { html: string }) {
  const iframeRef = useRef<HTMLIFrameElement>(null);

  useEffect(() => {
    const iframe = iframeRef.current;
    if (!iframe) return;

    const doc = iframe.contentDocument;
    if (!doc) return;

    doc.open();
    doc.write(`
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
  </style>
</head>
<body>
  ${html}
</body>
</html>
    `);
    doc.close();
  }, [html]);

  return (
    <iframe
      ref={iframeRef}
      sandbox="allow-scripts allow-same-origin"
      style={{ width: '100%', height: '100%', border: 'none' }}
    />
  );
}

PATH Agent Detection

PATH Agent检测

typescript
// apps/daemon/src/agents/detect.ts
import { exec } from 'child_process';
import { promisify } from 'util';

const execAsync = promisify(exec);

export async function detectAgents(): Promise<string[]> {
  const candidates = [
    'claude-code',
    'cursor-agent',
    'codex',
    'gemini',
    'gh-copilot'
  ];

  const detected: string[] = [];

  for (const cmd of candidates) {
    try {
      await execAsync(`which ${cmd}`, { timeout: 1000 });
      detected.push(cmd);
    } catch {
      // Not on PATH
    }
  }

  return detected;
}
typescript
// apps/daemon/src/agents/detect.ts
import { exec } from 'child_process';
import { promisify } from 'util';

const execAsync = promisify(exec);

export async function detectAgents(): Promise<string[]> {
  const candidates = [
    'claude-code',
    'cursor-agent',
    'codex',
    'gemini',
    'gh-copilot'
  ];

  const detected: string[] = [];

  for (const cmd of candidates) {
    try {
      await execAsync(`which ${cmd}`, { timeout: 1000 });
      detected.push(cmd);
    } catch {
      // 不在PATH中
    }
  }

  return detected;
}

Troubleshooting

故障排除

Agent Not Detected

Agent未检测到

Problem: Daemon starts but shows "No agent CLI detected"
Solution:
bash
undefined
问题: 守护进程启动后显示"No agent CLI detected"
解决方案:
bash
undefined

Verify CLI is on PATH

验证CLI是否在PATH中

which claude-code which cursor-agent
which claude-code which cursor-agent

Add to PATH if missing (macOS/Linux)

若缺失则添加到PATH(macOS/Linux)

export PATH="$PATH:/path/to/agent/bin"
export PATH="$PATH:/path/to/agent/bin"

Restart daemon

重启守护进程

pnpm tools-dev stop pnpm tools-dev
undefined
pnpm tools-dev stop pnpm tools-dev
undefined

Windows ENAMETOOLONG Errors

Windows下ENAMETOOLONG错误

Problem: Long prompts fail on Windows
Solution: The daemon automatically falls back to stdin/prompt-file mode on Windows:
typescript
// apps/daemon/src/agents/spawn.ts
if (process.platform === 'win32' && promptLength > 8191) {
  // Use prompt file instead of command line arg
  await fs.writeFile(promptFilePath, prompt);
  spawn(agentCli, ['--prompt-file', promptFilePath]);
}
问题: 长提示词在Windows下失败
解决方案: 守护进程在Windows下会自动回退到标准输入/提示文件模式:
typescript
// apps/daemon/src/agents/spawn.ts
if (process.platform === 'win32' && promptLength > 8191) {
  // 使用提示文件而非命令行参数
  await fs.writeFile(promptFilePath, prompt);
  spawn(agentCli, ['--prompt-file', promptFilePath]);
}

Artifact Not Rendering

工件无法渲染

Problem: Generated HTML shows blank iframe
Check:
  1. Browser console for CSP errors
  2. Artifact contains valid HTML structure
  3. No external resource blocking (CORS)
typescript
// Debug artifact content
console.log('Artifact HTML:', artifact.content.substring(0, 500));

// Check iframe sandbox
const iframe = document.querySelector('iframe');
console.log('Sandbox:', iframe.getAttribute('sandbox'));
问题: 生成的HTML显示空白iframe
检查项:
  1. 浏览器控制台是否有CSP错误
  2. 工件是否包含有效的HTML结构
  3. 是否有外部资源被阻止(CORS)
typescript
// 调试工件内容
console.log('Artifact HTML:', artifact.content.substring(0, 500));

// 检查iframe沙箱设置
const iframe = document.querySelector('iframe');
console.log('Sandbox:', iframe.getAttribute('sandbox'));

Export Fails

导出失败

Problem: PDF/PPTX export returns 500 error
Solution:
bash
undefined
问题: PDF/PPTX导出返回500错误
解决方案:
bash
undefined

Install Puppeteer dependencies (Linux)

安装Puppeteer依赖(Linux)

sudo apt-get install -y
chromium-browser
fonts-liberation
libnss3
libxss1
sudo apt-get install -y
chromium-browser
fonts-liberation
libnss3
libxss1

macOS (ensure Chromium is available)

macOS(确保Chromium可用)

brew install chromium
brew install chromium

Verify export endpoint

验证导出端点

curl -X POST http://localhost:3001/api/export
-H "Content-Type: application/json"
-d '{"projectId":"test","format":"pdf","artifactId":"art_1"}'
undefined
curl -X POST http://localhost:3001/api/export
-H "Content-Type: application/json"
-d '{"projectId":"test","format":"pdf","artifactId":"art_1"}'
undefined

SQLite Lock Errors

SQLite锁定错误

Problem:
database is locked
during concurrent operations
Solution:
typescript
// apps/daemon/src/db/client.ts
import Database from 'better-sqlite3';

export const db = new Database('.od/app.sqlite', {
  timeout: 5000, // Wait up to 5s for lock
  verbose: console.log
});

// Enable WAL mode for better concurrency
db.pragma('journal_mode = WAL');
问题: 并发操作时出现
database is locked
解决方案:
typescript
// apps/daemon/src/db/client.ts
import Database from 'better-sqlite3';

export const db = new Database('.od/app.sqlite', {
  timeout: 5000, // 等待锁的最长时间为5秒
  verbose: console.log
});

// 启用WAL模式以提升并发性能
db.pragma('journal_mode = WAL');

BYOK Proxy SSRF Protection

BYOK代理SSRF保护

Problem: Custom baseURL rejected
Expected: Daemon blocks internal IPs for security:
typescript
// apps/daemon/src/proxy/validate.ts
const BLOCKED_RANGES = [
  '127.0.0.0/8',
  '10.0.0.0/8',
  '172.16.0.0/12',
  '192.168.0.0/16'
];

export function validateBaseUrl(url: string) {
  const hostname = new URL(url).hostname;
  if (isPrivateIP(hostname)) {
    throw new Error('SSRF blocked: internal IP detected');
  }
}
问题: 自定义baseURL被拒绝
预期行为: 守护进程会阻止内部IP以保障安全:
typescript
// apps/daemon/src/proxy/validate.ts
const BLOCKED_RANGES = [
  '127.0.0.0/8',
  '10.0.0.0/8',
  '172.16.0.0/12',
  '192.168.0.0/16'
];

export function validateBaseUrl(url: string) {
  const hostname = new URL(url).hostname;
  if (isPrivateIP(hostname)) {
    throw new Error('SSRF blocked: internal IP detected');
  }
}

Resources

资源

License

许可证

Apache-2.0 — see LICENSE
Apache-2.0 — 详见 LICENSE