Skip to content

Commit 55eec27

Browse files
committed
```
feat(uiux): 添加UI/UX Pro Max功能增强 - 添加UI/UX意图、上下文策略和原因字段到McpRequest类型定义 - 实现条件性上下文构建逻辑,支持UI/UX上下文策略控制 - 添加UI/UX Pro Max配置项,包括默认语言、输出格式、结果上限等 - 实现UI/UX上下文信号透传机制 - 新增BeautifyResult、PersistSummary等UI/UX相关数据结构 - 实现结果数量上限控制和UI提示词美化功能 - 添加路径净化工具防止路径穿越安全问题 - 实现统一响应结构和文案本地化支持 - 支持多语言输出(中文/英文)和多种输出格式 ```
1 parent 9c8d006 commit 55eec27

File tree

12 files changed

+730
-113
lines changed

12 files changed

+730
-113
lines changed

src/frontend/types/popup.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ export interface McpRequest {
66
predefined_options?: string[]
77
is_markdown?: boolean
88
project_root_path?: string
9+
uiux_intent?: 'none' | 'beautify' | 'page_refactor' | 'uiux_search'
10+
uiux_context_policy?: 'auto' | 'force' | 'forbid'
11+
uiux_reason?: string
912
}
1013

1114
// 自定义prompt类型定义

src/frontend/utils/conditionalContext.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
1-
import type { CustomPrompt } from '../types/popup'
1+
import type { CustomPrompt, McpRequest } from '../types/popup'
22

33
// 复用条件性 prompt 的上下文拼接逻辑,保持与弹窗输入一致
4-
export function buildConditionalContext(prompts: CustomPrompt[]): string {
4+
export function buildConditionalContext(prompts: CustomPrompt[], request?: McpRequest | null): string {
55
const conditionalTexts: string[] = []
66

7+
// 根据 UI/UX 上下文策略决定是否追加条件性上下文
8+
const intent = request?.uiux_intent ?? 'none'
9+
const policy = request?.uiux_context_policy ?? 'auto'
10+
if (policy === 'forbid' || (policy === 'auto' && intent === 'none')) {
11+
return ''
12+
}
13+
714
prompts.forEach((prompt) => {
815
const isEnabled = prompt.current_state ?? false
916
const template = isEnabled ? prompt.template_true : prompt.template_false

src/rust/config/settings.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,16 @@ pub struct McpConfig {
128128
pub context7_api_key: Option<String>, // Context7 API密钥 (可选,免费使用时可为空)
129129
pub skill_python_path: Option<String>, // Skill Python 路径(可选,默认走 PATH)
130130

131+
// UI/UX Pro Max 配置
132+
/// 默认语言("zh" | "en")
133+
pub uiux_default_lang: Option<String>,
134+
/// 默认输出格式("json" | "text")
135+
pub uiux_output_format: Option<String>,
136+
/// 最大结果数上限(默认 10)
137+
pub uiux_max_results_cap: Option<u32>,
138+
/// 是否启用 UI 提示词美化(默认 true)
139+
pub uiux_beautify_enabled: Option<bool>,
140+
131141
// 图标工坊配置
132142
/// 默认保存路径(相对于项目根目录,如 "assets/icons")
133143
pub icon_default_save_path: Option<String>,
@@ -318,6 +328,11 @@ pub fn default_mcp_config() -> McpConfig {
318328
acemcp_proxy_password: None,
319329
context7_api_key: None,
320330
skill_python_path: None,
331+
// UI/UX Pro Max 默认配置
332+
uiux_default_lang: Some("zh".to_string()),
333+
uiux_output_format: Some("json".to_string()),
334+
uiux_max_results_cap: Some(10),
335+
uiux_beautify_enabled: Some(true),
321336
// 图标工坊配置默认值
322337
icon_default_save_path: None, // 使用默认 "assets/icons"
323338
icon_default_format: None, // 默认 SVG

src/rust/mcp/tools/interaction/mcp.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ impl InteractionTool {
2525
},
2626
is_markdown: request.is_markdown,
2727
project_root_path: request.project_root_path,
28+
// 透传 UI/UX 上下文控制信号
29+
uiux_intent: request.uiux_intent,
30+
uiux_context_policy: request.uiux_context_policy,
31+
uiux_reason: request.uiux_reason,
2832
};
2933

3034
match create_tauri_popup(&popup_request) {

src/rust/mcp/tools/uiux/engine.rs

Lines changed: 98 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use serde::Serialize;
1313
use serde_json::Value;
1414

1515
use crate::log_debug;
16+
use super::sanitize::{sanitize_path_segment, sanitize_slug};
1617

1718
const MAX_RESULTS: usize = 3;
1819
const BOX_WIDTH: usize = 90;
@@ -534,6 +535,26 @@ pub struct SuggestResult {
534535
pub matched_keywords: Vec<String>,
535536
}
536537

538+
#[derive(Debug, Clone, Serialize)]
539+
pub struct BeautifyResult {
540+
pub style: Vec<HashMap<String, String>>,
541+
pub color: Vec<HashMap<String, String>>,
542+
pub typography: Vec<HashMap<String, String>>,
543+
}
544+
545+
#[derive(Debug, Clone, Serialize)]
546+
pub struct PersistSummary {
547+
pub design_system_dir: String,
548+
pub created_files: Vec<String>,
549+
}
550+
551+
#[derive(Debug, Clone)]
552+
pub struct DesignSystemOutput {
553+
pub design_system: DesignSystem,
554+
pub persisted: Option<PersistSummary>,
555+
pub formatted: String,
556+
}
557+
537558
struct UiuxStore {
538559
domains: HashMap<&'static str, DomainIndex>,
539560
stacks: HashMap<&'static str, DomainIndex>,
@@ -647,6 +668,13 @@ fn normalize_format(value: Option<&str>, default: &str) -> String {
647668
.to_lowercase()
648669
}
649670

671+
/// 结果数量上限控制,避免过大输出
672+
pub fn cap_max_results(value: Option<u32>, cap: u32, default: u32) -> usize {
673+
let cap = cap.max(1);
674+
let raw = value.unwrap_or(default).max(1);
675+
raw.min(cap) as usize
676+
}
677+
650678
pub fn search_domain(query: &str, domain: Option<&str>, max_results: Option<usize>) -> SearchResult {
651679
let store = &*UIUX_STORE;
652680
let requested_domain = domain.unwrap_or_else(|| detect_domain(query));
@@ -769,6 +797,38 @@ pub fn format_search_json(result: &SearchResult) -> Result<String, String> {
769797
serde_json::to_string_pretty(result).map_err(|e| format!("JSON 序列化失败: {}", e))
770798
}
771799

800+
/// UI 提示词美化:基于现有数据组合,避免重复
801+
pub fn beautify_prompt(query: &str, max_results: usize) -> BeautifyResult {
802+
let limit = max_results.max(1);
803+
let style = search_domain(query, Some("style"), Some(limit));
804+
let color = search_domain(query, Some("color"), Some(limit));
805+
let typography = search_domain(query, Some("typography"), Some(limit));
806+
807+
BeautifyResult {
808+
style: dedupe_results(style.results),
809+
color: dedupe_results(color.results),
810+
typography: dedupe_results(typography.results),
811+
}
812+
}
813+
814+
fn dedupe_results(rows: Vec<HashMap<String, String>>) -> Vec<HashMap<String, String>> {
815+
let mut seen = HashSet::new();
816+
let mut output = Vec::new();
817+
for row in rows {
818+
let mut items: Vec<_> = row.iter().collect();
819+
items.sort_by(|a, b| a.0.cmp(b.0));
820+
let signature = items
821+
.iter()
822+
.map(|(k, v)| format!("{}={}", k, v))
823+
.collect::<Vec<_>>()
824+
.join("|");
825+
if seen.insert(signature) {
826+
output.push(row);
827+
}
828+
}
829+
output
830+
}
831+
772832
#[derive(Debug, Clone, Serialize)]
773833
pub struct PatternInfo {
774834
pub name: String,
@@ -1993,25 +2053,18 @@ fn format_page_override_md(design_system: &DesignSystem, page_name: &str, page_q
19932053
lines.join("\n")
19942054
}
19952055

1996-
struct PersistResult {
1997-
design_system_dir: PathBuf,
1998-
created_files: Vec<PathBuf>,
1999-
}
2000-
20012056
fn persist_design_system(
20022057
design_system: &DesignSystem,
20032058
page: Option<&str>,
20042059
output_dir: Option<&Path>,
20052060
page_query: Option<&str>,
2006-
) -> Result<PersistResult, String> {
2061+
) -> Result<PersistSummary, String> {
20072062
let base_dir = output_dir
20082063
.map(|p| p.to_path_buf())
20092064
.unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
20102065

2011-
let project_slug = design_system
2012-
.project_name
2013-
.to_lowercase()
2014-
.replace(' ', "-");
2066+
// 关键路径使用净化结果防止路径穿越
2067+
let project_slug = sanitize_slug(&design_system.project_name);
20152068

20162069
let design_system_dir = base_dir.join("design-system").join(&project_slug);
20172070
let pages_dir = design_system_dir.join("pages");
@@ -2026,16 +2079,19 @@ fn persist_design_system(
20262079
let mut created = vec![master_file];
20272080

20282081
if let Some(page_name) = page {
2029-
let page_slug = page_name.to_lowercase().replace(' ', "-");
2082+
let page_slug = sanitize_path_segment(page_name);
20302083
let page_file = pages_dir.join(format!("{}.md", page_slug));
20312084
std::fs::write(&page_file, format_page_override_md(design_system, page_name, page_query))
20322085
.map_err(|e| format!("写入页面覆盖文件失败: {}", e))?;
20332086
created.push(page_file);
20342087
}
20352088

2036-
Ok(PersistResult {
2037-
design_system_dir,
2038-
created_files: created,
2089+
Ok(PersistSummary {
2090+
design_system_dir: design_system_dir.to_string_lossy().to_string(),
2091+
created_files: created
2092+
.into_iter()
2093+
.map(|path| path.to_string_lossy().to_string())
2094+
.collect(),
20392095
})
20402096
}
20412097

@@ -2046,56 +2102,54 @@ pub fn generate_design_system(
20462102
persist: bool,
20472103
page: Option<&str>,
20482104
output_dir: Option<&Path>,
2049-
) -> Result<String, String> {
2105+
) -> Result<DesignSystemOutput, String> {
20502106
let generator = DesignSystemGenerator::new();
20512107
let design_system = generator.generate(query, project_name);
20522108

2053-
if persist {
2054-
let persist_result = persist_design_system(&design_system, page, output_dir, Some(query))?;
2055-
let mut output = match normalize_format(format, "ascii").as_str() {
2056-
"markdown" => format_markdown(&design_system),
2057-
_ => format_ascii_box(&design_system),
2058-
};
2109+
let persisted = if persist {
2110+
Some(persist_design_system(&design_system, page, output_dir, Some(query))?)
2111+
} else {
2112+
None
2113+
};
20592114

2060-
let project_slug = design_system
2061-
.project_name
2062-
.to_lowercase()
2063-
.replace(' ', "-");
2064-
output.push_str("\n");
2065-
output.push_str(&"=".repeat(60));
2066-
output.push_str("\n");
2067-
output.push_str(&format!(
2115+
let mut formatted = match normalize_format(format, "ascii").as_str() {
2116+
"markdown" => format_markdown(&design_system),
2117+
_ => format_ascii_box(&design_system),
2118+
};
2119+
2120+
if persist {
2121+
let project_slug = sanitize_slug(&design_system.project_name);
2122+
formatted.push_str("\n");
2123+
formatted.push_str(&"=".repeat(60));
2124+
formatted.push_str("\n");
2125+
formatted.push_str(&format!(
20682126
"✅ Design system persisted to design-system/{}/\n",
20692127
project_slug
20702128
));
2071-
output.push_str(&format!(
2129+
formatted.push_str(&format!(
20722130
" 📄 design-system/{}/MASTER.md (Global Source of Truth)\n",
20732131
project_slug
20742132
));
20752133
if let Some(page_name) = page {
2076-
let page_slug = page_name.to_lowercase().replace(' ', "-");
2077-
output.push_str(&format!(
2134+
let page_slug = sanitize_path_segment(page_name);
2135+
formatted.push_str(&format!(
20782136
" 📄 design-system/{}/pages/{}.md (Page Overrides)\n",
20792137
project_slug, page_slug
20802138
));
20812139
}
2082-
output.push_str("\n");
2083-
output.push_str(&format!(
2140+
formatted.push_str("\n");
2141+
formatted.push_str(&format!(
20842142
"📖 Usage: When building a page, check design-system/{}/pages/[page].md first.\n",
20852143
project_slug
20862144
));
2087-
output.push_str(" If exists, its rules override MASTER.md. Otherwise, use MASTER.md.\n");
2088-
output.push_str(&"=".repeat(60));
2089-
2090-
// 避免未使用告警
2091-
let _ = persist_result;
2092-
2093-
return Ok(output);
2145+
formatted.push_str(" If exists, its rules override MASTER.md. Otherwise, use MASTER.md.\n");
2146+
formatted.push_str(&"=".repeat(60));
20942147
}
20952148

2096-
Ok(match normalize_format(format, "ascii").as_str() {
2097-
"markdown" => format_markdown(&design_system),
2098-
_ => format_ascii_box(&design_system),
2149+
Ok(DesignSystemOutput {
2150+
design_system,
2151+
persisted,
2152+
formatted,
20992153
})
21002154
}
21012155

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// UI/UX Pro Max 文案本地化
2+
// 仅提供 zh/en 简洁文案,避免过度设计
3+
4+
use super::engine::{SearchResult, SuggestResult};
5+
use super::types::{UiuxLang, UiuxMode};
6+
7+
pub fn localize_text(lang: UiuxLang, zh: &str, en: &str) -> String {
8+
match lang {
9+
UiuxLang::Zh => zh.to_string(),
10+
UiuxLang::En => en.to_string(),
11+
}
12+
}
13+
14+
pub fn error_text(lang: UiuxLang, message: &str) -> String {
15+
match lang {
16+
UiuxLang::Zh => format!("发生错误: {}", message),
17+
UiuxLang::En => format!("Error: {}", message),
18+
}
19+
}
20+
21+
pub fn search_summary(lang: UiuxLang, mode: UiuxMode, result: &SearchResult) -> String {
22+
if let Some(err) = &result.error {
23+
return error_text(lang, err);
24+
}
25+
26+
match mode {
27+
UiuxMode::Beautify => localize_text(lang, "已生成 UI 美化建议。", "UI beautify suggestions generated."),
28+
_ => {
29+
let zh = format!("已获取检索结果,领域:{},共 {} 条。", result.domain, result.count);
30+
let en = format!("Search completed. Domain: {}. Results: {}.", result.domain, result.count);
31+
localize_text(lang, &zh, &en)
32+
}
33+
}
34+
}
35+
36+
pub fn stack_summary(lang: UiuxLang, result: &SearchResult) -> String {
37+
if let Some(err) = &result.error {
38+
return error_text(lang, err);
39+
}
40+
41+
let stack = result.stack.clone().unwrap_or_else(|| "-".to_string());
42+
let zh = format!("已获取栈指南:{},共 {} 条。", stack, result.count);
43+
let en = format!("Stack guidelines: {}. Results: {}.", stack, result.count);
44+
localize_text(lang, &zh, &en)
45+
}
46+
47+
pub fn design_system_summary(lang: UiuxLang, project_name: &str, persisted: bool) -> String {
48+
if persisted {
49+
let zh = format!("已生成并写入设计系统:{}。", project_name);
50+
let en = format!("Design system generated and persisted: {}.", project_name);
51+
localize_text(lang, &zh, &en)
52+
} else {
53+
let zh = format!("已生成设计系统建议:{}。", project_name);
54+
let en = format!("Design system recommendations generated: {}.", project_name);
55+
localize_text(lang, &zh, &en)
56+
}
57+
}
58+
59+
pub fn beautify_summary(lang: UiuxLang) -> String {
60+
localize_text(lang, "已生成 UI 美化建议。", "UI beautify suggestions generated.")
61+
}
62+
63+
pub fn suggest_summary(lang: UiuxLang, result: &SuggestResult) -> String {
64+
if result.should_suggest {
65+
let keywords = if result.matched_keywords.is_empty() {
66+
"-".to_string()
67+
} else {
68+
result.matched_keywords.join(", ")
69+
};
70+
let zh = format!("建议使用 UI/UX 工具,匹配关键词:{}。", keywords);
71+
let en = format!("UI/UX tool suggested. Matched keywords: {}.", keywords);
72+
localize_text(lang, &zh, &en)
73+
} else {
74+
localize_text(lang, "暂无明显 UI/UX 需求。", "No strong UI/UX signal detected.")
75+
}
76+
}

0 commit comments

Comments
 (0)