技能备份 - 2026-04-15 (40个技能)
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user