From d4d9edeb7850b3e9269277f86d8fadb22b981557 Mon Sep 17 00:00:00 2001 From: Fam Zheng Date: Mon, 9 Mar 2026 08:42:23 +0000 Subject: [PATCH] feat: startup git clone for template repo + pass config through - ensure_repo_ready() at startup: clone if missing, fetch if exists - TemplateRepoConfig gains local_path field - list_all_templates/select_template/extract_repo_template accept repo config - Remove hardcoded repo_dir(), use config.local_path --- doc/todo.md | 16 ++++++++---- src/agent.rs | 13 +++++++--- src/api/workflows.rs | 6 +++-- src/main.rs | 16 ++++++++++++ src/template.rs | 60 ++++++++++++++++++++++++++++++++++++++------ 5 files changed, 93 insertions(+), 18 deletions(-) diff --git a/doc/todo.md b/doc/todo.md index 984d9c9..55bece1 100644 --- a/doc/todo.md +++ b/doc/todo.md @@ -1,7 +1,13 @@ -需求输入和展示,多行,复杂需求,界面不够优化,输入框不够大。 +# Tori TODO -❯ 在前端,计划和日志,每一个条目,都应有一个小小的comment按钮,按一下,直接快速引用,然后输入docus到反馈输入那里,表示要评论的是这 - 个地方。这样llm也知道用户具体在指啥。 同时,允许多处引用,再点一个其他的comment按钮,就引用两处,等等。按钮做的不要太眨眼间,比 - 如用hover显示或者就是小一点不占地方,但要ui意图清晰易用。 +## 前端 -时间观察app +- [ ] 需求输入优化 — 多行、复杂需求时输入框不够大,展示不够好 +- [ ] 时间观察 app(timer/scheduler 可视化) + +## Runtime + +- [ ] 回退粒度 — 支持有选择地回退某些步骤,而非 docker-cache 式全部 invalidate +- [ ] 长任务生命周期 — `execute` 同步等待不适合 30min+ 的构建/测试任务 +- [ ] 产出物累积 — scratchpad 之外需要结构化的"工作产出"概念 +- [ ] 上下文管理 — `current_step_chat_history` 需要 token 预算控制 + 历史截断 diff --git a/src/agent.rs b/src/agent.rs index 4add026..3114133 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -73,11 +73,17 @@ pub struct AgentManager { next_port: AtomicU16, pool: SqlitePool, llm_config: LlmConfig, + template_repo: Option, kb: Option>, } impl AgentManager { - pub fn new(pool: SqlitePool, llm_config: LlmConfig, kb: Option>) -> Arc { + pub fn new( + pool: SqlitePool, + llm_config: LlmConfig, + template_repo: Option, + kb: Option>, + ) -> Arc { Arc::new(Self { agents: RwLock::new(HashMap::new()), broadcast: RwLock::new(HashMap::new()), @@ -85,6 +91,7 @@ impl AgentManager { next_port: AtomicU16::new(9100), pool, llm_config, + template_repo, kb, }) } @@ -211,7 +218,7 @@ async fn agent_loop( tracing::info!("Using forced template: {:?}", forced_template); forced_template } else { - template::select_template(&llm, &requirement).await + template::select_template(&llm, &requirement, mgr.template_repo.as_ref()).await }; let loaded_template = if let Some(ref tid) = template_id { tracing::info!("Template selected for workflow {}: {}", workflow_id, tid); @@ -219,7 +226,7 @@ async fn agent_loop( if template::is_repo_template(tid) { // Repo template: extract from git then load - match template::extract_repo_template(tid).await { + match template::extract_repo_template(tid, mgr.template_repo.as_ref()).await { Ok(template_dir) => { if let Err(e) = template::apply_template(&template_dir, &workdir).await { tracing::error!("Failed to apply repo template {}: {}", tid, e); diff --git a/src/api/workflows.rs b/src/api/workflows.rs index ef68bb2..317f85c 100644 --- a/src/api/workflows.rs +++ b/src/api/workflows.rs @@ -197,6 +197,8 @@ async fn list_llm_calls( .map_err(db_err) } -async fn list_templates() -> Json> { - Json(template::list_all_templates().await) +async fn list_templates( + State(state): State>, +) -> Json> { + Json(template::list_all_templates(state.config.template_repo.as_ref()).await) } diff --git a/src/main.rs b/src/main.rs index f38b0cc..df9c5b5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -38,6 +38,16 @@ pub struct TemplateRepoConfig { pub gitea_url: String, pub owner: String, pub repo: String, + #[serde(default = "default_repo_path")] + pub local_path: String, +} + +fn default_repo_path() -> String { + if std::path::Path::new("/app/oseng-templates").is_dir() { + "/app/oseng-templates".to_string() + } else { + "oseng-templates".to_string() + } } #[derive(Debug, Clone, serde::Deserialize)] @@ -84,9 +94,15 @@ async fn main() -> anyhow::Result<()> { } }; + // Ensure template repo is cloned before serving + if let Some(ref repo_cfg) = config.template_repo { + template::ensure_repo_ready(repo_cfg).await; + } + let agent_mgr = agent::AgentManager::new( database.pool.clone(), config.llm.clone(), + config.template_repo.clone(), kb_arc.clone(), ); diff --git a/src/template.rs b/src/template.rs index fe98944..8f868c8 100644 --- a/src/template.rs +++ b/src/template.rs @@ -2,6 +2,7 @@ use std::path::{Path, PathBuf}; use serde::Deserialize; +use crate::TemplateRepoConfig; use crate::llm::{ChatMessage, LlmClient}; use crate::tools::ExternalToolManager; @@ -46,7 +47,7 @@ fn builtin_dir() -> &'static str { } } -fn repo_dir() -> &'static str { +fn default_repo_dir() -> &'static str { if Path::new("/app/oseng-templates").is_dir() { "/app/oseng-templates" } else { @@ -133,8 +134,45 @@ async fn scan_examples_git(repo: &Path, ref_name: &str, template_path: &str) -> examples } +/// Ensure the template repo is a git clone; clone or fetch as needed. +/// Called at startup (before serve) so readiness probe can gate on it. +pub async fn ensure_repo_ready(cfg: &TemplateRepoConfig) { + let repo = Path::new(&cfg.local_path); + if repo.join(".git").is_dir() { + tracing::info!("Template repo already cloned at {}, fetching...", repo.display()); + let _ = tokio::process::Command::new("git") + .args(["fetch", "--all", "--prune", "-q"]) + .current_dir(repo) + .env("GIT_SSL_NO_VERIFY", "true") + .output() + .await; + return; + } + + // Not a git repo — remove stale dir (e.g. COPY'd without .git) and clone + let url = format!("{}/{}/{}.git", cfg.gitea_url, cfg.owner, cfg.repo); + tracing::info!("Cloning template repo {} → {}", url, repo.display()); + let _ = tokio::fs::remove_dir_all(repo).await; + let output = tokio::process::Command::new("git") + .args(["clone", &url, &repo.to_string_lossy()]) + .env("GIT_SSL_NO_VERIFY", "true") + .output() + .await; + match output { + Ok(o) if o.status.success() => { + tracing::info!("Template repo cloned successfully"); + } + Ok(o) => { + tracing::error!("git clone failed: {}", String::from_utf8_lossy(&o.stderr)); + } + Err(e) => { + tracing::error!("git clone error: {}", e); + } + } +} + /// List all templates from both built-in and repo (all branches). -pub async fn list_all_templates() -> Vec { +pub async fn list_all_templates(repo_cfg: Option<&TemplateRepoConfig>) -> Vec { let mut items = Vec::new(); // 1. Built-in templates (flat: each top-level dir with template.json) @@ -164,8 +202,11 @@ pub async fn list_all_templates() -> Vec { } } - // 2. Repo templates (all branches, find INSTRUCTIONS.md via git ls-tree) - let repo = Path::new(repo_dir()); + // 2. Repo templates (all branches, cloned at startup) + let repo_path = repo_cfg + .map(|c| c.local_path.as_str()) + .unwrap_or_else(|| default_repo_dir()); + let repo = Path::new(repo_path); if repo.join(".git").is_dir() { items.extend(scan_repo_all_branches(repo).await); } @@ -284,8 +325,11 @@ async fn read_git_file_json( /// Extract a template from a git branch to a local directory. /// Uses `git archive` to extract the template subtree. -pub async fn extract_repo_template(template_id: &str) -> anyhow::Result { - let repo = Path::new(repo_dir()); +pub async fn extract_repo_template(template_id: &str, repo_cfg: Option<&TemplateRepoConfig>) -> anyhow::Result { + let repo_path = repo_cfg + .map(|c| c.local_path.as_str()) + .unwrap_or_else(|| default_repo_dir()); + let repo = Path::new(repo_path); let dest = PathBuf::from("/tmp/tori-repo-templates").join(template_id); let _ = tokio::fs::remove_dir_all(&dest).await; tokio::fs::create_dir_all(&dest).await?; @@ -402,8 +446,8 @@ pub fn is_repo_template(template_id: &str) -> bool { // --- LLM template selection --- -pub async fn select_template(llm: &LlmClient, requirement: &str) -> Option { - let all = list_all_templates().await; +pub async fn select_template(llm: &LlmClient, requirement: &str, repo_cfg: Option<&TemplateRepoConfig>) -> Option { + let all = list_all_templates(repo_cfg).await; if all.is_empty() { return None; }