@clawhub-wujiaming88-b650469411
Create professional, publication-quality technical architecture diagrams using pure SVG in HTML, then screenshot via Playwright. Produces crisp, pixel-perfec...
---
name: svg-architecture-diagram
description: >-
Create professional, publication-quality technical architecture diagrams using
pure SVG in HTML, then screenshot via Playwright. Produces crisp, pixel-perfect
diagrams with precise connection lines, color-coded modules, and clear text at
any resolution. Use when: (1) user asks for a system architecture diagram,
(2) user wants a technical component diagram or flow chart, (3) user needs a
data flow or pipeline visualization, (4) any diagram requiring accurate text
labels and precise connecting lines. Triggers: "architecture diagram",
"架构图", "技术架构", "system diagram", "component diagram", "flow diagram",
"数据流图", "模块图", "draw architecture", "画架构图", "technical diagram".
Prefer this over AI image generation for any diagram with text labels.
---
# SVG Architecture Diagram
Create professional technical architecture diagrams using pure SVG, rendered to high-res PNG via Playwright.
## Why SVG (not CSS positioning or AI image generation)
| Approach | Lines/Arrows | Text Quality | Precision |
|----------|-------------|-------------|-----------|
| **SVG (this skill)** | ✅ Perfect: `<line>`, `<path>`, `<marker>` | ✅ Crisp at any size | ✅ Pixel-perfect |
| CSS absolute positioning | ❌ Hacky: borders, pseudo-elements | ✅ OK | ❌ Hard to align |
| AI image generation | ❌ No control | ❌ Garbled text | ❌ No precision |
## Quick Start
### Step 1: Plan the diagram
Identify:
- **Modules** — group related components (color-coded)
- **Hierarchy** — top-to-bottom flow (user → core → subsystems → output)
- **Connections** — data flow (solid lines), feedback (dashed lines)
### Step 2: Create the HTML file
Write a single HTML file with an inline SVG. Standard canvas: **1600×1000px**.
```html
<!DOCTYPE html>
<html><head>
<meta charset="UTF-8">
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
* { margin: 0; padding: 0; box-sizing: border-box; }
body { width: 1600px; height: 1000px; background: #fafafa; overflow: hidden; }
</style>
</head><body>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1600 1000" width="1600" height="1000">
<defs>
<!-- Arrow markers — one per color -->
<marker id="arr-indigo" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<path d="M0,0 L8,3 L0,6 Z" fill="#6366f1"/>
</marker>
<!-- Shadow filter -->
<filter id="shadow" x="-4%" y="-4%" width="108%" height="108%">
<feDropShadow dx="0" dy="2" stdDeviation="4" flood-color="#000" flood-opacity="0.08"/>
</filter>
</defs>
<!-- Diagram content here -->
</svg>
</body></html>
```
### Step 3: Build the diagram using these SVG patterns
**Filled header card** (module title):
```svg
<rect x="X" y="Y" width="W" height="40" rx="10" fill="#6366f1" filter="url(#shadow)"/>
<text x="CENTER" y="Y+25" text-anchor="middle" font-size="13" font-weight="700" fill="#fff">🔄 Module Name</text>
```
**Outlined detail card** (sub-component):
```svg
<rect x="X" y="Y" width="W" height="65" rx="10" fill="#fff" stroke="#6366f1" stroke-width="2" filter="url(#shadow)"/>
<text x="X+20" y="Y+22" font-size="12" font-weight="700" fill="#6366f1">Component Title</text>
<text x="X+20" y="Y+40" font-size="11" fill="#6b7280">Description line 1</text>
<text x="X+20" y="Y+55" font-size="10" fill="#9ca3af">Metadata / specs</text>
```
**Connection line** (with arrow):
```svg
<line x1="FROM_X" y1="FROM_Y" x2="TO_X" y2="TO_Y" stroke="#6366f1" stroke-width="2" marker-end="url(#arr-indigo)"/>
```
**Curved connection** (L-shape or bend):
```svg
<path d="M startX,startY L midX,midY L endX,endY" stroke="#6366f1" stroke-width="2" fill="none" marker-end="url(#arr-indigo)"/>
```
**Dashed feedback line**:
```svg
<path d="M x1,y1 L x2,y2" stroke="#8b5cf6" stroke-width="2" fill="none" stroke-dasharray="6,4" marker-end="url(#arr-purple)"/>
```
**Connection label**:
```svg
<text x="MID_X" y="MID_Y-5" font-size="10" fill="#6366f1" font-weight="500">label text</text>
```
### Step 4: Screenshot with Playwright
```python
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page(
viewport={"width": 1600, "height": 1000},
device_scale_factor=4, # 4x ultra-high res (default)
)
page.goto("file:///path/to/diagram.html", wait_until="networkidle")
page.wait_for_timeout(1500)
page.screenshot(path="diagram.png", full_page=True)
browser.close()
```
Or use the bundled script: `scripts/screenshot.py <input.html> [output.png]`
## Design System
See `references/design-system.md` for the complete color palette, card styles, arrow markers, and text sizing rules.
## Critical Rules (prevent common issues)
### Text Overflow Prevention
1. **Max characters per line** at font-size 11px ≈ 7px/char:
- 300px container → max 37 chars
- 340px container → max 43 chars
- 440px container → max 57 chars
2. **Long text → split into multiple `<text>` elements** with Y offset +15px each
3. **Always leave 20px padding** on each side of text inside cards
4. **Test at 1x scale** before generating final 4x screenshot
### Connection Line Rules
1. **Never use CSS for connections** — always SVG `<line>` or `<path>`
2. **One `<marker>` per color** — define in `<defs>`, reference with `marker-end`
3. **Straight lines** when possible; use `<path>` L-segments for bends
4. **Avoid crossing lines** — rearrange layout if lines would cross
5. **Label every connection** — brief verb/noun near the midpoint
6. **⚠️ Minimum 20px gap between vertically stacked cards** — Arrow markers are 8px long. If the gap between cards is less than 20px, the arrow will completely cover the line, making it look like "arrow only, no line". Use card height 34px + gap 22px = 56px per step.
7. **Connection line length must be at least 17px** — This ensures 9px visible line + 8px arrow marker. Example: card bottom at y=324, next card top at y=346, line from y1=324 to y2=343 (19px).
8. **Never make line length < marker size (8px)** — The line will be invisible.
### Layout Rules
1. **Top-to-bottom** primary flow (input at top, output at bottom/right)
2. **Left-right symmetry** when possible
3. **Group related modules** vertically (e.g., memory layers stacked)
4. **Minimum 20px gap** between vertically stacked cards (see Connection Line Rules)
5. **Color-code by function** — see design system for standard palette
6. **Include a legend** (bottom-right corner) explaining colors and line types
7. **Include a title** (top center) and source attribution (bottom center)
### Font Rules
1. **Font family**: `font-family="Inter, 'PingFang SC', 'Microsoft YaHei', sans-serif"` — set on root `<svg>` or first `<text>`
2. Load Inter via Google Fonts in `<style>` block
3. **Chinese text**: use `PingFang SC` / `Microsoft YaHei` fallback
4. **Font sizes**: titles 13-14px, descriptions 10-11px, metadata 9-10px
## Examples
Two complete working examples are included:
- `references/example-hermes.html` — Hermes Agent architecture (6 modules, medium complexity)
- `references/example-openclaw.html` — OpenClaw platform architecture (12 modules, high complexity, demonstrates proper vertical card spacing for Agent Loop steps)
## Delivery
Output `MEDIA:<path>` for inline delivery, or `openclaw message send --channel telegram --target <id> --media <path> --force-document` for Telegram.
If PNG exceeds ~1MB for Telegram delivery, convert to JPEG (quality=95):
```python
from PIL import Image
img = Image.open("diagram.png")
img.save("diagram.jpg", "JPEG", quality=95, optimize=True)
```
Default is 4x (6400×4000px for 1600×1000 canvas). Always use maximum resolution.
FILE:references/design-system.md
# SVG Architecture Diagram — Color Palette Reference
## Standard Module Colors
| Module Type | Fill Color | Border | Text | Use For |
|-------------|-----------|--------|------|---------|
| User / External | `#ec4899` (pink) | — | white | User input, LLM, external services |
| Core Engine | `#6366f1` (indigo) | — | white | Main loop, core processing, orchestration |
| Storage / Memory | `#10b981` (green) | `#10b981` | green/gray | Database, cache, memory layers |
| Plugin / Extension | `#f59e0b` (amber) | `#f59e0b` | amber/gray | Skills, plugins, extensions |
| AI / Learning | `#8b5cf6` (purple) | `#8b5cf6` | purple/gray | Self-evolution, ML, optimization |
| Infrastructure | `#64748b` (slate) | `#94a3b8` | slate/gray | Tools, shell, file system |
| Security / Guard | `#ef4444` (red) | `#ef4444` | red | Security, validation, guard |
| Output / Result | `#10b981` (green) | — | white | Output, delivery, response |
## Card Styles
### Filled Card (header/title blocks)
```svg
<rect x="X" y="Y" width="W" height="40" rx="10" fill="#6366f1" filter="url(#shadow)"/>
<text x="CENTER" y="Y+25" text-anchor="middle" font-size="14" font-weight="700" fill="#fff">Title</text>
```
### Outlined Card (detail blocks)
```svg
<rect x="X" y="Y" width="W" height="H" rx="10" fill="#fff" stroke="#6366f1" stroke-width="2" filter="url(#shadow)"/>
<text x="X+20" y="Y+22" font-size="12" font-weight="700" fill="#6366f1">Title</text>
<text x="X+20" y="Y+40" font-size="11" fill="#6b7280">Description</text>
<text x="X+20" y="Y+55" font-size="10" fill="#9ca3af">Metadata</text>
```
### Highlight Card (warnings/special)
```svg
<rect x="X" y="Y" width="W" height="H" rx="10" fill="#fffbeb" stroke="#f59e0b" stroke-width="1"/>
```
## Arrow Markers
```svg
<marker id="arr-indigo" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<path d="M0,0 L8,3 L0,6 Z" fill="#6366f1"/>
</marker>
```
## Connection Lines
| Type | Style | Use |
|------|-------|-----|
| Data flow | `stroke-width="2"` solid + arrow | Primary data movement |
| Feedback | `stroke-width="2" stroke-dasharray="6,4"` + arrow | Feedback loops, optimization |
| Internal | `stroke-width="1.5"` solid + arrow | Within-module connections |
| Label | `font-size="10" font-weight="500"` | Connection description |
## Text Sizing Rules
| Element | Font Size | Weight | Anchor |
|---------|-----------|--------|--------|
| Page title | 22px | 700 | middle |
| Subtitle | 12px | 400 | middle |
| Module header | 13-14px | 700 | middle |
| Card title | 11-12px | 700 | left |
| Description | 10-11px | 400 | left |
| Metadata | 9-10px | 400 | left |
| Connection label | 10px | 500 | left |
| Legend | 11px | 400 | left |
## Anti-Overflow Rules
1. **Max text width** = container width - 40px (20px padding each side)
2. At 11px font, ~7px per character → max ~43 chars in 300px container
3. Long text → split into two `<text>` lines (Y offset +16px)
4. Use `text-anchor="middle"` for centered headers
5. Use left-aligned for descriptions in outlined cards
## Card Spacing Rules (CRITICAL)
1. **Vertical stacked cards**: card height 34px + gap 22px = **56px per step**
2. **Connection line**: start at card bottom (y + height), end at next card top (y) minus 3px
3. **Minimum line length**: 17px (9px visible line + 8px arrow marker)
4. **Arrow marker size**: 8px — if line < 8px, arrow covers entire line (invisible)
5. **Example**: card at y=290 h=34 → bottom=324. Next card y=346. Line: y1=324 y2=343 (19px ✓)
FILE:references/example-hermes.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
width: 1600px; height: 1000px;
background: #fafafa;
font-family: 'Inter', 'PingFang SC', 'Microsoft YaHei', sans-serif;
overflow: hidden;
}
</style>
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1600 1000" width="1600" height="1000">
<defs>
<!-- 箭头标记 -->
<marker id="arr-indigo" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<path d="M0,0 L8,3 L0,6 Z" fill="#6366f1"/>
</marker>
<marker id="arr-green" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<path d="M0,0 L8,3 L0,6 Z" fill="#10b981"/>
</marker>
<marker id="arr-amber" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<path d="M0,0 L8,3 L0,6 Z" fill="#f59e0b"/>
</marker>
<marker id="arr-purple" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<path d="M0,0 L8,3 L0,6 Z" fill="#8b5cf6"/>
</marker>
<marker id="arr-slate" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<path d="M0,0 L8,3 L0,6 Z" fill="#64748b"/>
</marker>
<marker id="arr-pink" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<path d="M0,0 L8,3 L0,6 Z" fill="#ec4899"/>
</marker>
<!-- 圆角矩形 filter -->
<filter id="shadow" x="-4%" y="-4%" width="108%" height="108%">
<feDropShadow dx="0" dy="2" stdDeviation="4" flood-color="#000" flood-opacity="0.08"/>
</filter>
</defs>
<!-- ========== 背景标题 ========== -->
<text x="800" y="40" text-anchor="middle" font-size="22" font-weight="700" fill="#1f2937" font-family="Inter, sans-serif">Hermes Agent 技术架构图</text>
<text x="800" y="62" text-anchor="middle" font-size="12" fill="#9ca3af" font-family="Inter, sans-serif">Nous Research · MIT License · 95,600+ GitHub Stars</text>
<!-- ========== 顶层:用户 → Agent Loop → LLM ========== -->
<!-- 用户输入 -->
<rect x="80" y="90" width="180" height="60" rx="12" fill="#ec4899" filter="url(#shadow)"/>
<text x="170" y="117" text-anchor="middle" font-size="14" font-weight="700" fill="#fff">👤 User Input</text>
<text x="170" y="134" text-anchor="middle" font-size="11" fill="rgba(255,255,255,0.85)">用户输入 / 任务指令</text>
<!-- Agent Loop -->
<rect x="580" y="85" width="440" height="70" rx="14" fill="#6366f1" filter="url(#shadow)"/>
<text x="800" y="115" text-anchor="middle" font-size="16" font-weight="700" fill="#fff">🔄 Agent Loop(核心循环)</text>
<text x="800" y="135" text-anchor="middle" font-size="10" fill="rgba(255,255,255,0.85)">构建 Prompt → 调用 LLM → 执行工具 → 返回结果 → Periodic Nudge</text>
<!-- LLM -->
<rect x="1340" y="90" width="180" height="60" rx="12" fill="#ec4899" filter="url(#shadow)"/>
<text x="1430" y="117" text-anchor="middle" font-size="14" font-weight="700" fill="#fff">🧠 LLM</text>
<text x="1430" y="134" text-anchor="middle" font-size="11" fill="rgba(255,255,255,0.85)">Claude / GPT / Qwen</text>
<!-- 连线:用户 → Agent Loop -->
<line x1="260" y1="120" x2="575" y2="120" stroke="#ec4899" stroke-width="2.5" marker-end="url(#arr-pink)"/>
<!-- 连线:Agent Loop → LLM -->
<line x1="1020" y1="115" x2="1335" y2="115" stroke="#ec4899" stroke-width="2.5" marker-end="url(#arr-pink)"/>
<!-- 连线:LLM → Agent Loop (返回) -->
<line x1="1335" y1="130" x2="1020" y2="130" stroke="#6366f1" stroke-width="2" stroke-dasharray="6,4" marker-end="url(#arr-indigo)"/>
<!-- ========== 中部左:三层记忆架构 ========== -->
<!-- 记忆系统标题 -->
<rect x="60" y="195" width="340" height="40" rx="10" fill="#10b981" filter="url(#shadow)"/>
<text x="230" y="220" text-anchor="middle" font-size="13" font-weight="700" fill="#fff">🧠 三层记忆架构</text>
<!-- L1 -->
<rect x="60" y="250" width="340" height="65" rx="10" fill="#fff" stroke="#10b981" stroke-width="2" filter="url(#shadow)"/>
<text x="80" y="272" font-size="12" font-weight="700" fill="#10b981">L1: Session Context</text>
<text x="80" y="290" font-size="11" fill="#6b7280">当前对话工作记忆 · 内存存储</text>
<text x="80" y="305" font-size="10" fill="#9ca3af">容量: 模型上下文窗口 · 检索: 即时</text>
<!-- L2 -->
<rect x="60" y="328" width="340" height="65" rx="10" fill="#fff" stroke="#10b981" stroke-width="2" filter="url(#shadow)"/>
<text x="80" y="350" font-size="12" font-weight="700" fill="#10b981">L2: Persistent Store</text>
<text x="80" y="368" font-size="10" fill="#6b7280">SQLite + FTS5 全文搜索 · Skills · 记忆</text>
<text x="80" y="383" font-size="10" fill="#9ca3af">容量: 无限 · 检索: <10ms</text>
<!-- L3 -->
<rect x="60" y="406" width="340" height="65" rx="10" fill="#fff" stroke="#10b981" stroke-width="2" filter="url(#shadow)"/>
<text x="80" y="428" font-size="12" font-weight="700" fill="#10b981">L3: External Knowledge</text>
<text x="80" y="446" font-size="11" fill="#6b7280">MCP 服务器 · 外部知识库 · RAG</text>
<text x="80" y="461" font-size="10" fill="#9ca3af">容量: 无限 · 检索: 按需</text>
<!-- 记忆层之间的连线 -->
<line x1="230" y1="315" x2="230" y2="325" stroke="#10b981" stroke-width="1.5" marker-end="url(#arr-green)"/>
<line x1="230" y1="393" x2="230" y2="403" stroke="#10b981" stroke-width="1.5" marker-end="url(#arr-green)"/>
<!-- 连线:Agent Loop ↔ 记忆系统 -->
<path d="M580,140 L450,140 L450,270 L400,270" stroke="#10b981" stroke-width="2" fill="none" marker-end="url(#arr-green)"/>
<text x="455" y="200" font-size="10" fill="#10b981" font-weight="500">读取/写入记忆</text>
<!-- ========== 中部中:Agent Loop 详细流程 ========== -->
<!-- 流程步骤 -->
<rect x="545" y="195" width="155" height="42" rx="8" fill="#eef2ff" stroke="#6366f1" stroke-width="1.5"/>
<text x="622" y="213" text-anchor="middle" font-size="11" font-weight="600" fill="#6366f1">① 构建 System Prompt</text>
<text x="622" y="228" text-anchor="middle" font-size="9" fill="#9ca3af">注入 Skills 索引+记忆</text>
<rect x="545" y="250" width="155" height="42" rx="8" fill="#eef2ff" stroke="#6366f1" stroke-width="1.5"/>
<text x="622" y="268" text-anchor="middle" font-size="11" font-weight="600" fill="#6366f1">② LLM 推理</text>
<text x="622" y="283" text-anchor="middle" font-size="9" fill="#9ca3af">生成回复或工具调用</text>
<rect x="545" y="305" width="155" height="42" rx="8" fill="#eef2ff" stroke="#6366f1" stroke-width="1.5"/>
<text x="622" y="323" text-anchor="middle" font-size="11" font-weight="600" fill="#6366f1">③ 执行工具</text>
<text x="622" y="338" text-anchor="middle" font-size="9" fill="#9ca3af">Shell/文件/搜索/Skill</text>
<rect x="545" y="360" width="155" height="42" rx="8" fill="#eef2ff" stroke="#6366f1" stroke-width="1.5"/>
<text x="622" y="378" text-anchor="middle" font-size="11" font-weight="600" fill="#6366f1">④ 返回结果</text>
<text x="622" y="393" text-anchor="middle" font-size="9" fill="#9ca3af">输出给用户或继续循环</text>
<rect x="545" y="415" width="155" height="42" rx="8" fill="#fef3c7" stroke="#f59e0b" stroke-width="1.5"/>
<text x="622" y="433" text-anchor="middle" font-size="11" font-weight="600" fill="#d97706">⑤ Periodic Nudge</text>
<text x="622" y="448" text-anchor="middle" font-size="9" fill="#9ca3af">每10-15 turns 自省</text>
<!-- 流程步骤连线 -->
<line x1="622" y1="237" x2="622" y2="247" stroke="#6366f1" stroke-width="1.5" marker-end="url(#arr-indigo)"/>
<line x1="622" y1="292" x2="622" y2="302" stroke="#6366f1" stroke-width="1.5" marker-end="url(#arr-indigo)"/>
<line x1="622" y1="347" x2="622" y2="357" stroke="#6366f1" stroke-width="1.5" marker-end="url(#arr-indigo)"/>
<line x1="622" y1="402" x2="622" y2="412" stroke="#f59e0b" stroke-width="1.5" marker-end="url(#arr-amber)"/>
<!-- 循环回到顶部 -->
<path d="M545,430 L520,430 L520,215 L542,215" stroke="#6366f1" stroke-width="1.5" fill="none" stroke-dasharray="5,3" marker-end="url(#arr-indigo)"/>
<text x="510" y="320" font-size="9" fill="#6366f1" transform="rotate(-90, 510, 320)" text-anchor="middle">循环</text>
<!-- ========== 中部右:Skill 系统 ========== -->
<!-- Skill 系统标题 -->
<rect x="780" y="195" width="340" height="40" rx="10" fill="#f59e0b" filter="url(#shadow)"/>
<text x="950" y="220" text-anchor="middle" font-size="14" font-weight="700" fill="#fff">⚡ Skill 系统 Skills System</text>
<!-- skill_manage 工具 -->
<rect x="780" y="250" width="340" height="55" rx="10" fill="#fff" stroke="#f59e0b" stroke-width="2" filter="url(#shadow)"/>
<text x="800" y="272" font-size="12" font-weight="700" fill="#d97706">skill_manage 工具</text>
<text x="800" y="290" font-size="11" fill="#6b7280">create / read / update / delete / list</text>
<!-- Pattern Extraction -->
<rect x="780" y="318" width="340" height="55" rx="10" fill="#fff" stroke="#f59e0b" stroke-width="2" filter="url(#shadow)"/>
<text x="800" y="340" font-size="11" font-weight="700" fill="#d97706">Pattern Extraction</text>
<text x="800" y="358" font-size="10" fill="#6b7280">从执行轨迹提取可复用工作流</text>
<!-- Skill 存储 -->
<rect x="780" y="386" width="165" height="55" rx="10" fill="#fff" stroke="#f59e0b" stroke-width="2" filter="url(#shadow)"/>
<text x="800" y="408" font-size="12" font-weight="700" fill="#d97706">📁 Skill 存储</text>
<text x="800" y="426" font-size="10" fill="#6b7280">~/.hermes/skills/</text>
<!-- Skills Guard -->
<rect x="955" y="386" width="165" height="55" rx="10" fill="#fff" stroke="#ef4444" stroke-width="2" filter="url(#shadow)"/>
<text x="975" y="408" font-size="11" font-weight="700" fill="#ef4444">🛡️ Guard</text>
<text x="975" y="426" font-size="9" fill="#6b7280">安全扫描·防注入</text>
<!-- Skill 格式说明 -->
<rect x="780" y="454" width="340" height="60" rx="10" fill="#fffbeb" stroke="#f59e0b" stroke-width="1"/>
<text x="800" y="474" font-size="11" font-weight="600" fill="#d97706">SKILL.md 格式:</text>
<text x="800" y="491" font-size="10" fill="#6b7280">When to Use → Procedure → Pitfalls</text>
<text x="800" y="505" font-size="9" fill="#9ca3af">触发: 5+ 工具调用 · 错误恢复 · 用户纠正</text>
<!-- Skill 系统内部连线 -->
<line x1="950" y1="305" x2="950" y2="315" stroke="#f59e0b" stroke-width="1.5" marker-end="url(#arr-amber)"/>
<line x1="950" y1="373" x2="950" y2="383" stroke="#f59e0b" stroke-width="1.5" marker-end="url(#arr-amber)"/>
<line x1="950" y1="441" x2="950" y2="451" stroke="#f59e0b" stroke-width="1.5" marker-end="url(#arr-amber)"/>
<!-- 连线:Agent Loop ↔ Skill 系统 -->
<path d="M700,320 L775,320" stroke="#f59e0b" stroke-width="2" fill="none" marker-end="url(#arr-amber)"/>
<text x="730" y="312" font-size="10" fill="#d97706" font-weight="500">调用</text>
<!-- ========== 底部左:Self-Evolution ========== -->
<rect x="60" y="530" width="480" height="45" rx="10" fill="#8b5cf6" filter="url(#shadow)"/>
<text x="300" y="558" text-anchor="middle" font-size="14" font-weight="700" fill="#fff">🔮 Self-Evolution 自进化系统</text>
<!-- DSPy -->
<rect x="60" y="590" width="230" height="75" rx="10" fill="#fff" stroke="#8b5cf6" stroke-width="2" filter="url(#shadow)"/>
<text x="80" y="612" font-size="12" font-weight="700" fill="#7c3aed">DSPy 优化器</text>
<text x="80" y="630" font-size="11" fill="#6b7280">自动优化 System Prompt</text>
<text x="80" y="648" font-size="10" fill="#9ca3af">基于任务结果反馈迭代提升</text>
<!-- GEPA -->
<rect x="310" y="590" width="230" height="75" rx="10" fill="#fff" stroke="#8b5cf6" stroke-width="2" filter="url(#shadow)"/>
<text x="330" y="612" font-size="12" font-weight="700" fill="#7c3aed">GEPA 循环</text>
<text x="330" y="630" font-size="10" fill="#6b7280">Generate → Evaluate → Prune</text>
<text x="330" y="648" font-size="10" fill="#9ca3af">→ Augment 循环精炼</text>
<!-- GEPA 循环箭头 -->
<path d="M425,668 Q425,690 370,690 Q315,690 315,668" stroke="#8b5cf6" stroke-width="1.5" fill="none" stroke-dasharray="4,3" marker-end="url(#arr-purple)"/>
<text x="370" y="700" text-anchor="middle" font-size="9" fill="#8b5cf6">循环精炼</text>
<!-- 连线:Self-Evolution ↔ Skill 系统 -->
<path d="M425,575 L425,530 Q425,515 500,515 L780,400" stroke="#8b5cf6" stroke-width="2" fill="none" stroke-dasharray="6,4" marker-end="url(#arr-purple)"/>
<text x="600" y="505" font-size="10" fill="#7c3aed" font-weight="500">优化 Skill 质量</text>
<!-- 连线:Self-Evolution ↔ Agent Loop -->
<path d="M300,530 L300,470 L545,440" stroke="#8b5cf6" stroke-width="2" fill="none" stroke-dasharray="6,4" marker-end="url(#arr-purple)"/>
<text x="370" y="480" font-size="10" fill="#7c3aed" font-weight="500">优化 Prompt</text>
<!-- ========== 底部右:工具系统 ========== -->
<rect x="620" y="555" width="500" height="40" rx="10" fill="#64748b" filter="url(#shadow)"/>
<text x="870" y="580" text-anchor="middle" font-size="14" font-weight="700" fill="#fff">🛠️ 工具系统 Tool System</text>
<!-- 工具卡片 -->
<rect x="620" y="608" width="115" height="52" rx="8" fill="#fff" stroke="#94a3b8" stroke-width="1.5"/>
<text x="677" y="628" text-anchor="middle" font-size="11" font-weight="600" fill="#475569">📂 文件操作</text>
<text x="677" y="645" text-anchor="middle" font-size="9" fill="#9ca3af">read/write/edit</text>
<rect x="745" y="608" width="115" height="52" rx="8" fill="#fff" stroke="#94a3b8" stroke-width="1.5"/>
<text x="802" y="628" text-anchor="middle" font-size="11" font-weight="600" fill="#475569">💻 Shell 执行</text>
<text x="802" y="645" text-anchor="middle" font-size="9" fill="#9ca3af">bash / python</text>
<rect x="870" y="608" width="115" height="52" rx="8" fill="#fff" stroke="#94a3b8" stroke-width="1.5"/>
<text x="927" y="628" text-anchor="middle" font-size="11" font-weight="600" fill="#475569">🌐 Web 搜索</text>
<text x="927" y="645" text-anchor="middle" font-size="9" fill="#9ca3af">search / fetch</text>
<rect x="995" y="608" width="125" height="52" rx="8" fill="#fff" stroke="#f59e0b" stroke-width="1.5"/>
<text x="1057" y="628" text-anchor="middle" font-size="11" font-weight="600" fill="#d97706">⚡ skill_manage</text>
<text x="1057" y="645" text-anchor="middle" font-size="9" fill="#9ca3af">Skill CRUD</text>
<!-- 连线:Agent Loop → 工具系统 -->
<path d="M622,457 L622,480 Q622,500 700,500 L870,552" stroke="#64748b" stroke-width="2" fill="none" marker-end="url(#arr-slate)"/>
<text x="750" y="495" font-size="10" fill="#64748b" font-weight="500">工具调用</text>
<!-- ========== 右侧:输出 ========== -->
<rect x="1200" y="195" width="180" height="60" rx="12" fill="#10b981" filter="url(#shadow)"/>
<text x="1290" y="222" text-anchor="middle" font-size="14" font-weight="700" fill="#fff">📤 输出</text>
<text x="1290" y="239" text-anchor="middle" font-size="11" fill="rgba(255,255,255,0.85)">回复用户 / 执行结果</text>
<!-- 连线:Agent Loop → 输出 -->
<path d="M700,380 Q750,380 750,260 L750,225 L1195,225" stroke="#10b981" stroke-width="2" fill="none" marker-end="url(#arr-green)"/>
<!-- ========== 质量指标 ========== -->
<rect x="1200" y="300" width="340" height="160" rx="12" fill="#fff" stroke="#e5e7eb" stroke-width="1.5" filter="url(#shadow)"/>
<text x="1220" y="325" font-size="13" font-weight="700" fill="#1f2937">📊 关键指标 Key Metrics</text>
<line x1="1220" y1="335" x2="1520" y2="335" stroke="#e5e7eb" stroke-width="1"/>
<text x="1220" y="358" font-size="11" fill="#6b7280">• GitHub Stars: 95,600+</text>
<text x="1220" y="378" font-size="11" fill="#6b7280">• 开源协议: MIT</text>
<text x="1220" y="398" font-size="11" fill="#6b7280">• 记忆检索: <10ms (FTS5)</text>
<text x="1220" y="418" font-size="11" fill="#6b7280">• Skill 触发: 5+ 工具调用</text>
<text x="1220" y="438" font-size="11" fill="#6b7280">• 自省频率: 每 10-15 turns</text>
<!-- ========== 图例 ========== -->
<rect x="1200" y="490" width="340" height="130" rx="10" fill="#fff" stroke="#e5e7eb" stroke-width="1"/>
<text x="1220" y="515" font-size="12" font-weight="700" fill="#1f2937">图例 Legend</text>
<rect x="1220" y="528" width="14" height="14" rx="3" fill="#ec4899"/>
<text x="1244" y="540" font-size="11" fill="#6b7280">用户 / LLM</text>
<rect x="1340" y="528" width="14" height="14" rx="3" fill="#6366f1"/>
<text x="1364" y="540" font-size="11" fill="#6b7280">Agent Loop</text>
<rect x="1220" y="552" width="14" height="14" rx="3" fill="#10b981"/>
<text x="1244" y="564" font-size="11" fill="#6b7280">记忆系统</text>
<rect x="1340" y="552" width="14" height="14" rx="3" fill="#f59e0b"/>
<text x="1364" y="564" font-size="11" fill="#6b7280">Skill 系统</text>
<rect x="1220" y="576" width="14" height="14" rx="3" fill="#8b5cf6"/>
<text x="1244" y="588" font-size="11" fill="#6b7280">Self-Evolution</text>
<rect x="1340" y="576" width="14" height="14" rx="3" fill="#64748b"/>
<text x="1364" y="588" font-size="11" fill="#6b7280">工具系统</text>
<line x1="1220" y1="604" x2="1260" y2="604" stroke="#6366f1" stroke-width="2"/>
<text x="1270" y="608" font-size="10" fill="#6b7280">数据流</text>
<line x1="1340" y1="604" x2="1380" y2="604" stroke="#8b5cf6" stroke-width="2" stroke-dasharray="5,3"/>
<text x="1390" y="608" font-size="10" fill="#6b7280">反馈/优化</text>
<!-- 底部来源 -->
<text x="800" y="985" text-anchor="middle" font-size="10" fill="#d1d5db">基于 Hermes Agent 源码 + Nous Research 官方文档 + 社区评测 · W.ai Deep Tech Research · 2026-04</text>
</svg>
</body>
</html>
FILE:references/example-openclaw.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
width: 1600px; height: 1100px;
background: #fafafa;
font-family: 'Inter', 'PingFang SC', 'Microsoft YaHei', sans-serif;
overflow: hidden;
}
</style>
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1600 1100" width="1600" height="1100">
<defs>
<marker id="arr-indigo" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<path d="M0,0 L8,3 L0,6 Z" fill="#6366f1"/>
</marker>
<marker id="arr-green" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<path d="M0,0 L8,3 L0,6 Z" fill="#10b981"/>
</marker>
<marker id="arr-amber" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<path d="M0,0 L8,3 L0,6 Z" fill="#f59e0b"/>
</marker>
<marker id="arr-purple" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<path d="M0,0 L8,3 L0,6 Z" fill="#8b5cf6"/>
</marker>
<marker id="arr-slate" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<path d="M0,0 L8,3 L0,6 Z" fill="#64748b"/>
</marker>
<marker id="arr-pink" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<path d="M0,0 L8,3 L0,6 Z" fill="#ec4899"/>
</marker>
<marker id="arr-red" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<path d="M0,0 L8,3 L0,6 Z" fill="#ef4444"/>
</marker>
<filter id="shadow" x="-4%" y="-4%" width="108%" height="108%">
<feDropShadow dx="0" dy="2" stdDeviation="4" flood-color="#000" flood-opacity="0.08"/>
</filter>
</defs>
<!-- ========== 标题 ========== -->
<text x="800" y="38" text-anchor="middle" font-size="22" font-weight="700" fill="#1f2937">OpenClaw 完整技术架构图</text>
<text x="800" y="58" text-anchor="middle" font-size="12" fill="#9ca3af">Multi-Channel AI Agent Platform · Gateway → Sessions → Agent Loop → Tools</text>
<!-- ========== 第一层:消息渠道 Channels ========== -->
<rect x="195" y="80" width="105" height="48" rx="8" fill="#ec4899" filter="url(#shadow)"/>
<text x="247" y="101" text-anchor="middle" font-size="11" font-weight="700" fill="#fff">Telegram</text>
<text x="247" y="118" text-anchor="middle" font-size="9" fill="rgba(255,255,255,0.8)">grammY</text>
<rect x="310" y="80" width="105" height="48" rx="8" fill="#ec4899" filter="url(#shadow)"/>
<text x="362" y="101" text-anchor="middle" font-size="11" font-weight="700" fill="#fff">WhatsApp</text>
<text x="362" y="118" text-anchor="middle" font-size="9" fill="rgba(255,255,255,0.8)">Baileys</text>
<rect x="425" y="80" width="105" height="48" rx="8" fill="#ec4899" filter="url(#shadow)"/>
<text x="477" y="106" text-anchor="middle" font-size="11" font-weight="700" fill="#fff">Discord</text>
<rect x="540" y="80" width="105" height="48" rx="8" fill="#ec4899" filter="url(#shadow)"/>
<text x="592" y="106" text-anchor="middle" font-size="11" font-weight="700" fill="#fff">Slack</text>
<rect x="655" y="80" width="105" height="48" rx="8" fill="#ec4899" filter="url(#shadow)"/>
<text x="707" y="106" text-anchor="middle" font-size="11" font-weight="700" fill="#fff">Signal</text>
<rect x="770" y="80" width="105" height="48" rx="8" fill="#ec4899" filter="url(#shadow)"/>
<text x="822" y="106" text-anchor="middle" font-size="11" font-weight="700" fill="#fff">iMessage</text>
<rect x="885" y="80" width="105" height="48" rx="8" fill="#ec4899" filter="url(#shadow)"/>
<text x="937" y="106" text-anchor="middle" font-size="11" font-weight="700" fill="#fff">WebChat</text>
<rect x="1000" y="80" width="105" height="48" rx="8" fill="#ec4899" filter="url(#shadow)"/>
<text x="1052" y="106" text-anchor="middle" font-size="11" font-weight="700" fill="#fff">CLI</text>
<!-- ========== 第二层:Gateway ========== -->
<rect x="195" y="155" width="910" height="55" rx="12" fill="#6366f1" filter="url(#shadow)"/>
<text x="650" y="180" text-anchor="middle" font-size="16" font-weight="700" fill="#fff">🌐 Gateway(单一守护进程 · WebSocket 127.0.0.1:18789)</text>
<text x="650" y="198" text-anchor="middle" font-size="10" fill="rgba(255,255,255,0.85)">消息路由 · 认证 · 设备配对 · JSON Schema 协议 · Canvas Host · HTTP API + WS API</text>
<!-- 渠道 → Gateway 连线 -->
<line x1="362" y1="128" x2="362" y2="152" stroke="#ec4899" stroke-width="2" marker-end="url(#arr-pink)"/>
<line x1="592" y1="128" x2="592" y2="152" stroke="#ec4899" stroke-width="2" marker-end="url(#arr-pink)"/>
<line x1="822" y1="128" x2="822" y2="152" stroke="#ec4899" stroke-width="2" marker-end="url(#arr-pink)"/>
<line x1="1052" y1="128" x2="1052" y2="152" stroke="#ec4899" stroke-width="2" marker-end="url(#arr-pink)"/>
<!-- ========== 第三层左:Agent Loop ========== -->
<rect x="60" y="240" width="280" height="40" rx="10" fill="#6366f1" filter="url(#shadow)"/>
<text x="200" y="265" text-anchor="middle" font-size="13" font-weight="700" fill="#fff">🔄 Agent Loop 核心循环</text>
<rect x="60" y="290" width="280" height="34" rx="8" fill="#eef2ff" stroke="#6366f1" stroke-width="1.5"/>
<text x="200" y="311" text-anchor="middle" font-size="11" font-weight="600" fill="#6366f1">① RPC 接收 → 解析 Session</text>
<rect x="60" y="346" width="280" height="34" rx="8" fill="#eef2ff" stroke="#6366f1" stroke-width="1.5"/>
<text x="200" y="367" text-anchor="middle" font-size="11" font-weight="600" fill="#6366f1">② 会话队列串行化</text>
<rect x="60" y="402" width="280" height="34" rx="8" fill="#eef2ff" stroke="#6366f1" stroke-width="1.5"/>
<text x="200" y="423" text-anchor="middle" font-size="11" font-weight="600" fill="#6366f1">③ Prompt 组装</text>
<rect x="60" y="458" width="280" height="34" rx="8" fill="#eef2ff" stroke="#6366f1" stroke-width="1.5"/>
<text x="200" y="479" text-anchor="middle" font-size="11" font-weight="600" fill="#6366f1">④ 模型推理(流式输出)</text>
<rect x="60" y="514" width="280" height="34" rx="8" fill="#eef2ff" stroke="#6366f1" stroke-width="1.5"/>
<text x="200" y="535" text-anchor="middle" font-size="11" font-weight="600" fill="#6366f1">⑤ 工具执行</text>
<rect x="60" y="570" width="280" height="34" rx="8" fill="#eef2ff" stroke="#6366f1" stroke-width="1.5"/>
<text x="200" y="591" text-anchor="middle" font-size="11" font-weight="600" fill="#6366f1">⑥ Transcript 持久化</text>
<rect x="60" y="626" width="280" height="34" rx="8" fill="#fef3c7" stroke="#f59e0b" stroke-width="1.5"/>
<text x="200" y="647" text-anchor="middle" font-size="11" font-weight="600" fill="#d97706">⑦ 生命周期 (end/error)</text>
<!-- Agent Loop 步骤连线 (22px gap, line+arrow clearly visible) -->
<line x1="200" y1="324" x2="200" y2="343" stroke="#6366f1" stroke-width="2" marker-end="url(#arr-indigo)"/>
<line x1="200" y1="380" x2="200" y2="399" stroke="#6366f1" stroke-width="2" marker-end="url(#arr-indigo)"/>
<line x1="200" y1="436" x2="200" y2="455" stroke="#6366f1" stroke-width="2" marker-end="url(#arr-indigo)"/>
<line x1="200" y1="492" x2="200" y2="511" stroke="#6366f1" stroke-width="2" marker-end="url(#arr-indigo)"/>
<line x1="200" y1="548" x2="200" y2="567" stroke="#6366f1" stroke-width="2" marker-end="url(#arr-indigo)"/>
<line x1="200" y1="604" x2="200" y2="623" stroke="#6366f1" stroke-width="2" marker-end="url(#arr-indigo)"/>
<!-- 循环箭头 -->
<path d="M60,645 L35,645 L35,298 L57,298" stroke="#6366f1" stroke-width="1.5" fill="none" stroke-dasharray="5,3" marker-end="url(#arr-indigo)"/>
<text x="30" y="440" font-size="9" fill="#6366f1" transform="rotate(-90, 30, 440)" text-anchor="middle">循环</text>
<!-- Gateway → Agent Loop -->
<path d="M400,210 L200,210 L200,237" stroke="#6366f1" stroke-width="2" fill="none" marker-end="url(#arr-indigo)"/>
<!-- ========== 第三层中左:Context Engine ========== -->
<rect x="380" y="240" width="280" height="40" rx="10" fill="#10b981" filter="url(#shadow)"/>
<text x="520" y="265" text-anchor="middle" font-size="13" font-weight="700" fill="#fff">🧠 Context Engine 上下文引擎</text>
<rect x="380" y="292" width="280" height="42" rx="10" fill="#fff" stroke="#10b981" stroke-width="2" filter="url(#shadow)"/>
<text x="400" y="312" font-size="11" font-weight="700" fill="#10b981">System Prompt Builder</text>
<text x="400" y="327" font-size="10" fill="#9ca3af">组装系统提示 + 注入上下文</text>
<rect x="380" y="344" width="280" height="42" rx="10" fill="#fff" stroke="#10b981" stroke-width="2" filter="url(#shadow)"/>
<text x="400" y="364" font-size="11" font-weight="700" fill="#10b981">Skills Loader</text>
<text x="400" y="379" font-size="10" fill="#9ca3af">扫描 + 注入技能描述到 Prompt</text>
<rect x="380" y="396" width="280" height="42" rx="10" fill="#fff" stroke="#10b981" stroke-width="2" filter="url(#shadow)"/>
<text x="400" y="416" font-size="11" font-weight="700" fill="#10b981">Memory System</text>
<text x="400" y="431" font-size="10" fill="#9ca3af">MEMORY.md + memory/*.md + lossless-claw</text>
<rect x="380" y="448" width="280" height="42" rx="10" fill="#fff" stroke="#10b981" stroke-width="2" filter="url(#shadow)"/>
<text x="400" y="468" font-size="11" font-weight="700" fill="#10b981">Bootstrap Files</text>
<text x="400" y="483" font-size="10" fill="#9ca3af">SOUL / IDENTITY / USER / TOOLS / AGENTS</text>
<rect x="380" y="500" width="280" height="36" rx="10" fill="#fff" stroke="#10b981" stroke-width="2" filter="url(#shadow)"/>
<text x="400" y="522" font-size="11" font-weight="700" fill="#10b981">Transcript Repair</text>
<!-- Agent Loop → Context Engine -->
<line x1="340" y1="395" x2="377" y2="395" stroke="#10b981" stroke-width="2" marker-end="url(#arr-green)"/>
<text x="348" y="388" font-size="9" fill="#10b981" font-weight="500">上下文</text>
<!-- ========== 第三层中右:Sessions + Cron ========== -->
<rect x="700" y="240" width="240" height="40" rx="10" fill="#10b981" filter="url(#shadow)"/>
<text x="820" y="265" text-anchor="middle" font-size="13" font-weight="700" fill="#fff">📋 Sessions 会话管理</text>
<rect x="700" y="292" width="240" height="100" rx="10" fill="#fff" stroke="#10b981" stroke-width="2" filter="url(#shadow)"/>
<text x="720" y="312" font-size="11" font-weight="700" fill="#10b981">会话管理</text>
<text x="720" y="330" font-size="10" fill="#6b7280">• Session Key 解析</text>
<text x="720" y="346" font-size="10" fill="#6b7280">• Transcript 存储 + 写锁</text>
<text x="720" y="362" font-size="10" fill="#6b7280">• Model Overrides</text>
<text x="720" y="378" font-size="10" fill="#6b7280">• Sub-agent 编排</text>
<!-- Cron -->
<rect x="700" y="408" width="240" height="40" rx="10" fill="#f59e0b" filter="url(#shadow)"/>
<text x="820" y="433" text-anchor="middle" font-size="13" font-weight="700" fill="#fff">⏰ Cron 定时任务</text>
<rect x="700" y="458" width="240" height="80" rx="10" fill="#fff" stroke="#f59e0b" stroke-width="2" filter="url(#shadow)"/>
<text x="720" y="478" font-size="11" font-weight="700" fill="#d97706">Schedule Types</text>
<text x="720" y="496" font-size="10" fill="#6b7280">at / every / cron expression</text>
<text x="720" y="512" font-size="10" fill="#6b7280">Payload: systemEvent / agentTurn</text>
<text x="720" y="528" font-size="10" fill="#6b7280">Delivery: announce / webhook</text>
<!-- Gateway → Sessions -->
<path d="M700,210 L820,210 L820,237" stroke="#10b981" stroke-width="2" fill="none" marker-end="url(#arr-green)"/>
<!-- Gateway → Cron -->
<path d="M900,210 L950,210 L950,425 L940,425" stroke="#f59e0b" stroke-width="2" fill="none" stroke-dasharray="6,4" marker-end="url(#arr-amber)"/>
<!-- ========== 第三层右:LLM Providers ========== -->
<rect x="980" y="240" width="230" height="40" rx="10" fill="#8b5cf6" filter="url(#shadow)"/>
<text x="1095" y="265" text-anchor="middle" font-size="13" font-weight="700" fill="#fff">🧠 LLM Providers 模型层</text>
<rect x="980" y="292" width="230" height="42" rx="10" fill="#fff" stroke="#8b5cf6" stroke-width="2" filter="url(#shadow)"/>
<text x="1000" y="312" font-size="11" font-weight="700" fill="#8b5cf6">Anthropic</text>
<text x="1000" y="327" font-size="10" fill="#9ca3af">Claude Opus / Sonnet / Haiku</text>
<rect x="980" y="344" width="230" height="42" rx="10" fill="#fff" stroke="#8b5cf6" stroke-width="2" filter="url(#shadow)"/>
<text x="1000" y="364" font-size="11" font-weight="700" fill="#8b5cf6">AWS Bedrock</text>
<text x="1000" y="379" font-size="10" fill="#9ca3af">Anthropic / Amazon / Stability</text>
<rect x="980" y="396" width="230" height="42" rx="10" fill="#fff" stroke="#8b5cf6" stroke-width="2" filter="url(#shadow)"/>
<text x="1000" y="416" font-size="11" font-weight="700" fill="#8b5cf6">OpenAI</text>
<text x="1000" y="431" font-size="10" fill="#9ca3af">GPT-4o / o1 / o3</text>
<rect x="980" y="448" width="110" height="42" rx="10" fill="#fff" stroke="#8b5cf6" stroke-width="2" filter="url(#shadow)"/>
<text x="1000" y="468" font-size="11" font-weight="700" fill="#8b5cf6">Google</text>
<text x="1000" y="483" font-size="10" fill="#9ca3af">Gemini</text>
<rect x="1100" y="448" width="110" height="42" rx="10" fill="#fff" stroke="#8b5cf6" stroke-width="2" filter="url(#shadow)"/>
<text x="1120" y="468" font-size="11" font-weight="700" fill="#8b5cf6">Mistral</text>
<text x="1120" y="483" font-size="10" fill="#9ca3af">Large / Medium</text>
<rect x="980" y="500" width="230" height="36" rx="10" fill="#fff" stroke="#8b5cf6" stroke-width="2" filter="url(#shadow)"/>
<text x="1000" y="522" font-size="11" font-weight="700" fill="#8b5cf6">本地模型 Ollama / vLLM</text>
<!-- Agent Loop → LLM -->
<path d="M340,445 Q360,445 360,360 L360,260 L650,210 L1000,210 L1095,210 L1095,237" stroke="#8b5cf6" stroke-width="2" fill="none" marker-end="url(#arr-purple)"/>
<!-- ========== 右侧上:Nodes ========== -->
<rect x="1260" y="240" width="280" height="40" rx="10" fill="#ec4899" filter="url(#shadow)"/>
<text x="1400" y="265" text-anchor="middle" font-size="13" font-weight="700" fill="#fff">📱 Nodes 伴侣设备</text>
<rect x="1260" y="292" width="135" height="42" rx="10" fill="#fff" stroke="#ec4899" stroke-width="2" filter="url(#shadow)"/>
<text x="1327" y="312" text-anchor="middle" font-size="11" font-weight="700" fill="#ec4899">macOS App</text>
<text x="1327" y="327" text-anchor="middle" font-size="9" fill="#9ca3af">camera/screen</text>
<rect x="1405" y="292" width="135" height="42" rx="10" fill="#fff" stroke="#ec4899" stroke-width="2" filter="url(#shadow)"/>
<text x="1472" y="312" text-anchor="middle" font-size="11" font-weight="700" fill="#ec4899">iOS App</text>
<text x="1472" y="327" text-anchor="middle" font-size="9" fill="#9ca3af">location/canvas</text>
<rect x="1260" y="344" width="135" height="42" rx="10" fill="#fff" stroke="#ec4899" stroke-width="2" filter="url(#shadow)"/>
<text x="1327" y="364" text-anchor="middle" font-size="11" font-weight="700" fill="#ec4899">Android App</text>
<text x="1327" y="379" text-anchor="middle" font-size="9" fill="#9ca3af">camera/screen</text>
<rect x="1405" y="344" width="135" height="42" rx="10" fill="#fff" stroke="#ec4899" stroke-width="2" filter="url(#shadow)"/>
<text x="1472" y="364" text-anchor="middle" font-size="11" font-weight="700" fill="#ec4899">Headless Node</text>
<text x="1472" y="379" text-anchor="middle" font-size="9" fill="#9ca3af">server mode</text>
<!-- Gateway → Nodes -->
<path d="M1105,182 L1200,182 L1400,182 L1400,237" stroke="#ec4899" stroke-width="2" fill="none" stroke-dasharray="6,4" marker-end="url(#arr-pink)"/>
<text x="1250" y="175" font-size="10" fill="#ec4899" font-weight="500">WebSocket</text>
<!-- ========== 右侧中:Skills 系统 ========== -->
<rect x="1260" y="408" width="280" height="40" rx="10" fill="#f59e0b" filter="url(#shadow)"/>
<text x="1400" y="433" text-anchor="middle" font-size="13" font-weight="700" fill="#fff">⚡ Skills 技能系统</text>
<rect x="1260" y="458" width="280" height="80" rx="10" fill="#fff" stroke="#f59e0b" stroke-width="2" filter="url(#shadow)"/>
<text x="1280" y="478" font-size="11" font-weight="700" fill="#d97706">Skill Sources</text>
<text x="1280" y="496" font-size="10" fill="#6b7280">• 本地 Skills (~/.openclaw/skills/)</text>
<text x="1280" y="512" font-size="10" fill="#6b7280">• ClawHub(社区 Skill 市场)</text>
<text x="1280" y="528" font-size="10" fill="#6b7280">• Workspace Skills · 安全扫描</text>
<!-- Skills → Context Engine -->
<path d="M1260,480 L680,420 L663,420" stroke="#f59e0b" stroke-width="2" fill="none" stroke-dasharray="6,4" marker-end="url(#arr-amber)"/>
<text x="950" y="445" font-size="10" fill="#d97706" font-weight="500">注入 Skill 描述</text>
<!-- ========== 第四层:Tool System ========== -->
<rect x="60" y="625" width="860" height="40" rx="10" fill="#64748b" filter="url(#shadow)"/>
<text x="490" y="650" text-anchor="middle" font-size="14" font-weight="700" fill="#fff">🛠️ Tool System 工具系统</text>
<rect x="60" y="678" width="120" height="48" rx="8" fill="#fff" stroke="#94a3b8" stroke-width="1.5"/>
<text x="120" y="698" text-anchor="middle" font-size="11" font-weight="600" fill="#475569">exec</text>
<text x="120" y="715" text-anchor="middle" font-size="9" fill="#9ca3af">Shell 执行</text>
<rect x="190" y="678" width="120" height="48" rx="8" fill="#fff" stroke="#94a3b8" stroke-width="1.5"/>
<text x="250" y="698" text-anchor="middle" font-size="11" font-weight="600" fill="#475569">read/write/edit</text>
<text x="250" y="715" text-anchor="middle" font-size="9" fill="#9ca3af">文件操作</text>
<rect x="320" y="678" width="120" height="48" rx="8" fill="#fff" stroke="#94a3b8" stroke-width="1.5"/>
<text x="380" y="698" text-anchor="middle" font-size="11" font-weight="600" fill="#475569">web_search</text>
<text x="380" y="715" text-anchor="middle" font-size="9" fill="#9ca3af">网络搜索+抓取</text>
<rect x="450" y="678" width="120" height="48" rx="8" fill="#fff" stroke="#94a3b8" stroke-width="1.5"/>
<text x="510" y="698" text-anchor="middle" font-size="11" font-weight="600" fill="#475569">sessions</text>
<text x="510" y="715" text-anchor="middle" font-size="9" fill="#9ca3af">子Agent编排</text>
<rect x="580" y="678" width="120" height="48" rx="8" fill="#fff" stroke="#94a3b8" stroke-width="1.5"/>
<text x="640" y="698" text-anchor="middle" font-size="11" font-weight="600" fill="#475569">memory</text>
<text x="640" y="715" text-anchor="middle" font-size="9" fill="#9ca3af">记忆检索</text>
<rect x="710" y="678" width="100" height="48" rx="8" fill="#fff" stroke="#94a3b8" stroke-width="1.5"/>
<text x="760" y="698" text-anchor="middle" font-size="11" font-weight="600" fill="#475569">image</text>
<text x="760" y="715" text-anchor="middle" font-size="9" fill="#9ca3af">图像分析</text>
<rect x="820" y="678" width="100" height="48" rx="8" fill="#fff" stroke="#94a3b8" stroke-width="1.5"/>
<text x="870" y="698" text-anchor="middle" font-size="11" font-weight="600" fill="#475569">MCP</text>
<text x="870" y="715" text-anchor="middle" font-size="9" fill="#9ca3af">外部工具协议</text>
<!-- Agent Loop → Tools -->
<path d="M200,592 L200,622" stroke="#64748b" stroke-width="2" fill="none" marker-end="url(#arr-slate)"/>
<text x="210" y="612" font-size="9" fill="#64748b" font-weight="500">工具调用</text>
<!-- ========== 第四层右:Plugin System ========== -->
<rect x="960" y="625" width="280" height="40" rx="10" fill="#f59e0b" filter="url(#shadow)"/>
<text x="1100" y="650" text-anchor="middle" font-size="13" font-weight="700" fill="#fff">🔌 Plugin System 插件系统</text>
<rect x="960" y="678" width="280" height="80" rx="10" fill="#fff" stroke="#f59e0b" stroke-width="2" filter="url(#shadow)"/>
<text x="980" y="698" font-size="11" font-weight="700" fill="#d97706">Plugin Architecture</text>
<text x="980" y="716" font-size="10" fill="#6b7280">• Activation Planner · Plugin SDK</text>
<text x="980" y="732" font-size="10" fill="#6b7280">• Hook: tool lifecycle + gateway pipeline</text>
<text x="980" y="748" font-size="10" fill="#6b7280">• lossless-claw · clawguard-bench ...</text>
<!-- Plugin ↔ Agent Loop -->
<path d="M960,645 L400,530" stroke="#f59e0b" stroke-width="2" fill="none" stroke-dasharray="6,4" marker-end="url(#arr-amber)"/>
<text x="680" y="578" font-size="10" fill="#d97706" font-weight="500">插件钩子</text>
<!-- ========== 底部右:Security ========== -->
<rect x="1260" y="625" width="280" height="40" rx="10" fill="#ef4444" filter="url(#shadow)"/>
<text x="1400" y="650" text-anchor="middle" font-size="13" font-weight="700" fill="#fff">🛡️ Security 安全层</text>
<rect x="1260" y="678" width="280" height="80" rx="10" fill="#fff" stroke="#ef4444" stroke-width="2" filter="url(#shadow)"/>
<text x="1280" y="698" font-size="11" font-weight="700" fill="#ef4444">Security Controls</text>
<text x="1280" y="716" font-size="10" fill="#6b7280">• Exec Approvals(执行审批)</text>
<text x="1280" y="732" font-size="10" fill="#6b7280">• Device Pairing(设备配对)</text>
<text x="1280" y="748" font-size="10" fill="#6b7280">• Auth: shared-secret / tailscale / ...</text>
<!-- Security → Gateway (渗透全局) -->
<path d="M1400,625 L1400,580 L1150,210 L1110,210" stroke="#ef4444" stroke-width="1.5" fill="none" stroke-dasharray="6,4" marker-end="url(#arr-red)"/>
<text x="1300" y="560" font-size="9" fill="#ef4444" font-weight="500">安全策略</text>
<!-- ========== 图例 ========== -->
<rect x="60" y="790" width="1480" height="100" rx="10" fill="#fff" stroke="#e5e7eb" stroke-width="1"/>
<text x="100" y="818" font-size="13" font-weight="700" fill="#1f2937">图例 Legend</text>
<!-- 颜色图例 -->
<rect x="100" y="832" width="14" height="14" rx="3" fill="#ec4899"/>
<text x="122" y="844" font-size="11" fill="#6b7280">Channels / Nodes</text>
<rect x="260" y="832" width="14" height="14" rx="3" fill="#6366f1"/>
<text x="282" y="844" font-size="11" fill="#6b7280">Gateway / Agent Loop</text>
<rect x="450" y="832" width="14" height="14" rx="3" fill="#10b981"/>
<text x="472" y="844" font-size="11" fill="#6b7280">Context / Sessions</text>
<rect x="630" y="832" width="14" height="14" rx="3" fill="#8b5cf6"/>
<text x="652" y="844" font-size="11" fill="#6b7280">LLM Providers</text>
<rect x="790" y="832" width="14" height="14" rx="3" fill="#f59e0b"/>
<text x="812" y="844" font-size="11" fill="#6b7280">Skills / Plugins / Cron</text>
<rect x="980" y="832" width="14" height="14" rx="3" fill="#64748b"/>
<text x="1002" y="844" font-size="11" fill="#6b7280">Tool System</text>
<rect x="1120" y="832" width="14" height="14" rx="3" fill="#ef4444"/>
<text x="1142" y="844" font-size="11" fill="#6b7280">Security</text>
<!-- 线条图例 -->
<line x1="100" y1="866" x2="140" y2="866" stroke="#6366f1" stroke-width="2"/>
<text x="150" y="870" font-size="10" fill="#6b7280">数据流</text>
<line x1="260" y1="866" x2="300" y2="866" stroke="#8b5cf6" stroke-width="2" stroke-dasharray="5,3"/>
<text x="310" y="870" font-size="10" fill="#6b7280">反馈/异步</text>
<!-- 底部来源 -->
<text x="800" y="920" text-anchor="middle" font-size="10" fill="#d1d5db">基于 OpenClaw 源码架构分析 · 2026-04-25</text>
</svg>
</body>
</html>
FILE:scripts/screenshot.py
#!/usr/bin/env python3
"""
SVG Architecture Diagram → High-res PNG screenshot via Playwright.
Usage:
python3 screenshot.py <input.html> [output.png] [--scale 2] [--width 1600] [--height 1000]
Defaults optimized for architecture diagrams:
- scale: 2 (high-res, good balance of quality and file size)
- width: 1600
- height: 1000
"""
import argparse
import sys
import os
def main():
parser = argparse.ArgumentParser(description="SVG diagram screenshot via Playwright")
parser.add_argument("html", help="Path to HTML file containing SVG diagram")
parser.add_argument("output", nargs="?", default=None, help="Output PNG path")
parser.add_argument("--scale", type=int, default=4, help="Device scale factor (default: 4)")
parser.add_argument("--width", type=int, default=1600, help="Viewport width (default: 1600)")
parser.add_argument("--height", type=int, default=1000, help="Viewport height (default: 1000)")
parser.add_argument("--wait", type=int, default=1500, help="Wait ms after load (default: 1500)")
args = parser.parse_args()
html_path = args.html
if not html_path.startswith(("http://", "https://", "file://")):
abs_path = os.path.abspath(html_path)
if not os.path.exists(abs_path):
print(f"Error: File not found: {abs_path}", file=sys.stderr)
sys.exit(1)
html_path = f"file://{abs_path}"
if args.output:
output_path = args.output
else:
from pathlib import Path
output_path = f"{Path(args.html).stem}.png"
try:
from playwright.sync_api import sync_playwright
except ImportError:
print("Error: playwright not installed. Run: pip install playwright && playwright install chromium", file=sys.stderr)
sys.exit(1)
print(f"Rendering: {html_path}")
print(f"Viewport: {args.width}x{args.height} @ {args.scale}x")
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page(
viewport={"width": args.width, "height": args.height},
device_scale_factor=args.scale,
)
page.goto(html_path, wait_until="networkidle")
if args.wait > 0:
page.wait_for_timeout(args.wait)
page.screenshot(path=output_path, full_page=True)
browser.close()
file_size = os.path.getsize(output_path)
size_str = f"{file_size / 1024:.0f}KB" if file_size < 1024 * 1024 else f"{file_size / (1024 * 1024):.1f}MB"
try:
from PIL import Image
img = Image.open(output_path)
print(f"✅ {output_path} ({img.size[0]}x{img.size[1]}, {size_str})")
except ImportError:
print(f"✅ {output_path} ({size_str})")
if __name__ == "__main__":
main()
Generate ultra-high-resolution screenshots from HTML content using Playwright. Default 4x device scale factor produces crisp, pixel-perfect output ideal for...
---
name: web-render-screenshot
description: >-
Generate ultra-high-resolution screenshots from HTML content using Playwright.
Default 4x device scale factor produces crisp, pixel-perfect output ideal for
UI mockups, dashboards, data visualizations, infographics, website previews,
and any content requiring accurate text rendering. Use when: (1) user wants a
website/webpage screenshot or mockup image, (2) content has text, numbers,
tables, charts that must be legible, (3) user wants a UI design rendered as an
image, (4) HTML Canvas or data-driven visuals need to be exported as PNG/JPEG.
Triggers: "website screenshot", "网页截图", "网站首页图", "UI mockup image",
"dashboard image", "数据可视化图片", "render HTML to image". Prefer this over
AI image generation (stable-image-ultra) when the content has structured text,
data, or UI elements. AI image generation cannot produce readable text.
---
# Web Render Screenshot
Generate ultra-high-resolution PNG/JPEG images from HTML content via Playwright headless Chromium.
## When to Use
| Scenario | Use This Skill | Use AI Image Gen Instead |
|----------|---------------|------------------------|
| Website UI / mockup | ✅ | ❌ |
| Dashboard / data viz | ✅ | ❌ |
| Text-heavy infographic | ✅ | ❌ |
| Weather/finance/news page | ✅ | ❌ |
| Artistic illustration | ❌ | ✅ |
| Photorealistic scene | ❌ | ✅ |
**Rule**: If it has text that must be readable → use this skill.
## Quick Start
### Step 1: Create the HTML
Write a self-contained HTML file with inline CSS. Guidelines:
- Use system fonts or Google Fonts CDN for Chinese support: `"PingFang SC", "Microsoft YaHei", "Noto Sans SC", sans-serif`
- All styles inline (no external CSS files)
- Design for the target viewport (default: 1920×1080)
- Use emoji for icons when appropriate (universally supported)
- Ensure `overflow: hidden` on body if single-viewport capture is desired
### Step 2: Take the Screenshot
Use the bundled script:
```bash
python3 scripts/screenshot.py <input.html> [output.png] [options]
```
Or inline Python when the script cannot be invoked directly:
```python
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page(
viewport={"width": 1920, "height": 1080},
device_scale_factor=4, # 4x ultra-high res (default)
)
page.goto("file:///path/to/input.html", wait_until="networkidle")
page.wait_for_timeout(1000)
page.screenshot(path="output.png", full_page=True)
browser.close()
```
### Step 3: Deliver
Output `MEDIA:<path>` for inline delivery, or use `openclaw message send` with `--force-document` for Telegram to avoid compression.
## Default Parameters
| Parameter | Default | Description |
|-----------|---------|-------------|
| `device_scale_factor` | **4** | Ultra-high resolution (4x). Output = viewport × 4 |
| `viewport.width` | 1920 | CSS pixel width |
| `viewport.height` | 1080 | CSS pixel height |
| `full_page` | true | Capture entire scrollable content |
| `wait` | 1000ms | Wait after page load for rendering |
| `format` | png | Lossless output (use jpeg for smaller files) |
### Resolution Reference
| Scale | Output for 1920×1080 viewport | File Size (typical) | Use Case |
|-------|-------------------------------|--------------------:|----------|
| 1x | 1920×1080 | ~100KB | Draft / preview |
| 2x | 3840×2160 | ~1-2MB | Standard high-res |
| 3x | 5760×3240 | ~3-5MB | High-quality print |
| **4x** | **7680×4320** | **5-8MB** | **Ultra-high (default)** |
## Script Options
```
python3 scripts/screenshot.py <html> [output] [options]
Arguments:
html HTML file path or URL
output Output file path (default: <input-stem>.png)
Options:
--scale N Device scale factor (default: 4)
--width N Viewport width (default: 1920)
--height N Viewport height (default: 1080)
--full-page Capture full page (default)
--no-full-page Capture viewport only
--wait N Wait time in ms (default: 1000)
--format FORMAT png or jpeg (default: png)
--quality N JPEG quality 0-100 (default: 92)
```
## HTML Design Tips
1. **Glassmorphism**: `backdrop-filter: blur(20px); background: rgba(255,255,255,0.12);`
2. **Gradient backgrounds**: `background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);`
3. **Card shadows**: `box-shadow: 0 8px 32px rgba(0,0,0,0.1);`
4. **Chinese fonts**: Always include fallback chain for cross-platform rendering
5. **Emoji icons**: Use emoji for weather/status/category icons — they render at any scale
6. **Grid layout**: CSS Grid for complex dashboards; Flexbox for simpler layouts
## File Size Management
If the output PNG is too large for delivery (e.g., >5MB for Telegram):
```python
from PIL import Image
img = Image.open("output.png")
img.save("output.jpg", "JPEG", quality=92, optimize=True) # Usually 5-10x smaller
```
Or use `--format jpeg --quality 92` with the screenshot script.
FILE:scripts/screenshot.py
#!/usr/bin/env python3
"""
High-resolution HTML screenshot via Playwright.
Usage:
python3 screenshot.py <html_file> [output.png] [--scale 4] [--width 1920] [--height 1080] [--full-page]
Defaults:
- scale: 4 (ultra-high resolution)
- width: 1920
- height: 1080
- full-page: enabled (captures entire scrollable content)
"""
import argparse
import sys
import os
from pathlib import Path
def main():
parser = argparse.ArgumentParser(description="High-res HTML screenshot via Playwright")
parser.add_argument("html", help="Path to HTML file or URL")
parser.add_argument("output", nargs="?", default=None, help="Output PNG path (default: same name as input with .png)")
parser.add_argument("--scale", type=int, default=4, help="Device scale factor (default: 4 = ultra-high res)")
parser.add_argument("--width", type=int, default=1920, help="Viewport width in CSS pixels (default: 1920)")
parser.add_argument("--height", type=int, default=1080, help="Viewport height in CSS pixels (default: 1080)")
parser.add_argument("--full-page", action="store_true", default=True, help="Capture full scrollable page (default: true)")
parser.add_argument("--no-full-page", action="store_true", help="Capture only the viewport")
parser.add_argument("--wait", type=int, default=1000, help="Wait time in ms after page load (default: 1000)")
parser.add_argument("--format", choices=["png", "jpeg"], default="png", help="Output format (default: png)")
parser.add_argument("--quality", type=int, default=92, help="JPEG quality 0-100 (only for jpeg format)")
args = parser.parse_args()
full_page = not args.no_full_page
# Determine input URL
html_path = args.html
if not html_path.startswith(("http://", "https://", "file://")):
abs_path = os.path.abspath(html_path)
if not os.path.exists(abs_path):
print(f"Error: File not found: {abs_path}", file=sys.stderr)
sys.exit(1)
html_path = f"file://{abs_path}"
# Determine output path
if args.output:
output_path = args.output
else:
base = Path(args.html).stem if not args.html.startswith("http") else "screenshot"
output_path = f"{base}.png"
try:
from playwright.sync_api import sync_playwright
except ImportError:
print("Error: playwright not installed. Run: pip install playwright && playwright install chromium", file=sys.stderr)
sys.exit(1)
print(f"Rendering: {html_path}")
print(f"Viewport: {args.width}x{args.height} @ {args.scale}x scale")
print(f"Output resolution: {args.width * args.scale}x{args.height * args.scale}+ pixels")
print(f"Full page: {full_page}")
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page(
viewport={"width": args.width, "height": args.height},
device_scale_factor=args.scale,
)
page.goto(html_path, wait_until="networkidle")
if args.wait > 0:
page.wait_for_timeout(args.wait)
screenshot_args = {"path": output_path, "full_page": full_page}
if args.format == "jpeg":
screenshot_args["type"] = "jpeg"
screenshot_args["quality"] = args.quality
page.screenshot(**screenshot_args)
browser.close()
file_size = os.path.getsize(output_path)
size_str = f"{file_size / 1024:.0f}KB" if file_size < 1024 * 1024 else f"{file_size / (1024 * 1024):.1f}MB"
# Get actual image dimensions
try:
from PIL import Image
img = Image.open(output_path)
actual_w, actual_h = img.size
print(f"✅ Saved: {output_path} ({actual_w}x{actual_h}px, {size_str})")
except ImportError:
print(f"✅ Saved: {output_path} ({size_str})")
if __name__ == "__main__":
main()
Behavioral guidelines to reduce common LLM coding pitfalls, derived from Andrej Karpathy's observations. Apply these four principles when writing, editing, o...
---
name: karpathy-coding-guidelines
description: Behavioral guidelines to reduce common LLM coding pitfalls, derived from Andrej Karpathy's observations. Apply these four principles when writing, editing, or reviewing code — especially for non-trivial changes. Triggers on coding tasks, code reviews, refactoring, bug fixes, feature implementation, or when the user asks for careful/disciplined coding behavior.
---
# Karpathy Coding Guidelines
Four principles to reduce common LLM coding mistakes. Bias toward caution over speed; for trivial tasks, use judgment.
## 1. Think Before Coding
**Don't assume. Don't hide confusion. Surface tradeoffs.**
Before implementing:
- State assumptions explicitly. If uncertain, ask.
- If multiple interpretations exist, present them — don't pick silently.
- If a simpler approach exists, say so. Push back when warranted.
- If something is unclear, stop. Name what's confusing. Ask.
## 2. Simplicity First
**Minimum code that solves the problem. Nothing speculative.**
- No features beyond what was asked.
- No abstractions for single-use code.
- No "flexibility" or "configurability" that wasn't requested.
- No error handling for impossible scenarios.
- If 200 lines could be 50, rewrite it.
Test: Would a senior engineer say this is overcomplicated? If yes, simplify.
## 3. Surgical Changes
**Touch only what you must. Clean up only your own mess.**
When editing existing code:
- Don't "improve" adjacent code, comments, or formatting.
- Don't refactor things that aren't broken.
- Match existing style, even if you'd do it differently.
- If you notice unrelated dead code, mention it — don't delete it.
When your changes create orphans:
- Remove imports/variables/functions that YOUR changes made unused.
- Don't remove pre-existing dead code unless asked.
Test: Every changed line should trace directly to the user's request.
## 4. Goal-Driven Execution
**Define success criteria. Loop until verified.**
Transform tasks into verifiable goals:
- "Add validation" → "Write tests for invalid inputs, then make them pass"
- "Fix the bug" → "Write a test that reproduces it, then make it pass"
- "Refactor X" → "Ensure tests pass before and after"
For multi-step tasks, state a brief plan:
```
1. [Step] → verify: [check]
2. [Step] → verify: [check]
3. [Step] → verify: [check]
```
Strong success criteria enable independent looping. Weak criteria ("make it work") require constant clarification.
---
**Working indicators:** Fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, clarifying questions come before implementation rather than after mistakes.
Generate presentations by extracting visual style from a reference template and recreating slides from scratch using PptxGenJS. Use when: user provides a PPT...
---
name: ppt-from-template
description: >-
Generate presentations by extracting visual style from a reference template and
recreating slides from scratch using PptxGenJS. Use when: user provides a PPT/PDF
as style reference and wants new slides in that style; user says "按这个风格做PPT",
"模仿这个模板", "参考这个PDF做演示", "make slides like this", "use this template",
"做个PPT", "用这个模板做课件", "帮我做演示文稿"; user uploads a .pptx file as template.
Supports: template directory auto-discovery, user upload, multi-template selection,
image/video placeholders, large decks (30-100+ pages via batched generation).
Requires: pptx skill (PptxGenJS), python-pptx, pdftoppm (poppler-utils).
NOT for: editing existing PPT (use pptx skill), creating slides without style reference
(use pptx skill directly).
---
# PPT from Template
Generate presentations by extracting style from a reference template, then building fresh slides with PptxGenJS.
**Core principle:** Never modify existing PPT XML. Look at the style, then draw from scratch.
## Template Management
### Template Directory
Default template directory: `{workspaceDir}/template/`
On every invocation:
1. Scan `{workspaceDir}/template/` for `*.pptx` files.
2. If **one** template found → use it automatically.
3. If **multiple** templates found → list them and ask user to choose.
4. If **none** found → tell user to upload a `.pptx` file or provide a path.
### User Upload
Accept `.pptx` files uploaded by user. Before processing:
- **Size check**: reject files > 50 MB with message: "模板文件超过 50MB 限制,请压缩后重试"
- Save to `{workspaceDir}/template/` for reuse.
## Workflow
### Phase 1: Extract Style
```bash
# If .pptx, convert to PDF for visual analysis:
python3 {baseDir}/scripts/pptx_to_pdf.py template.pptx /tmp/ppt_style/template.pdf
# Extract page images:
bash {baseDir}/scripts/extract_pages.sh /tmp/ppt_style/template.pdf /tmp/ppt_style/ 150
# Precision extraction from PPTX XML:
python3 {baseDir}/scripts/extract_style.py template.pptx -o /tmp/ppt_style/style_raw.yaml
```
Read `style_raw.yaml` for exact data (hex colors, font names, font sizes in pt, positions in inches, fill types, line styles). Then read 3-5 page images for semantic understanding (element roles, layout classification).
Extract two levels:
| Level | Source | What to Capture |
|-------|--------|----------------|
| Global | style_raw.yaml | Colors (hex), typography (font/size/weight), decorations |
| Per-layout | style_raw.yaml + images | Element inventory: type, role, x/y/w/h, style properties |
Element types: `text`, `image`, `video`, `shape`, `line`, `numbered_list`, `step_list`, `tag_group`, `chart`, `table`.
For images/videos: set `placeholder: true` with `description` for user replacement.
Write results to `style.yaml`. Schema: [references/style-schema.md](references/style-schema.md).
### Phase 2: Generate PPT
Use the `pptx` skill (PptxGenJS) to create slides:
1. Read `style.yaml` for visual parameters.
2. Combine with user content (topic, page count, outline).
3. Generate PptxGenJS JavaScript applying extracted style.
4. Run JS to produce `.pptx`.
5. QA: convert to images, verify style fidelity.
### Large Deck Strategy (>15 pages)
PptxGenJS generation is fast (seconds), but **writing the JS code** for many slides can hit context/time limits. Mitigate:
| Pages | Strategy |
|-------|----------|
| ≤15 | Single generation pass |
| 16-30 | Split into 2 JS files: slides 1-15, slides 16-30. Generate sequentially, merge via `pptx-merge` or generate in one `pres` object across two code blocks |
| 31-100+ | Generate a **slide factory function** per layout type, then loop over a content array. One JS file, data-driven |
**Slide factory pattern** for large decks:
```javascript
// Define layout factories from style.yaml
function makeCover(pres, content) { /* ... */ }
function makeContent(pres, content) { /* ... */ }
function makeSection(pres, content) { /* ... */ }
// Content array — easy to extend
const slides = [
{ layout: "cover", title: "...", subtitle: "..." },
{ layout: "content", title: "...", items: [...] },
// ... 50+ entries
];
slides.forEach(s => layoutFactories[s.layout](pres, s));
```
This keeps code size O(layouts) not O(pages).
### Output Size Control
Target output < 20 MB. PptxGenJS output is typically small (100-500 KB for text-only decks). Large files come from embedded images. If output approaches 20 MB:
1. Reduce image resolution/quality in PptxGenJS options.
2. Use placeholders instead of embedded images.
3. Split into multiple files if unavoidable.
4. Warn user: "PPT 接近 20MB 限制,已使用图片占位符,请手动替换"
### Placeholder Convention
- **Image**: Dark rectangle + dashed gray border + 🖼️ + label + suggestion
- **Video**: Dark rectangle + dashed red border + ▶️ + label + suggestion
### Key Rules
- Never modify existing PPT XML — always generate from scratch.
- `style.yaml` is reusable — once extracted, generate unlimited PPTs in the same style.
- Use `shrinkText: true` in PptxGenJS to auto-fit long text.
- Match slide dimensions exactly from `style_raw.yaml` (not hardcoded 16:9).
## File Convention
```
{workspaceDir}/
├── template/ ← .pptx templates (auto-discovered)
│ └── *.pptx ← max 50 MB each
├── output/ ← generated PPTs
│ └── *.pptx ← max 20 MB each
/tmp/ppt_style/ ← working directory (ephemeral)
├── template.pdf
├── page-*.jpg
├── style_raw.yaml ← from extract_style.py
└── style.yaml ← merged (exact + semantic)
```
## Troubleshooting
| Problem | Solution |
|---------|----------|
| Template > 50 MB | Ask user to compress or remove embedded media |
| Output > 20 MB | Use placeholders, reduce image count/resolution |
| >30 pages timeout | Use slide factory pattern, data-driven generation |
| No template found | Prompt user to upload .pptx or specify path |
| Multiple templates | List options, ask user to choose |
| Fonts not available | Fall back to Arial/sans-serif; note in output |
| Complex gradients | Describe in style.yaml; use background image |
FILE:references/style-schema.md
# Style Schema
The `style.yaml` file captures the complete visual identity extracted from a reference PDF, including per-layout element definitions.
## Example
```yaml
source: 培训课模板.pdf
pages_analyzed: 6
slide_size: "16:9" # 10" x 5.625"
# ── Global Colors ──
colors:
primary: "2040E0"
secondary: "E8A020"
accent: "E02020"
background: "0A0A0A"
text_primary: "FFFFFF"
text_body: "E8A020"
text_muted: "D4A574"
# ── Global Typography ──
typography:
title: { size: 44, weight: bold, font: "Arial Black" }
subtitle: { size: 28, weight: bold, font: "Arial" }
body: { size: 22, weight: bold, font: "Arial" }
caption: { size: 14, weight: normal, font: "Arial" }
# ── Layouts with Element Definitions ──
layouts:
- type: cover
source_page: 1
background:
type: solid # solid | image | gradient
color: "0A0A0A"
elements:
- id: title
type: text
role: title # title | subtitle | body | label | tagline
x: 0.6 # inches from left
y: 0.4
w: 5.0
h: 1.2
style:
fontSize: 48
fontFace: "Arial Black"
color: "FFFFFF"
bold: true
align: left
- id: keyword_tags
type: tag_group # special: row of filled rectangles with text
x: 6.2
y: 0.55
items_per_row: 3
tag_w: 1.15
tag_h: 0.55
gap: 0.15
style:
fill: "2040E0"
fontSize: 16
color: "FFFFFF"
bold: true
- id: divider
type: line
x: 0.6
y: 2.0
w: 8.8
style:
color: "E8A020"
width: 1.5
- id: hero_title
type: text
role: title
x: 0.6
y: 2.3
w: 8.8
h: 2.5
style:
fontSize: 56
fontFace: "Arial Black"
color: "E8A020"
bold: true
- id: tagline
type: text
role: tagline
x: 0.6
y: 4.8
w: 8.8
h: 0.5
style:
fontSize: 18
color: "D4A574"
- type: content
source_page: 5
background:
type: solid
color: "0A0A0A"
elements:
- id: title_block
type: shape
shape: rectangle
x: 4.5
y: 0.3
w: 3.5
h: 0.8
style:
fill: "E8A020"
- id: title_text
type: text
role: title
x: 4.5
y: 0.3
w: 3.5
h: 0.8
style:
fontSize: 32
fontFace: "Arial Black"
color: "0A0A0A"
bold: true
align: center
valign: middle
- id: left_image
type: image
role: illustration # illustration | photo | mockup | icon | decorative
x: 0.3
y: 1.5
w: 3.8
h: 3.5
placeholder: true # AI should generate or user provides
description: "Phone mockup or product screenshot"
- id: numbered_list
type: numbered_list
x: 4.5
y: 1.5
w: 5.1
item_h: 0.7
max_items: 5
style:
number_color: "E02020"
number_size: 22
text_color: "E8A020"
text_size: 20
bold: true
- type: section
source_page: 7
background:
type: image
description: "Full-bleed photo with dark overlay"
overlay_color: "000000"
overlay_opacity: 40
elements:
- id: main_text_1
type: text
role: title
x: 0.5
y: 1.2
w: 9.0
h: 1.5
style:
fontSize: 52
fontFace: "Arial Black"
color: "E8A020"
bold: true
align: center
- id: main_text_2
type: text
role: subtitle
x: 0.5
y: 3.0
w: 9.0
h: 1.5
style:
fontSize: 52
fontFace: "Arial Black"
color: "E8A020"
bold: true
align: center
- type: detail
source_page: 10
background:
type: solid
color: "0A0A0A"
elements:
- id: left_panel
type: image
role: mockup
x: 0.0
y: 0.0
w: 2.5
h: 5.625
placeholder: true
description: "Phone or device mockup, full height"
- id: title
type: text
role: title
x: 3.0
y: 0.4
w: 6.5
h: 0.8
style:
fontSize: 36
fontFace: "Arial Black"
color: "FFFFFF"
bold: true
- id: step_list
type: step_list
x: 3.0
y: 1.6
w: 6.5
item_h: 1.2
max_items: 3
style:
number_color: "E02020"
number_size: 28
title_color: "FFFFFF"
title_size: 24
subtitle_color: "D4A574"
subtitle_size: 16
- type: closing
source_page: 37
background:
type: solid
color: "0A0A0A"
elements:
- id: quote
type: text
role: title
x: 0.6
y: 0.6
w: 8.8
h: 2.0
style:
fontSize: 40
fontFace: "Arial Black"
color: "FFFFFF"
bold: true
- id: divider
type: line
x: 0.6
y: 3.0
w: 8.8
style:
color: "E8A020"
width: 1.5
- id: left_text
type: text
role: body
x: 0.6
y: 3.4
w: 4.5
h: 1.0
style:
fontSize: 36
color: "D4A574"
bold: true
- id: vertical_divider
type: line
x: 5.8
y: 3.4
w: 0
h: 1.0
direction: vertical
style:
color: "E8A020"
width: 2
- id: cta
type: text
role: body
x: 6.1
y: 3.4
w: 3.5
h: 1.0
style:
fontSize: 28
color: "E02020"
bold: true
- id: footer
type: text
role: caption
x: 0.6
y: 4.8
w: 8.8
h: 0.4
style:
fontSize: 14
color: "D4A574"
align: center
# ── Decorations ──
decorations:
card_style: none
card_shadow: false
accent_bar: none
icon_style: none
divider: thin_golden_line
keyword_tags: filled_blue_rectangles
text_style: ultra_bold_impact
```
## Element Types
| Type | Description | Key Properties |
|------|-------------|----------------|
| `text` | Text box | role, fontSize, color, bold, align, valign |
| `image` | Image placeholder | role (illustration/photo/mockup/icon/decorative), placeholder, description |
| `video` | Video placeholder | role, description, thumbnail_description |
| `shape` | Shape (rectangle, oval, line) | shape, fill, line, transparency |
| `line` | Divider line | color, width, direction (horizontal/vertical) |
| `numbered_list` | Numbered items | number_color, text_color, item_h, max_items |
| `step_list` | Steps with title + subtitle | number/title/subtitle styles, item_h |
| `tag_group` | Row of filled tag rectangles | items_per_row, tag_w, tag_h, gap, fill |
| `icon_group` | Row/grid of icons with labels | cols, icon_size, icon_style |
| `chart` | Chart placeholder | chart_type (bar/line/pie), description |
| `table` | Table placeholder | rows, cols, header_style, cell_style |
## Element Roles
| Role | Typical Usage |
|------|--------------|
| `title` | Main heading (36-56pt, bold) |
| `subtitle` | Secondary heading (24-32pt) |
| `body` | Body text (16-22pt) |
| `label` | Small label/tag (12-16pt) |
| `tagline` | Slogan or motto (14-18pt) |
| `caption` | Footer, source citation (10-14pt) |
| `illustration` | Conceptual or decorative image |
| `photo` | Realistic photo |
| `mockup` | Device/product mockup |
| `icon` | Small icon in circle/square |
| `decorative` | Background decoration, no content |
## Background Types
| Type | Properties |
|------|-----------|
| `solid` | `color` (hex) |
| `image` | `description`, `overlay_color`, `overlay_opacity` |
| `gradient` | `color_start`, `color_end`, `direction` (horizontal/vertical/diagonal) |
## Position & Size
All values in **inches** relative to slide origin (top-left = 0,0).
- 16:9 slide: 10.0" wide × 5.625" tall
- x, y = top-left corner of element
- w, h = width and height
FILE:scripts/extract_pages.sh
#!/bin/bash
# extract_pages.sh — Convert PDF to page images for style analysis
# Usage: bash extract_pages.sh input.pdf output_dir/ [dpi]
set -euo pipefail
PDF="?Usage: extract_pages.sh input.pdf output_dir/ [dpi]"
OUTDIR="?Usage: extract_pages.sh input.pdf output_dir/ [dpi]"
DPI="-150"
if ! command -v pdftoppm &>/dev/null; then
echo "❌ pdftoppm not found. Install: apt install poppler-utils" >&2
exit 1
fi
if [ ! -f "$PDF" ]; then
echo "❌ File not found: $PDF" >&2
exit 1
fi
mkdir -p "$OUTDIR"
echo "📄 Converting: $PDF → $OUTDIR/ (DPI DPI)"
pdftoppm -jpeg -r "$DPI" "$PDF" "OUTDIR/page"
COUNT=$(ls -1 "OUTDIR"/page-*.jpg 2>/dev/null | wc -l)
echo "✅ Done. $COUNT page(s) extracted."
ls -lh "OUTDIR"/page-*.jpg 2>/dev/null | head -5
if [ "$COUNT" -gt 5 ]; then
echo " ... and $((COUNT - 5)) more"
fi
FILE:scripts/extract_style.py
#!/usr/bin/env python3
"""extract_style.py — Extract precise style data from a PPTX template.
Usage:
python3 extract_style.py <template.pptx> [--output style_raw.yaml] [--pages 1,5,7,13,26,42]
Outputs a raw style YAML with exact colors, fonts, positions, sizes extracted
from python-pptx. Designed to complement AI visual analysis — the script
provides precision, the AI provides semantic understanding (roles, layout types).
"""
import sys
import os
import argparse
import json
from collections import Counter, defaultdict
try:
from pptx import Presentation
from pptx.util import Inches, Pt, Emu
from pptx.enum.shapes import MSO_SHAPE_TYPE
from pptx.dml.color import RGBColor
except ImportError:
print("ERROR: python-pptx not installed. Run: pip install python-pptx", file=sys.stderr)
sys.exit(1)
try:
import yaml
HAS_YAML = True
except ImportError:
HAS_YAML = False
# ── Helpers ──
def emu_to_inches(emu):
"""Convert EMU to inches, rounded to 2 decimal places."""
if emu is None:
return None
return round(emu / 914400, 2)
def pt_from_emu(emu):
"""Convert EMU font size to points."""
if emu is None:
return None
return round(emu / 12700, 1)
def rgb_to_hex(rgb):
"""Convert RGBColor or theme color to hex string."""
if rgb is None:
return None
if isinstance(rgb, RGBColor):
return str(rgb).upper()
return str(rgb).upper()
def extract_color_safe(color_obj):
"""Safely extract color info from a pptx color object."""
result = {}
try:
if color_obj is None:
return result
# Try RGB first
try:
if color_obj.rgb is not None:
result["rgb"] = str(color_obj.rgb).upper()
except (AttributeError, TypeError):
pass
# Theme color
try:
if color_obj.theme_color is not None:
result["theme"] = str(color_obj.theme_color)
except (AttributeError, TypeError):
pass
# Brightness
try:
if color_obj.brightness is not None and color_obj.brightness != 0:
result["brightness"] = round(color_obj.brightness, 2)
except (AttributeError, TypeError):
pass
except Exception:
pass
return result
def classify_shape_type(shape):
"""Classify shape into our element types."""
st = shape.shape_type
if st == MSO_SHAPE_TYPE.PICTURE:
return "image"
elif st == MSO_SHAPE_TYPE.MEDIA:
return "video"
elif st == MSO_SHAPE_TYPE.TABLE:
return "table"
elif st == MSO_SHAPE_TYPE.CHART:
return "chart"
elif st == MSO_SHAPE_TYPE.GROUP:
return "group"
elif st == MSO_SHAPE_TYPE.TEXT_BOX:
return "text"
elif st == MSO_SHAPE_TYPE.AUTO_SHAPE:
# Check if it has text
if shape.has_text_frame and shape.text_frame.text.strip():
return "text_shape" # shape with text (like tag rectangles)
return "shape"
elif st == MSO_SHAPE_TYPE.FREEFORM:
return "freeform"
elif st == MSO_SHAPE_TYPE.PLACEHOLDER:
ph = shape.placeholder_format
if ph:
idx = ph.idx
if idx == 0:
return "text" # title placeholder
elif idx == 1:
return "text" # body placeholder
elif idx in (10, 11, 12, 13, 14, 15, 16, 17, 18):
return "image" # picture placeholder
return "placeholder"
else:
return "other"
def extract_text_style(paragraph):
"""Extract text formatting from a paragraph."""
style = {}
# Paragraph-level
if paragraph.alignment is not None:
style["align"] = str(paragraph.alignment).split("(")[0].lower().replace("pp_align.", "")
# Get dominant run style
for run in paragraph.runs:
font = run.font
if font.size is not None:
style["fontSize"] = pt_from_emu(font.size)
if font.name is not None:
style["fontFace"] = font.name
if font.bold is not None:
style["bold"] = font.bold
if font.italic is not None:
style["italic"] = font.italic
if font.underline is not None:
style["underline"] = bool(font.underline)
color_info = extract_color_safe(font.color)
if color_info:
style["color"] = color_info
break # Use first run as representative
return style
def extract_fill(shape):
"""Extract fill properties from a shape."""
fill_info = {}
try:
fill = shape.fill
if fill is None:
return fill_info
fill_type = fill.type
if fill_type is not None:
fill_info["type"] = str(fill_type).replace("MSO_FILL_TYPE.", "").lower()
try:
if fill.fore_color and fill.fore_color.rgb:
fill_info["color"] = str(fill.fore_color.rgb).upper()
except (AttributeError, TypeError):
pass
try:
if fill.fore_color and fill.fore_color.theme_color:
fill_info["theme"] = str(fill.fore_color.theme_color)
except (AttributeError, TypeError):
pass
except Exception:
pass
return fill_info
def extract_line_style(shape):
"""Extract line/border properties."""
line_info = {}
try:
ln = shape.line
if ln is None:
return line_info
if ln.width is not None:
line_info["width"] = round(ln.width / 12700, 1) # EMU to pt
color_info = extract_color_safe(ln.color)
if color_info:
line_info["color"] = color_info
if ln.dash_style is not None:
line_info["dash"] = str(ln.dash_style)
except Exception:
pass
return line_info
def extract_background(slide):
"""Extract slide background properties."""
bg_info = {}
try:
bg = slide.background
fill = bg.fill
if fill.type is not None:
bg_info["type"] = str(fill.type).replace("MSO_FILL_TYPE.", "").lower()
try:
if fill.fore_color and fill.fore_color.rgb:
bg_info["color"] = str(fill.fore_color.rgb).upper()
except (AttributeError, TypeError):
pass
try:
if fill.fore_color and fill.fore_color.theme_color:
bg_info["theme"] = str(fill.fore_color.theme_color)
except (AttributeError, TypeError):
pass
except Exception:
pass
return bg_info
# ── Main extraction ──
def extract_slide(slide, slide_num):
"""Extract all style data from a single slide."""
slide_data = {
"page": slide_num,
"background": extract_background(slide),
"elements": [],
}
for shape in slide.shapes:
elem = {
"name": shape.name,
"type": classify_shape_type(shape),
"x": emu_to_inches(shape.left),
"y": emu_to_inches(shape.top),
"w": emu_to_inches(shape.width),
"h": emu_to_inches(shape.height),
}
# Rotation
if shape.rotation != 0:
elem["rotation"] = shape.rotation
# Fill (for shapes)
fill_info = extract_fill(shape)
if fill_info:
elem["fill"] = fill_info
# Line/border
line_info = extract_line_style(shape)
if line_info:
elem["line"] = line_info
# Text content & styling
if shape.has_text_frame:
tf = shape.text_frame
text = tf.text.strip()
if text:
elem["text_preview"] = text[:80] + ("..." if len(text) > 80 else "")
elem["paragraph_count"] = len(tf.paragraphs)
# Extract style from each paragraph
para_styles = []
for para in tf.paragraphs:
ps = extract_text_style(para)
if ps:
ps["text_preview"] = para.text.strip()[:40]
para_styles.append(ps)
if para_styles:
elem["text_styles"] = para_styles
# Word wrap
if tf.word_wrap is not None:
elem["word_wrap"] = tf.word_wrap
# Image info
if elem["type"] == "image":
try:
if hasattr(shape, "image"):
img = shape.image
elem["image_format"] = img.content_type
elem["image_size_bytes"] = len(img.blob)
except Exception:
pass
# Table info
if elem["type"] == "table":
try:
tbl = shape.table
elem["rows"] = len(tbl.rows)
elem["cols"] = len(tbl.columns)
# Sample first cell style
if len(tbl.rows) > 0 and len(tbl.columns) > 0:
cell = tbl.cell(0, 0)
if cell.text.strip():
elem["sample_cell"] = cell.text.strip()[:30]
except Exception:
pass
slide_data["elements"].append(elem)
return slide_data
def compute_global_stats(slides_data):
"""Aggregate statistics across all analyzed slides."""
all_colors = Counter()
all_fonts = Counter()
all_sizes = Counter()
all_bg_colors = Counter()
element_types = Counter()
for sd in slides_data:
# Background
bg = sd.get("background", {})
if "color" in bg:
all_bg_colors[bg["color"]] += 1
for elem in sd["elements"]:
element_types[elem["type"]] += 1
# Fill colors
fill = elem.get("fill", {})
if "color" in fill:
all_colors[fill["color"]] += 1
# Text colors, fonts, sizes
for ps in elem.get("text_styles", []):
color = ps.get("color", {})
if "rgb" in color:
all_colors[color["rgb"]] += 1
if "fontFace" in ps:
all_fonts[ps["fontFace"]] += 1
if "fontSize" in ps:
all_sizes[ps["fontSize"]] += 1
return {
"top_colors": dict(all_colors.most_common(15)),
"top_fonts": dict(all_fonts.most_common(10)),
"top_font_sizes": dict(all_sizes.most_common(15)),
"background_colors": dict(all_bg_colors.most_common(5)),
"element_type_counts": dict(element_types.most_common()),
}
def main():
parser = argparse.ArgumentParser(description="Extract style data from PPTX template")
parser.add_argument("template", help="Path to PPTX template file")
parser.add_argument("--output", "-o", default=None, help="Output file (default: stdout)")
parser.add_argument("--pages", "-p", default=None, help="Comma-separated page numbers to analyze (default: all)")
parser.add_argument("--json", action="store_true", help="Output JSON instead of YAML")
args = parser.parse_args()
if not os.path.exists(args.template):
print(f"ERROR: File not found: {args.template}", file=sys.stderr)
sys.exit(1)
prs = Presentation(args.template)
# Slide size
slide_w = emu_to_inches(prs.slide_width)
slide_h = emu_to_inches(prs.slide_height)
total_slides = len(prs.slides)
# Determine which pages to analyze
if args.pages:
pages = [int(p.strip()) for p in args.pages.split(",")]
else:
pages = list(range(1, total_slides + 1))
print(f"📐 Slide size: {slide_w}\" × {slide_h}\" ({total_slides} slides)", file=sys.stderr)
print(f"📊 Analyzing pages: {pages}", file=sys.stderr)
# Extract each slide
slides_data = []
for i, slide in enumerate(prs.slides, 1):
if i in pages:
sd = extract_slide(slide, i)
slides_data.append(sd)
elem_count = len(sd["elements"])
text_count = sum(1 for e in sd["elements"] if e["type"] in ("text", "text_shape"))
img_count = sum(1 for e in sd["elements"] if e["type"] == "image")
print(f" Page {i}: {elem_count} elements ({text_count} text, {img_count} images)", file=sys.stderr)
# Global stats
stats = compute_global_stats(slides_data)
# Build output
output = {
"source": os.path.basename(args.template),
"slide_size": f"{slide_w} x {slide_h} inches",
"total_slides": total_slides,
"pages_analyzed": len(slides_data),
"global_stats": stats,
"slides": slides_data,
}
# Serialize
if args.json or not HAS_YAML:
text = json.dumps(output, ensure_ascii=False, indent=2, default=str)
else:
text = yaml.dump(output, allow_unicode=True, default_flow_style=False, sort_keys=False, width=120)
if args.output:
with open(args.output, "w", encoding="utf-8") as f:
f.write(text)
print(f"\n✅ Output saved to {args.output}", file=sys.stderr)
else:
print(text)
if __name__ == "__main__":
main()
FILE:scripts/pptx_to_pdf.py
#!/usr/bin/env python3
"""pptx_to_pdf.py — Convert .pptx to .pdf via LibreOffice headless.
Usage: python3 pptx_to_pdf.py input.pptx [output.pdf]
"""
import subprocess, sys, os, shutil, tempfile
def convert(pptx_path, pdf_path=None):
if not os.path.isfile(pptx_path):
print(f"❌ File not found: {pptx_path}", file=sys.stderr)
sys.exit(1)
# Find soffice
soffice = shutil.which("soffice")
if not soffice:
# Try common sandbox/skill paths
for p in [
os.path.expanduser("~/.openclaw/skills/pptx/scripts/office/soffice.py"),
]:
if os.path.isfile(p):
soffice = p
break
if not soffice:
print("❌ LibreOffice (soffice) not found.", file=sys.stderr)
sys.exit(1)
# Convert to temp dir first to handle soffice naming
with tempfile.TemporaryDirectory() as tmpdir:
cmd = [soffice, "--headless", "--convert-to", "pdf", "--outdir", tmpdir, pptx_path]
if soffice.endswith(".py"):
cmd = [sys.executable] + cmd
print(f"🔄 Converting: {os.path.basename(pptx_path)} → PDF")
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
if result.returncode != 0:
print(f"❌ Conversion failed:\n{result.stderr}", file=sys.stderr)
sys.exit(1)
# Find output PDF
base = os.path.splitext(os.path.basename(pptx_path))[0]
tmp_pdf = os.path.join(tmpdir, base + ".pdf")
if not os.path.isfile(tmp_pdf):
# Try to find any PDF
pdfs = [f for f in os.listdir(tmpdir) if f.endswith(".pdf")]
if pdfs:
tmp_pdf = os.path.join(tmpdir, pdfs[0])
else:
print("❌ No PDF output found.", file=sys.stderr)
sys.exit(1)
# Move to final location
if pdf_path is None:
pdf_path = os.path.splitext(pptx_path)[0] + ".pdf"
os.makedirs(os.path.dirname(os.path.abspath(pdf_path)), exist_ok=True)
shutil.move(tmp_pdf, pdf_path)
size_kb = os.path.getsize(pdf_path) / 1024
print(f"✅ Output: {pdf_path} ({size_kb:.0f} KB)")
return pdf_path
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python3 pptx_to_pdf.py input.pptx [output.pdf]")
sys.exit(1)
pptx_path = sys.argv[1]
pdf_path = sys.argv[2] if len(sys.argv) > 2 else None
convert(pptx_path, pdf_path)
Generate the highest quality photorealistic images using Stability AI's Stable Image Ultra and Stable Diffusion 3.5 Large models via AWS Bedrock. The most po...
---
name: stable-image-ultra
description: >-
Generate the highest quality photorealistic images using Stability AI's Stable Image Ultra
and Stable Diffusion 3.5 Large models via AWS Bedrock. The most powerful text-to-image models
on Bedrock. Supports multiple AWS auth methods: environment variables, credentials file,
named profiles, IAM instance roles, SSO, or direct access keys.
Use when the user asks for ultra-high-quality, photorealistic, or premium image generation.
Triggers: "ultra quality image", "photorealistic image", "best quality image", "stable image ultra",
"SD3.5", "stability ai", "ultra画质", "超高清图片", "照片级图片", "最高画质",
"generate image", "生成图片", "画一张", "做配图", "生图".
This is the DEFAULT image generation skill for ALL agents. Always use this unless the user
explicitly asks for text-heavy diagrams (use HTML Canvas instead).
Requires AWS Bedrock access with Stability AI models enabled in us-west-2.
NOT for: editing existing images, generating video, or HTML/CSS canvas rendering.
---
# Stable Image Ultra — 团队默认生图技能
Generate the highest quality images on AWS Bedrock via Stability AI models.
## Models
| Model | ID | Strength | Price |
|-------|-----|---------|-------|
| **Stable Image Ultra 1.1** | `stability.stable-image-ultra-v1:1` | Photorealism, luxury, fine detail, skin texture | ~$0.08/img |
| **SD 3.5 Large** | `stability.sd3-5-large-v1:0` | Creative diversity, prompt adherence, typography | ~$0.06/img |
Default: **Stable Image Ultra**(最高画质)。
## ⚡ Quality-First Policy(铁律)
**所有生图任务默认最高画质,不限成本。**
1. **模型**: 永远默认 Stable Image Ultra 1.1,除非用户明确要求 SD 3.5
2. **格式**: 永远 PNG(无损)
3. **Prompt**: 永远用英文,永远详细描述(见下方 Prompt 工程指南)
4. **Negative Prompt**: 每次都必须带,排除低质量因素
5. **不用 nova-canvas**: nova-canvas 已从默认选项中移除
## AWS Auth Methods
| Method | How to Use |
|--------|------------|
| **Bearer token** | `AWS_BEARER_TOKEN_BEDROCK` env var or `--bearer-token` |
| **Environment variables** | Set `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY` |
| **Credentials file** | Configure `~/.aws/credentials` |
| **Named profile** | `--profile my-profile` or `AWS_PROFILE` env var |
| **Direct keys** | `--access-key AKIA... --secret-key ...` |
| **Temporary credentials** | Add `--session-token` with direct keys |
| **IAM instance role** | Auto-detected on EC2/ECS/Lambda |
| **AWS SSO** | Run `aws sso login` first |
Auto-detection order: direct keys → profile → bearer token → env vars → credentials file → instance role → SSO.
## Quick Start
```bash
# 最高画质(默认)
python3 {baseDir}/scripts/generate.py "detailed English prompt" -o output.png --negative "blurry, low quality, artifacts"
# 指定比例
python3 {baseDir}/scripts/generate.py "prompt" -o output.png --aspect-ratio 16:9 --negative "blurry, low quality"
# SD 3.5 Large(创意多样性)
python3 {baseDir}/scripts/generate.py "prompt" -o output.png --model sd35 --negative "blurry, low quality"
```
## Parameters
| Flag | Default | Description |
|------|---------|-------------|
| `prompt` | — | Text description of the image (max 10,000 chars) |
| `-o, --output` | output.png | Output file path |
| `-m, --model` | `ultra` | Model: `ultra` or `sd35` |
| `-n, --count` | 1 | Number of images (1-5) |
| `--negative` | — | Negative prompt (what to avoid) — **必填** |
| `--aspect-ratio` | 1:1 | Aspect ratio: 1:1, 16:9, 21:9, 2:3, 3:2, 4:5, 5:4, 9:16, 9:21 |
| `--seed` | random | Seed for reproducibility |
| `--region` | us-west-2 | AWS region (Stability AI models require us-west-2) |
## 🎯 Prompt 工程指南(核心——决定出图质量)
### 铁律:每个 prompt 必须包含 5 个要素
1. **主体描述** — 是什么(人物/场景/物体),越具体越好
2. **风格/媒介** — 照片/插画/油画/3D渲染,指定相机/镜头更好
3. **光线** — studio lighting / natural light / golden hour / dramatic lighting
4. **细节强调** — ultra detailed, 8K, sharp focus, fine texture, magazine quality
5. **构图** — close-up / full body / aerial view / centered composition
### 万能 Negative Prompt(每次都带)
```
blurry, low quality, watermark, text, logo, artifacts, noise, grain, pixelated, distorted, oversaturated, cartoon, anime, illustration, ugly, deformed
```
### 验证过的高质量 Prompt 模板
#### 人像/头像
```
Ultra-sharp professional corporate headshot portrait photograph of a [年龄 性别 种族] [职业],
wearing [服装细节:面料、颜色、配饰],
[表情:warm confident smile / serious determined look],
photographed with 85mm f/1.4 portrait lens creating beautiful bokeh,
three-point studio lighting setup with key light at 45 degrees,
clean [背景颜色] studio backdrop,
shot from [构图:chest up / full body / three-quarter],
ultra high resolution 8K quality, skin detail like a magazine cover,
Hasselblad medium format camera quality
```
负面:`cartoon, anime, illustration, painting, blurry, soft focus, distorted face, extra fingers, low quality, watermark, text, noise, grain, oversaturated, plastic skin, uncanny valley, artificial looking`
#### 风景/场景
```
Breathtaking [视角:aerial view / panoramic / eye-level] of [具体场景],
[季节/时间:autumn forest, morning fog, golden hour sunlight],
[天气/氛围:dramatic clouds, mist in valleys, sun rays breaking through],
ultra detailed landscape photography, shot with [镜头:wide angle 14mm / telephoto 200mm],
National Geographic quality, vibrant natural colors, sharp focus throughout,
[画面层次:foreground detail, middle ground subject, background depth]
```
负面:`blurry, low quality, watermark, text, artificial, oversaturated, flat lighting, dull colors, haze, smog`
#### 产品/静物
```
Professional product photography of [产品],
[材质细节:brushed aluminum, matte ceramic, polished wood grain],
on [表面:marble countertop / dark slate / white seamless],
[光线:soft diffused studio lighting with subtle reflections],
sharp focus on product, shallow depth of field background blur,
commercial advertising quality, 8K resolution,
shot with Phase One IQ4 150MP medium format camera
```
负面:`blurry, low quality, watermark, text, cheap looking, plastic, artificial, flat lighting, harsh shadows`
#### 概念/创意
```
[具体场景描述,用隐喻和具象化],
[艺术风格:watercolor / isometric / flat illustration / pop art / paper craft],
[配色方案:warm earth tones / pastel palette / vibrant saturated],
[细节:intricate details, fine textures, visible brushstrokes],
professional digital art, trending on ArtStation,
cinematic composition, dramatic lighting
```
负面:`blurry, low quality, watermark, text, ugly, amateur, generic, stock photo, clipart`
### Prompt 写法禁忌
- ❌ 不用中文 prompt(效果差很多)
- ❌ 不写模糊的描述(如 "a nice picture")
- ❌ 不省略光线和细节描述
- ❌ 不忘记 negative prompt
- ❌ 不指望 AI 能写清晰文字(需要文字用 HTML Canvas)
### Aspect Ratio 选择指南
| 场景 | 推荐比例 |
|------|---------|
| 头像/头图 | 1:1 |
| 博客配图/Banner | 16:9 |
| 电影级场景 | 21:9 |
| 手机壁纸/海报 | 9:16 |
| 证件照/肖像 | 2:3 或 4:5 |
| 风景/横幅 | 3:2 或 16:9 |
## ⚡ 执行方式(铁律:默认 subagent)
**生图过程耗时较长(15-60s),必须用 subagent 异步执行,避免阻塞主对话。**
```javascript
sessions_spawn({
task: `使用 stable-image-ultra 生图:
## 要求
- Prompt: "<英文 prompt>"
- Negative: "<negative prompt>"
- Output: <输出路径>
- Aspect Ratio: <比例>
## 完成后
生成完毕后,报告文件路径、分辨率和文件大小。`,
label: "生图-<简述>",
runTimeoutSeconds: 180
})
// 派发后必须 yield 等待结果
sessions_yield({ message: "等待生图完成" })
```
subagent 返回后,由调用方负责将图片发送给用户(根据当前 channel 选择合适方式)。
### 例外(可以内联执行)
- 调用方本身就是 subagent(不需要再嵌套)
- 用户明确要求「直接生成」且愿意等待
## Workflow
1. 理解用户需求 → 确定主题、风格、用途
2. 用英文撰写详细 prompt(参考上方模板)
3. 选择合适的 aspect_ratio
4. 必须带 negative prompt
5. spawn subagent 执行 `generate.py`(runTimeoutSeconds=180)
6. subagent 完成后,调用方发送结果给用户(根据当前 channel 选择合适的发送方式)
## Important Notes
- **Region**: 两个模型都只在 `us-west-2` (Oregon) 可用
- **Pricing**: Ultra ~$0.08/image, SD3.5 Large ~$0.06/image — **不限成本**
- **Resolution**: 输出固定 1024×1024(1:1)或等效像素面积(其他比例)
- **文字**: AI 生图无法生成清晰文字,需要文字的场景用 HTML Canvas + Playwright 截图
- **发送**: 根据当前 channel 选择合适的发送方式(Telegram 用 `--force-document` 避免压缩,其他渠道按需处理)
FILE:scripts/generate.py
#!/usr/bin/env python3
"""Generate images using Stability AI models (Stable Image Ultra / SD 3.5 Large) via AWS Bedrock."""
import argparse
import base64
import json
import os
import sys
import urllib.request
import urllib.error
MODELS = {
"ultra": "stability.stable-image-ultra-v1:1",
"sd35": "stability.sd3-5-large-v1:0",
}
MODEL_LABELS = {
"ultra": "Stable Image Ultra",
"sd35": "Stable Diffusion 3.5 Large",
}
# Stability AI models on Bedrock are only available in us-west-2
DEFAULT_REGION = "us-west-2"
def invoke_via_bearer_token(args, model_id, body):
"""Call Bedrock invoke-model using Bearer Token."""
token = os.environ.get("AWS_BEARER_TOKEN_BEDROCK", "")
region = args.region
encoded_model = urllib.parse.quote(model_id, safe="")
url = f"https://bedrock-runtime.{region}.amazonaws.com/model/{encoded_model}/invoke"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json",
}
req = urllib.request.Request(url, data=body.encode(), headers=headers, method="POST")
try:
with urllib.request.urlopen(req, timeout=180) as resp:
return json.loads(resp.read())
except urllib.error.HTTPError as e:
err_body = e.read().decode() if e.fp else ""
print(f"Error: Bedrock API {e.code}: {err_body}", file=sys.stderr)
sys.exit(1)
def invoke_via_boto3(args, model_id, body):
"""Call Bedrock invoke-model using boto3."""
try:
import boto3
except ImportError:
print("Error: boto3 not installed. Run: pip install boto3", file=sys.stderr)
sys.exit(1)
kwargs = {"region_name": args.region}
if args.access_key and args.secret_key:
kwargs["aws_access_key_id"] = args.access_key
kwargs["aws_secret_access_key"] = args.secret_key
if args.session_token:
kwargs["aws_session_token"] = args.session_token
print(" Auth: explicit keys")
client = boto3.client("bedrock-runtime", **kwargs)
elif args.profile:
session = boto3.Session(profile_name=args.profile, region_name=args.region)
print(f" Auth: profile ({args.profile})")
client = session.client("bedrock-runtime")
else:
print(" Auth: boto3 auto-detect")
client = boto3.client("bedrock-runtime", **kwargs)
response = client.invoke_model(modelId=model_id, body=body)
return json.loads(response["body"].read())
def detect_auth_method(args):
"""Determine which auth method to use."""
if args.access_key and args.secret_key:
return "boto3"
if args.profile:
return "boto3"
if os.environ.get("AWS_BEARER_TOKEN_BEDROCK"):
return "bearer"
try:
import boto3
session = boto3.Session(region_name=args.region)
creds = session.get_credentials()
if creds:
return "boto3"
except Exception:
pass
return None
def main():
# Need urllib.parse for URL encoding
import urllib.parse
parser = argparse.ArgumentParser(
description="Generate images with Stability AI models on AWS Bedrock (Ultra / SD 3.5 Large)",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Models:
ultra Stable Image Ultra — highest quality, photorealistic (default)
sd35 Stable Diffusion 3.5 Large — creative diversity, prompt adherence
AWS auth methods (auto-detected):
AWS_BEARER_TOKEN_BEDROCK Bearer token (OpenClaw managed)
--access-key + --secret-key Direct IAM credentials
--profile / AWS_PROFILE Named profile (~/.aws/credentials)
AWS_ACCESS_KEY_ID env var Environment variables
~/.aws/credentials Shared credentials file
IAM instance role EC2/ECS/Lambda
AWS SSO aws sso login
Note: Stability AI models on Bedrock are only available in us-west-2 (Oregon).
""")
parser.add_argument("prompt", help="Text description of the image (max 10,000 chars)")
parser.add_argument("-o", "--output", default="output.png", help="Output path (default: output.png)")
parser.add_argument("-m", "--model", choices=["ultra", "sd35"], default="ultra",
help="Model: ultra (default) or sd35")
parser.add_argument("-n", "--count", type=int, default=1, help="Number of images 1-5 (default: 1)")
parser.add_argument("--negative", help="Negative prompt (what to avoid)")
parser.add_argument("--aspect-ratio", dest="aspect_ratio", default=None,
help="Aspect ratio: 1:1(default), 16:9, 21:9, 2:3, 3:2, 4:5, 5:4, 9:16, 9:21")
parser.add_argument("--seed", type=int, default=None, help="Seed for reproducibility")
# AWS auth
parser.add_argument("--region", default=DEFAULT_REGION,
help=f"AWS region (default: {DEFAULT_REGION}, required for Stability AI)")
parser.add_argument("--profile", default=None, help="AWS named profile")
parser.add_argument("--access-key", default=None, help="AWS Access Key ID")
parser.add_argument("--secret-key", default=None, help="AWS Secret Access Key")
parser.add_argument("--session-token", default=None, help="AWS Session Token")
parser.add_argument("--bearer-token", default=None, help="Bearer token (overrides env)")
args = parser.parse_args()
if args.bearer_token:
os.environ["AWS_BEARER_TOKEN_BEDROCK"] = args.bearer_token
if len(args.prompt) > 10000:
print(f"Error: prompt exceeds 10,000 chars ({len(args.prompt)})", file=sys.stderr)
sys.exit(1)
method = detect_auth_method(args)
if not method:
print("Error: No AWS credentials found. Options:", file=sys.stderr)
print(" 1. AWS_BEARER_TOKEN_BEDROCK env var", file=sys.stderr)
print(" 2. AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY env vars", file=sys.stderr)
print(" 3. ~/.aws/credentials file", file=sys.stderr)
print(" 4. --profile <name>", file=sys.stderr)
print(" 5. --access-key + --secret-key", file=sys.stderr)
print(" 6. --bearer-token <token>", file=sys.stderr)
sys.exit(1)
model_id = MODELS[args.model]
model_label = MODEL_LABELS[args.model]
# Build request
body_dict = {"prompt": args.prompt}
# Aspect ratio
if args.aspect_ratio:
body_dict["aspect_ratio"] = args.aspect_ratio
# Negative prompt
if args.negative:
body_dict["negative_prompt"] = args.negative
# Seed
if args.seed is not None:
body_dict["seed"] = args.seed
# Output format — always PNG for maximum quality
body_dict["output_format"] = "png"
body = json.dumps(body_dict)
print(f"Generating with {model_label} (region: {args.region})...")
try:
if method == "bearer":
print(" Auth: Bearer token")
result = invoke_via_bearer_token(args, model_id, body)
else:
result = invoke_via_boto3(args, model_id, body)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
# Check for content filter
finish_reasons = result.get("finish_reasons", [])
if finish_reasons and finish_reasons[0] is not None:
print(f"Error: Content filtered — {finish_reasons[0]}", file=sys.stderr)
sys.exit(1)
if "images" not in result or not result["images"]:
print(f"Error: No images returned. Response: {json.dumps(result, indent=2)}", file=sys.stderr)
sys.exit(1)
# Save images
output_dir = os.path.dirname(args.output) or "."
base_name = os.path.splitext(os.path.basename(args.output))[0]
ext = os.path.splitext(args.output)[1] or ".png"
os.makedirs(output_dir, exist_ok=True)
paths = []
images = result["images"]
seeds = result.get("seeds", [])
for i, img_b64 in enumerate(images):
if len(images) == 1:
path = args.output
else:
path = os.path.join(output_dir, f"{base_name}_{i+1}{ext}")
with open(path, "wb") as f:
f.write(base64.b64decode(img_b64))
size_kb = os.path.getsize(path) / 1024
seed_info = f", seed={seeds[i]}" if i < len(seeds) else ""
print(f" [{i+1}/{len(images)}] {path} ({size_kb:.0f} KB{seed_info})")
paths.append(path)
print(f"Done. {len(paths)} image(s) saved via {model_label}.")
return paths
if __name__ == "__main__":
main()
Generate images using Amazon Nova Canvas via AWS Bedrock. Supports multiple AWS auth methods: environment variables, credentials file, named profiles, IAM in...
---
name: nova-canvas
description: >-
Generate images using Amazon Nova Canvas via AWS Bedrock. Supports multiple AWS auth
methods: environment variables, credentials file, named profiles, IAM instance roles,
SSO, or direct access keys. Use when the user asks to generate, create, draw, or produce
an image, picture, illustration, photo, artwork, poster, icon, banner, or any visual
content. Triggers: "generate an image", "draw me", "create a picture", "make an
illustration", "画一张", "帮我画", "生成图片", "做一张图", "AI绘图". Requires AWS
Bedrock access with Nova Canvas model enabled. NOT for: editing existing images,
generating video, or HTML/CSS canvas rendering.
---
# Nova Canvas
Generate images via Amazon Nova Canvas on AWS Bedrock.
## AWS Auth Methods
| Method | How to Use |
|--------|------------|
| **Bearer token** | `AWS_BEARER_TOKEN_BEDROCK` env var or `--bearer-token` |
| **Environment variables** | Set `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY` |
| **Credentials file** | Configure `~/.aws/credentials` |
| **Named profile** | `--profile my-profile` or `AWS_PROFILE` env var |
| **Direct keys** | `--access-key AKIA... --secret-key ...` |
| **Temporary credentials** | Add `--session-token` with direct keys |
| **IAM instance role** | Auto-detected on EC2/ECS/Lambda |
| **AWS SSO** | Run `aws sso login` first |
Auto-detection order: direct keys → profile → bearer token → env vars → credentials file → instance role → SSO.
## Quick Start
```bash
python3 {baseDir}/scripts/generate.py "your prompt" -o output.png
python3 {baseDir}/scripts/generate.py "your prompt" -o output.png --profile work
python3 {baseDir}/scripts/generate.py "your prompt" -o output.png --access-key AKIA... --secret-key ...
```
## Parameters
| Flag | Default | Description |
|------|---------|-------------|
| `prompt` | — | Text description of the image |
| `-o, --output` | output.png | Output file path |
| `-W, --width` | 1024 | Width 512-4096, divisible by 64 |
| `-H, --height` | 1024 | Height 512-4096, divisible by 64 |
| `-n, --count` | 1 | Number of images (1-5) |
| `-q, --quality` | standard | `standard` or `premium` |
| `-s, --seed` | random | Seed for reproducibility |
| `--negative` | — | Negative prompt (what to avoid) |
| `--cfg` | 8.0 | CFG scale 1.1-10.0 |
| `--region` | us-east-1 | AWS region |
| `--profile` | — | AWS named profile |
| `--access-key` | — | AWS Access Key ID |
| `--secret-key` | — | AWS Secret Access Key |
| `--session-token` | — | AWS Session Token |
| `--bearer-token` | — | Bearer token (overrides env) |
## Workflow
1. Craft a detailed English prompt (Nova Canvas performs best in English).
2. Choose size: square 1024×1024, landscape 1280×768, portrait 768×1280.
3. Run `generate.py` with `timeout=120`.
4. Send resulting image to user via `message` tool.
## Prompt Tips
- Detailed English prompts yield best results.
- Specify style: "oil painting", "watercolor", "3D render", "photograph", "anime".
- Use `--negative "blurry, low quality, text, watermark"` to exclude unwanted elements.
FILE:scripts/generate.py
#!/usr/bin/env python3
"""Generate images using Amazon Nova Canvas via AWS Bedrock. Supports multiple AWS auth methods."""
import argparse
import base64
import json
import os
import sys
import urllib.request
import urllib.error
def invoke_via_bearer_token(args, body):
"""Call Bedrock invoke-model using Bearer Token (AWS_BEARER_TOKEN_BEDROCK)."""
token = os.environ.get("AWS_BEARER_TOKEN_BEDROCK", "")
region = args.region
url = f"https://bedrock-runtime.{region}.amazonaws.com/model/amazon.nova-canvas-v1%3A0/invoke"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json",
}
req = urllib.request.Request(url, data=body.encode(), headers=headers, method="POST")
try:
with urllib.request.urlopen(req, timeout=120) as resp:
return json.loads(resp.read())
except urllib.error.HTTPError as e:
err_body = e.read().decode() if e.fp else ""
print(f"Error: Bedrock API {e.code}: {err_body}", file=sys.stderr)
sys.exit(1)
def invoke_via_boto3(args, body):
"""Call Bedrock invoke-model using boto3 (standard AWS credential chain)."""
try:
import boto3
except ImportError:
print("Error: boto3 not installed. Run: pip install boto3", file=sys.stderr)
sys.exit(1)
kwargs = {"region_name": args.region}
# Priority 1: Explicit keys
if args.access_key and args.secret_key:
kwargs["aws_access_key_id"] = args.access_key
kwargs["aws_secret_access_key"] = args.secret_key
if args.session_token:
kwargs["aws_session_token"] = args.session_token
print(" Auth: explicit keys")
client = boto3.client("bedrock-runtime", **kwargs)
# Priority 2: Named profile
elif args.profile:
session = boto3.Session(profile_name=args.profile, region_name=args.region)
print(f" Auth: profile ({args.profile})")
client = session.client("bedrock-runtime")
# Priority 3: Auto (env vars → credentials file → instance role → SSO)
else:
print(" Auth: boto3 auto-detect")
client = boto3.client("bedrock-runtime", **kwargs)
response = client.invoke_model(modelId="amazon.nova-canvas-v1:0", body=body)
return json.loads(response["body"].read())
def detect_auth_method(args):
"""Determine which auth method to use."""
# Explicit keys always win
if args.access_key and args.secret_key:
return "boto3"
# Explicit profile
if args.profile:
return "boto3"
# Bearer token
if os.environ.get("AWS_BEARER_TOKEN_BEDROCK"):
return "bearer"
# Check boto3 credentials
try:
import boto3
session = boto3.Session(region_name=args.region)
creds = session.get_credentials()
if creds:
return "boto3"
except Exception:
pass
return None
def main():
parser = argparse.ArgumentParser(
description="Generate images with Amazon Nova Canvas (AWS Bedrock)",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
AWS auth methods (auto-detected):
AWS_BEARER_TOKEN_BEDROCK Bearer token (OpenClaw managed)
--access-key + --secret-key Direct IAM credentials
--profile / AWS_PROFILE Named profile (~/.aws/credentials)
AWS_ACCESS_KEY_ID env var Environment variables
~/.aws/credentials Shared credentials file
IAM instance role EC2/ECS/Lambda
AWS SSO aws sso login
""")
# Image params
parser.add_argument("prompt", help="Text description of the image")
parser.add_argument("-o", "--output", default="output.png", help="Output path (default: output.png)")
parser.add_argument("-W", "--width", type=int, default=1024, help="Width 512-4096, ÷64 (default: 1024)")
parser.add_argument("-H", "--height", type=int, default=1024, help="Height 512-4096, ÷64 (default: 1024)")
parser.add_argument("-n", "--count", type=int, default=1, help="Number of images 1-5 (default: 1)")
parser.add_argument("-q", "--quality", choices=["standard", "premium"], default="standard")
parser.add_argument("-s", "--seed", type=int, default=None, help="Seed (0-858993459)")
parser.add_argument("--negative", help="Negative prompt")
parser.add_argument("--cfg", type=float, default=8.0, help="CFG scale 1.1-10.0 (default: 8.0)")
# AWS auth params
parser.add_argument("--region", default="us-east-1", help="AWS region (default: us-east-1, Nova Canvas availability)")
parser.add_argument("--profile", default=None, help="AWS named profile")
parser.add_argument("--access-key", default=None, help="AWS Access Key ID")
parser.add_argument("--secret-key", default=None, help="AWS Secret Access Key")
parser.add_argument("--session-token", default=None, help="AWS Session Token")
parser.add_argument("--bearer-token", default=None, help="Bearer token (overrides AWS_BEARER_TOKEN_BEDROCK)")
args = parser.parse_args()
# Allow --bearer-token to override env
if args.bearer_token:
os.environ["AWS_BEARER_TOKEN_BEDROCK"] = args.bearer_token
# Validate dimensions
for name, val in [("width", args.width), ("height", args.height)]:
if val < 512 or val > 4096:
print(f"Error: {name} must be 512-4096, got {val}", file=sys.stderr)
sys.exit(1)
if val % 64 != 0:
print(f"Error: {name} must be divisible by 64, got {val}", file=sys.stderr)
sys.exit(1)
# Detect auth
method = detect_auth_method(args)
if not method:
print("Error: No AWS credentials found. Options:", file=sys.stderr)
print(" 1. AWS_BEARER_TOKEN_BEDROCK env var (OpenClaw managed)", file=sys.stderr)
print(" 2. AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY env vars", file=sys.stderr)
print(" 3. ~/.aws/credentials file", file=sys.stderr)
print(" 4. --profile <name>", file=sys.stderr)
print(" 5. --access-key + --secret-key", file=sys.stderr)
print(" 6. --bearer-token <token>", file=sys.stderr)
print(" 7. IAM instance role (EC2/ECS/Lambda)", file=sys.stderr)
print(" 8. AWS SSO (aws sso login)", file=sys.stderr)
sys.exit(1)
# Build request body
text_params = {"text": args.prompt}
if args.negative:
text_params["negativeText"] = args.negative
img_cfg = {
"numberOfImages": args.count,
"height": args.height,
"width": args.width,
"quality": args.quality,
"cfgScale": args.cfg,
}
if args.seed is not None:
img_cfg["seed"] = args.seed
body = json.dumps({
"taskType": "TEXT_IMAGE",
"textToImageParams": text_params,
"imageGenerationConfig": img_cfg,
})
print(f"Generating {args.count} image(s) at {args.width}x{args.height} ({args.quality})...")
# Invoke
try:
if method == "bearer":
print(" Auth: Bearer token")
result = invoke_via_bearer_token(args, body)
else:
result = invoke_via_boto3(args, body)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
if "images" not in result or not result["images"]:
print(f"Error: No images returned. {json.dumps(result, indent=2)}", file=sys.stderr)
sys.exit(1)
# Save
output_dir = os.path.dirname(args.output) or "."
base_name = os.path.splitext(os.path.basename(args.output))[0]
ext = os.path.splitext(args.output)[1] or ".png"
os.makedirs(output_dir, exist_ok=True)
paths = []
for i, img_b64 in enumerate(result["images"]):
path = args.output if args.count == 1 else os.path.join(output_dir, f"{base_name}_{i+1}{ext}")
with open(path, "wb") as f:
f.write(base64.b64decode(img_b64))
size_kb = os.path.getsize(path) / 1024
print(f" [{i+1}/{len(result['images'])}] {path} ({size_kb:.0f} KB)")
paths.append(path)
print(f"Done. {len(paths)} image(s) saved.")
return paths
if __name__ == "__main__":
main()
OpenClaw 系统诊断和性能分析工具。分析 agent 推理耗时、Token 用量、工具调用统计、 Run 时间线、Gateway 重启历史。支持多种模式:批量分析(默认)、实时跟踪(-f)、 摘要统计(-s)、高级诊断(--advanced)。支持多 Agent 过滤。 使用场景:当用户询问 OpenCla...
---
name: openclaw-diagnostics
description: >
OpenClaw 系统诊断和性能分析工具。分析 agent 推理耗时、Token 用量、工具调用统计、
Run 时间线、Gateway 重启历史。支持多种模式:批量分析(默认)、实时跟踪(-f)、
摘要统计(-s)、高级诊断(--advanced)。支持多 Agent 过滤。
使用场景:当用户询问 OpenClaw 运行状态、性能瓶颈、推理延迟、Token 消耗、
工具执行统计、错误排查、agent 活动分析时触发。
触发词:诊断、diagnostics、性能分析、推理耗时、token统计、运行状态、
agent分析、工具调用统计、Run详情、Gateway重启。
指令触发:/diag — 直接执行诊断,支持参数透传。
metadata:
openclaw:
tools:
- exec
- read
---
# OpenClaw 诊断工具
## 指令模式
当用户发送 `/diag` 指令时,直接执行脚本,不做额外解释:
| 用户输入 | 执行命令 | 说明 |
|----------|----------|------|
| `/diag` | `-s` | 今日摘要(默认) |
| `/diag full` | `(无-s)` | 完整报告(含 Run 详情 + 错误列表) |
| `/diag full -l 3` | `-l 3` | 最近 3 个 Run 完整详情 |
| `/diag -a waicode` | `-s -a waicode` | 指定 agent 摘要 |
| `/diag -a main full` | `-a main` | 指定 agent 完整报告 |
| `/diag 2026-03-19` | `-s 2026-03-19` | 指定日期摘要 |
| `/diag errors` | `(无-s)` | 执行完整报告,只提取错误部分汇总 |
规则:
1. 无参数时默认 `-s`(摘要模式,最简洁)
2. `full` 关键词 → 去掉 `-s`,输出含 Run 详情
3. `errors` 关键词 → 执行完整报告,只摘出错误列表
4. `-a`、`-l`、日期参数直接透传给脚本
5. 去除 ANSI 颜色码:管道 `| sed 's/\x1b\[[0-9;]*m//g'`
6. 不支持 `-f`(实时跟踪),该模式需在 SSH 终端运行
7. **直出模式**:脚本输出直接用 `message` 工具原样发送给用户,不经过模型总结。
具体做法:
- 执行脚本,将 stdout 存入变量
- 用 `message(action="send", message=output)` 发送原始输出
- 然后回复 `NO_REPLY`(避免重复发送)
- 如果输出超过 4000 字符,按 4000 字符分段发送(Telegram 消息长度限制)
- 每段用 ``` 代码块包裹,保持等宽字体排版
## 自然语言模式
当用户用自然语言询问(如"运行状态怎么样"、"waicode今天干了啥")时,
自行选择合适参数执行脚本,并用中文汇总关键信息。
## 快速使用
```bash
# 诊断今天的数据
bash scripts/openclaw-diag.sh
# 诊断指定日期
bash scripts/openclaw-diag.sh 2026-03-19
# 只看摘要
bash scripts/openclaw-diag.sh -s
# 实时跟踪(类似 tail -f)
bash scripts/openclaw-diag.sh -f
# 高级实时跟踪(自动开启 debug 日志,退出时恢复)
bash scripts/openclaw-diag.sh -f --advanced
# 只看指定 agent
bash scripts/openclaw-diag.sh -a waicode
# 最近 5 个 Run
bash scripts/openclaw-diag.sh -l 5
```
## 模式说明
| 模式 | 参数 | 说明 |
|------|------|------|
| 摘要统计 | `-s`(默认) | KPI 概览,最简洁 |
| 完整报告 | 无 `-s` | 含 Run 详情 + 时间线 + 错误列表 |
| Agent 过滤 | `-a <name>` | 只看指定 agent |
| 限制数量 | `-l N` | 只显示最近 N 个 Run |
| 指定日期 | `YYYY-MM-DD` | 默认今天 |
参数可组合:`-s -a main`、`-l 3 -a wairesearch`。
> 实时跟踪(`-f`)和高级模式(`--advanced`)需在 SSH 终端运行,
> 详见 [references/advanced-mode.md](references/advanced-mode.md)。
## 数据源
脚本有两种数据源,自动切换:
| 数据源 | 路径 | 需要配置 | 精度 |
|--------|------|----------|------|
| Debug 日志 | `/tmp/openclaw/openclaw-YYYY-MM-DD.log` | `diagnostics.enabled: true` | 精确 Run 边界 |
| Session 文件 | `~/.openclaw/agents/*/sessions/*.jsonl` | 无需配置 | 虚拟 Run(消息时间戳推算) |
无 debug 日志时自动降级为 session 模式,核心指标(推理耗时、Token、工具统计)仍然准确。
## 输出内容
### 摘要统计
- 模型调用次数、平均推理延迟、Token 吞吐量
- 工具调用次数、成功率、总耗时
- Thinking 统计(次数、平均深度)
- Per-Agent 活动分布
### Run 详情(非摘要模式)
- 每个 Run 的时间线(推理段 + 工具调用段)
- 推理耗时、输出 Token、吞吐速率
- 工具调用参数摘要
### 错误列表
- 最近 20 条错误,按时间倒序
## 使用指南
### 日常检查
```bash
# 快速了解今天的运行概况
bash scripts/openclaw-diag.sh -s
```
### 性能排查
```bash
# 查看某天详细 Run 数据,找到慢查询
bash scripts/openclaw-diag.sh 2026-03-19 -l 10
```
### 特定 Agent 分析
```bash
# 只看 waicode 的活动
bash scripts/openclaw-diag.sh -a waicode -s
```
### 实时监控(SSH 终端)
```bash
# 需在 SSH 终端运行,不适合 Telegram/聊天
bash scripts/openclaw-diag.sh -f
bash scripts/openclaw-diag.sh -f --advanced
```
## 注意事项
- 脚本依赖 `python3`(3.7+,使用 `datetime.fromisoformat`)和 `bash`
- 高级模式(`--advanced`)会临时修改 `openclaw.json` 并重启 Gateway,退出时自动恢复
- 无 Swap 的机器上并发多 Agent 时注意内存
- 时间戳统一为 UTC 处理,不受本地时区影响
FILE:references/advanced-mode.md
# 高级模式参考
## 高级模式工作原理
`--advanced` 仅在实时跟踪模式 (`-f`) 下生效:
1. 检查 `openclaw.json` 中 `diagnostics.enabled` 和 `logLevel` 配置
2. 如果未开启,备份配置文件,修改为 `diagnostics.enabled: true` + `logLevel: debug`
3. 自动重启 Gateway(等待最多 60 秒)
4. 开始实时跟踪
5. Ctrl+C 退出时提示是否恢复原配置
## 配置项
| 配置 | 作用 | 高级模式设置 |
|------|------|------------|
| `diagnostics.enabled` | 启用 Run 事件记录 | `true` |
| `logLevel` | 日志详细度 | `debug` |
## 手动开启(不用 --advanced)
```bash
openclaw config set diagnostics.enabled true
openclaw config set logLevel debug
openclaw gateway restart
```
## Session 文件结构
路径:`~/.openclaw/agents/{agent}/sessions/{id}.jsonl`
每行一个 JSON 对象,关键字段:
- `role`: user / assistant / toolCall / toolResult / custom_message
- `timestamp`: ISO 8601 UTC
- `model`: 模型名称
- `usage`: `{input, output, cacheRead, cacheWrite, totalTokens}`
- `toolResult.details`: `{exitCode, durationMs, stdout, stderr}`
## 虚拟 Run 构造算法
当无 debug 日志时:
1. 扫描所有 agent 的 session 文件(含 .reset.* 和 .deleted.*)
2. 按日期过滤消息(UTC ±1 天容错)
3. 每条 user 消息标记为 Run 起点
4. 到下一条 user 消息或文件末尾为 Run 终点
5. 聚合 Run 内的 assistant/toolCall/toolResult 统计
## 多 Agent 支持
脚本扫描 `~/.openclaw/agents/*/sessions/` 下所有 agent。
`-a <name>` 过滤逻辑:
- 批量模式:通过 session 文件路径提取 agent 名称,过滤 Run
- 实时跟踪:从日志中 `sessionKey=agent:{name}:{id}` 提取 agent,过滤事件
FILE:scripts/openclaw-diag.sh
#!/bin/bash
# OpenClaw 诊断日志解析工具 v3.2
# 用法:
# ./openclaw-diag.sh # 解析今天的日志
# ./openclaw-diag.sh 2026-03-11 # 解析指定日期
# ./openclaw-diag.sh -f # 实时跟踪模式(标准)
# ./openclaw-diag.sh -f --advanced # 实时跟踪模式(高级:自动开启 debug 日志)
# ./openclaw-diag.sh -l 5 # 只看最近5个run
# ./openclaw-diag.sh -s # 只看摘要统计
# ./openclaw-diag.sh -a myagent 2026-03-19 # 按 agent 过滤
# ./openclaw-diag.sh -s -a main # 指定 agent 的摘要
# ./openclaw-diag.sh -f -a myagent # 实时跟踪指定 agent
#
# 功能:
# - 解析 OpenClaw 诊断日志,展示 Run 时间线
# - 从 session 文件提取工具调用参数和 Token 用量
# - 计算推理分段耗时和 Token 速率 (inference_ms, tokens_per_sec)
# - 实时跟踪模式 (-f) 流式输出
# - 高级模式 (--advanced) 自动开启 diagnostics + debug 日志
# - 摘要模式 (-s) 快速统计
#
# 注: 完整探测功能(health/gateway/doctor/config/models)请使用:
# python3 openclaw-dashboard.py --cli [--probe <name>] [--json]
#
# 数据源:
# - 日志文件: /tmp/openclaw/openclaw-YYYY-MM-DD.log
# - 会话文件: ~/.openclaw/agents/*/sessions/*.jsonl
set -euo pipefail
# 颜色
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
GRAY='\033[0;90m'
BOLD='\033[1m'
NC='\033[0m'
# 默认参数
DATE=$(date +%F)
FOLLOW=false
ADVANCED=false
LAST_N=0
SUMMARY_ONLY=false
AGENT_FILTER=""
# ============================================================
# 高级模式:配置管理函数
# ============================================================
# 查找 openclaw.json 路径
find_openclaw_config() {
local config=""
for p in \
"/root/.openclaw/openclaw.json" \
"$HOME/.openclaw/openclaw.json" \
"/etc/openclaw/openclaw.json"; do
if [ -f "$p" ]; then
config="$p"
break
fi
done
# 尝试 openclaw config validate 输出
if [ -z "$config" ]; then
config=$(openclaw config validate 2>&1 | grep -oP '(?<=config: ).*\.json' | head -1 2>/dev/null || true)
fi
echo "$config"
}
# 备份 openclaw.json
backup_config() {
local config="$1"
local backup="config.diag-backup.$(date +%s)"
cp "$config" "$backup"
echo "$backup"
}
# 检查配置项当前值
check_config_value() {
local config="$1"
local key_path="$2"
python3 -c "
import json, sys
with open('$config') as f:
data = json.load(f)
keys = '$key_path'.split('.')
obj = data
for k in keys:
obj = obj.get(k, None) if isinstance(obj, dict) else None
if obj is None:
break
print(obj if obj is not None else 'NOT_SET')
" 2>/dev/null
}
# 修改配置项
set_config_value() {
local config="$1"
local key_path="$2"
local value="$3"
python3 -c "
import json
with open('$config') as f:
data = json.load(f)
keys = '$key_path'.split('.')
obj = data
for k in keys[:-1]:
if k not in obj or not isinstance(obj[k], dict):
obj[k] = {}
obj = obj[k]
val = '$value'
if val == 'true':
obj[keys[-1]] = True
elif val == 'false':
obj[keys[-1]] = False
elif val.isdigit():
obj[keys[-1]] = int(val)
else:
obj[keys[-1]] = val
with open('$config', 'w') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
f.write('\n')
" 2>/dev/null
}
# 启用高级诊断模式
enable_advanced_mode() {
local config
config=$(find_openclaw_config)
if [ -z "$config" ]; then
echo -e "RED✘ 找不到 openclaw.json 配置文件NC" >&2
return 1
fi
echo -e "BOLD[高级模式] 配置检查NC"
echo -e "GRAY配置文件: $configNC"
echo ""
# 检查当前状态
local diag_enabled
local log_level
diag_enabled=$(check_config_value "$config" "diagnostics.enabled")
log_level=$(check_config_value "$config" "logging.level")
local need_change=false
local changes=""
if [ "$diag_enabled" != "True" ]; then
changes="changes diagnostics.enabled: REDdiag_enabledNC → GREENtrueNC\n"
need_change=true
else
changes="changes diagnostics.enabled: GREENtrueNC (已启用)\n"
fi
if [ "$log_level" != "debug" ]; then
changes="changes logging.level: REDlog_levelNC → GREENdebugNC\n"
need_change=true
else
changes="changes logging.level: GREENdebugNC (已启用)\n"
fi
echo -e "$changes"
if [ "$need_change" = false ]; then
echo -e "GREEN✔ 高级模式已经启用,无需修改配置NC"
echo ""
return 0
fi
# 提醒需要重启
echo -e "YELLOW⚠ 开启高级模式需要修改配置并重启 OpenClaw GatewayNC"
echo -e "YELLOW 修改内容: diagnostics.enabled=true, logging.level=debugNC"
echo ""
read -p "$(echo -e "BOLD确认修改配置并重启? [y/N] NC")" confirm
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
echo -e "GRAY已取消NC"
return 1
fi
# 备份
echo ""
local backup
backup=$(backup_config "$config")
echo -e "GREEN✔ 配置已备份: GRAY$backupNC"
# 将备份路径保存供后续恢复使用
ADVANCED_BACKUP="$backup"
ADVANCED_CONFIG="$config"
# 修改配置
if [ "$diag_enabled" != "True" ]; then
set_config_value "$config" "diagnostics.enabled" "true"
echo -e "GREEN✔ diagnostics.enabled → trueNC"
fi
if [ "$log_level" != "debug" ]; then
set_config_value "$config" "logging.level" "debug"
echo -e "GREEN✔ logging.level → debugNC"
fi
# 重启 Gateway
echo ""
echo -e "BLUE⟳ 正在重启 OpenClaw Gateway...NC"
if openclaw gateway restart 2>&1 | tail -3; then
echo -e "GREEN✔ Gateway 重启成功NC"
else
echo -e "RED✘ Gateway 重启失败,正在恢复配置...NC"
cp "$backup" "$config"
echo -e "YELLOW⟳ 配置已恢复,再次重启...NC"
openclaw gateway restart 2>&1 | tail -3 || true
return 1
fi
echo ""
echo -e "GREEN✔ 高级模式已启用NC"
echo -e "GRAY退出时将提示恢复原始配置NC"
echo ""
return 0
}
# 恢复配置(高级模式退出时调用,仅执行一次)
RESTORE_DONE=false
restore_config() {
# 防止重入(EXIT + INT/TERM 重复触发)
if [ "$RESTORE_DONE" = true ]; then
return 0
fi
RESTORE_DONE=true
if [ -z "-" ] || [ -z "-" ]; then
return 0
fi
echo ""
echo -e "BOLD[高级模式] 退出清理NC"
echo ""
read -p "$(echo -e "YELLOW是否恢复原始配置并重启 Gateway? [Y/n] NC")" restore_confirm </dev/tty
if [[ "$restore_confirm" =~ ^[Nn]$ ]]; then
echo -e "GRAY保留当前高级模式配置NC"
echo -e "GRAY备份文件: $ADVANCED_BACKUPNC"
echo -e "GRAY手动恢复: cp $ADVANCED_BACKUP $ADVANCED_CONFIG && openclaw gateway restartNC"
return 0
fi
echo -e "BLUE⟳ 恢复原始配置...NC"
cp "$ADVANCED_BACKUP" "$ADVANCED_CONFIG"
echo -e "GREEN✔ 配置已恢复NC"
echo -e "BLUE⟳ 重启 OpenClaw Gateway...NC"
if openclaw gateway restart 2>&1 | tail -3; then
echo -e "GREEN✔ Gateway 重启成功,已退出高级模式NC"
else
echo -e "RED✘ Gateway 重启失败NC"
echo -e "YELLOW请手动执行: openclaw gateway restartNC"
fi
# 清理备份文件
read -p "$(echo -e "GRAY删除备份文件? [y/N] NC")" del_backup </dev/tty
if [[ "$del_backup" =~ ^[Yy]$ ]]; then
rm -f "$ADVANCED_BACKUP"
echo -e "GRAY已删除 $ADVANCED_BACKUPNC"
fi
}
# ============================================================
# 参数解析
# ============================================================
# 解析参数
while [[ $# -gt 0 ]]; do
case $1 in
-f|--follow) FOLLOW=true; shift ;;
--advanced) ADVANCED=true; shift ;;
-a|--agent) AGENT_FILTER=$2; shift 2 ;;
-l|--last) LAST_N=$2; shift 2 ;;
-s|--summary) SUMMARY_ONLY=true; shift ;;
-h|--help)
echo "OpenClaw 诊断日志解析工具 v3.1"
echo ""
echo "用法: $0 [选项] [日期]"
echo ""
echo "选项:"
echo " -f, --follow 实时跟踪模式"
echo " --advanced 高级模式(自动开启 diagnostics + debug 日志)"
echo " -a, --agent NAME 只看指定 agent(如 main/myagent)"
echo " -l N, --last N 只显示最近 N 个 run"
echo " -s, --summary 只显示摘要统计"
echo " -h, --help 帮助"
echo ""
echo "示例:"
echo " $0 # 解析今天的日志(全部 agent)"
echo " $0 2026-03-11 # 解析指定日期"
echo " $0 -f # 实时跟踪(标准)"
echo " $0 -f --advanced # 实时跟踪(高级:自动开启 debug)"
echo " $0 -f -a myagent # 只跟踪指定 agent"
echo " $0 -a main # 只看 main agent"
echo " $0 -l 3 # 最近3个run"
echo " $0 -s # 摘要统计"
echo ""
echo "高级模式说明:"
echo " --advanced 会自动修改 openclaw.json 配置:"
echo " - diagnostics.enabled = true"
echo " - logging.level = debug"
echo " 启用前会备份配置,退出时提示恢复原始配置并重启"
exit 0
;;
*)
if [[ $1 =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
DATE=$1
else
echo "未知参数: $1 (用 -h 查看帮助)"
exit 1
fi
shift
;;
esac
done
LOG="/tmp/openclaw/openclaw-DATE.log"
# 自动查找会话文件目录
SESSIONS_DIR=""
for d in \
"/root/.openclaw/agents/main/sessions" \
"/root/.openclaw/agents/*/sessions" \
"$HOME/.openclaw/agents/main/sessions" \
"$HOME/.openclaw/agents/*/sessions"; do
if [ -d "$d" ] 2>/dev/null; then
SESSIONS_DIR="$d"
break
fi
done
# 如果通配符没展开,尝试 find
if [ -z "$SESSIONS_DIR" ] || [ ! -d "$SESSIONS_DIR" ]; then
SESSIONS_DIR=$(find "HOME/.openclaw/agents" -type d -name "sessions" 2>/dev/null | head -1)
fi
if [ "$FOLLOW" = true ]; then
# 高级模式:启用 diagnostics + debug
if [ "$ADVANCED" = true ]; then
enable_advanced_mode || exit 1
# 注册退出清理(仅 EXIT,覆盖所有退出路径包括 Ctrl+C)
trap restore_config EXIT
fi
echo -e "BOLD[实时跟踪] Ctrl+C 退出NC"
if [ "$ADVANCED" = true ]; then
echo -e "GREEN模式: 高级(diagnostics + debug)NC"
else
echo -e "GRAY模式: 标准(提示: 使用 --advanced 开启完整日志)NC"
fi
if [ -n "$AGENT_FILTER" ]; then
echo -e "CYAN过滤: 仅显示 agent=AGENT_FILTERNC"
else
echo -e "GRAY过滤: 全部 agent(提示: 使用 -a <name> 过滤指定 agent)NC"
fi
echo -e "GRAY日志文件: $LOGNC"
echo ""
export AGENT_FILTER_ENV="$AGENT_FILTER"
tail -f "$LOG" 2>/dev/null | python3 -c "
import json, sys, os, re
from datetime import datetime
# Agent 颜色映射
AGENT_COLORS = {
'main': '\033[0;36m', # cyan
# 自定义 agent 颜色(按需添加)
# 'agent_name': '\033[0;32m', # green
}
NC = '\033[0m'
BOLD = '\033[1m'
GRAY = '\033[0;90m'
agent_filter = os.environ.get('AGENT_FILTER_ENV', '')
# 从日志消息中提取 agent 名称
def extract_agent(msg):
# sessionKey=agent:<name>:<id> → <name>
m = re.search(r'sessionKey=agent:([^:\s]+)', msg)
if m:
return m.group(1)
# lane=session:agent:<name>:<id> → <name>
m = re.search(r'lane=session:agent:([^:\s]+)', msg)
if m:
return m.group(1)
# [<agent>] starting provider → <name>
m = re.search(r'\[(\w+)\]\s+(?:starting|stopping)', msg)
if m and m.group(1) != 'openclaw':
return m.group(1)
return ''
def agent_tag(agent):
if not agent:
return ''
color = AGENT_COLORS.get(agent, GRAY)
return f'{color}[{agent}]{NC} '
# 跟踪每个 agent 的上一次时间戳
agent_prev_times = {}
# 当前活跃 agent(无标记事件继承最近一次有标记的 agent)
current_agent = ''
for line in sys.stdin:
try:
obj = json.loads(line.strip())
t = obj.get('time', '')
parts = [obj.get(str(i), '') for i in range(3) if isinstance(obj.get(str(i), ''), str)]
msg = ' '.join(parts)
level = obj.get('_meta', {}).get('logLevelName', '')
# 提取 agent
agent = extract_agent(msg)
if agent:
current_agent = agent
else:
agent = current_agent
# 过滤
if agent_filter and agent != agent_filter:
continue
label = None
detail = ''
if 'embedded run start:' in msg:
label = '[RUN-START] 开始处理请求'
if 'model=' in msg:
detail = 'model=' + msg.split('model=')[1].split(' ')[0]
elif 'run agent start' in msg:
label = '[MODEL-SEND] 请求已发送给模型, 等待推理'
elif 'run agent end' in msg:
label = '[MODEL-DONE] 模型推理完成'
elif 'tool start' in msg:
tool = msg.split('tool=')[1].split(' ')[0] if 'tool=' in msg else '?'
label = f'[TOOL-START] 开始执行工具: {tool}'
elif 'tool end' in msg:
tool = msg.split('tool=')[1].split(' ')[0] if 'tool=' in msg else '?'
label = f'[TOOL-END] 工具执行完成: {tool}'
elif 'sendMessage' in msg:
label = '[MSG-SEND] 消息发送到通道'
detail = msg.split('sendMessage')[1][:60].strip() if 'sendMessage' in msg else ''
elif 'lane dequeue' in msg:
label = '[DEQUEUE] 消息从队列取出'
if 'waitMs=' in msg:
detail = '排队等待 ' + msg.split('waitMs=')[1].split(' ')[0] + 'ms'
elif 'pre-prompt' in msg:
label = '[PROMPT] 构建提示词'
if 'messages=' in msg:
detail = '历史消息 ' + msg.split('messages=')[1].split(' ')[0] + ' 条'
elif 'session state' in msg:
# 会话状态变更
new_state = ''
if 'new=processing' in msg:
new_state = '→ processing'
elif 'new=idle' in msg:
new_state = '→ idle'
if new_state:
label = f'[SESSION] {new_state}'
if 'reason=' in msg:
detail = msg.split('reason=')[1].split(' ')[0].strip('\"')
elif 'spawn' in msg.lower() and ('sub-agent' in msg.lower() or 'sessions_spawn' in msg.lower()):
label = '[SPAWN] 子 agent 派发'
detail = msg[:80]
elif level == 'ERROR':
label = '[ERROR] 错误'
detail = msg[:80]
elif level == 'WARN':
label = '[WARN] 警告'
detail = msg[:80]
if not label:
continue
ts = t[11:23]
# 每个 agent 独立计算时间差
track_key = agent or '__global__'
try:
curr = datetime.fromisoformat(t.replace('+00:00', ''))
prev = agent_prev_times.get(track_key)
if prev:
delta_ms = (curr - prev).total_seconds() * 1000
if delta_ms >= 1000:
delta_str = f'+{delta_ms/1000:.1f}s'
else:
delta_str = f'+{delta_ms:.0f}ms'
else:
delta_str = '---'
agent_prev_times[track_key] = curr
except:
delta_str = '?'
tag = agent_tag(agent)
detail_str = f' {detail}' if detail else ''
print(f'{ts} {delta_str:>8} {tag}{label}{detail_str}', flush=True)
except:
pass
"
exit 0
fi
# 非实时模式
# --advanced 在非实时模式下仅提示,不自动修改配置
if [ "$ADVANCED" = true ] && [ "$FOLLOW" = false ]; then
echo -e "YELLOW提示: --advanced 仅在实时跟踪模式 (-f) 下生效NC"
echo -e "GRAY用法: $0 -f --advancedNC"
echo ""
fi
NO_LOG=false
if [ ! -f "$LOG" ]; then
NO_LOG=true
fi
echo -e "BOLD[OpenClaw 诊断报告]NC"
if [ "$NO_LOG" = true ]; then
echo -e "YELLOW日志文件: $LOG (不存在,使用 session 数据)NC"
else
echo -e "GRAY日志文件: $LOGNC"
fi
echo -e "GRAY会话目录: -未找到NC"
echo -e "GRAY日期: $DATENC"
if [ -n "$AGENT_FILTER" ]; then
echo -e "CYAN过滤: agent=AGENT_FILTERNC"
fi
echo ""
export DIAG_LOG="$LOG"
export DIAG_DATE="$DATE"
export DIAG_LAST_N="$LAST_N"
export DIAG_SUMMARY="$SUMMARY_ONLY"
export DIAG_SESSIONS_DIR="-"
export DIAG_AGENT_FILTER="AGENT_FILTER"
python3 << 'PYEOF'
import json, sys, os, glob
from datetime import datetime
from collections import defaultdict
LOG = os.environ.get("DIAG_LOG", "/tmp/openclaw/openclaw.log")
LAST_N = int(os.environ.get("DIAG_LAST_N", "0"))
SUMMARY_ONLY = os.environ.get("DIAG_SUMMARY", "false") == "true"
SESSIONS_DIR = os.environ.get("DIAG_SESSIONS_DIR", "")
DIAG_DATE = os.environ.get("DIAG_DATE", "")
AGENT_FILTER = os.environ.get("DIAG_AGENT_FILTER", "")
# ============================================================
# 1. 从会话文件中提取工具调用参数
# ============================================================
# toolCallId -> {name, summary, workdir}
tool_params = {}
# toolCallId -> {toolName, isError, exitCode, durationMs, status, cwd, diff, url, tookMs, child_sess_id}
tool_details = {}
# 每次推理的 token 用量: toolCallId -> usage dict
# 一个 assistant 消息可能包含多个 toolCall, 它们共享同一个 usage
# 我们用第一个 toolCallId 作为 key, 也建立反向映射
inference_usage = {} # toolCallId -> {input, output, cacheRead, cacheWrite, totalTokens, cost, all_tool_ids}
# 没有 toolCall 的推理(纯文本回复)按时间戳索引
text_reply_usage = [] # [(timestamp, usage)]
# session_uuid → agent 映射 (用于日志 Run 的 agent 归属判定)
session_uuid_to_agent = {}
# 推理事件序列 (session-based): sess_ref -> [event_dict]
session_infer_events = defaultdict(list)
# 所有推理事件(用于时间窗口匹配)
all_infer_events = []
def extract_tool_summary(name, args):
"""从工具参数中提取可读摘要"""
cmd = args.get("command", "")
path = args.get("path", "") or args.get("file_path", "")
workdir = args.get("workdir", "")
query = args.get("query", "")
url = args.get("url", "")
action = args.get("action", "")
tsk_desc = args.get("task", "")
text = args.get("text", "")
message = args.get("message", "")
old_str = args.get("old_string", "") or args.get("oldText", "")
new_str = args.get("new_string", "") or args.get("newText", "")
parts = []
if name == "exec":
# 取命令第一行
first_line = cmd.split("\n")[0][:90] if cmd else ""
parts.append(first_line)
if workdir:
parts.append(f"cwd={workdir}")
elif name == "read":
parts.append(path)
elif name == "write":
parts.append(path)
elif name == "edit":
parts.append(path)
if old_str:
preview = old_str[:40].replace("\n", " ")
parts.append(f'替换: "{preview}..."')
elif name == "web_search":
parts.append(f'搜索: "{query}"')
elif name == "web_fetch":
parts.append(url)
elif name == "browser":
parts.append(action)
if url:
parts.append(url)
elif name == "message":
parts.append(action)
if message:
parts.append(message[:50])
elif name == "sessions_spawn":
agent = args.get("agentId", "?")
parts.append(f"agent={agent}")
if task:
parts.append(tsk_desc[:50])
elif name == "memory_search":
parts.append(f'查询: "{query}"')
elif name == "memory_get":
parts.append(path)
elif name == "session_status":
parts.append("查看状态")
elif name == "process":
parts.append(action)
sid = args.get("sessionId", "")
if sid:
parts.append(f"session={sid}")
elif name == "tts":
parts.append(text[:50] if text else "")
else:
# 通用: 取前几个有值的参数
for k, v in list(args.items())[:3]:
if v and isinstance(v, str):
parts.append(f"{k}={v[:40]}")
return " ".join(filter(None, parts))
if SESSIONS_DIR and os.path.isdir(SESSIONS_DIR):
# 扫描所有 agent 的 sessions 目录
# 往上两层到 agents/ 目录,扫描所有 agent 的 sessions
agents_root = os.path.dirname(os.path.dirname(SESSIONS_DIR)) # agents/main/sessions → agents/
session_dirs = glob.glob(os.path.join(agents_root, "*/sessions"))
if not session_dirs:
session_dirs = [SESSIONS_DIR]
all_session_files = []
for sd in session_dirs:
for ext_pat in ["*.jsonl", "*.jsonl.reset.*", "*.jsonl.deleted.*"]:
all_session_files.extend(glob.glob(os.path.join(sd, ext_pat)))
if not all_session_files:
for ext_pat in ["*.jsonl", "*.jsonl.reset.*", "*.jsonl.deleted.*"]:
all_session_files.extend(glob.glob(os.path.join(SESSIONS_DIR, ext_pat)))
for sf in all_session_files:
try:
# 从文件路径推导 sess_ref: agents/{agent}/sessions/{id}.jsonl -> {agent}:{id}
sf_parts = sf.replace("\\", "/").split("/")
sf_fname = os.path.basename(sf)
sf_base = sf_fname
for _sfx in [".deleted.", ".reset."]:
_idx = sf_base.find(_sfx)
if _idx >= 0:
sf_base = sf_base[:_idx]
sf_base = sf_base.replace(".jsonl", "")
sf_session_id = sf_base
sf_agent = ""
for pi, p in enumerate(sf_parts):
if p == "agents" and pi + 1 < len(sf_parts):
sf_agent = sf_parts[pi + 1]
break
sf_sess_ref = f"{sf_agent}:{sf_session_id}" if sf_agent else sf_session_id
# 建立 session_uuid → agent 映射
if sf_agent and sf_session_id:
session_uuid_to_agent[sf_session_id] = sf_agent
# Agent 过滤:跳过不匹配的 agent 的 session 文件
if AGENT_FILTER and sf_agent and sf_agent != AGENT_FILTER:
continue
# 每个 session 文件独立跟踪 prev_top_timestamp
prev_top_timestamp = ""
infer_round = 0
with open(sf) as f:
for line in f:
try:
obj = json.loads(line.strip())
if obj.get("type") != "message":
continue
msg = obj.get("message", {})
content = msg.get("content", [])
if not isinstance(content, list):
content = []
role = msg.get("role", "")
usage = msg.get("usage", {})
model = msg.get("model", "")
timestamp = obj.get("timestamp", "")
# delivery-mirror: 记录 timestamp 但跳过后续处理
if role == "assistant" and model == "delivery-mirror":
if timestamp:
prev_top_timestamp = timestamp
continue
# user 消息: 记录 timestamp
if role == "user":
if timestamp:
prev_top_timestamp = timestamp
continue
# toolResult 消息: 记录 timestamp + 提取 details
if role == "toolResult":
if timestamp:
prev_top_timestamp = timestamp
tcid = msg.get("toolCallId", "")
tool_name = msg.get("toolName", "")
is_error = msg.get("isError", False)
details = msg.get("details", {})
if not isinstance(details, dict):
details = {}
if tcid:
tool_details[tcid] = {
"toolName": tool_name,
"isError": is_error,
"exitCode": details.get("exitCode"),
"durationMs": details.get("durationMs"),
"status": details.get("status", ""),
"cwd": details.get("cwd", ""),
"diff": details.get("diff", ""),
"url": details.get("url", ""),
"tookMs": details.get("tookMs"),
"child_sess_id": details.get("child_sess_id", ""),
}
continue
# 以下仅处理 assistant 消息 (非 delivery-mirror)
if role != "assistant":
continue
# 收集 toolCall 参数
tool_ids_in_msg = []
for block in content:
if not isinstance(block, dict):
continue
if block.get("type") == "toolCall":
tid = block.get("id", "")
name = block.get("name", "?")
args_raw = block.get("arguments", "{}")
try:
args = json.loads(args_raw) if isinstance(args_raw, str) else (args_raw if isinstance(args_raw, dict) else {})
except:
args = {}
workdir = args.get("workdir", "")
summary = extract_tool_summary(name, args)
tool_params[tid] = {
"name": name,
"summary": summary,
"workdir": workdir,
}
tool_ids_in_msg.append(tid)
# 计算 per-call 推理耗时 (与 Python dashboard 一致)
inference_ms = 0
tokens_per_sec = 0.0
if prev_top_timestamp and timestamp:
try:
cur_dt = datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
prev_dt = datetime.fromisoformat(prev_top_timestamp.replace("Z", "+00:00"))
delta = (cur_dt - prev_dt).total_seconds() * 1000
if delta > 0:
inference_ms = round(delta)
output_tokens = usage.get("output", 0) if isinstance(usage, dict) else 0
if output_tokens > 0 and inference_ms > 0:
tokens_per_sec = round(output_tokens / (inference_ms / 1000), 1)
except (ValueError, TypeError):
pass
# 收集每次推理的 usage (仅 assistant, 非 delivery-mirror)
if usage:
usage_record = {
"input": usage.get("input", 0),
"output": usage.get("output", 0),
"cacheRead": usage.get("cacheRead", 0),
"cacheWrite": usage.get("cacheWrite", 0),
"totalTokens": usage.get("totalTokens", 0),
"cost": usage.get("cost", {}),
"timestamp": timestamp,
"tool_ids": tool_ids_in_msg,
"inference_ms": inference_ms,
"tokens_per_sec": tokens_per_sec,
}
if tool_ids_in_msg:
for tid in tool_ids_in_msg:
inference_usage[tid] = usage_record
else:
text_reply_usage.append((timestamp, usage_record))
# 推理事件提取: assistant 且 prev_top_timestamp 有值
if prev_top_timestamp and timestamp and inference_ms > 0:
infer_round += 1
infer_evt = {
"sess_ref": sf_sess_ref,
"send_ts": prev_top_timestamp,
"recv_ts": timestamp,
"inference_ms": inference_ms,
"round": infer_round,
"input_tokens": usage.get("input", 0) if isinstance(usage, dict) else 0,
"output_tokens": usage.get("output", 0) if isinstance(usage, dict) else 0,
"cache_read": usage.get("cacheRead", 0) if isinstance(usage, dict) else 0,
"tokens_per_sec": tokens_per_sec,
"model": model,
}
session_infer_events[sf_sess_ref].append(infer_evt)
all_infer_events.append(infer_evt)
except:
pass
except:
pass
# ============================================================
# 2. 解析日志事件
# ============================================================
events = []
if os.path.isfile(LOG):
with open(LOG) as f:
for line in f:
try:
obj = json.loads(line.strip())
t = obj.get("time", "")
parts = [obj.get(str(i), "") for i in range(3) if isinstance(obj.get(str(i), ""), str)]
msg = " ".join(parts)
level = obj.get("_meta", {}).get("logLevelName", "")
events.append((t, level, msg))
except:
pass
# ============================================================
# 3. 提取 run 信息
# ============================================================
runs = {}
for t, level, msg in events:
run_id = None
if "runId=" in msg:
run_id = msg.split("runId=")[1].split(" ")[0]
if not run_id:
continue
if run_id not in runs:
runs[run_id] = {
"start": None, "end": None,
"events": [], "tools": [],
"model": "", "channel": "",
"first_recv_ts": None,
"prompt_messages": 0,
"sess_ref": "",
"session_uuid": "", # session file UUID (from sessionId= in log)
}
r = runs[run_id]
r["events"].append((t, msg))
if "embedded run start:" in msg:
r["start"] = t
if "model=" in msg:
r["model"] = msg.split("model=")[1].split(" ")[0]
if "messageChannel=" in msg:
r["channel"] = msg.split("messageChannel=")[1].split(" ")[0]
elif "channel=" in msg.lower():
r["channel"] = msg.lower().split("channel=")[1].split(" ")[0]
if "sessionId=" in msg:
# Extract session UUID for matching against session files
r["session_uuid"] = msg.split("sessionId=")[1].split(" ")[0]
elif "run agent start" in msg:
r["first_recv_ts"] = t
elif "run agent end" in msg or "run end" in msg:
r["end"] = t
elif "tool start" in msg:
tool_name = msg.split("tool=")[1].split(" ")[0] if "tool=" in msg else "?"
tool_id = msg.split("toolCallId=")[1].split(" ")[0] if "toolCallId=" in msg else ""
r["tools"].append({"name": tool_name, "start": t, "end": None, "id": tool_id})
elif "tool end" in msg:
tool_id = msg.split("toolCallId=")[1].split(" ")[0] if "toolCallId=" in msg else ""
for tool in reversed(r["tools"]):
if tool["id"] == tool_id or (not tool["end"] and tool["id"] == ""):
tool["end"] = t
break
elif "pre-prompt" in msg and "messages=" in msg:
try:
r["prompt_messages"] = int(msg.split("messages=")[1].split(" ")[0])
except:
pass
if "sessionKey=" in msg:
r["sess_ref"] = msg.split("sessionKey=")[1].split(" ")[0]
# 从 sessionFile= 提取 session UUID,构造与 session_infer_events 一致的 key
if "sessionFile=" in msg:
_sf_path = msg.split("sessionFile=")[1].split(" ")[0]
_sf_name = _sf_path.split("/")[-1].replace(".jsonl", "")
_sf_agent = ""
_sf_parts = _sf_path.replace("\\\\", "/").split("/")
for _pi, _pp in enumerate(_sf_parts):
if _pp == "agents" and _pi + 1 < len(_sf_parts):
_sf_agent = _sf_parts[_pi + 1]
break
r["sess_ref"] = f"{_sf_agent}:{_sf_name}" if _sf_agent else _sf_name
# 收集 sendMessage 事件
sends = [(t, msg) for t, level, msg in events if "sendMessage" in msg]
# 收集错误
errors = [(t, msg) for t, level, msg in events if level == "ERROR"]
def parse_time(t):
"""Parse ISO timestamp to datetime, normalizing to UTC (naive datetime for comparison)."""
if not t:
return None
try:
# Handle various timezone formats: Z, +00:00, +08:00, etc.
import re
# Remove trailing Z
s = t.replace("Z", "+00:00")
# Parse with fromisoformat (Python 3.7+)
dt = datetime.fromisoformat(s)
# If timezone-aware, convert to UTC and strip tzinfo for comparison
if dt.tzinfo is not None:
from datetime import timezone
dt = dt.astimezone(timezone.utc).replace(tzinfo=None)
return dt
except:
return None
def fmt_duration(ms):
if ms >= 60000:
return f"{ms/60000:.1f}min"
elif ms >= 1000:
return f"{ms/1000:.1f}s"
else:
return f"{ms:.0f}ms"
def bar(ms, max_ms=30000, width=20):
filled = min(int(ms / max_ms * width), width)
return "#" * filled + "." * (width - filled)
# 按时间排序
sorted_runs = sorted(runs.items(), key=lambda x: x[1]["start"] or "")
# Agent 过滤:通过 session_uuid 和 sess_ref 判断 run 属于哪个 agent
if AGENT_FILTER:
def _get_run_agent(r):
# 优先通过 session_uuid 查 agent
uuid = r.get("session_uuid", "")
if uuid and uuid in session_uuid_to_agent:
return session_uuid_to_agent[uuid]
# 回退:从 sess_ref 提取 (agent:xxx:main)
sref = r.get("sess_ref", "")
if sref.startswith("agent:"):
parts = sref.split(":")
return parts[1] if len(parts) > 1 else ""
return ""
sorted_runs = [(rid, r) for rid, r in sorted_runs if _get_run_agent(r) == AGENT_FILTER or not _get_run_agent(r)]
# 也过滤 errors/sends:仅保留明确包含目标 agent 的行
errors = [(t, m) for t, m in errors if f'agent:{AGENT_FILTER}:' in m]
sends = [(t, m) for t, m in sends if f'agent:{AGENT_FILTER}:' in m]
if LAST_N > 0:
sorted_runs = sorted_runs[-LAST_N:]
# ============================================================
# 3.5 虚拟 Run 构造 (当无 debug 日志但有 session 数据时)
# ============================================================
virtual_runs_mode = False
if not sorted_runs and all_infer_events:
virtual_runs_mode = True
# 按日期过滤 session 事件
date_filtered_events = []
for evt in all_infer_events:
send_ts = evt.get("send_ts", "")
if DIAG_DATE and send_ts[:10] != DIAG_DATE:
continue
date_filtered_events.append(evt)
# 按 sess_ref 分组
session_groups = defaultdict(list)
for evt in date_filtered_events:
sref = evt["sess_ref"]
# Agent 过滤(虚拟 Run: sess_ref 格式 <agent>:uuid)
if AGENT_FILTER:
agent_part = sref.split(":")[0] if ":" in sref else ""
if agent_part and agent_part != AGENT_FILTER:
continue
session_groups[sref].append(evt)
# 为每个 sess_ref 构造虚拟 Run
virtual_run_id = 0
for sref, evts in sorted(session_groups.items(), key=lambda x: min(e["send_ts"] for e in x[1])):
virtual_run_id += 1
vrun_id = f"virtual-{virtual_run_id}"
sorted_evts = sorted(evts, key=lambda e: e["send_ts"])
v_start = sorted_evts[0]["send_ts"]
v_end = sorted_evts[-1]["recv_ts"]
v_model = ""
for e in sorted_evts:
if e.get("model"):
v_model = e["model"]
break
# 从 tool_params 和 tool_details 中按时间窗口匹配工具调用
v_start_dt = parse_time(v_start)
v_end_dt = parse_time(v_end)
v_tools = []
if v_start_dt and v_end_dt:
# 收集此 session 所有推理事件的 tool_ids
matched_tool_ids = set()
for e in sorted_evts:
# 查找 inference_usage 中与此事件时间匹配的 tool_ids
for tid, usage_rec in inference_usage.items():
if usage_rec.get("timestamp") == e["recv_ts"]:
matched_tool_ids.update(usage_rec.get("tool_ids", []))
for tid in matched_tool_ids:
tp = tool_params.get(tid, {})
td = tool_details.get(tid, {})
tname = tp.get("name", "") or td.get("toolName", "unknown")
# 估算工具时间: 使用 tool_details 中的 durationMs
t_dur_ms = td.get("durationMs") or td.get("tookMs") or 0
v_tools.append({
"name": tname,
"start": "", # 无精确时间
"end": "",
"id": tid,
"virtual_duration_ms": t_dur_ms,
})
runs[vrun_id] = {
"start": v_start,
"end": v_end,
"events": [],
"tools": v_tools,
"model": v_model,
"channel": "",
"first_recv_ts": v_start,
"prompt_messages": 0,
"sess_ref": sref,
"virtual": True,
}
# 重新排序
sorted_runs = sorted(runs.items(), key=lambda x: x[1]["start"] or "")
# Agent 过滤(虚拟 Run 的 sess_ref 格式: <agent>:uuid)
if AGENT_FILTER:
sorted_runs = [(rid, r) for rid, r in sorted_runs if r.get("sess_ref", "").startswith(AGENT_FILTER + ":") or not r.get("sess_ref")]
if LAST_N > 0:
sorted_runs = sorted_runs[-LAST_N:]
# ============================================================
# 3.6 日期过滤全局数据 (虚拟 Run 模式下,确保统计只包含目标日期)
# ============================================================
# 将日期转换为 UTC 日期范围进行比较(考虑时区偏移,最多 ±14 小时)
def date_matches_utc(ts, target_date):
"""检查 UTC 时间戳的日期部分是否严格匹配目标日期。
时间戳本身就是 UTC,无需 ±1 天容差。"""
if not ts or not target_date:
return True # 无日期过滤
return ts[:10] == target_date
if DIAG_DATE:
# 收集目标日期所有 session 推理事件的 tool_ids 和 timestamps
date_valid_tool_ids = set()
date_valid_timestamps = set()
for evt in all_infer_events:
send_ts = evt.get("send_ts", "")
recv_ts = evt.get("recv_ts", "")
if date_matches_utc(send_ts, DIAG_DATE) or date_matches_utc(recv_ts, DIAG_DATE):
date_valid_timestamps.add(recv_ts)
# 从 inference_usage 中收集日期匹配的 tool_ids
for tid, u in list(inference_usage.items()):
if date_matches_utc(u.get("timestamp", ""), DIAG_DATE):
date_valid_tool_ids.update(u.get("tool_ids", []))
date_valid_tool_ids.add(tid)
# 只在有日志 Run 时过滤 tool_params(虚拟 Run 模式保留所有,因为已通过 session 筛选)
if not virtual_runs_mode and date_valid_tool_ids:
tool_params = {k: v for k, v in tool_params.items() if k in date_valid_tool_ids}
tool_details = {k: v for k, v in tool_details.items() if k in date_valid_tool_ids}
# 始终按日期过滤 inference_usage 和 text_reply_usage(包括虚拟 Run 模式)
inference_usage = {k: v for k, v in inference_usage.items() if date_matches_utc(v.get("timestamp", ""), DIAG_DATE)}
text_reply_usage = [(ts, u) for ts, u in text_reply_usage if date_matches_utc(ts, DIAG_DATE)]
# 虚拟 Run 模式下也按日期过滤 tool_params/tool_details
if virtual_runs_mode:
# 收集日期匹配的 tool_ids(从已过滤的 inference_usage 中提取)
vr_valid_tool_ids = set()
for tid, u in inference_usage.items():
vr_valid_tool_ids.update(u.get("tool_ids", []))
vr_valid_tool_ids.add(tid)
tool_params = {k: v for k, v in tool_params.items() if k in vr_valid_tool_ids}
tool_details = {k: v for k, v in tool_details.items() if k in vr_valid_tool_ids}
# 过滤 all_infer_events (用于后续统计)
all_infer_events = [e for e in all_infer_events if date_matches_utc(e.get("send_ts", ""), DIAG_DATE) or date_matches_utc(e.get("recv_ts", ""), DIAG_DATE)]
# ============================================================
# 4. 摘要统计
# ============================================================
print("=" * 68)
print(f"{'[摘要统计]':^64}")
print("=" * 68)
if virtual_runs_mode:
print()
print(" ⚠️ 未检测到 debug 日志,使用 session 数据构建时间线(精度有限)")
print()
total_runs = len(sorted_runs)
total_tools = sum(len(r["tools"]) for _, r in sorted_runs)
total_errors = len(errors)
total_sends = len(sends)
def calc_inference_segments(r):
"""精确计算每段推理时间,合并批量工具调用 (gap < 500ms)。
返回: (segments_list, total_inference_ms, total_tool_ms)
segments_list: [(label, ms, tool_indices)] tool_indices = list of int
"""
BATCH_GAP_MS = 500
segments = [] # [(label, ms, tool_indices)]
total_tool_ms = 0
if not r["first_recv_ts"]:
return segments, 0, 0
ft = parse_time(r["first_recv_ts"])
if not ft:
return segments, 0, 0
tools = r["tools"]
if not tools:
if r["end"]:
run_end = parse_time(r["end"])
if run_end:
total_ms = (run_end - ft).total_seconds() * 1000
segments.append(("推理#1(生成回复)", total_ms, []))
return segments, sum(ms for _, ms, _ in segments), 0
# Parse all tool times
parsed = [] # [(start_dt, end_dt, index)]
for idx, tool in enumerate(tools):
ts = parse_time(tool["start"])
te = parse_time(tool["end"]) if tool["end"] else None
if ts:
parsed.append((ts, te, idx))
if not parsed:
return segments, 0, 0
# Group into batches by gap < 500ms
batches = [] # each batch = [(start_dt, end_dt, tool_idx), ...]
current_batch = [parsed[0]]
for i in range(1, len(parsed)):
prev_end = current_batch[-1][1] # end of previous tool
cur_start = parsed[i][0]
if prev_end and cur_start and (cur_start - prev_end).total_seconds() * 1000 < BATCH_GAP_MS:
current_batch.append(parsed[i])
else:
batches.append(current_batch)
current_batch = [parsed[i]]
batches.append(current_batch)
# Calculate tool_ms
for batch in batches:
for ts, te, idx in batch:
if ts and te:
total_tool_ms += (te - ts).total_seconds() * 1000
# Build inference segments
prev_end = ft # starts at agent_start (first_recv_ts)
for b_idx, batch in enumerate(batches):
batch_start = batch[0][0] # first tool start in batch
tool_indices = [item[2] for item in batch]
# Inference before this batch
infer_ms = (batch_start - prev_end).total_seconds() * 1000
if infer_ms > 0:
segments.append((f"推理#{len(segments)+1}", infer_ms, tool_indices))
# Update prev_end to end of last tool in batch
last_end = batch[-1][1]
if last_end:
prev_end = last_end
else:
prev_end = batch[-1][0]
# Final segment: last batch end -> run_end
if r["end"] and prev_end:
run_end = parse_time(r["end"])
if run_end:
last_ms = (run_end - prev_end).total_seconds() * 1000
if last_ms > 0:
segments.append((f"推理#{len(segments)+1}(生成回复)", last_ms, []))
total_infer = sum(ms for _, ms, _ in segments)
return segments, total_infer, total_tool_ms
run_durations = []
model_times = []
tool_times = []
for run_id, r in sorted_runs:
if r["start"] and r["end"]:
s = parse_time(r["start"])
e = parse_time(r["end"])
if s and e:
run_durations.append((e - s).total_seconds() * 1000)
# 从 session 数据计算推理总时间
session_uuid_g = r.get("session_uuid", "")
matched_g = []
# 策略1: 通过 session_uuid 匹配
if session_uuid_g:
run_start_g = parse_time(r["start"])
run_end_g = parse_time(r["end"])
for sref, evts in session_infer_events.items():
if session_uuid_g in sref:
if run_start_g and run_end_g:
for evt_g in evts:
evt_s_g = parse_time(evt_g["send_ts"])
evt_r_g = parse_time(evt_g["recv_ts"])
if evt_s_g and evt_r_g and evt_s_g >= run_start_g and evt_r_g <= run_end_g:
matched_g.append(evt_g)
else:
matched_g.extend(evts)
break
# 策略2: 通过 sess_ref 匹配 (虚拟 Run)
if not matched_g:
sess_ref_g = r.get("sess_ref", "")
if sess_ref_g in session_infer_events:
matched_g = list(session_infer_events[sess_ref_g])
# 策略3: 时间窗口匹配 (fallback)
if not matched_g and r["start"] and r["end"]:
run_start_g = parse_time(r["start"])
run_end_g = parse_time(r["end"])
if run_start_g and run_end_g:
for evt_g in all_infer_events:
try:
evt_s_g = parse_time(evt_g["send_ts"])
evt_r_g = parse_time(evt_g["recv_ts"])
if evt_s_g and evt_r_g and evt_s_g >= run_start_g and evt_r_g <= run_end_g:
matched_g.append(evt_g)
except:
pass
total_infer_g = sum(e["inference_ms"] for e in matched_g)
if total_infer_g > 0:
model_times.append(total_infer_g)
for tool in r["tools"]:
if tool["start"] and tool["end"]:
ts = parse_time(tool["start"])
te = parse_time(tool["end"])
if ts and te:
tool_times.append((te - ts).total_seconds() * 1000)
elif tool.get("virtual_duration_ms", 0) > 0:
tool_times.append(tool["virtual_duration_ms"])
print(f" Run 总数: {total_runs}")
print(f" 工具调用总数: {total_tools}")
print(f" 消息发送总数: {total_sends}")
print(f" 错误总数: {total_errors}")
if tool_params:
print(f" 工具参数已加载: {len(tool_params)} 条 (来自会话文件)")
else:
print(f" 工具参数: 未加载 (会话目录未找到或为空)")
# 工具调用成功率统计 (基于 tool_details)
if tool_details:
td_total = len(tool_details)
td_errors = sum(1 for d in tool_details.values() if d.get("isError"))
td_success_rate = ((td_total - td_errors) / td_total * 100) if td_total > 0 else 0
td_durations = [d["durationMs"] for d in tool_details.values() if d.get("durationMs") is not None and d["durationMs"] > 0]
avg_ms_str = f"{sum(td_durations)/len(td_durations):.0f}ms" if td_durations else "N/A"
# Top 3 工具
td_name_counts = defaultdict(int)
for d in tool_details.values():
if d.get("toolName"):
td_name_counts[d["toolName"]] += 1
top3 = sorted(td_name_counts.items(), key=lambda x: -x[1])[:3]
top3_str = ", ".join(f"{n}({c})" for n, c in top3)
print(f" 工具调用: {td_total} 次 (失败 {td_errors}, 成功率 {td_success_rate:.0f}%)")
print(f" 工具平均耗时: {avg_ms_str}")
if top3_str:
print(f" Top 工具: {top3_str}")
print()
if run_durations:
avg_run = sum(run_durations) / len(run_durations)
max_run = max(run_durations)
min_run = min(run_durations)
print(f" Run 耗时: 平均 {fmt_duration(avg_run)} 最短 {fmt_duration(min_run)} 最长 {fmt_duration(max_run)}")
if model_times:
avg_model = sum(model_times) / len(model_times)
max_model = max(model_times)
print(f" 模型推理: 平均 {fmt_duration(avg_model)} 最长 {fmt_duration(max_model)}")
# 从 session-based per-call 数据计算平均推理延迟和吞吐量
all_inference_ms = []
all_tokens_per_sec = []
for u in inference_usage.values():
if u.get("inference_ms", 0) > 0:
all_inference_ms.append(u["inference_ms"])
if u.get("tokens_per_sec", 0) > 0:
all_tokens_per_sec.append(u["tokens_per_sec"])
for _, u in text_reply_usage:
if u.get("inference_ms", 0) > 0:
all_inference_ms.append(u["inference_ms"])
if u.get("tokens_per_sec", 0) > 0:
all_tokens_per_sec.append(u["tokens_per_sec"])
# 去重 (同一 usage_record 可能被多个 toolCallId 引用)
seen_ids = set()
dedup_inference_ms = []
dedup_tokens_per_sec = []
for u in inference_usage.values():
if id(u) in seen_ids:
continue
seen_ids.add(id(u))
if u.get("inference_ms", 0) > 0:
dedup_inference_ms.append(u["inference_ms"])
if u.get("tokens_per_sec", 0) > 0:
dedup_tokens_per_sec.append(u["tokens_per_sec"])
for _, u in text_reply_usage:
if id(u) in seen_ids:
continue
seen_ids.add(id(u))
if u.get("inference_ms", 0) > 0:
dedup_inference_ms.append(u["inference_ms"])
if u.get("tokens_per_sec", 0) > 0:
dedup_tokens_per_sec.append(u["tokens_per_sec"])
if dedup_inference_ms:
avg_inf = sum(dedup_inference_ms) / len(dedup_inference_ms)
print(f" 推理延迟: 平均 {fmt_duration(avg_inf)} (基于 session 时间戳, {len(dedup_inference_ms)} 次调用)")
if dedup_tokens_per_sec:
avg_tps = sum(dedup_tokens_per_sec) / len(dedup_tokens_per_sec)
print(f" Token 吞吐: 平均 {avg_tps:.1f} tok/s (基于 session 时间戳, {len(dedup_tokens_per_sec)} 次调用)")
if tool_times:
avg_tool = sum(tool_times) / len(tool_times)
max_tool = max(tool_times)
print(f" 工具执行: 平均 {fmt_duration(avg_tool)} 最长 {fmt_duration(max_tool)}")
# 工具使用统计
if total_tools > 0:
tool_counts = defaultdict(int)
tool_dur = defaultdict(list)
for _, r in sorted_runs:
for tool in r["tools"]:
tool_counts[tool["name"]] += 1
if tool["start"] and tool["end"]:
ts = parse_time(tool["start"])
te = parse_time(tool["end"])
if ts and te:
tool_dur[tool["name"]].append((te - ts).total_seconds() * 1000)
elif tool.get("virtual_duration_ms", 0) > 0:
tool_dur[tool["name"]].append(tool["virtual_duration_ms"])
print()
print(" 工具使用排行:")
for name, count in sorted(tool_counts.items(), key=lambda x: -x[1]):
avg = sum(tool_dur.get(name, [0])) / max(len(tool_dur.get(name, [1])), 1)
print(f" {name:<20} {count:>3}次 平均耗时 {fmt_duration(avg)}")
# Agent 活动分布
agent_stats = defaultdict(lambda: {"infer_count": 0, "total_infer_ms": 0, "total_output": 0, "tool_count": 0, "sessions": set()})
for sref, evts in session_infer_events.items():
agent_name = sref.split(":")[0] if ":" in sref else "unknown"
for evt in evts:
if not (date_matches_utc(evt.get("send_ts", ""), DIAG_DATE) or date_matches_utc(evt.get("recv_ts", ""), DIAG_DATE)):
continue
agent_stats[agent_name]["infer_count"] += 1
agent_stats[agent_name]["total_infer_ms"] += evt.get("inference_ms", 0)
agent_stats[agent_name]["total_output"] += evt.get("output_tokens", 0)
agent_stats[agent_name]["sessions"].add(sref)
# 工具统计(从 tool_details)
for td in tool_details.values():
# 尝试从 sess_ref 推导 agent
td_sref = td.get("sess_ref", "")
td_agent = td_sref.split(":")[0] if ":" in td_sref else ""
if td_agent:
agent_stats[td_agent]["tool_count"] += 1
if len(agent_stats) > 1 or (len(agent_stats) == 1 and list(agent_stats.keys())[0] != "main"):
print()
print(" Agent 活动分布:")
for agent_name in sorted(agent_stats.keys()):
s = agent_stats[agent_name]
infer = s["infer_count"]
total_ms = s["total_infer_ms"]
total_out = s["total_output"]
sessions = len(s["sessions"])
avg_ms_str = fmt_duration(total_ms / infer) if infer > 0 else "N/A"
tps = total_out / (total_ms / 1000) if total_ms > 0 else 0
tps_str = f"{tps:.1f} tok/s" if tps > 0 else "N/A"
print(f" {agent_name:<15} 推理 {infer:>3}次 平均 {avg_ms_str:>8} 吞吐 {tps_str:>10} 会话 {sessions}")
if SUMMARY_ONLY:
if errors:
print()
print(" 最近错误:")
for t, emsg in errors[-5:]:
display = emsg
try:
pj = json.loads(emsg)
if isinstance(pj, dict):
for k in ("error", "message", "msg"):
if k in pj:
display = f"{k}: {pj[k]}"; break
except (json.JSONDecodeError, TypeError, ValueError):
pass
if len(display) > 100:
display = display[:97] + "..."
print(f" {t[11:19]} [ERROR] {display}")
sys.exit(0)
# ============================================================
# 5. 每个 Run 详情
# ============================================================
print()
print("=" * 68)
print(f"{'[Run 详情]':^64}")
print("=" * 68)
for i, (run_id, r) in enumerate(sorted_runs):
if not r["start"]:
continue
start_time = parse_time(r["start"])
end_time = parse_time(r["end"]) if r["end"] else None
total_ms = (end_time - start_time).total_seconds() * 1000 if end_time and start_time else None
print()
if r["end"]:
status = "[完成]"
else:
status = "[进行中]"
if r.get("virtual"):
status = "[虚拟]"
total_str = fmt_duration(total_ms) if total_ms else "进行中"
print(f" Run #{i+1} {status} 总耗时: {total_str}")
print(f" {'-' * 62}")
print(f" Run ID: {run_id}")
print(f" 模型: {r['model']}")
print(f" 渠道: {r['channel']}")
if r["sess_ref"]:
print(f" 会话: {r['sess_ref']}")
if r["prompt_messages"]:
print(f" 历史消息数: {r['prompt_messages']}")
_start_dt = parse_time(r['start'])
_end_dt = parse_time(r['end']) if r['end'] else None
_start_str = _start_dt.strftime("%H:%M:%S.%f")[:-3] if _start_dt else r['start'][11:23]
_end_str = _end_dt.strftime("%H:%M:%S.%f")[:-3] if _end_dt else (r['end'][11:23] if r['end'] else "")
print(f" 开始时间: {_start_str}", end="")
if _end_str:
print(f" 结束时间: {_end_str}")
else:
print()
# 时间线
print()
print(f" {'时间':>12} {'间隔':>9} {'步骤说明'}")
print(f" {'─'*12} {'─'*9} {'─'*42}")
timeline = []
timeline.append((r["start"], "[RUN-START] 开始处理请求"))
for t, msg in r["events"]:
if "pre-prompt" in msg:
detail = ""
if "messages=" in msg:
detail = f", 包含 {msg.split('messages=')[1].split(' ')[0]} 条历史消息"
timeline.append((t, f"[PROMPT] 构建提示词{detail}"))
break
# 收集所有 model send/recv 事件(支持多轮推理)
# 从 session 获取推理事件
matched_session_events = []
sess_ref = r.get("sess_ref", "")
session_uuid = r.get("session_uuid", "")
# 策略1: 通过 session_uuid 匹配 (session_infer_events keyed by "{agent}:{uuid}")
# session_uuid 来自日志的 sessionId=
if session_uuid:
for sref, evts in session_infer_events.items():
# sk format: "main:b5a65d81-..." or just "b5a65d81-..."
if session_uuid in sref:
# 按 Run 时间窗口过滤
run_start_dt = parse_time(r["start"])
run_end_dt = parse_time(r["end"])
if run_start_dt and run_end_dt:
for evt in evts:
evt_send_dt = parse_time(evt["send_ts"])
evt_recv_dt = parse_time(evt["recv_ts"])
if evt_send_dt and evt_recv_dt:
if evt_send_dt >= run_start_dt and evt_recv_dt <= run_end_dt:
matched_session_events.append(evt)
else:
matched_session_events.extend(evts)
break
# 策略2: 通过 sess_ref 精确匹配 (虚拟 Run 使用)
if not matched_session_events and sess_ref and sess_ref in session_infer_events:
matched_session_events = list(session_infer_events[sess_ref])
# 对虚拟 Run,按日期过滤(session 可能跨天)
if r.get("virtual") and DIAG_DATE:
matched_session_events = [e for e in matched_session_events if e["send_ts"][:10] == DIAG_DATE]
# 策略3: 时间窗口匹配 (fallback)
if not matched_session_events and r["start"] and r["end"]:
run_start_dt = parse_time(r["start"])
run_end_dt = parse_time(r["end"])
if run_start_dt and run_end_dt:
for evt in all_infer_events:
try:
evt_send_dt = parse_time(evt["send_ts"])
evt_recv_dt = parse_time(evt["recv_ts"])
if evt_send_dt and evt_recv_dt:
if evt_send_dt >= run_start_dt and evt_recv_dt <= run_end_dt:
matched_session_events.append(evt)
except:
pass
if matched_session_events:
# 使用 session 推理事件
for idx_evt, evt in enumerate(matched_session_events, 1):
send_ts = evt["send_ts"]
recv_ts = evt["recv_ts"]
inf_ms = evt["inference_ms"]
in_tok = evt["input_tokens"]
out_tok = evt["output_tokens"]
cache_read = evt.get("cache_read", 0)
tps = evt["tokens_per_sec"]
timeline.append((send_ts, f"[MODEL-SEND] 模型推理开始 (第{idx_evt}次)"))
recv_detail = f"[MODEL-RECV] 模型推理完成 (第{idx_evt}次) 耗时 {fmt_duration(inf_ms)}"
if in_tok or out_tok:
tok_parts = [f"in={in_tok}", f"out={out_tok}"]
if cache_read:
tok_parts.append(f"cache={cache_read}")
recv_detail += f" | {' '.join(tok_parts)}"
if tps > 0:
recv_detail += f" ({tps:.1f} tok/s)"
timeline.append((recv_ts, recv_detail))
# 标记已使用 session 数据,避免日志重复
used_session_model_events = True
for tool in r["tools"]:
tid = tool["id"]
tname = tool["name"]
# 从会话文件获取工具参数
param_info = tool_params.get(tid, {})
param_summary = param_info.get("summary", "")
param_workdir = param_info.get("workdir", "")
# 构建工具开始的描述
start_label = f"[TOOL-START] 开始执行工具: {tname}"
if param_summary:
start_label += f"\n {'':>9} {'':>13}{param_summary}"
if param_workdir:
start_label += f"\n {'':>9} {'':>13}工作目录: {param_workdir}"
# 虚拟 Run 的工具没有精确时间戳,跳过 timeline 添加
if not tool["start"]:
# 对虚拟 Run,在 timeline 不添加工具的时间戳行
# 但在汇总中通过 virtual_duration_ms 计入
continue
timeline.append((tool["start"], start_label))
if tool["end"]:
ts = parse_time(tool["start"])
te = parse_time(tool["end"])
dur = fmt_duration((te - ts).total_seconds() * 1000) if ts and te else "?"
# 附加 details 信息
detail_extra = ""
td = tool_details.get(tid, {})
if td:
if tname == "exec":
ec = td.get("exitCode")
dm = td.get("durationMs")
parts = []
if ec is not None:
if ec != 0:
parts.append(f"\033[31mexitCode={ec}\033[0m")
else:
parts.append(f"exitCode={ec}")
if dm is not None:
parts.append(f"duration={dm}ms")
if parts:
detail_extra = " " + " ".join(parts)
elif tname == "edit":
diff = td.get("diff", "")
if diff:
diff_short = diff.replace("\n", " ")[:60]
detail_extra = f" diff: {diff_short}"
elif tname == "web_fetch":
took = td.get("tookMs")
if took is not None:
detail_extra = f" took={took}ms"
elif tname == "sessions_spawn":
csk = td.get("child_sess_id", "")
if csk:
detail_extra = f" child={csk}"
if td.get("isError"):
detail_extra += " \033[31m[FAILED]\033[0m"
timeline.append((tool["end"], f"[TOOL-END] 工具执行完成: {tname} (耗时 {dur}){detail_extra}"))
if r["end"]:
timeline.append((r["end"], "[RUN-END] 处理完成, 准备返回结果"))
timeline.sort(key=lambda x: parse_time(x[0]) or datetime.min)
prev = None
for t, label in timeline:
curr = parse_time(t)
ts = curr.strftime("%H:%M:%S.%f")[:-3] if curr else t[11:23]
if prev:
delta_ms = (curr - prev).total_seconds() * 1000
delta_str = fmt_duration(delta_ms)
if delta_ms > 5000:
marker = " << 慢"
elif delta_ms > 1000:
marker = " < 较慢"
else:
marker = ""
else:
delta_str = "---"
marker = ""
prev = curr
# 处理多行标签(工具参数)
lines = label.split("\n")
print(f" {ts:>12} {delta_str:>9} {lines[0]}{marker}")
for extra_line in lines[1:]:
print(f" {extra_line}")
# ========== Run 汇总 (纯 session 数据) ==========
print()
# 从 matched_session_events 计算推理/token 汇总
run_total_input = 0
run_total_output = 0
run_total_cache_read = 0
run_total_cache_write = 0
run_total_tokens = 0
run_inference_count = len(matched_session_events)
total_infer = 0
for evt_s in matched_session_events:
total_infer += evt_s["inference_ms"]
run_total_input += evt_s["input_tokens"]
run_total_output += evt_s["output_tokens"]
run_total_cache_read += evt_s.get("cache_read", 0)
run_total_cache_write += evt_s.get("cache_write", 0)
# 工具总耗时
total_tool = 0
for tool in r["tools"]:
if tool["start"] and tool["end"]:
ts_t = parse_time(tool["start"])
te_t = parse_time(tool["end"])
if ts_t and te_t:
total_tool += (te_t - ts_t).total_seconds() * 1000
elif tool.get("virtual_duration_ms", 0) > 0:
total_tool += tool["virtual_duration_ms"]
run_total_tokens = run_total_input + run_total_output + run_total_cache_read + run_total_cache_write
# --- 汇总输出 ---
print(f" {'─'*62}")
print(f" [Run 汇总]")
print()
if total_ms:
print(f" 端到端耗时: {fmt_duration(total_ms)}")
print(f" 模型推理总耗时: {fmt_duration(total_infer)}", end="")
if total_ms and total_infer > 0:
print(f" ({total_infer/total_ms*100:.0f}%)", end="")
print()
if total_tool > 0:
print(f" 工具执行总耗时: {fmt_duration(total_tool)}", end="")
if total_ms:
print(f" ({total_tool/total_ms*100:.0f}%)", end="")
print()
if total_ms:
other_ms = total_ms - total_infer - total_tool
if other_ms > 200:
print(f" 其他开销: {fmt_duration(other_ms)} ({other_ms/total_ms*100:.0f}%)")
print(f" 推理调用次数: {run_inference_count}")
print(f" 工具调用次数: {len(r['tools'])}")
print()
# token 统计
print(f" Token 统计:")
print(f" 输入 tok: {run_total_input:>8}")
print(f" 输出 tok: {run_total_output:>8}")
print(f" 缓存读取: {run_total_cache_read:>8}")
print(f" 缓存写入: {run_total_cache_write:>8}")
if run_total_output > 0 and total_infer > 0:
tps = run_total_output / (total_infer / 1000)
print(f" 输出速率: {tps:>7.1f} tokens/s")
# 耗时分布条形图
if total_ms and total_ms > 0:
print()
print(f" 耗时分布:")
print(f" 模型推理 {bar(total_infer, total_ms)} {fmt_duration(total_infer):>8}")
if total_tool > 0:
print(f" 工具执行 {bar(total_tool, total_ms)} {fmt_duration(total_tool):>8}")
# 推理分段明细 (纯 session 数据)
if matched_session_events:
print()
print(f" 推理分段明细:")
print(f" {'段':^24} {'耗时':>8} {'输出tok':>10} {'速率':>12}")
print(f" {'─'*24} {'─'*8} {'─'*10} {'─'*12}")
for idx_s, evt in enumerate(matched_session_events, 1):
inf_ms = evt["inference_ms"]
out_tok = evt["output_tokens"]
tps = evt["tokens_per_sec"]
label = f"推理#{idx_s}"
dur_str = fmt_duration(inf_ms) if inf_ms > 0 else "-"
out_str = str(out_tok) if out_tok > 0 else "(未知)"
rate_str = f"{tps:.1f} tok/s" if tps > 0 else "-"
print(f" {label:<24} {dur_str:>8} {out_str:>10} {rate_str:>12}")
# ============================================================
# 6. 错误列表
# ============================================================
if errors:
print()
print("=" * 68)
print(f"{'[错误列表]':^64}")
print("=" * 68)
shown = min(len(errors), 20)
print(f" 共 {len(errors)} 条错误,显示最近 {shown} 条:")
print()
INDENT = " "
MAX_WIDTH = 120
for idx, (t, emsg) in enumerate(errors[-20:], 1):
# 尝试从 JSON 格式消息中提取关键字段
display_msg = emsg
try:
parsed_json = json.loads(emsg)
if isinstance(parsed_json, dict):
key_parts = []
for k in ("error", "message", "msg", "reason", "description"):
if k in parsed_json:
key_parts.append(f"{k}: {parsed_json[k]}")
if key_parts:
display_msg = " | ".join(key_parts)
except (json.JSONDecodeError, TypeError, ValueError):
pass
print(f" #{idx:<3} {t[11:19]} [ERROR]")
# 自动换行缩进
line = display_msg
while len(line) > MAX_WIDTH:
# 找一个合适的断点
cut = MAX_WIDTH
# 尝试在空格处断开
sp = line.rfind(" ", 0, cut)
if sp > cut // 2:
cut = sp + 1
print(f"{INDENT}{line[:cut]}")
line = line[cut:]
if line:
print(f"{INDENT}{line}")
print()
print()
print("=" * 68)
PYEOF
搜索和获取中国软考(计算机技术与软件专业技术资格考试)高级科目历年真题与模拟题。 覆盖全部5个高级科目:系统架构设计师、信息系统项目管理师、系统分析师、网络规划设计师、系统规划与管理师。 支持真题查询(2020-2025)、模拟题练习(5套完整三科)、论文范文、案例分析技巧。 支持按科目、知识领域、时间范围、科目...
---
name: ruankao-questions
version: 3.1.0
description: >
搜索和获取中国软考(计算机技术与软件专业技术资格考试)高级科目历年真题与模拟题。
覆盖全部5个高级科目:系统架构设计师、信息系统项目管理师、系统分析师、网络规划设计师、系统规划与管理师。
支持真题查询(2020-2025)、模拟题练习(5套完整三科)、论文范文、案例分析技巧。
支持按科目、知识领域、时间范围、科目类型筛选。
触发词:软考真题、架构师真题、历年真题、刷题、模拟题、模拟卷、ruankao questions、搜索真题、软考模拟。
metadata:
openclaw:
tools:
- exec
- web_search
- web_fetch
- read
---
# 软考高级真题与模拟题搜索
> ⚠️ 本 Skill 仅支持**软考高级科目**,不支持中级和初级。
## 使用方式
用户提供以下参数(均可选,至少一个):
- **考试科目**:系统架构设计师、信息系统项目管理师、系统分析师、网络规划设计师、系统规划与管理师(默认:系统架构设计师)
- **知识领域**:如"设计模式"、"架构风格"、"质量属性"、"微服务"等
- **时间范围**:如"2024"、"2020-2025"、"最近三年"
- **科目类型**:综合知识 / 案例分析 / 论文(默认全部)
- **内容类型**:真题 / 模拟题(默认真题)
示例:
```
搜索真题:设计模式 2020-2025
搜索真题:微服务架构 案例分析
搜索真题:2024下半年 论文
搜索真题:信息系统项目管理师 2025
搜索模拟题:系统架构设计师 卷3 AI
搜索模拟题:综合知识 押题
```
## 搜索流程
### 第1步:加载数据源配置
读取 `references/sources.md` 获取数据源和科目映射。
### 第2步:确定搜索范围
根据用户输入确定:
- 目标考试科目(仅限5个高级科目)
- 目标年份列表
- 目标知识领域关键词
- 目标科目类型(综合/案例/论文)
- 内容类型(真题/模拟题)
> 如果用户查询中级或初级科目,回复"本资料库仅收录高级科目真题"并建议其他资源。
### 第3步:从 awesome-ruankao 仓库检索(最高优先)
**wujiaming88/awesome-ruankao** (GitHub)(内容已验证,纯 Markdown 可直接读取):
#### 真题检索
```bash
# 列出某科目所有年份
gh api "repos/wujiaming88/awesome-ruankao/contents/真题/{科目名}" --jq '.[].name'
# 获取某年份文件列表
gh api "repos/wujiaming88/awesome-ruankao/contents/真题/{科目名}/{年份目录}" --jq '.[].name'
# 获取文件下载链接并读取
gh api "repos/wujiaming88/awesome-ruankao/contents/真题/{科目名}/{年份目录}/综合知识.md" --jq '.download_url'
# 然后 web_fetch 该URL获取Markdown内容
```
#### 模拟题检索
```bash
# 列出模拟题目录
gh api "repos/wujiaming88/awesome-ruankao/contents/模拟题/系统架构设计师/2026年5月" --jq '.[].name'
# 获取模拟卷内容
gh api "repos/wujiaming88/awesome-ruankao/contents/模拟题/系统架构设计师/2026年5月/模拟卷{N}/综合知识_题目.md" --jq '.download_url'
```
### 第4步:Web搜索兜底(按需)
当仓库中找不到目标内容时:
```
web_search("{科目} {年份} {科目类型} 真题 {知识点}")
```
**有效搜索站点**:
- 博客园 cnblogs.com — 最容易抓取,内容质量高
- 信管网 cnitpm.com — 项目管理类真题权威来源
- 环球网校 hqwx.com — 综合知识题目较全
- 51CTO题库 t.51cto.com — 在线做题
- 软考在线 rkpass.cn — 多科目在线练习
**不推荐**:CSDN(521反爬)、知乎(403)、希赛网(引流页)
### 第5步:整理输出
**真题输出格式:**
```markdown
## 搜索结果:{科目} | {时间范围} | {科目类型}
### {年份}{上/下半年} - {科目类型}
**第 X 题**:
(题目内容)
- A. ...
- B. ...
- C. ...
- D. ...
**答案**:X
**解析**:(解析内容)
---
来源:awesome-ruankao 资料库
```
**模拟题输出格式:**
```markdown
## 模拟题:{科目} | {卷号} | {科目类型}
> 📝 卷号定位:{卷特色描述}
> 难度分布:简单25% | 中等50% | 困难25%
### 第 X 题:
(题目内容)
**答案**:X
**解析**:(解析内容)
---
来源:awesome-ruankao 模拟题库 · 模拟卷{N}
```
## 科目覆盖(仅高级)
| 科目 | 真题年份 | 覆盖范围 | 模拟题 |
|------|---------|----------|--------|
| 系统架构设计师 | 2020-2025(8次考试) | 综合知识+案例分析+论文 | ✅ 5套完整三科 |
| 信息系统项目管理师 | 2021-2025(8次考试) | 综合知识+案例分析+论文 | — |
| 系统分析师 | 2021-2025(7次考试) | 综合知识+案例分析+论文 | — |
| 网络规划设计师 | 2021-2025(5次考试) | 综合知识+案例分析+论文 | — |
| 系统规划与管理师 | 2021-2025(5次考试) | 综合知识+案例分析+论文 | — |
## 模拟题库
### 系统架构设计师 — 2026年5月模拟题(5套)
| 卷号 | 定位 | 特色 |
|------|------|------|
| 模拟卷1 | 均衡入门 | AI热点10+道,综合覆盖 |
| 模拟卷2 | 架构安全 | 服务网格/DevSecOps/隐私计算重点 |
| 模拟卷3 | AI深度 | 13道AI题(Transformer/MoE/RAG/Agent/量化) |
| 模拟卷4 | 法规数学 | 数学已验算,法规+项目管理重点 |
| 模拟卷5 | 🎯冲刺押题 | 每题标注押题理由 |
每套包含:综合知识(题目+答案)+ 案例分析(题目+答案)+ 论文(题目+答案)= 6个文件
**仓库路径**:`模拟题/系统架构设计师/2026年5月/模拟卷{1-5}/`
## 考试安排要点
- **系统架构设计师**:2024年起每年2次(上半年+下半年)
- **信息系统项目管理师**:2024年起调整为一年1次(上半年5月)
- **系统分析师**:传统上半年考试,2024年起增加下半年
- **网络规划设计师**:每年仅下半年1次(11月)
- **系统规划与管理师**:2023年及以前上半年,2024年起改为下半年
- **全面机考**:2024年起所有科目实行计算机化考试
## 注意事项
- 本资料库**仅收录高级科目**,中级/初级请参考其他资源
- 本地资料库内容已验证,优先使用
- 回忆版真题标注"回忆版,可能与原题有出入"
- 搜索结果按年份倒序排列(最新优先)
- 每次搜索最多展示10道相关题目,避免信息过载
- 模拟题与真题分开展示,明确标注来源
FILE:references/sources.md
# 真题与模拟题数据源配置
## 数据源
1. **wujiaming88/awesome-ruankao** (GitHub)(唯一主数据源)
2. **Web搜索兜底**(仓库中找不到时)
---
## 源: wujiaming88/awesome-ruankao (GitHub)
- **API**: `gh api repos/wujiaming88/awesome-ruankao/contents/{path}`
- **分支**: `main`
- **覆盖**: 5个高级科目,2020-2025年,166个文件,9MB
- **特点**: 内容已验证、纯Markdown可直接web_fetch、含完整答案解析、含模拟题
### 目录结构
```
真题/
├── 系统架构设计师/ ← 最完整(2020-2025,8次考试三科齐全)
│ ├── 2025年下半年/ 综合知识+案例分析+论文
│ ├── 2025年上半年/ 综合知识+案例分析+论文
│ ├── 2024年下半年/ 综合知识+案例分析+论文
│ ├── 2024年上半年/ 综合知识+案例分析+论文
│ ├── 2023年下半年/ 三科齐全
│ ├── 2022年下半年/ 三科齐全
│ ├── 2021年下半年/ 三科齐全
│ └── 2020年下半年/ 三科齐全
├── 信息系统项目管理师/ ← 2021-2025(8次考试)
│ ├── 2025年上半年/
│ ├── 2024年上半年/
│ ├── 2023年下半年/
│ ├── 2023年上半年/
│ ├── 2022年下半年/
│ ├── 2022年上半年/
│ ├── 2021年下半年/
│ └── 2021年上半年/
├── 系统分析师/ ← 2021-2025(7次考试)
│ ├── 2025年下半年/
│ ├── 2025年上半年/
│ ├── 2024年下半年/
│ ├── 2024年上半年/
│ ├── 2023年上半年/
│ ├── 2022年上半年/
│ └── 2021年上半年/
├── 网络规划设计师/ ← 2021-2025(5次下半年考试)
│ ├── 2025年下半年/
│ ├── 2024年下半年/
│ ├── 2023年下半年/
│ ├── 2022年下半年/
│ └── 2021年下半年/
└── 系统规划与管理师/ ← 2021-2025(5次考试)
├── 2025年下半年/
├── 2024年下半年/
├── 2023年上半年/
├── 2022年上半年/
└── 2021年上半年/
模拟题/
└── 系统架构设计师/
└── 2026年5月/
├── 模拟卷1/ 综合知识_题目/答案 + 案例分析_题目/答案 + 论文_题目/答案
├── 模拟卷2/ 同上(架构安全重点)
├── 模拟卷3/ 同上(AI深度卷)
├── 模拟卷4/ 同上(法规数学重点)
└── 模拟卷5/ 同上(冲刺押题卷)
论文/ 万能模板+范文集
案例分析/ 解题框架+质量属性专题
docs/备考攻略/ 学习路线+技巧
docs/政策与考试指南/ 2025-2026政策、机考改革
资源/ 教材书单、视频课程
```
### 访问示例
```bash
# 列出所有科目
gh api "repos/wujiaming88/awesome-ruankao/contents/真题" --jq '.[].name'
# 列出某科目年份
gh api "repos/wujiaming88/awesome-ruankao/contents/真题/系统架构设计师" --jq '.[].name'
# 获取文件内容
gh api "repos/wujiaming88/awesome-ruankao/contents/真题/系统架构设计师/2024年下半年/综合知识.md" --jq '.download_url'
# 然后 web_fetch 该URL
# 列出模拟题
gh api "repos/wujiaming88/awesome-ruankao/contents/模拟题/系统架构设计师/2026年5月" --jq '.[].name'
# 获取模拟卷内容
gh api "repos/wujiaming88/awesome-ruankao/contents/模拟题/系统架构设计师/2026年5月/模拟卷3/综合知识_题目.md" --jq '.download_url'
```
---
## Web 搜索兜底
当 awesome-ruankao 仓库中找不到目标内容时使用。
### 推荐站点
| 站点 | URL | 适用内容 | 可抓取性 |
|------|-----|----------|----------|
| 博客园 | cnblogs.com | 综合知识、案例分析 | ✅ 好 |
| 信管网 | cnitpm.com | 项目管理类真题 | ✅ 好 |
| 环球网校 | hqwx.com | 综合知识题目 | ✅ 好 |
| 51CTO题库 | t.51cto.com | 在线做题 | ✅ 好 |
| 软考在线 | rkpass.cn | 多科目在线练习 | ✅ 好 |
| IT顾问 | itgu.com | 论文真题 | ✅ 好 |
### 不推荐站点
| 站点 | 问题 |
|------|------|
| CSDN | 全站521反爬,不可用 |
| 知乎 | 403封禁爬虫 |
| 希赛网 educity.cn | 引流页,内容在PDF下载后 |
### 搜索模板
```
{科目名} {年份} {科目类型} 真题 答案 解析
site:cnblogs.com {科目名} {年份} 真题
site:cnitpm.com {科目名} {年份} 真题
```
---
## 知识领域映射
| 知识领域 | 关键词 | 常考科目类型 |
|----------|--------|-------------|
| 软件架构设计 | 架构风格、ABSD、DSSA、ATAM、SAD | 综合+案例 |
| 设计模式 | 23种GoF、创建型、结构型、行为型 | 综合+案例 |
| 质量属性 | 可用性、性能、安全性、可修改性、效用树 | 案例(必考) |
| 数据库 | 规范化、ER图、NoSQL、Redis、MongoDB | 综合+案例 |
| 信息安全 | 加密、PKI、数字签名、访问控制、零信任 | 综合 |
| 计算机网络 | TCP/IP、VPN、防火墙、HTTP、SDN | 综合 |
| 新技术 | 云原生、微服务、大数据、AI、区块链、边缘计算、数字孪生 | 综合+论文 |
| AI与大模型 | Transformer、MoE、RAG、Agent、RLHF、量化 | 综合+论文(2024起高频) |
| 软件工程 | 需求工程、UML、测试、DFD | 综合+案例 |
| 分布式系统 | 分布式锁、一致性哈希、CAP、消息队列、Service Mesh | 案例 |
| 项目管理 | 进度、风险、成本、挣值、敏捷 | 综合 |
| 法律法规 | 知识产权、著作权、标准化 | 综合 |
| 数学算法 | 图论、概率、组合 | 综合 |
| 专业英语 | 技术英语阅读 | 综合 |
## 科目类型
| 科目 | 代号 | 说明 |
|------|------|------|
| 综合知识 | choice | 75道单选题,150分钟 |
| 案例分析 | case | 问答题,1必答+4选2,90分钟 |
| 论文 | essay | 4选1,摘要300字+正文2200字,120分钟 |
生成中国软考系统架构设计师论文。根据论题和要求,按照万能模板结构生成高分论文。 论文包含摘要(300-330字)和正文(2000-2500字)五段式结构:项目背景、理论过渡、技术实践、总结展望。 使用场景:当用户提供软考论文题目、系统架构设计师考试论文、软考论文练习时触发。 触发词:软考论文、架构设计师论文、系统...
--- name: ruankao-essay description: > 生成中国软考系统架构设计师论文。根据论题和要求,按照万能模板结构生成高分论文。 论文包含摘要(300-330字)和正文(2000-2500字)五段式结构:项目背景、理论过渡、技术实践、总结展望。 使用场景:当用户提供软考论文题目、系统架构设计师考试论文、软考论文练习时触发。 触发词:软考论文、架构设计师论文、系统架构师论文、ruankao essay、软考作文。 --- # 软考系统架构设计师论文生成 ## 使用方式 用户提供: - **论题**(必填):如"论微服务架构设计" - **论题要求/子问题**(必填):题目中给出的具体要求和问题 - **项目背景**(可选):用户自定义项目信息;未提供则自动构造合理项目背景 ## 生成流程 1. 读取模板:`read references/template.md` 2. 分析论题,提取需要覆盖的技术要点(通常2-3个) 3. 按模板五段式结构生成论文 4. 检查字数:摘要300-330字,正文2000-2500字 5. 检查高分要素是否全部满足 ## 论文结构(严格遵循) | 段落 | 内容 | 字数 | |------|------|------| | 摘要 | 项目名+角色+主题+技术点+结果 | 300-330字 | | 第1段 | 项目背景(公司/痛点/模块/规模/角色) | 400-500字 | | 第2段 | 理论过渡(定义/概念/选型理由) | ~250字 | | 第3段 | 技术实践(2-3个要点,含问题和解决方案)⭐ | 800-1000字 | | 第4段 | 总结展望(成果/不足1-2点/改进/收获) | 300-400字 | ## 关键规则 ### 必须做到 - 每段开头有明确主题句 - 技术实践段必须有**具体技术名词**、**具体做法**、**遇到的问题**、**解决方案** - 必须写1-2个不足之处(显得真实客观) - 项目背景要合理可信(公司类型与项目规模匹配) - 角色固定为"系统架构设计师" ### 必须避免 - 通篇理论无项目细节 - 流水账无重点 - 项目背景不合理 - 缺少不足和展望 ## 项目背景自动构造规则 当用户未提供项目背景时,根据论题自动构造: - 选择与论题匹配的行业(如微服务→电商/金融;大数据→物流/医疗) - 公司规模中等偏上(团队15-25人,用户10-50万) - 开发周期6-12个月 - 上线时间设为2025年 ## 输出格式 ``` 【摘要】 (摘要内容,300-330字) 【正文】 一、项目背景 (400-500字) 二、XXX的理论概述 (200-300字) 三、XXX在项目中的实践 (一)技术要点1 ... (二)技术要点2 ... (三)技术要点3(如有) ... (800-1000字) 四、总结与展望 (300-400字) ``` ## 字数统计 生成完毕后,输出各段字数和总字数统计,确保满足要求。 FILE:references/template.md # 软考系统架构设计师论文万能模板 ## 总要求 - 摘要:300-330字 - 正文:2000-2500字 - 总计:约2500字以上 --- ## 一、摘要(300-330字) ### 模板 ``` 2024年X月,我参与了XX公司的XXX系统的开发建设,该系统主要用于XXX(一句话说清系统功能)。我在该项目中担任系统架构设计师,负责整体架构设计工作。 本文结合该项目实践,讨论了XXX(论文主题)在项目中的应用。首先介绍了项目背景和架构需求,然后重点阐述了**(技术要点1)、(技术要点2)、(技术要点3)**的具体实践过程,最后总结了项目的实施效果和个人心得。 该系统于2025年X月上线运行,目前运行稳定,得到了用户的一致好评,取得了良好的社会效益和经济效益。 ``` ### 要点 项目名 + 角色 + 主题 + 技术点 + 结果,缺一不可。 --- ## 二、正文第1段:项目背景(400-500字) ### 目标 让阅卷老师相信"你确实做过这个项目"。 ### 必写内容 - 项目所属公司/单位 - 为什么做这个系统(业务痛点) - 系统主要功能模块 - 项目规模(团队人数、开发周期、用户量) - 你的角色和职责 ### 模板句式 ``` 随着XX行业的快速发展,XX公司原有系统面临XXX问题(性能瓶颈/维护困难/扩展性不足等)。为了解决上述问题,公司决定开发XXX系统。该系统主要包括XX模块、XX模块、XX模块等,服务于约XX万用户。项目团队共XX人,开发周期约X个月。我作为系统架构设计师,主要负责技术选型、架构设计和核心模块的技术攻关。 ``` --- ## 三、正文第2段:理论过渡(~250字) ### 目标 简要阐述论文主题的理论知识,展示你懂理论。 ### 必写内容 - 论文主题的定义 - 核心概念和关键技术点 - 为什么选择这个技术方案 ### 模板句式 ``` XXX(论文主题)是指XXX(定义)。其核心思想包括XXX、XXX、XXX。与传统的XXX方案相比,XXX具有XXX、XXX等优势。结合本项目的业务特点和质量属性需求,我们决定采用XXX方案进行架构设计。 ``` --- ## 四、正文第3段:技术实践(800-1000字)⭐ 最重要 ### 目标 得分关键,必须具体、有细节。 ### 结构 分2-3个技术要点展开,每个要点包含: - 怎么做的? - 遇到什么问题? - 怎么解决的? ### 示例(微服务架构) ``` (1)服务拆分策略 我们按照业务领域将系统拆分为用户服务、订单服务、支付服务、库存服务等X个微服务。拆分时遵循"高内聚、低耦合"原则,每个服务独立数据库... (2)服务通信机制 服务间同步调用采用RESTful API + OpenFeign,异步通信采用RabbitMQ消息队列。为解决分布式事务问题,我们采用了Saga模式... (3)服务治理 使用Spring Cloud Gateway作为API网关,Nacos实现服务注册与发现,Sentinel实现熔断限流... ``` ### 高分秘诀 必须有:具体技术名词 + 具体做法 + 遇到的问题 + 解决方案 --- ## 五、正文第4段:总结与展望(300-400字) ### 必写内容 - 项目成果(上线时间、运行效果、用户反馈) - 不足之处(必须写1-2点,显得真实客观) - 改进方向 - 个人收获 ### 模板句式 ``` 该系统于2025年X月正式上线,截至目前已稳定运行X个月,日均处理XX万笔业务,系统可用性达99.9%。通过采用XXX架构,系统性能提升了XX%,运维效率显著提高。 但项目也存在一些不足:一是XXX(比如监控体系还不够完善);二是XXX(比如部分服务的自动化测试覆盖率不够高)。后续我们计划引入XXX进行改进。 通过本项目的实践,我深刻认识到XXX的重要性,也积累了宝贵的架构设计经验,为今后的工作打下了坚实基础。 ``` --- ## 高分要素总结 ### ✅ 要做到 1. 结构清晰,分段明确(阅卷老师30秒扫一篇) 2. 技术细节充分,体现"你真的做过" 3. 理论和实践结合,不能只有理论空话 4. 写1-2个不足,显得真实客观 5. 字数达标(2500字以上) ### ❌ 要避免 1. 通篇理论,没有项目细节 2. 流水账,没有重点 3. 项目背景编得太假(比如"某大型央企"但细节全是小公司的) 4. 不足和展望不写