Skip to content

Commit 039feaf

Browse files
MarkShawn2020claude
andcommitted
feat(workspace): 添加项目诊断功能
- 添加 diagnostics.rs 后端模块 - 添加 ProjectDiagnostics.tsx 诊断视图 - 添加 FilePath.tsx 共享组件 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 57d2e42 commit 039feaf

11 files changed

Lines changed: 806 additions & 29 deletions

File tree

marketplace/lovstudio/statuslines/vibe-genius.sh

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,11 @@ detect_provider() {
160160

161161
case "$base_url" in
162162
*"zenmux"*) echo "zenmux" ;;
163-
*"openrouter"*) echo "openrouter" ;;
164163
*"modelgate"*) echo "modelgate" ;;
164+
*"qiniu"*) echo "qiniu" ;;
165+
*"siliconflow"*) echo "siliconflow" ;;
166+
*"univibe"*) echo "univibe" ;;
167+
*"openrouter"*) echo "openrouter" ;;
165168
*"openai"*) echo "openai" ;;
166169
*"anthropic"*) echo "anthropic" ;;
167170
*"localhost"*|*"127.0.0.1"*) echo "local" ;;

src-tauri/src/diagnostics.rs

Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
use regex::Regex;
2+
use serde::{Deserialize, Serialize};
3+
use std::collections::HashSet;
4+
use std::fs;
5+
use std::path::Path;
6+
7+
#[derive(Debug, Clone, Serialize, Deserialize)]
8+
pub struct TechStack {
9+
pub runtime: String, // node, python, rust, unknown
10+
pub package_manager: Option<String>,
11+
pub orm: Option<String>,
12+
pub frameworks: Vec<String>,
13+
}
14+
15+
#[derive(Debug, Clone, Serialize, Deserialize)]
16+
pub struct LeakedSecret {
17+
pub file: String,
18+
pub line: usize,
19+
pub key_name: String,
20+
pub preview: String, // 脱敏预览
21+
}
22+
23+
#[derive(Debug, Clone, Serialize, Deserialize)]
24+
pub struct EnvCheckResult {
25+
pub missing_keys: Vec<String>,
26+
pub leaked_secrets: Vec<LeakedSecret>,
27+
pub env_example_exists: bool,
28+
pub env_exists: bool,
29+
}
30+
31+
/// 检测项目技术栈
32+
pub fn detect_tech_stack(project_path: &str) -> Result<TechStack, String> {
33+
let path = Path::new(project_path);
34+
35+
let mut stack = TechStack {
36+
runtime: "unknown".to_string(),
37+
package_manager: None,
38+
orm: None,
39+
frameworks: Vec::new(),
40+
};
41+
42+
// Node.js 检测
43+
let package_json_path = path.join("package.json");
44+
if package_json_path.exists() {
45+
stack.runtime = "node".to_string();
46+
47+
// 检测包管理器
48+
if path.join("pnpm-lock.yaml").exists() {
49+
stack.package_manager = Some("pnpm".to_string());
50+
} else if path.join("yarn.lock").exists() {
51+
stack.package_manager = Some("yarn".to_string());
52+
} else if path.join("package-lock.json").exists() {
53+
stack.package_manager = Some("npm".to_string());
54+
} else if path.join("bun.lockb").exists() {
55+
stack.package_manager = Some("bun".to_string());
56+
}
57+
58+
// 解析 package.json 检测 ORM 和框架
59+
if let Ok(content) = fs::read_to_string(&package_json_path) {
60+
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) {
61+
let deps = json
62+
.get("dependencies")
63+
.and_then(|v| v.as_object())
64+
.map(|m| m.keys().cloned().collect::<Vec<_>>())
65+
.unwrap_or_default();
66+
67+
let dev_deps = json
68+
.get("devDependencies")
69+
.and_then(|v| v.as_object())
70+
.map(|m| m.keys().cloned().collect::<Vec<_>>())
71+
.unwrap_or_default();
72+
73+
let all_deps: HashSet<_> = deps.iter().chain(dev_deps.iter()).collect();
74+
75+
// ORM 检测
76+
if all_deps.contains(&"prisma".to_string())
77+
|| all_deps.contains(&"@prisma/client".to_string())
78+
{
79+
stack.orm = Some("prisma".to_string());
80+
} else if all_deps.contains(&"drizzle-orm".to_string()) {
81+
stack.orm = Some("drizzle".to_string());
82+
} else if all_deps.contains(&"typeorm".to_string()) {
83+
stack.orm = Some("typeorm".to_string());
84+
} else if all_deps.contains(&"sequelize".to_string()) {
85+
stack.orm = Some("sequelize".to_string());
86+
}
87+
88+
// 框架检测
89+
if all_deps.contains(&"next".to_string()) {
90+
stack.frameworks.push("Next.js".to_string());
91+
}
92+
if all_deps.contains(&"react".to_string()) {
93+
stack.frameworks.push("React".to_string());
94+
}
95+
if all_deps.contains(&"vue".to_string()) {
96+
stack.frameworks.push("Vue".to_string());
97+
}
98+
if all_deps.contains(&"express".to_string()) {
99+
stack.frameworks.push("Express".to_string());
100+
}
101+
if all_deps.contains(&"@tauri-apps/api".to_string()) {
102+
stack.frameworks.push("Tauri".to_string());
103+
}
104+
if all_deps.contains(&"vite".to_string()) {
105+
stack.frameworks.push("Vite".to_string());
106+
}
107+
}
108+
}
109+
}
110+
111+
// Python 检测
112+
let pyproject_path = path.join("pyproject.toml");
113+
let requirements_path = path.join("requirements.txt");
114+
if pyproject_path.exists() || requirements_path.exists() {
115+
if stack.runtime == "unknown" {
116+
stack.runtime = "python".to_string();
117+
} else {
118+
stack.runtime = format!("{}/python", stack.runtime);
119+
}
120+
121+
// 检测包管理器
122+
if path.join("poetry.lock").exists() {
123+
stack.package_manager = Some("poetry".to_string());
124+
} else if path.join("Pipfile.lock").exists() {
125+
stack.package_manager = Some("pipenv".to_string());
126+
} else if path.join("uv.lock").exists() {
127+
stack.package_manager = Some("uv".to_string());
128+
}
129+
130+
// 检测 ORM (从 pyproject.toml 或 requirements.txt)
131+
let deps_content = if pyproject_path.exists() {
132+
fs::read_to_string(&pyproject_path).unwrap_or_default()
133+
} else {
134+
fs::read_to_string(&requirements_path).unwrap_or_default()
135+
};
136+
137+
if deps_content.contains("alembic") {
138+
stack.orm = Some("alembic".to_string());
139+
} else if deps_content.contains("django") {
140+
stack.orm = Some("django".to_string());
141+
stack.frameworks.push("Django".to_string());
142+
} else if deps_content.contains("sqlalchemy") {
143+
stack.orm = Some("sqlalchemy".to_string());
144+
}
145+
146+
if deps_content.contains("fastapi") {
147+
stack.frameworks.push("FastAPI".to_string());
148+
}
149+
if deps_content.contains("flask") {
150+
stack.frameworks.push("Flask".to_string());
151+
}
152+
}
153+
154+
// Rust 检测
155+
let cargo_path = path.join("Cargo.toml");
156+
if cargo_path.exists() {
157+
if stack.runtime == "unknown" {
158+
stack.runtime = "rust".to_string();
159+
} else {
160+
stack.runtime = format!("{}/rust", stack.runtime);
161+
}
162+
stack.package_manager = Some("cargo".to_string());
163+
164+
if let Ok(content) = fs::read_to_string(&cargo_path) {
165+
if content.contains("sqlx") {
166+
stack.orm = Some("sqlx".to_string());
167+
} else if content.contains("diesel") {
168+
stack.orm = Some("diesel".to_string());
169+
} else if content.contains("sea-orm") {
170+
stack.orm = Some("sea-orm".to_string());
171+
}
172+
173+
if content.contains("tauri") {
174+
stack.frameworks.push("Tauri".to_string());
175+
}
176+
if content.contains("actix") {
177+
stack.frameworks.push("Actix".to_string());
178+
}
179+
if content.contains("axum") {
180+
stack.frameworks.push("Axum".to_string());
181+
}
182+
}
183+
}
184+
185+
Ok(stack)
186+
}
187+
188+
/// 检查环境变量
189+
pub fn check_env_vars(project_path: &str) -> Result<EnvCheckResult, String> {
190+
let path = Path::new(project_path);
191+
let env_example_path = path.join(".env.example");
192+
let env_path = path.join(".env");
193+
194+
let env_example_exists = env_example_path.exists();
195+
let env_exists = env_path.exists();
196+
197+
let mut missing_keys = Vec::new();
198+
let mut leaked_secrets = Vec::new();
199+
200+
// 检查 .env.example vs .env 的完整性
201+
if env_example_exists && env_exists {
202+
let example_keys = parse_env_keys(&env_example_path);
203+
let env_keys = parse_env_keys(&env_path);
204+
205+
for key in example_keys {
206+
if !env_keys.contains(&key) {
207+
missing_keys.push(key);
208+
}
209+
}
210+
} else if env_example_exists && !env_exists {
211+
// .env 不存在,所有 example 的 key 都算 missing
212+
missing_keys = parse_env_keys(&env_example_path);
213+
}
214+
215+
// 扫描源代码中的敏感信息泄露
216+
leaked_secrets = scan_for_leaked_secrets(path);
217+
218+
Ok(EnvCheckResult {
219+
missing_keys,
220+
leaked_secrets,
221+
env_example_exists,
222+
env_exists,
223+
})
224+
}
225+
226+
fn parse_env_keys(path: &Path) -> Vec<String> {
227+
let mut keys = Vec::new();
228+
if let Ok(content) = fs::read_to_string(path) {
229+
for line in content.lines() {
230+
let line = line.trim();
231+
if line.is_empty() || line.starts_with('#') {
232+
continue;
233+
}
234+
if let Some(pos) = line.find('=') {
235+
let key = line[..pos].trim().to_string();
236+
if !key.is_empty() {
237+
keys.push(key);
238+
}
239+
}
240+
}
241+
}
242+
keys
243+
}
244+
245+
fn scan_for_leaked_secrets(project_path: &Path) -> Vec<LeakedSecret> {
246+
let mut secrets = Vec::new();
247+
248+
// 敏感信息正则 - 匹配硬编码的 API keys, tokens, passwords
249+
let secret_pattern = Regex::new(
250+
r#"(?i)(api[_-]?key|secret|password|token|credential|private[_-]?key)\s*[=:]\s*['"]([\w\-_./+=]{8,})['""]"#
251+
).unwrap();
252+
253+
// 要扫描的文件扩展名
254+
let scan_extensions = ["ts", "tsx", "js", "jsx", "py", "rs", "go", "java", "rb"];
255+
256+
// 要排除的目录
257+
let exclude_dirs = ["node_modules", "target", ".git", "dist", "build", "__pycache__", ".venv", "venv"];
258+
259+
scan_directory(project_path, &secret_pattern, &scan_extensions, &exclude_dirs, &mut secrets);
260+
261+
secrets
262+
}
263+
264+
fn scan_directory(
265+
dir: &Path,
266+
pattern: &Regex,
267+
extensions: &[&str],
268+
exclude_dirs: &[&str],
269+
secrets: &mut Vec<LeakedSecret>,
270+
) {
271+
let entries = match fs::read_dir(dir) {
272+
Ok(e) => e,
273+
Err(_) => return,
274+
};
275+
276+
for entry in entries.flatten() {
277+
let path = entry.path();
278+
let file_name = path.file_name().unwrap_or_default().to_string_lossy();
279+
280+
if path.is_dir() {
281+
// 跳过排除目录
282+
if exclude_dirs.iter().any(|&d| file_name == d) {
283+
continue;
284+
}
285+
scan_directory(&path, pattern, extensions, exclude_dirs, secrets);
286+
} else if path.is_file() {
287+
// 检查扩展名
288+
let ext = path.extension().unwrap_or_default().to_string_lossy();
289+
if !extensions.iter().any(|&e| ext == e) {
290+
continue;
291+
}
292+
293+
// 跳过测试文件和配置示例
294+
if file_name.contains(".test.") || file_name.contains(".spec.") || file_name.contains(".example") {
295+
continue;
296+
}
297+
298+
// 扫描文件内容
299+
if let Ok(content) = fs::read_to_string(&path) {
300+
for (line_num, line) in content.lines().enumerate() {
301+
// 跳过注释行
302+
let trimmed = line.trim();
303+
if trimmed.starts_with("//") || trimmed.starts_with("#") || trimmed.starts_with("*") {
304+
continue;
305+
}
306+
307+
for cap in pattern.captures_iter(line) {
308+
let key_name = cap.get(1).map(|m| m.as_str()).unwrap_or("unknown");
309+
let value = cap.get(2).map(|m| m.as_str()).unwrap_or("");
310+
311+
// 过滤掉明显的占位符
312+
if value.contains("your_") || value.contains("xxx") || value.contains("placeholder") || value == "undefined" || value == "null" {
313+
continue;
314+
}
315+
316+
// 脱敏预览
317+
let preview = if value.len() > 8 {
318+
format!("{}...{}", &value[..4], &value[value.len()-4..])
319+
} else {
320+
"****".to_string()
321+
};
322+
323+
secrets.push(LeakedSecret {
324+
file: path.strip_prefix(dir.parent().unwrap_or(dir))
325+
.unwrap_or(&path)
326+
.to_string_lossy()
327+
.to_string(),
328+
line: line_num + 1,
329+
key_name: key_name.to_string(),
330+
preview,
331+
});
332+
}
333+
}
334+
}
335+
}
336+
}
337+
}

src/App.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,7 @@ function App() {
355355
{view.type === "output-styles" && <OutputStylesView />}
356356
{view.type === "statusline" && (
357357
<StatuslineView
358-
marketplaceItems={catalog?.statuslines || []}
358+
installedTemplates={catalog?.statuslines.filter(s => s.source_id === "personal") || []}
359359
onBrowseMore={() => navigate({ type: "marketplace", category: "statuslines" })}
360360
/>
361361
)}
@@ -683,7 +683,7 @@ function App() {
683683
{view.type === "output-styles" && <OutputStylesView />}
684684
{view.type === "statusline" && (
685685
<StatuslineView
686-
marketplaceItems={catalog?.statuslines || []}
686+
installedTemplates={catalog?.statuslines.filter(s => s.source_id === "personal") || []}
687687
onBrowseMore={() => navigate({ type: "marketplace", category: "statuslines" })}
688688
/>
689689
)}

src/components/Terminal/TerminalPane.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -190,14 +190,15 @@ export function TerminalPane({
190190
}
191191
};
192192

193-
// Shift+Enter sends newline (\n) instead of carriage return (\r)
193+
// Shift+Enter: bracketed paste with U+2028 (Line Separator)
194+
// Note: leaves invisible char requiring one extra backspace - best working solution found
194195
term.attachCustomKeyEventHandler((event) => {
195196
if (event.type === 'keydown' && event.key === 'Enter' && event.shiftKey) {
196197
if (ptyReadySessions.has(sessionId)) {
197-
const encoder = new TextEncoder();
198-
invoke("pty_write", { id: sessionId, data: Array.from(encoder.encode('\n')) });
198+
// ESC[200~ + U+2028 + ESC[201~
199+
invoke("pty_write", { id: sessionId, data: [0x1b, 0x5b, 0x32, 0x30, 0x30, 0x7e, 0xe2, 0x80, 0xa8, 0x1b, 0x5b, 0x32, 0x30, 0x31, 0x7e] });
199200
}
200-
return false; // Prevent default Enter handling
201+
return false;
201202
}
202203
return true;
203204
});

0 commit comments

Comments
 (0)