技能备份 - 2026-04-15 (40个技能)

This commit is contained in:
root
2026-04-15 18:53:15 +08:00
parent f62f14814f
commit c65fce24e4
791 changed files with 190773 additions and 0 deletions
+61
View File
@@ -0,0 +1,61 @@
---
name: pm-visualizer
description: Visualizes Product Manager thoughts (Why, What, How, User Journey) into an editable Excalidraw diagram. Use when the user asks to "visualize specs", "create a PM diagram", or "map out product thoughts".
---
# PM Visualizer Skill
This skill converts unstructured Product Manager thoughts into a structured Excalidraw visualization.
## Features
- **Smart Layout**: Automatically columns "Why, What, How" and creates a horizontal flow for "User Journey".
- **Color Coding**: Visual distinction between problem (Why - Yellow), solution (What - Green), implementation (How - Blue), and flow (Journey - Red/Pink).
- **Grouped Elements**: Text is properly bound to containers so they move together.
## Workflow
1. **Analyze Request**: Extract the following sections from the user's prompt or context:
* **Title**: The feature or product name.
* **Why**: The problem statement, business goals, or "Why are we building this?".
* **What**: The solution requirements, features, or "What is it?".
* **How**: Technical implementation details, API strategy, or "How will we build it?".
* **Journey**: A sequential list of steps for the user journey or process flow.
2. **Prepare Data**: Create a JSON file (e.g., `temp_visual_data.json`) with this structure:
```json
{
"title": "Feature Name",
"why": ["Reason 1", "Reason 2"],
"what": ["Feature 1", "Feature 2"],
"how": ["Tech 1", "Tech 2"],
"journey": ["Step 1", "Step 2", "Step 3"]
}
```
3. **Generate Diagram**: Run the python script to generate the `.excalidraw` file.
```bash
python3 skills/pm-visualizer/scripts/layout_diagram.py temp_visual_data.json ~/Downloads/Documents/PM_Visuals/Output_Name.excalidraw
```
*Ensure the output directory exists first.*
4. **Cleanup**: Delete the temporary JSON input file.
5. **Report**: Inform the user the file is ready at the output path.
## Example
**User:** "Visualize a new 'Login with Google' feature. Why? Reduce friction. What? Google button on login page. How? OAuth2. Journey: User clicks button -> Google Popup -> Redirect to Dashboard."
**Codex Action:**
1. Create `login_spec.json`:
```json
{
"title": "Login with Google",
"why": ["Reduce friction", "Increase conversion"],
"what": ["Google Sign-in Button", "Profile Sync"],
"how": ["OAuth 2.0 Flow", "Google Identity SDK"],
"journey": ["User clicks 'Sign in with Google'", "Google permissions popup appears", "User approves access", "System verifies token", "User redirected to Dashboard"]
}
```
2. `mkdir -p ~/Downloads/Documents/PM_Visuals`
3. `python3 skills/pm-visualizer/scripts/layout_diagram.py login_spec.json ~/Downloads/Documents/PM_Visuals/Login_Spec.excalidraw`
+6
View File
@@ -0,0 +1,6 @@
{
"ownerId": "kn74gzdqfnysavbywf64wpkj0s80810s",
"slug": "thought-to-excalidraw",
"version": "1.0.0",
"publishedAt": 1769883872038
}
@@ -0,0 +1,98 @@
# Excalidraw JSON Schema Reference
An `.excalidraw` file is a JSON object with the following top-level structure:
```json
{
"type": "excalidraw",
"version": 2,
"source": "https://excalidraw.com",
"elements": [ ... ],
"appState": {
"viewBackgroundColor": "#ffffff",
"gridSize": null
}
}
```
## Element Types
### Common Properties (All Elements)
All elements share these fields:
- `id`: string (unique, e.g., "node-1")
- `x`: number (x position)
- `y`: number (y position)
- `width`: number
- `height`: number
- `angle`: number (usually 0)
- `strokeColor`: string (hex code, e.g., "#000000")
- `backgroundColor`: string (hex code, e.g., "#transparent")
- `fillStyle`: "hachure" | "cross-hatch" | "solid"
- `strokeWidth`: number (1 or 2)
- `strokeStyle`: "solid" | "dashed" | "dotted"
- `roughness`: number (0-2, 1 is standard hand-drawn look)
- `opacity`: number (100)
- `groupIds`: []
- `roundness`: { "type": 3 } (for rounded corners) or null
- `seed`: number (random integer)
- `version`: number (incrementing)
- `versionNonce`: number (random integer)
- `isDeleted`: false
- `boundElements`: [{ "id": "arrow-id", "type": "arrow" }] (for connecting arrows)
### 1. Rectangle (Container/Box)
```json
{
"type": "rectangle",
"label": { "text": "Label inside?" } // NOTE: Excalidraw uses separate "text" elements bound to containers usually, but simple rects just exist.
}
```
*Note: To put text 'inside' a box, creating a separate "text" element is often safer/standard, centered on the rect.*
### 2. Ellipse (Start/End nodes)
```json
{
"type": "ellipse"
}
```
### 3. Diamond (Decision)
```json
{
"type": "diamond"
}
```
### 4. Text (Labels)
```json
{
"type": "text",
"text": "Actual content\nNew line",
"fontSize": 20,
"fontFamily": 1, // 1: Virgil (Hand), 2: Helvetica, 3: Cascadia
"textAlign": "center", // "left", "center", "right"
"verticalAlign": "middle"
}
```
### 5. Arrow (Connectors)
Arrows connect two elements.
```json
{
"type": "arrow",
"points": [[0, 0], [100, 50]], // Relative points from x,y. [0,0] is the start.
"startBinding": { "elementId": "node-1", "focus": 0.5, "gap": 1 },
"endBinding": { "elementId": "node-2", "focus": 0.5, "gap": 1 }
}
```
*Tip: If binding is too complex to calc, just placing start/end points near the center of nodes works visualy.*
## Layout Strategy (Mental Model)
For a "Why, What, How" tree:
1. **Why (Root)**: Top or Left.
2. **What (Children)**: Level 2.
3. **How (Grandchildren)**: Level 3.
**User Journey**:
Linear flow: Step 1 -> Arrow -> Step 2 -> Arrow -> Step 3.
@@ -0,0 +1,295 @@
import json
import sys
import random
import time
def generate_id():
return ''.join(random.choices('abcdefghijklmnopqrstuvwxyz0123456789', k=10))
def create_element(type, x, y, width, height, **kwargs):
group_ids = kwargs.pop('groupIds', [])
return {
"id": generate_id(),
"type": type,
"x": x,
"y": y,
"width": width,
"height": height,
"angle": 0,
"strokeColor": "#000000",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": group_ids,
"roundness": {"type": 3} if type == "rectangle" else None,
"seed": random.randint(1, 100000),
"version": 1,
"versionNonce": random.randint(1, 100000),
"isDeleted": False,
"boundElements": [],
"updated": int(time.time() * 1000),
**kwargs
}
def wrap_text(text, max_chars):
words = text.split(' ')
lines = []
current_line = []
current_length = 0
for word in words:
if current_length + len(word) + 1 > max_chars:
lines.append(' '.join(current_line))
current_line = [word]
current_length = len(word)
else:
current_line.append(word)
current_length += len(word) + 1
if current_line:
lines.append(' '.join(current_line))
return '\n'.join(lines)
def estimate_text_dims(text, fontSize):
lines = text.split('\n')
max_line_chars = max(len(line) for line in lines) if lines else 0
text_width = max(50, max_line_chars * (fontSize * 0.6))
text_height = len(lines) * (fontSize * 1.5)
return text_width, text_height
def create_text(x, y, text, fontSize=20, groupIds=[], width=None, textAlign="left", verticalAlign="top"):
lines = text.split('\n')
if width is None:
width = max(len(line) for line in lines) * (fontSize * 0.6)
height = len(lines) * (fontSize * 1.25)
return create_element("text", x, y, width, height,
text=text, fontSize=fontSize, fontFamily=1,
textAlign=textAlign, verticalAlign=verticalAlign, groupIds=groupIds
)
def create_smart_box_with_text(x, y, text, fontSize=16, fixed_width=None, bgColor="transparent", group=None):
if group is None:
group = generate_id()
padding_x = 20
padding_y = 20
final_text = text
if fixed_width:
char_w = fontSize * 0.6
available_w = fixed_width - (padding_x * 2)
max_chars = int(available_w / char_w)
final_text = wrap_text(text, max_chars)
box_width = fixed_width
else:
final_text = wrap_text(text, 30)
t_w, _ = estimate_text_dims(final_text, fontSize)
box_width = t_w + (padding_x * 2)
t_w, t_h = estimate_text_dims(final_text, fontSize)
box_height = t_h + (padding_y * 2)
box = create_element("rectangle", x, y, box_width, box_height, backgroundColor=bgColor, groupIds=[group])
text_x = x + (box_width - t_w) / 2
text_y = y + (box_height - t_h) / 2
text_el = create_element("text", text_x, text_y, t_w, t_h,
text=final_text, fontSize=fontSize, fontFamily=1,
textAlign="center", verticalAlign="middle", groupIds=[group]
)
box["boundElements"] = [{"type": "text", "id": text_el["id"]}]
text_el["containerId"] = box["id"]
return box, text_el, box_width, box_height
def create_container_frame(x, y, w, h, label, color="#000000"):
frame_group = generate_id()
frame = create_element("rectangle", x, y, w, h,
backgroundColor="transparent", strokeStyle="dashed", strokeWidth=2,
strokeColor=color, opacity=50, roughness=2, groupIds=[frame_group]
)
label_w, label_h = estimate_text_dims(label, 20)
label_el = create_element("text", x + 10, y - label_h - 5, label_w, label_h,
text=label, fontSize=20, fontFamily=1, strokeColor=color, groupIds=[frame_group]
)
return [frame, label_el]
def create_arrow(start_id, end_id, start_x, start_y, end_x, end_y):
return create_element("arrow", start_x, start_y, end_x - start_x, end_y - start_y,
points=[[0, 0], [end_x - start_x, end_y - start_y]],
startBinding={"elementId": start_id, "focus": 0, "gap": 10},
endBinding={"elementId": end_id, "focus": 0, "gap": 10},
strokeWidth=2, endArrowhead="arrow"
)
def main():
if len(sys.argv) < 3:
print("Usage: python layout_diagram.py <input.json> <output.excalidraw>")
sys.exit(1)
input_path = sys.argv[1]
output_path = sys.argv[2]
with open(input_path, 'r') as f:
data = json.load(f)
# --- LAYOUT CONFIGURATION ---
START_X = 100
START_Y = 100
COL_GAP = 40
ROW_GAP = 30
# Colors
C_WHY = "#ffec99" # Yellow
C_WHAT = "#b2f2bb" # Green
C_HOW = "#a5d8ff" # Blue
C_JOURNEY = "#ffc9c9" # Red
# PM Framework Explanations (The "Meta-Text")
PM_GUIDES = {
"Why": "Focus on: User pain points,\nbusiness value, and 'Why now?'",
"What": "Focus on: Key features,\nfunctional requirements, and MVP scope.",
"How": "Focus on: Technical implementation,\ndata flow, and feasibility.",
"Journey": "Focus on: The step-by-step flow from user trigger to goal completion."
}
final_elements = []
# ==========================================
# 1. STRATEGY CORE (Why, What, How)
# ==========================================
strategy_elements = []
COL_WIDTH = 300
cols_data = [
("Why", data.get("why", []), C_WHY),
("What", data.get("what", []), C_WHAT),
("How", data.get("how", []), C_HOW)
]
current_col_x = START_X + 40 # Padding for frame
strategy_start_y = START_Y + 60 # Padding for frame
max_strategy_y = strategy_start_y
for title, items, color in cols_data:
# Column Title
t_box, t_txt, _, t_h = create_smart_box_with_text(
current_col_x, strategy_start_y, title, fontSize=24, fixed_width=COL_WIDTH, bgColor="transparent"
)
t_box['strokeStyle'] = "solid"
t_box['backgroundColor'] = "#ffffff"
t_box['strokeWidth'] = 2
# Meta-Text (Guide)
guide_y = strategy_start_y + t_h + 10
guide_text = create_text(current_col_x, guide_y, PM_GUIDES[title], fontSize=14, textAlign="center", width=COL_WIDTH)
guide_text['strokeColor'] = "#868e96" # Grey
strategy_elements.extend([t_box, t_txt, guide_text])
item_y = guide_y + guide_text['height'] + 30 # Gap before first box
for item in items:
box, txt, w, h = create_smart_box_with_text(
current_col_x, item_y, item, fontSize=16, fixed_width=COL_WIDTH, bgColor=color
)
strategy_elements.extend([box, txt])
item_y += h + ROW_GAP
if item_y > max_strategy_y:
max_strategy_y = item_y
current_col_x += COL_WIDTH + COL_GAP
# Calculate bounding box for Strategy Section
strat_w = (current_col_x - COL_GAP) - START_X + 40
strat_h = (max_strategy_y) - START_Y + 20
strat_frame = create_container_frame(START_X, START_Y, strat_w, strat_h, "Strategy Core", "#555555")
final_elements.extend(strat_frame)
final_elements.extend(strategy_elements)
# ==========================================
# 2. USER JOURNEY (Adaptive Layout)
# ==========================================
journey = data.get("journey", [])
if journey:
journey_start_y = START_Y + strat_h + 100 # Gap between sections
journey_start_x = START_X
journey_elements = []
IS_VERTICAL = len(journey) > 6
# Guide for Journey
j_guide_text = create_text(journey_start_x + 40, journey_start_y + 40, PM_GUIDES["Journey"], fontSize=14, textAlign="left", width=600)
j_guide_text['strokeColor'] = "#868e96"
journey_elements.append(j_guide_text)
prev_id = None
prev_bounds = None
cur_jx = journey_start_x + 40
cur_jy = journey_start_y + 80 # Adjusted for guide
max_j_w = 0
max_j_h = 0
for step in journey:
box, txt, w, h = create_smart_box_with_text(
cur_jx, cur_jy, step, fontSize=16, fixed_width=None, bgColor=C_JOURNEY
)
box['roundness'] = {"type": 3}
if w < 150: pass
journey_elements.extend([box, txt])
if prev_id:
px, py, pw, ph = prev_bounds
if IS_VERTICAL:
s_x, s_y = px + pw/2, py + ph
e_x, e_y = box['x'] + box['width']/2, box['y']
else:
s_x, s_y = px + pw, py + ph/2
e_x, e_y = box['x'], box['y'] + box['height']/2
arrow = create_arrow(prev_id, box['id'], s_x, s_y, e_x, e_y)
journey_elements.append(arrow)
prev_id = box['id']
prev_bounds = (box['x'], box['y'], box['width'], box['height'])
if IS_VERTICAL:
cur_jy += h + 60
right_edge = cur_jx + w
if right_edge > max_j_w: max_j_w = right_edge
max_j_h = cur_jy
else:
cur_jx += w + 60
bottom_edge = cur_jy + h
if bottom_edge > max_j_h: max_j_h = bottom_edge
max_j_w = cur_jx
j_frame_w = (max_j_w - journey_start_x) + 40
if IS_VERTICAL: j_frame_w += 40
j_frame_h = (max_j_h - journey_start_y)
if not IS_VERTICAL: j_frame_h += 40
j_frame = create_container_frame(journey_start_x, journey_start_y, j_frame_w, j_frame_h, "User Journey", "#555555")
final_elements.extend(j_frame)
final_elements.extend(journey_elements)
title_text = data.get("title", "Product Requirements")
final_elements.append(create_text(START_X, START_Y - 120, title_text, fontSize=40))
output_data = {
"type": "excalidraw",
"version": 2,
"source": "https://openclaw.ai",
"elements": final_elements,
"appState": {"viewBackgroundColor": "#ffffff", "gridSize": None},
"files": {}
}
with open(output_path, 'w') as f:
json.dump(output_data, f, indent=2)
print(f"Generated diagram with {len(final_elements)} elements.")
if __name__ == "__main__":
main()