Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions super_dev/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1338,6 +1338,7 @@ def _cmd_run_targeted_refresh(self, target: str) -> int:
style_solution=config.style_solution,
state_management=list(config.state_management or []),
testing_frameworks=list(config.testing_frameworks or []),
design_inspiration_slug=str(getattr(config, "design_inspiration_slug", "") or ""),
language_preferences=list(config.language_preferences or []),
knowledge_summary=knowledge_summary,
)
Expand Down Expand Up @@ -2794,6 +2795,9 @@ def _flush_resume_audit(status: str, failure_reason: str = "") -> None:
frontend=args.frontend,
backend=args.backend,
domain=args.domain,
design_inspiration_slug=str(
getattr(pipeline_config, "design_inspiration_slug", "") or ""
),
language_preferences=pipeline_config.language_preferences,
knowledge_summary=(
knowledge_bundle.get("research_summary", {})
Expand Down
203 changes: 200 additions & 3 deletions super_dev/cli_experience_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import argparse
import json
import sys
from datetime import datetime, timezone
from pathlib import Path

from .catalogs import (
Expand All @@ -14,7 +15,7 @@
PIPELINE_FRONTEND_TEMPLATE_IDS,
PLATFORM_IDS,
)
from .config import ConfigManager
from .config import ConfigManager, get_config_manager

SUPPORTED_PLATFORMS = list(PLATFORM_IDS)
SUPPORTED_PIPELINE_FRONTENDS = list(PIPELINE_FRONTEND_TEMPLATE_IDS)
Expand Down Expand Up @@ -292,9 +293,205 @@ def _cmd_create(self, args) -> int:

return 0

def _resolve_design_command_context(
self,
*,
idea: str = "",
frontend: str = "",
product_type: str = "",
industry: str = "",
style: str = "",
) -> dict[str, object]:
project_dir = Path.cwd()
config_manager = get_config_manager(project_dir)
config = config_manager.load()

config_exists = config_manager.exists()
base_name = str(config.name or "").strip() if config_exists else project_dir.name
if not base_name:
base_name = project_dir.name or "my-project"
project_name = self._sanitize_project_name(base_name)
description = str(idea or "").strip() or str(config.description or "").strip() or project_name
platform_value = str(config.platform or "web").strip() or "web"
frontend_value = str(frontend or config.frontend or "next").strip() or "next"
backend_value = str(config.backend or "node").strip() or "node"
domain_value = str(config.domain or "").strip()

from .creators import DocumentGenerator

generator = DocumentGenerator(
name=project_name,
description=description,
platform=platform_value,
frontend=frontend_value,
backend=backend_value,
domain=domain_value,
ui_library=config.ui_library,
style_solution=config.style_solution,
state_management=list(config.state_management or []),
testing_frameworks=list(config.testing_frameworks or []),
design_inspiration_slug=str(getattr(config, "design_inspiration_slug", "") or ""),
language_preferences=list(config.language_preferences or []),
)
analysis = generator._analyze_project_for_design()
if product_type:
analysis["product_type"] = product_type.strip().lower()
if industry:
analysis["industry"] = industry.strip().lower()
if style:
analysis["style"] = style.strip().lower()

return {
"project_dir": project_dir,
"project_name": project_name,
"description": description,
"platform": platform_value,
"frontend": frontend_value,
"backend": backend_value,
"domain": domain_value,
"analysis": analysis,
"config_manager": config_manager,
"config_exists": config_exists,
}

def _render_design_inspiration_list(self, inspirations: list[dict[str, object]]) -> None:
if not inspirations:
self.console.print("[yellow]未找到匹配的设计灵感锚点[/yellow]")
return

self.console.print(f"[green]设计灵感锚点 ({len(inspirations)} 个):[/green]\n")
for idx, item in enumerate(inspirations, 1):
signals = " / ".join(str(signal) for signal in list(item.get("signals", []))[:3])
self.console.print(
f"[cyan]{idx}. {item.get('name', 'N/A')}[/cyan] [dim]slug={item.get('slug', 'N/A')}[/dim]"
)
self.console.print(f" 方向: {item.get('direction', 'N/A')}")
self.console.print(f" 理由: {item.get('rationale', 'N/A')}")
if signals:
self.console.print(f" 参考信号: {signals}")
source = str(item.get("source", "")).strip()
if source:
self.console.print(f" 来源: {source}")
self.console.print()

def _cmd_design(self, args) -> int:
"""设计智能引擎命令"""
from .design import DesignIntelligenceEngine, DesignSystemGenerator, TokenGenerator
from .design import (
DesignIntelligenceEngine,
DesignSystemGenerator,
TokenGenerator,
UIIntelligenceAdvisor,
)

if args.design_command == "list":
advisor = UIIntelligenceAdvisor()
inspirations = [
item.to_dict()
for item in advisor.list_design_references(
product_type=str(getattr(args, "product_type", "") or "").strip().lower() or None,
industry=str(getattr(args, "industry", "") or "").strip().lower() or None,
style=str(getattr(args, "style", "") or "").strip().lower() or None,
frontend=str(getattr(args, "frontend", "") or "").strip() or None,
limit=max(int(getattr(args, "max_results", 10) or 10), 1),
)
]
self._render_design_inspiration_list(inspirations)
return 0

if args.design_command == "recommend":
advisor = UIIntelligenceAdvisor()
context = self._resolve_design_command_context(
idea=str(getattr(args, "idea", "") or ""),
frontend=str(getattr(args, "frontend", "") or ""),
product_type=str(getattr(args, "product_type", "") or ""),
industry=str(getattr(args, "industry", "") or ""),
style=str(getattr(args, "style", "") or ""),
)
analysis = context["analysis"]
if not isinstance(analysis, dict):
self.console.print("[red]无法解析当前项目的设计上下文[/red]")
return 1

profile = advisor.recommend(
description=str(context["description"]),
frontend=str(context["frontend"]),
product_type=str(analysis.get("product_type", "general")),
industry=str(analysis.get("industry", "general")),
style=str(analysis.get("style", "modern")),
)
inspirations = [
item
for item in list(profile.get("design_references", []))[: max(int(getattr(args, "max_results", 3) or 3), 1)]
if isinstance(item, dict)
]

self.console.print("[cyan]设计灵感推荐[/cyan]")
self.console.print(
f" 项目: {context['project_name']} | 前端: {context['frontend']} | 产品类型: {analysis.get('product_type', 'general')}"
)
self.console.print(
f" 行业: {analysis.get('industry', 'general')} | 风格: {analysis.get('style', 'modern')}"
)
self.console.print(" 真源: 内部仍以 output/*-uiux.md + output/*-ui-contract.json 为准\n")
self._render_design_inspiration_list(inspirations)
return 0

if args.design_command == "apply":
advisor = UIIntelligenceAdvisor()
selected = advisor.get_design_reference(args.slug)
if selected is None:
self.console.print(f"[red]未知设计灵感 slug: {args.slug}[/red]")
self.console.print("[dim]先运行 `super-dev design list` 查看可用 slug[/dim]")
return 1

context = self._resolve_design_command_context(idea=str(getattr(args, "idea", "") or ""))
config_manager = context["config_manager"]
if not isinstance(config_manager, ConfigManager):
self.console.print("[red]无法加载项目配置管理器[/red]")
return 1

if not bool(context["config_exists"]):
config_manager.create(
name=str(context["project_name"]),
description=str(context["description"]),
platform=str(context["platform"]),
frontend=str(context["frontend"]),
backend=str(context["backend"]),
domain=str(context["domain"]),
)

update_payload: dict[str, object] = {"design_inspiration_slug": selected.slug}
if str(getattr(args, "idea", "") or "").strip() and not str(config_manager.config.description or "").strip():
update_payload["description"] = str(getattr(args, "idea", "")).strip()
updated_config = config_manager.update(**update_payload)

output_dir = Path.cwd() / str(updated_config.output_dir or "output")
output_dir.mkdir(parents=True, exist_ok=True)
project_name = self._sanitize_project_name(str(updated_config.name or context["project_name"]))
record_path = output_dir / f"{project_name}-design-inspiration.json"
record_payload = {
"slug": selected.slug,
"name": selected.name,
"rationale": selected.rationale,
"direction": selected.direction,
"source": selected.source,
"signals": list(selected.signals),
"cautions": list(selected.cautions),
"applied_at": datetime.now(timezone.utc).isoformat(),
}
record_path.write_text(
json.dumps(record_payload, ensure_ascii=False, indent=2),
encoding="utf-8",
)

self.console.print(f"[green]✓[/green] 已应用设计灵感: {selected.name} ({selected.slug})")
self.console.print(f" 来源: {selected.source}")
self.console.print(f" 已写入配置: design_inspiration_slug = {selected.slug}")
self.console.print(f" 记录文件: {record_path}")

if getattr(args, "write_uiux", True):
return self._cmd_run_targeted_refresh("uiux")
return 0

if args.design_command == "search":
# 搜索设计资产
Expand Down Expand Up @@ -898,6 +1095,6 @@ def _cmd_design(self, args) -> int:

else:
self.console.print("[yellow]请指定设计子命令[/yellow]")
self.console.print(" 可用命令: search, generate, tokens, landing, chart, ux, stack, codegen")
self.console.print(" 可用命令: list, recommend, apply, search, generate, tokens, landing, chart, ux, stack, codegen")
self.console.print(" 使用 'super-dev design <command> -h' 查看帮助")
return 1
52 changes: 52 additions & 0 deletions super_dev/cli_parser_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -1068,6 +1068,58 @@ def _create_parser(self) -> argparse.ArgumentParser:
description="使用 'super-dev design <command> -h' 查看帮助",
)

design_list_parser = design_subparsers.add_parser(
"list",
help="列出设计灵感锚点",
description="列出内置设计灵感库,供 UI/UX 方向选择与参考",
)
design_list_parser.add_argument("--product-type", help="产品类型过滤 (landing/saas/dashboard/content/ecommerce)")
design_list_parser.add_argument("--industry", help="行业过滤 (fintech/education/healthcare/general)")
design_list_parser.add_argument("--style", help="风格过滤 (minimal/professional/playful/luxury/modern)")
design_list_parser.add_argument("--frontend", help="前端栈过滤 (react/vue/next/uni-app/electron 等)")
design_list_parser.add_argument(
"-n", "--max-results", type=int, default=10, help="最大结果数 (默认: 10)"
)

design_recommend_parser = design_subparsers.add_parser(
"recommend",
help="推荐设计灵感锚点",
description="结合当前项目配置或需求描述,推荐最合适的设计灵感方向",
)
design_recommend_parser.add_argument(
"--idea", default="", help="显式需求描述;未提供时优先读取 super-dev.yaml 中的 description"
)
design_recommend_parser.add_argument("--product-type", help="显式指定产品类型")
design_recommend_parser.add_argument("--industry", help="显式指定行业")
design_recommend_parser.add_argument("--style", help="显式指定风格")
design_recommend_parser.add_argument("--frontend", help="显式指定前端栈")
design_recommend_parser.add_argument(
"-n", "--max-results", type=int, default=3, help="最大推荐数 (默认: 3)"
)

design_apply_parser = design_subparsers.add_parser(
"apply",
help="应用设计灵感锚点",
description="将指定设计灵感写入项目配置,并可同步重生成 uiux/ui-contract",
)
design_apply_parser.add_argument("slug", help="设计灵感 slug,例如 linear.app / vercel / stripe")
design_apply_parser.add_argument(
"--idea", default="", help="可选需求描述;仅在当前项目 description 为空时作为补充上下文"
)
design_apply_parser.add_argument(
"--write-uiux",
dest="write_uiux",
action="store_true",
help="应用后同步重生成 output/*-uiux.md 和 output/*-ui-contract.json(默认开启)",
)
design_apply_parser.add_argument(
"--no-write-uiux",
dest="write_uiux",
action="store_false",
help="仅写入配置与灵感记录,不重生成 UI 文档",
)
design_apply_parser.set_defaults(write_uiux=True)

# design search
design_search_parser = design_subparsers.add_parser(
"search", help="搜索设计资产", description="搜索 UI 风格、配色、字体、组件等设计资产"
Expand Down
22 changes: 17 additions & 5 deletions super_dev/config/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class ProjectConfig:
style_solution: str | None = None # 样式方案
state_management: list[str] = field(default_factory=list) # 状态管理
testing_frameworks: list[str] = field(default_factory=list) # 测试框架
design_inspiration_slug: str = "" # 显式设计灵感锚点

# 领域知识
domain: str = "" # fintech, ecommerce, medical, social, iot, education
Expand Down Expand Up @@ -113,6 +114,16 @@ class ConfigManager:
"language_preferences": [],
"knowledge_allowed_domains": [],
"knowledge_cache_ttl_seconds": 1800,
"phases": [
"discovery",
"intelligence",
"drafting",
"redteam",
"qa",
"delivery",
"deployment",
],
"experts": ["PM", "ARCHITECT", "UI", "UX", "SECURITY", "CODE"],
"quality_gate": 80,
"host_compatibility_min_score": 80,
"host_compatibility_min_ready_hosts": 1,
Expand All @@ -130,6 +141,7 @@ class ConfigManager:
"style_solution": None,
"state_management": [],
"testing_frameworks": [],
"design_inspiration_slug": "",
}

def __init__(self, project_dir: Path | None = None):
Expand Down Expand Up @@ -169,11 +181,14 @@ def load(self) -> ProjectConfig:
loaded = yaml.safe_load(f)
data = loaded if isinstance(loaded, dict) else {}

# Validate against schema (warnings only, not blocking)
# 合并默认配置
config_data: dict[str, Any] = {**self.DEFAULT_CONFIG, **data}

# Validate merged config (warnings only, not blocking)
try:
from .schema_validator import validate_config

schema_errors = validate_config(data)
schema_errors = validate_config(config_data)
if schema_errors:
import logging

Expand All @@ -184,9 +199,6 @@ def load(self) -> ProjectConfig:
except Exception:
pass

# 合并默认配置
config_data: dict[str, Any] = {**self.DEFAULT_CONFIG, **data}

# 过滤掉 ProjectConfig 不支持的字段,避免 TypeError
valid_fields = {f.name for f in dataclasses.fields(ProjectConfig)}
config_data = {k: v for k, v in config_data.items() if k in valid_fields}
Expand Down
13 changes: 13 additions & 0 deletions super_dev/config/schema_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,19 @@
"knowledge_allowed_domains",
"knowledge_cache_ttl_seconds",
"language_preferences",
"ui_library",
"style_solution",
"state_management",
"testing_frameworks",
"design_inspiration_slug",
"database",
"author",
"execution_mode",
"overseer_enabled",
"codex_review_enabled",
"codex_review_phases",
"overseer_halt_on_critical",
"plan_failure_budget",
}


Expand Down Expand Up @@ -103,6 +112,10 @@ def validate_config(config: dict) -> list[str]:
if ttl is not None and (not isinstance(ttl, int) or ttl <= 0):
errors.append("'knowledge_cache_ttl_seconds' must be a positive integer if present")

design_inspiration_slug = config.get("design_inspiration_slug")
if design_inspiration_slug is not None and not isinstance(design_inspiration_slug, str):
errors.append("'design_inspiration_slug' must be a string if present")

return errors


Expand Down
Loading
Loading