1e186369c9
Project CI / AI game creator shell Rust crates (push) Successful in 1m24s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m56s
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
Project CI / Frontend tests (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/446 Co-authored-by: Linghong <ink29535@proton.me> Co-committed-by: Linghong <ink29535@proton.me>
105 lines
3.3 KiB
Rust
105 lines
3.3 KiB
Rust
//! 正式项目成果事件。只接收宿主已提交的版本,不参与业务写入。
|
|
use super::contract::{ChangeKind, Context, EventData, ProjectSaved, RevisionCreated, Source};
|
|
use super::store::AnalyticsWriter;
|
|
use std::io::Read;
|
|
use std::path::Path;
|
|
|
|
pub(crate) fn saved(
|
|
capture: Option<(Context, AnalyticsWriter)>,
|
|
project_id: &str,
|
|
source: Source,
|
|
operation_id: &str,
|
|
data: ProjectSaved,
|
|
event_time: Option<&str>,
|
|
) {
|
|
let Some((context, writer)) = capture else {
|
|
return;
|
|
};
|
|
if let Ok(mut event) = context.capture(
|
|
EventData::ProjectSave(data),
|
|
Some(project_id.into()),
|
|
source,
|
|
None,
|
|
) {
|
|
if let Some(time) = event_time {
|
|
event.event_time = time.into();
|
|
}
|
|
writer.try_record(
|
|
context.route,
|
|
event,
|
|
format!("{project_id}:{operation_id}:project_save"),
|
|
);
|
|
}
|
|
}
|
|
|
|
pub(crate) fn file_content_changed(
|
|
root: &Path,
|
|
path: &str,
|
|
content: &[u8],
|
|
max_bytes: u64,
|
|
) -> Option<bool> {
|
|
let target = crate::resolve_local_project_path(root, path).ok()?;
|
|
match std::fs::symlink_metadata(&target) {
|
|
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Some(true),
|
|
Err(_) => return None,
|
|
Ok(_) => {}
|
|
}
|
|
let (file, metadata) =
|
|
crate::open_project_snapshot_regular_file(&target, "成果内容比较").ok()?;
|
|
if metadata.len() > max_bytes {
|
|
return None;
|
|
}
|
|
let mut before = Vec::new();
|
|
file.take(max_bytes.saturating_add(1))
|
|
.read_to_end(&mut before)
|
|
.ok()?;
|
|
(before.len() as u64 <= max_bytes).then(|| before != content)
|
|
}
|
|
|
|
pub(crate) fn revision(
|
|
capture: Option<(Context, AnalyticsWriter)>,
|
|
project_id: &str,
|
|
source: Source,
|
|
data: RevisionCreated,
|
|
) {
|
|
let Some((context, writer)) = capture else {
|
|
return;
|
|
};
|
|
let key = format!("{project_id}:{}:project_revision_created", data.revision_id);
|
|
if let Ok(event) = context.capture(
|
|
EventData::ProjectRevisionCreated(data),
|
|
Some(project_id.into()),
|
|
source,
|
|
None,
|
|
) {
|
|
writer.try_record(context.route, event, key);
|
|
}
|
|
}
|
|
|
|
// 只给已知成果分类;未知文件不猜字段,控制面和构建缓存不计成果。
|
|
pub(crate) fn file_change_kind(path: &str) -> Option<ChangeKind> {
|
|
let path = path.to_ascii_lowercase();
|
|
if crate::should_skip_project_snapshot_path(&path)
|
|
|| path.split('/').any(|part| part.starts_with('.'))
|
|
{
|
|
return None;
|
|
}
|
|
if path.starts_with("design_artifacts/") {
|
|
return Some(ChangeKind::DesignDocument);
|
|
}
|
|
if path.starts_with("ui/") {
|
|
return Some(ChangeKind::Ui);
|
|
}
|
|
let extension = path.rsplit_once('.')?.1;
|
|
match extension {
|
|
"png" | "jpg" | "jpeg" | "webp" | "gif" | "svg" | "avif" | "mp3" | "wav" | "ogg"
|
|
| "flac" | "mp4" | "webm" | "glb" | "gltf" | "ttf" | "woff" | "woff2" => {
|
|
Some(ChangeKind::Asset)
|
|
}
|
|
"js" | "jsx" | "ts" | "tsx" | "mjs" | "cjs" | "html" | "css" | "scss" | "json" | "vue"
|
|
| "svelte" | "gd" | "tscn" | "tres" | "cs" | "shader" | "glsl" | "wgsl" | "vert"
|
|
| "frag" | "rs" | "py" | "lua" | "cpp" | "h" | "c" | "hpp" => Some(ChangeKind::Code),
|
|
_ => None,
|
|
}
|
|
}
|