Skip to content

Commit 41e2792

Browse files
author
文思为
committed
add
1 parent 149e3e2 commit 41e2792

17 files changed

Lines changed: 920 additions & 516 deletions

File tree

config/config.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ password = "123456"
2626

2727
[assist]
2828
# base_url = "http://117.72.103.91:8888"
29-
base_url = "http://127.0.0.1:8888"
29+
base_url = "https://analyze.warpparse.ai"
3030

3131
[warparse]
3232
enabled = false

src/api/project.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
use actix_web::{HttpRequest, HttpResponse, post};
1+
use actix_web::{HttpRequest, HttpResponse, post, web};
22
use urlencoding::decode;
33

44
use crate::error::AppError;
5-
use crate::server::project::import_project_from_files_logic;
5+
use crate::server::project::{ProjectImportRequest, import_project_from_files_logic};
66

77
fn operator_from_request(req: &HttpRequest) -> Option<String> {
88
req.headers().get("x-operator").and_then(|value| {
@@ -18,8 +18,11 @@ fn operator_from_request(req: &HttpRequest) -> Option<String> {
1818
}
1919

2020
#[post("/api/project/import")]
21-
pub async fn import_project_from_files(http_req: HttpRequest) -> Result<HttpResponse, AppError> {
21+
pub async fn import_project_from_files(
22+
http_req: HttpRequest,
23+
req: web::Json<ProjectImportRequest>,
24+
) -> Result<HttpResponse, AppError> {
2225
let operator = operator_from_request(&http_req);
23-
let resp = import_project_from_files_logic(operator).await?;
26+
let resp = import_project_from_files_logic(operator, req.into_inner()).await?;
2427
Ok(HttpResponse::Ok().json(resp))
2528
}

src/db/default_rules_loader.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::error::AppError;
2+
use crate::server::Setting;
23
use crate::utils::project::resolve_project_root;
34
use rust_embed::RustEmbed;
45
use std::{
@@ -65,9 +66,7 @@ fn init_default_configs_with_mappings(
6566
}
6667

6768
pub fn runtime_default_configs_dir() -> Option<PathBuf> {
68-
let candidate = std::env::current_dir()
69-
.unwrap_or_else(|_| PathBuf::from("."))
70-
.join("default_configs");
69+
let candidate = Setting::workspace_root().join("default_configs");
7170

7271
if candidate.is_dir() {
7372
Some(candidate)

src/server/project.rs

Lines changed: 170 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,28 @@
1-
use std::path::PathBuf;
1+
use std::ffi::OsStr;
2+
use std::fs;
3+
use std::path::{Path, PathBuf};
24

35
use chrono::Utc;
4-
use serde::Serialize;
6+
use serde::{Deserialize, Serialize};
57

68
use crate::db::RuleType;
79
use crate::error::AppError;
8-
use crate::server::ProjectLayout;
910
use crate::server::sync::sync_to_gitea_all;
1011
use crate::server::{
11-
OperationLogAction, OperationLogBiz, OperationLogParams, Setting,
12+
OperationLogAction, OperationLogBiz, OperationLogParams, ProjectLayout, Setting,
1213
write_operation_log_for_result,
1314
};
1415
use crate::utils::knowledge::reload_knowledge;
15-
use crate::utils::project_check::check_component;
16+
use crate::utils::project_check::check_component_in_dir;
1617
use crate::utils::{ProjectSnapshot, load_project_snapshot_from_layout};
1718

19+
const LEGACY_REQUIRED_DIRS: [&str; 4] = ["conf", "connectors", "topology", "models"];
20+
21+
#[derive(Debug, Deserialize)]
22+
pub struct ProjectImportRequest {
23+
pub source_dir: String,
24+
}
25+
1826
#[derive(Serialize)]
1927
pub struct ProjectImportResponse {
2028
pub summary: ProjectImportSummary,
@@ -30,7 +38,9 @@ pub struct ProjectImportSummary {
3038
pub rule_breakdown: Vec<ProjectImportBreakdown>,
3139
pub warnings: Vec<String>,
3240
pub failed_files: usize,
33-
pub project_root: String,
41+
pub source_dir: String,
42+
pub project_models: String,
43+
pub project_infra: String,
3444
}
3545

3646
#[derive(Serialize)]
@@ -47,22 +57,24 @@ pub struct ProjectImportValidation {
4757

4858
pub async fn import_project_from_files_logic(
4959
operator: Option<String>,
60+
req: ProjectImportRequest,
5061
) -> Result<ProjectImportResponse, AppError> {
5162
let operator_for_log = operator.clone();
5263
let setting = Setting::load();
5364
let layout = setting.project_layout();
54-
let project_path = resolve_project_path(&layout);
65+
let source_dir = normalize_source_dir(&req.source_dir)?;
66+
67+
validate_legacy_project_dir(&source_dir)?;
68+
check_component_in_dir(&source_dir, RuleType::All.to_check_component())?;
69+
overwrite_project_layout_from_legacy_dir(&source_dir, &layout)?;
5570

5671
let snapshot = load_project_snapshot_from_layout(&layout)?;
5772
if snapshot.rules.is_empty() && snapshot.knowledge.is_empty() {
5873
return Err(AppError::validation(
59-
"项目目录中未找到可导入的规则或知识库".to_string(),
74+
"导入后的项目目录中未找到可导入的规则或知识库".to_string(),
6075
));
6176
}
6277

63-
// 文件已经是主数据源;这里先执行组件校验,避免坏配置进入后续同步/发布链路。
64-
check_component(RuleType::All.to_check_component())?;
65-
6678
let result = async {
6779
let ProjectSnapshot {
6880
rules,
@@ -99,12 +111,14 @@ pub async fn import_project_from_files_logic(
99111
rule_breakdown: breakdown,
100112
warnings,
101113
failed_files,
102-
project_root: project_path.to_string_lossy().to_string(),
114+
source_dir: source_dir.to_string_lossy().to_string(),
115+
project_models: layout.models_root.to_string_lossy().to_string(),
116+
project_infra: layout.infra_root.to_string_lossy().to_string(),
103117
};
104118

105119
let validation = ProjectImportValidation {
106120
passed: true,
107-
message: "项目组件校验通过".to_string(),
121+
message: "目录拆分覆盖并校验通过".to_string(),
108122
};
109123

110124
Ok::<_, AppError>(ProjectImportResponse {
@@ -118,6 +132,7 @@ pub async fn import_project_from_files_logic(
118132
if let Some(op) = operator_for_log {
119133
log_params = log_params.with_operator(op);
120134
}
135+
log_params = log_params.with_field("source_dir", source_dir.to_string_lossy().to_string());
121136
if let Ok(ref resp) = result {
122137
log_params = log_params
123138
.with_field("rules_deleted", resp.summary.rules_deleted.to_string())
@@ -126,7 +141,8 @@ pub async fn import_project_from_files_logic(
126141
"knowledge_imported",
127142
resp.summary.knowledge_imported.to_string(),
128143
)
129-
.with_field("project_root", resp.summary.project_root.clone());
144+
.with_field("project_models", resp.summary.project_models.clone())
145+
.with_field("project_infra", resp.summary.project_infra.clone());
130146
}
131147

132148
write_operation_log_for_result(
@@ -140,14 +156,147 @@ pub async fn import_project_from_files_logic(
140156
result
141157
}
142158

143-
fn resolve_project_path(layout: &ProjectLayout) -> PathBuf {
144-
let base = Setting::workspace_root()
145-
.join("tmp")
146-
.join("project-import-view");
147-
let summary = format!(
148-
"{} + {}",
159+
fn normalize_source_dir(raw: &str) -> Result<PathBuf, AppError> {
160+
let trimmed = raw.trim();
161+
if trimmed.is_empty() {
162+
return Err(AppError::validation("请输入待导入的源目录".to_string()));
163+
}
164+
165+
let path = PathBuf::from(trimmed);
166+
let normalized = if path.is_absolute() {
167+
path
168+
} else {
169+
Setting::workspace_root().join(path)
170+
};
171+
172+
if !normalized.is_dir() {
173+
return Err(AppError::validation(format!(
174+
"源目录不存在或不是目录: {}",
175+
normalized.display()
176+
)));
177+
}
178+
179+
Ok(normalized)
180+
}
181+
182+
fn validate_legacy_project_dir(source_dir: &Path) -> Result<(), AppError> {
183+
let missing_dirs: Vec<String> = LEGACY_REQUIRED_DIRS
184+
.iter()
185+
.filter_map(|name| {
186+
let path = source_dir.join(name);
187+
if path.is_dir() {
188+
None
189+
} else {
190+
Some((*name).to_string())
191+
}
192+
})
193+
.collect();
194+
195+
if !missing_dirs.is_empty() {
196+
return Err(AppError::validation(format!(
197+
"源目录结构不完整,缺少必要目录: {}",
198+
missing_dirs.join(", ")
199+
)));
200+
}
201+
202+
Ok(())
203+
}
204+
205+
fn overwrite_project_layout_from_legacy_dir(
206+
source_dir: &Path,
207+
layout: &ProjectLayout,
208+
) -> Result<(), AppError> {
209+
info!(
210+
"开始按旧目录结构拆分覆盖双仓库: source_dir={}, models_dir={}, infra_dir={}",
211+
source_dir.display(),
212+
layout.models_root.display(),
213+
layout.infra_root.display()
214+
);
215+
216+
recreate_dir_preserving_git(&layout.models_root)?;
217+
recreate_dir_preserving_git(&layout.infra_root)?;
218+
219+
copy_named_entry(source_dir, &layout.infra_root, "conf")?;
220+
copy_named_entry(source_dir, &layout.infra_root, "connectors")?;
221+
copy_named_entry(source_dir, &layout.infra_root, "topology")?;
222+
copy_named_entry(source_dir, &layout.models_root, "models")?;
223+
224+
info!(
225+
"旧目录拆分覆盖完成: source_dir={}, models_dir={}, infra_dir={}",
226+
source_dir.display(),
149227
layout.models_root.display(),
150228
layout.infra_root.display()
151229
);
152-
PathBuf::from(format!("{} ({})", base.display(), summary))
230+
Ok(())
231+
}
232+
233+
fn recreate_dir_preserving_git(target_dir: &Path) -> Result<(), AppError> {
234+
fs::create_dir_all(target_dir).map_err(AppError::internal)?;
235+
236+
for entry in fs::read_dir(target_dir).map_err(AppError::internal)? {
237+
let entry = entry.map_err(AppError::internal)?;
238+
let path = entry.path();
239+
if entry.file_name() == OsStr::new(".git") {
240+
continue;
241+
}
242+
243+
if path.is_dir() {
244+
fs::remove_dir_all(&path).map_err(AppError::internal)?;
245+
} else {
246+
fs::remove_file(&path).map_err(AppError::internal)?;
247+
}
248+
}
249+
250+
Ok(())
251+
}
252+
253+
fn copy_named_entry(source_root: &Path, target_root: &Path, name: &str) -> Result<(), AppError> {
254+
let source = source_root.join(name);
255+
if !source.exists() {
256+
return Err(AppError::validation(format!(
257+
"源目录缺少必要内容: {}",
258+
source.display()
259+
)));
260+
}
261+
262+
let target = target_root.join(name);
263+
if target.exists() {
264+
if target.is_dir() {
265+
fs::remove_dir_all(&target).map_err(AppError::internal)?;
266+
} else {
267+
fs::remove_file(&target).map_err(AppError::internal)?;
268+
}
269+
}
270+
271+
if source.is_dir() {
272+
copy_dir_recursive(&source, &target)?;
273+
} else {
274+
if let Some(parent) = target.parent() {
275+
fs::create_dir_all(parent).map_err(AppError::internal)?;
276+
}
277+
fs::copy(&source, &target).map_err(AppError::internal)?;
278+
}
279+
280+
Ok(())
281+
}
282+
283+
fn copy_dir_recursive(source_dir: &Path, target_dir: &Path) -> Result<(), AppError> {
284+
fs::create_dir_all(target_dir).map_err(AppError::internal)?;
285+
286+
for entry in fs::read_dir(source_dir).map_err(AppError::internal)? {
287+
let entry = entry.map_err(AppError::internal)?;
288+
let source_path = entry.path();
289+
let target_path = target_dir.join(entry.file_name());
290+
291+
if source_path.is_dir() {
292+
copy_dir_recursive(&source_path, &target_path)?;
293+
} else if source_path.is_file() {
294+
if let Some(parent) = target_path.parent() {
295+
fs::create_dir_all(parent).map_err(AppError::internal)?;
296+
}
297+
fs::copy(&source_path, &target_path).map_err(AppError::internal)?;
298+
}
299+
}
300+
301+
Ok(())
153302
}

src/server/sandbox_runner.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,10 @@ async fn stage_run_wpgen(
444444
metrics.input_count = count;
445445
metrics.wpgen_generated = Some(count);
446446

447-
Ok(format!("wpgen已启动, 已发送{}条消息", count))
447+
Ok(format!(
448+
"wpgen已启动, 已发送{}条消息。命令: {}",
449+
count, output.command_line
450+
))
448451
}
449452

450453
async fn stage_analyse_runtime_output(

src/server/sync.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,22 @@ fn repo_name_for_group(group: ReleaseGroup) -> &'static str {
3232
}
3333
}
3434

35+
fn should_skip_gitea_sync() -> bool {
36+
std::env::var("WARP_STATION_SKIP_GITEA")
37+
.map(|value| value == "1" || value.eq_ignore_ascii_case("true"))
38+
.unwrap_or(false)
39+
}
40+
3541
/// 同步指定分组仓库到 Gitea(支持自动处理冲突)
3642
pub async fn sync_to_gitea(commit_message: &str, group: ReleaseGroup) {
43+
if should_skip_gitea_sync() {
44+
info!(
45+
"跳过 Gitea 同步: group={}, reason=WARP_STATION_SKIP_GITEA",
46+
group.as_ref()
47+
);
48+
return;
49+
}
50+
3751
let setting = Setting::load();
3852
let layout = setting.project_layout();
3953

@@ -68,6 +82,11 @@ const REPO_BASELINE_TAG: &str = "baseline";
6882

6983
/// 初始化双仓库 Gitea 仓库和基线 tag(系统首次启动且本地 .git 不存在时调用)
7084
pub async fn init_gitea_repo() -> Result<(), AppError> {
85+
if should_skip_gitea_sync() {
86+
info!("跳过 Gitea 仓库初始化: reason=WARP_STATION_SKIP_GITEA");
87+
return Ok(());
88+
}
89+
7190
let setting = Setting::load();
7291
let layout = setting.project_layout();
7392
let gitea_client = build_gitea_client(&setting)

0 commit comments

Comments
 (0)