From 4987bf4741b8b0c8b6c68efb7cc00bbb9873e51f Mon Sep 17 00:00:00 2001 From: zhubby Date: Fri, 17 Apr 2026 00:35:34 +0800 Subject: [PATCH] fix(skills): manage skills as git repositories Replace resource-bound skills with git-backed repositories so the backend can sync repository state and the frontend can manage repository-based skills consistently. Made-with: Cursor --- Cargo.lock | 1 + kagent-api-server/CHANGELOG.md | 8 + kagent-api-server/src/middleware/activity.rs | 4 +- kagent-api-server/src/routes/skills.rs | 130 +++++------ kagent-core/CHANGELOG.md | 7 + kagent-core/src/skills.rs | 200 +++++++++++++++-- kagent-db-migration/CHANGELOG.md | 7 + kagent-db-migration/src/lib.rs | 2 + ...00001_rework_skills_as_git_repositories.rs | 141 ++++++++++++ kagent-db/CHANGELOG.md | 7 + kagent-db/src/entities/skills.rs | 32 ++- kagent-git/Cargo.toml | 10 +- kagent-git/src/lib.rs | 102 +++++++++ kagent-ui/CHANGELOG.md | 4 + kagent-ui/app/assistant.tsx | 11 +- kagent-ui/app/login/page.tsx | 10 +- kagent-ui/app/register/page.tsx | 10 +- kagent-ui/app/stores/auth-store.ts | 6 +- kagent-ui/bun.lockb | Bin 489465 -> 489634 bytes .../components/assistant-ui/attachment.tsx | 2 + .../assistant-ui/create-thread-dialog.tsx | 2 +- .../assistant-ui/project-overview.tsx | 9 +- .../assistant-ui/resource-tree-dialog.tsx | 2 +- kagent-ui/components/assistant-ui/thread.tsx | 37 +-- kagent-ui/components/auth/auth-guard.tsx | 13 +- .../components/form/json-schema-form.tsx | 2 + kagent-ui/components/layout/app-top-bar.tsx | 42 ++-- .../project/settings/skill-mgmt.tsx | 212 ++++++++++-------- kagent-ui/components/ui/chart.tsx | 3 +- kagent-ui/components/ui/sidebar.tsx | 10 +- kagent-ui/components/ui/switch.tsx | 13 +- kagent-ui/eslint.config.mjs | 16 +- kagent-ui/hooks/use-thread-metadata.ts | 2 + kagent-ui/lib/kagent-codex/jsonrpc.ts | 7 +- .../lib/kagent-projects/settings.test.ts | 47 ++++ kagent-ui/lib/kagent-projects/settings.ts | 76 +++++-- kagent-ui/package.json | 6 +- 37 files changed, 891 insertions(+), 302 deletions(-) create mode 100644 kagent-db-migration/src/m20260416_000001_rework_skills_as_git_repositories.rs create mode 100644 kagent-ui/lib/kagent-projects/settings.test.ts diff --git a/Cargo.lock b/Cargo.lock index d09e87f..ad22ab1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10642,6 +10642,7 @@ version = "11.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4994acea2522cd2b3b85c1d9529a55991e3ad5e25cdcd3de9d505972c4379424" dependencies = [ + "chrono", "serde_json", "thiserror 2.0.17", "ts-rs-macros 11.1.0", diff --git a/kagent-api-server/CHANGELOG.md b/kagent-api-server/CHANGELOG.md index 84222bf..a8cb36e 100644 --- a/kagent-api-server/CHANGELOG.md +++ b/kagent-api-server/CHANGELOG.md @@ -1,5 +1,13 @@ # CHANGELOG +## 2026-04-16 +- skills API 重构为技能仓库管理接口: + - `GET /api/skills*` 响应改为返回 `repositoryName/gitUrl/syncStatus/commitId/lastSyncedAt/syncError`。 + - `POST /api/skills` 改为接收 Git 仓库地址,并在创建后自动执行首次同步。 + - `PATCH /api/skills/{skill_id}` 改为更新仓库名称、描述与 Git 地址;Git 地址变更时重置同步状态。 + - 新增 `POST /api/skills/{skill_id}/sync` 主动同步接口。 +- skills 相关活动日志名称提取逻辑同步更新为 `repositoryName`。 + ## 2026-02-24 - `/api/projects/item` 项目标识解析增强: - 新增对请求头 `x-kagent-project-id` 的支持,允许通过请求头传递项目上下文。 diff --git a/kagent-api-server/src/middleware/activity.rs b/kagent-api-server/src/middleware/activity.rs index 86d5145..c96ae91 100644 --- a/kagent-api-server/src/middleware/activity.rs +++ b/kagent-api-server/src/middleware/activity.rs @@ -291,7 +291,7 @@ async fn lookup_resource_snapshot( .ok()??; Some(ResourceSnapshot { project_id: Some(kagent_core::projects::external_project_id(row.project_id)), - resource_name: Some(row.skill_name), + resource_name: Some(row.repository_name), }) } "mcp_server" => { @@ -359,7 +359,7 @@ fn extract_resource_name_from_body( let key = match resource_type { "project" => "name", "credential" => "name", - "skill" => "skillName", + "skill" => "repositoryName", "mcp_server" => "name", "code_source" => "name", "model_provider" => "providerName", diff --git a/kagent-api-server/src/routes/skills.rs b/kagent-api-server/src/routes/skills.rs index db4b3da..34a0927 100644 --- a/kagent-api-server/src/routes/skills.rs +++ b/kagent-api-server/src/routes/skills.rs @@ -2,7 +2,7 @@ use axum::Json; use axum::Router; use axum::extract::{Path, State}; use axum::http::StatusCode; -use axum::routing::get; +use axum::routing::{get, post}; use crate::error::AppError; use crate::middleware::project_access::ProjectMember; @@ -17,18 +17,21 @@ pub(crate) fn router() -> Router { ApiRoute::SkillsItem.path(), get(get_skill).patch(patch_skill).delete(delete_skill), ) + .route("/api/skills/{skill_id}/sync", post(sync_skill)) } -// ===== DTO ===== - #[derive(serde::Serialize)] #[serde(rename_all = "camelCase")] struct SkillDto { id: String, project_id: String, - skill_name: String, + repository_name: String, description: String, - resource_id: Option, + git_url: Option, + sync_status: kagent_db::entities::skills::SkillSyncStatus, + commit_id: Option, + last_synced_at: Option, + sync_error: Option, created_at: i64, updated_at: i64, } @@ -38,20 +41,19 @@ impl From for SkillDto { Self { id: kagent_core::skills::external_skill_id(m.id), project_id: kagent_core::projects::external_project_id(m.project_id), - skill_name: m.skill_name, + repository_name: m.repository_name, description: m.description, - resource_id: m - .resource_id - .map(kagent_core::resources::external_resource_id), + git_url: m.git_url, + sync_status: m.sync_status, + commit_id: m.commit_id, + last_synced_at: m.last_synced_at.map(|value| value.timestamp_millis()), + sync_error: m.sync_error, created_at: m.created_at.timestamp_millis(), updated_at: m.updated_at.timestamp_millis(), } } } -// ===== handlers ===== - -/// 列出项目下的技能 async fn list_skills( ProjectMember { project_id, .. }: ProjectMember, State(state): State, @@ -68,46 +70,24 @@ async fn list_skills( #[derive(serde::Deserialize)] #[serde(rename_all = "camelCase")] struct CreateSkillBody { - skill_name: String, + repository_name: String, + #[serde(default)] description: String, - resource_id: Option, + git_url: String, } -/// 创建技能 async fn create_skill( ProjectMember { project_id, .. }: ProjectMember, State(state): State, Json(body): Json, ) -> Result, AppError> { - let resource_id = match body.resource_id.as_deref() { - Some(value) if value.trim().is_empty() => None, - Some(value) => Some( - kagent_core::resources::parse_resource_id(value.trim()) - .map_err(|e| AppError::bad_request(format!("invalid resource id: {e}")))?, - ), - None => None, - }; - - if let Some(resource_id) = resource_id { - let row = kagent_core::resources::get_resource_by_id(state.db.conn(), resource_id) - .await - .map_err(|e| AppError { - status: StatusCode::INTERNAL_SERVER_ERROR, - message: format!("check resource failed: {e:#}"), - })? - .ok_or_else(|| AppError::not_found("resource not found"))?; - if row.project_id != project_id.uuid() { - return Err(AppError::not_found("resource not found")); - } - } - let result = kagent_core::skills::create_skill( state.db.conn(), project_id, kagent_core::skills::CreateSkillInput { - skill_name: body.skill_name, + repository_name: body.repository_name, description: body.description, - resource_id, + git_url: body.git_url, }, ) .await; @@ -121,7 +101,7 @@ async fn create_skill( || msg.contains("UNIQUE constraint failed") { return Err(AppError::conflict( - "skill name already exists in this project", + "repository name already exists in this project", )); } Err(AppError { @@ -132,7 +112,6 @@ async fn create_skill( } } -/// 获取技能详情 async fn get_skill( State(state): State, axum::Extension(user): axum::Extension, @@ -148,7 +127,6 @@ async fn get_skill( })? .ok_or_else(|| AppError::not_found("skill not found"))?; - // 通过资源所属的 project_id 验证权限 let project_id = kagent_db::ids::ProjectId::new(row.project_id); require_project_member(state.db.conn(), project_id, user.id).await?; @@ -158,12 +136,11 @@ async fn get_skill( #[derive(serde::Deserialize)] #[serde(rename_all = "camelCase")] struct PatchSkillBody { - skill_name: Option, + repository_name: Option, description: Option, - resource_id: Option>, + git_url: Option, } -/// 更新技能 async fn patch_skill( State(state): State, axum::Extension(user): axum::Extension, @@ -172,17 +149,7 @@ async fn patch_skill( ) -> Result, AppError> { let skill_id = kagent_core::skills::parse_skill_id(&skill_id) .map_err(|e| AppError::bad_request(format!("invalid skill id: {e}")))?; - let resource_id = match body.resource_id { - Some(Some(value)) if value.trim().is_empty() => Some(None), - Some(Some(value)) => Some(Some( - kagent_core::resources::parse_resource_id(value.trim()) - .map_err(|e| AppError::bad_request(format!("invalid resource id: {e}")))?, - )), - Some(None) => Some(None), - None => None, - }; - // 先加载现有数据,验证权限 let existing = kagent_core::skills::get_skill_by_id(state.db.conn(), skill_id) .await .map_err(|e| AppError { @@ -191,30 +158,16 @@ async fn patch_skill( })? .ok_or_else(|| AppError::not_found("skill not found"))?; - // 通过资源所属的 project_id 验证权限 let project_id = kagent_db::ids::ProjectId::new(existing.project_id); require_project_member(state.db.conn(), project_id, user.id).await?; - if let Some(Some(resource_id)) = resource_id { - let row = kagent_core::resources::get_resource_by_id(state.db.conn(), resource_id) - .await - .map_err(|e| AppError { - status: StatusCode::INTERNAL_SERVER_ERROR, - message: format!("check resource failed: {e:#}"), - })? - .ok_or_else(|| AppError::not_found("resource not found"))?; - if row.project_id != project_id.uuid() { - return Err(AppError::not_found("resource not found")); - } - } - let result = kagent_core::skills::update_skill( state.db.conn(), skill_id, kagent_core::skills::UpdateSkillInput { - skill_name: body.skill_name, + repository_name: body.repository_name, description: body.description, - resource_id, + git_url: body.git_url, }, ) .await; @@ -229,7 +182,7 @@ async fn patch_skill( || msg.contains("UNIQUE constraint failed") { return Err(AppError::conflict( - "skill name already exists in this project", + "repository name already exists in this project", )); } Err(AppError { @@ -240,7 +193,6 @@ async fn patch_skill( } } -/// 删除技能 async fn delete_skill( State(state): State, axum::Extension(user): axum::Extension, @@ -249,7 +201,6 @@ async fn delete_skill( let skill_id = kagent_core::skills::parse_skill_id(&skill_id) .map_err(|e| AppError::bad_request(format!("invalid skill id: {e}")))?; - // 先获取资源验证权限 let existing = kagent_core::skills::get_skill_by_id(state.db.conn(), skill_id) .await .map_err(|e| AppError { @@ -258,7 +209,6 @@ async fn delete_skill( })? .ok_or_else(|| AppError::not_found("skill not found"))?; - // 通过资源所属的 project_id 验证权限 let project_id = kagent_db::ids::ProjectId::new(existing.project_id); require_project_member(state.db.conn(), project_id, user.id).await?; @@ -274,3 +224,33 @@ async fn delete_skill( Err(AppError::not_found("skill not found")) } } + +async fn sync_skill( + State(state): State, + axum::Extension(user): axum::Extension, + Path(skill_id): Path, +) -> Result, AppError> { + let skill_id = kagent_core::skills::parse_skill_id(&skill_id) + .map_err(|e| AppError::bad_request(format!("invalid skill id: {e}")))?; + + let existing = kagent_core::skills::get_skill_by_id(state.db.conn(), skill_id) + .await + .map_err(|e| AppError { + status: StatusCode::INTERNAL_SERVER_ERROR, + message: format!("get skill failed: {e:#}"), + })? + .ok_or_else(|| AppError::not_found("skill not found"))?; + + let project_id = kagent_db::ids::ProjectId::new(existing.project_id); + require_project_member(state.db.conn(), project_id, user.id).await?; + + let row = kagent_core::skills::sync_skill_repository(state.db.conn(), skill_id) + .await + .map_err(|e| AppError { + status: StatusCode::INTERNAL_SERVER_ERROR, + message: format!("sync skill failed: {e:#}"), + })? + .ok_or_else(|| AppError::not_found("skill not found"))?; + + Ok(Json(SkillDto::from(row))) +} diff --git a/kagent-core/CHANGELOG.md b/kagent-core/CHANGELOG.md index ecd79b5..6c945b4 100644 --- a/kagent-core/CHANGELOG.md +++ b/kagent-core/CHANGELOG.md @@ -1,5 +1,12 @@ # CHANGELOG +## 2026-04-16 +- skills 模块重构为技能仓库管理: + - `CreateSkillInput` / `UpdateSkillInput` 改为接收 `repository_name + git_url`。 + - 新增本地仓库目录解析与 Git 同步流程,仓库统一落在 `${KAGENT_HOME:-~/.kagent}/skills/`。 + - 创建技能时会自动触发首次同步,并持久化 `sync_status/commit_id/last_synced_at/sync_error`。 + - 新增手动同步入口 `sync_skill_repository`,支持复用已有本地 checkout 执行更新。 + ## 2026-02-12 - workspaces 生成 codex 配置快照时默认注入 kagent MCP server: - 默认添加 `mcp_servers.kagent`(stdio 命令:`kagent mcp`)。 diff --git a/kagent-core/src/skills.rs b/kagent-core/src/skills.rs index 8024636..9d34bdd 100644 --- a/kagent-core/src/skills.rs +++ b/kagent-core/src/skills.rs @@ -3,7 +3,8 @@ use anyhow::Context; use chrono::{DateTime, Utc}; use kagent_db::entities::skills; -use kagent_db::ids::{ProjectId, ResourceId, SkillId}; +use kagent_db::ids::{ProjectId, SkillId}; +use kagent_git::GitService; use sea_orm::ActiveModelTrait; use sea_orm::ColumnTrait; use sea_orm::DatabaseConnection; @@ -12,20 +13,34 @@ use sea_orm::Order; use sea_orm::QueryFilter; use sea_orm::QueryOrder; use sea_orm::Set; +use std::path::{Path, PathBuf}; use uuid::Uuid; #[derive(Debug, Clone)] pub struct CreateSkillInput { - pub skill_name: String, + pub repository_name: String, pub description: String, - pub resource_id: Option, + pub git_url: String, } #[derive(Debug, Clone, Default)] pub struct UpdateSkillInput { - pub skill_name: Option, + pub repository_name: Option, pub description: Option, - pub resource_id: Option>, + pub git_url: Option, +} + +impl UpdateSkillInput { + pub fn git_url_changed(&self, existing_git_url: &str) -> bool { + self.git_url + .as_deref() + .map(normalize_git_url) + .transpose() + .ok() + .flatten() + .flatten() + .is_some_and(|git_url| git_url != existing_git_url.trim()) + } } pub fn external_skill_id(uuid: Uuid) -> String { @@ -65,24 +80,30 @@ pub async fn create_skill( project_id: ProjectId, input: CreateSkillInput, ) -> anyhow::Result { - let skill_name = input.skill_name.trim().to_string(); - anyhow::ensure!(!skill_name.is_empty(), "skill name is empty"); - let description = input.description.trim().to_string(); - anyhow::ensure!(!description.is_empty(), "skill description is empty"); + let repository_name = normalize_repository_name(&input.repository_name)?; + let git_url = normalize_git_url(&input.git_url)? + .ok_or_else(|| anyhow::anyhow!("git url is empty"))?; + let description = normalize_description(&input.description); let now: DateTime = Utc::now(); let am = skills::ActiveModel { project_id: Set(project_id.uuid()), - skill_name: Set(skill_name), + repository_name: Set(repository_name), description: Set(description), - resource_id: Set(input.resource_id.map(|id| id.uuid())), + git_url: Set(Some(git_url)), + sync_status: Set(skills::SkillSyncStatus::Pending), + commit_id: Set(None), + last_synced_at: Set(None), + sync_error: Set(None), created_at: Set(now.into()), updated_at: Set(now.into()), ..Default::default() }; let row = am.insert(conn).await.context("create skill")?; - Ok(row) + sync_skill_repository(conn, SkillId::new(row.id)) + .await? + .ok_or_else(|| anyhow::anyhow!("skill disappeared after create")) } pub async fn update_skill( @@ -99,18 +120,18 @@ pub async fn update_skill( }; let mut am: skills::ActiveModel = existing.into(); - if let Some(skill_name) = input.skill_name { - let trimmed = skill_name.trim().to_string(); - anyhow::ensure!(!trimmed.is_empty(), "skill name is empty"); - am.skill_name = Set(trimmed); + if let Some(repository_name) = input.repository_name { + am.repository_name = Set(normalize_repository_name(&repository_name)?); } if let Some(description) = input.description { - let trimmed = description.trim().to_string(); - anyhow::ensure!(!trimmed.is_empty(), "skill description is empty"); - am.description = Set(trimmed); + am.description = Set(normalize_description(&description)); } - if let Some(resource_id) = input.resource_id { - am.resource_id = Set(resource_id.map(|id| id.uuid())); + if let Some(git_url) = input.git_url { + am.git_url = Set(normalize_git_url(&git_url)?); + am.sync_status = Set(skills::SkillSyncStatus::Pending); + am.commit_id = Set(None); + am.last_synced_at = Set(None); + am.sync_error = Set(None); } let now: DateTime = Utc::now(); @@ -127,3 +148,140 @@ pub async fn delete_skill(conn: &DatabaseConnection, skill_id: SkillId) -> anyho .context("delete skill")?; Ok(result.rows_affected > 0) } + +pub async fn sync_skill_repository( + conn: &DatabaseConnection, + skill_id: SkillId, +) -> anyhow::Result> { + let Some(existing) = get_skill_by_id(conn, skill_id).await? else { + return Ok(None); + }; + + let now: DateTime = Utc::now(); + let mut syncing: skills::ActiveModel = existing.clone().into(); + syncing.sync_status = Set(skills::SkillSyncStatus::Syncing); + syncing.sync_error = Set(None); + syncing.updated_at = Set(now.into()); + let syncing = syncing.update(conn).await.context("mark skill syncing")?; + + let sync_result = run_skill_repository_sync(SkillId::new(syncing.id), syncing.git_url.clone()).await; + let finished_at: DateTime = Utc::now(); + let mut updated: skills::ActiveModel = syncing.into(); + updated.updated_at = Set(finished_at.into()); + + match sync_result { + Ok(commit_id) => { + updated.sync_status = Set(skills::SkillSyncStatus::Ready); + updated.commit_id = Set(Some(commit_id)); + updated.last_synced_at = Set(Some(finished_at.into())); + updated.sync_error = Set(None); + } + Err(error) => { + updated.sync_status = Set(skills::SkillSyncStatus::Failed); + updated.sync_error = Set(Some(error.to_string())); + } + } + + let row = updated.update(conn).await.context("persist skill sync result")?; + Ok(Some(row)) +} + +fn normalize_repository_name(repository_name: &str) -> anyhow::Result { + let repository_name = repository_name.trim().to_string(); + anyhow::ensure!(!repository_name.is_empty(), "repository name is empty"); + Ok(repository_name) +} + +fn normalize_description(description: &str) -> String { + description.trim().to_string() +} + +fn normalize_git_url(git_url: &str) -> anyhow::Result> { + let git_url = git_url.trim().to_string(); + if git_url.is_empty() { + return Ok(None); + } + anyhow::ensure!( + git_url.starts_with("http://") || git_url.starts_with("https://") || git_url.starts_with("git@"), + "git url must start with http://, https://, or git@" + ); + Ok(Some(git_url)) +} + +fn skill_repository_checkout_dir(kagent_home: &Path, skill_id: SkillId) -> PathBuf { + kagent_home.join("skills").join(skill_id.uuid().to_string()) +} + +async fn run_skill_repository_sync(skill_id: SkillId, git_url: Option) -> anyhow::Result { + let git_url = git_url + .filter(|value| !value.trim().is_empty()) + .ok_or_else(|| anyhow::anyhow!("git url is empty"))?; + let checkout_dir = skill_repository_checkout_dir(&kagent_config::default_kagent_home(), skill_id); + + tokio::task::spawn_blocking(move || sync_skill_repository_blocking(&git_url, &checkout_dir)) + .await + .context("join skill repository sync task")? +} + +fn sync_skill_repository_blocking(git_url: &str, checkout_dir: &Path) -> anyhow::Result { + let git_service = GitService::new(); + if checkout_dir.exists() { + match git_service.open_repo(checkout_dir) { + Ok(_) => { + return git_service + .sync_repository_to_default_branch_head(checkout_dir) + .map(|head| head.oid) + .map_err(anyhow::Error::from); + } + Err(_) => remove_managed_checkout_dir(checkout_dir)?, + } + } + + GitService::clone_repository(git_url, checkout_dir, None).context("clone skill repository")?; + git_service + .get_head_info(checkout_dir) + .map(|head| head.oid) + .map_err(anyhow::Error::from) +} + +fn remove_managed_checkout_dir(checkout_dir: &Path) -> anyhow::Result<()> { + if checkout_dir.is_dir() { + std::fs::remove_dir_all(checkout_dir) + .with_context(|| format!("remove invalid checkout dir {}", checkout_dir.display()))?; + } else if checkout_dir.exists() { + std::fs::remove_file(checkout_dir) + .with_context(|| format!("remove invalid checkout file {}", checkout_dir.display()))?; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use uuid::Uuid; + + #[test] + fn skill_repository_checkout_dir_uses_kagent_home_and_skill_uuid() { + let skill_id = + SkillId::new(Uuid::parse_str("8ec8573a-b0c7-44df-a62b-0b4d3254c652").unwrap()); + let root = std::path::Path::new("/tmp/example-home"); + + let path = skill_repository_checkout_dir(root, skill_id); + + assert_eq!( + path, + std::path::PathBuf::from("/tmp/example-home/skills/8ec8573a-b0c7-44df-a62b-0b4d3254c652") + ); + } + + #[test] + fn update_input_reports_git_url_change_when_new_url_differs() { + let input = UpdateSkillInput { + git_url: Some(" https://example.com/org/repo.git ".to_string()), + ..Default::default() + }; + + assert!(input.git_url_changed("https://example.com/old/repo.git")); + assert!(!input.git_url_changed("https://example.com/org/repo.git")); + } +} diff --git a/kagent-db-migration/CHANGELOG.md b/kagent-db-migration/CHANGELOG.md index b647e8a..e1e4acd 100644 --- a/kagent-db-migration/CHANGELOG.md +++ b/kagent-db-migration/CHANGELOG.md @@ -1,5 +1,12 @@ # CHANGELOG +## 2026-04-16 +- 新增迁移 `m20260416_000001_rework_skills_as_git_repositories`: + - 将 `skills.skill_name` 重命名为 `repository_name`。 + - 删除 `resource_id` 字段及其索引。 + - 新增 `git_url`、`sync_status`、`commit_id`、`last_synced_at`、`sync_error` 字段。 + - 新增 `sync_status` CHECK 约束,以及 `(project_id, repository_name)` 唯一索引。 + ## 2026-02-11 - 新增迁移 `m20260211_000001_add_kanban_cards_new_branch_name`: - 为 `kanban_cards` 表新增可空字段 `new_branch_name`。 diff --git a/kagent-db-migration/src/lib.rs b/kagent-db-migration/src/lib.rs index 348c08c..01fb7ba 100644 --- a/kagent-db-migration/src/lib.rs +++ b/kagent-db-migration/src/lib.rs @@ -54,6 +54,7 @@ mod m20260210_000006_add_kanban_cards_project_seq; mod m20260210_000007_add_kanban_cards_duration_days; mod m20260211_000001_add_kanban_cards_new_branch_name; mod m20260211_000002_add_kanban_cards_code_source_id; +mod m20260416_000001_rework_skills_as_git_repositories; pub struct Migrator; @@ -113,6 +114,7 @@ impl MigratorTrait for Migrator { Box::new(m20260210_000007_add_kanban_cards_duration_days::Migration), Box::new(m20260211_000001_add_kanban_cards_new_branch_name::Migration), Box::new(m20260211_000002_add_kanban_cards_code_source_id::Migration), + Box::new(m20260416_000001_rework_skills_as_git_repositories::Migration), ] } } diff --git a/kagent-db-migration/src/m20260416_000001_rework_skills_as_git_repositories.rs b/kagent-db-migration/src/m20260416_000001_rework_skills_as_git_repositories.rs new file mode 100644 index 0000000..2c026b5 --- /dev/null +++ b/kagent-db-migration/src/m20260416_000001_rework_skills_as_git_repositories.rs @@ -0,0 +1,141 @@ +use sea_orm_migration::prelude::*; + +/// 将 skills 从资源绑定模型调整为 Git 仓库管理模型。 +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .get_connection() + .execute_unprepared( + r#" +DROP INDEX IF EXISTS idx_skills_project_id_name_unique; +DROP INDEX IF EXISTS idx_skills_resource_id; + +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_name = 'skills' AND column_name = 'skill_name' + ) AND NOT EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_name = 'skills' AND column_name = 'repository_name' + ) THEN + ALTER TABLE skills RENAME COLUMN skill_name TO repository_name; + END IF; +END $$; + +ALTER TABLE skills + DROP COLUMN IF EXISTS resource_id, + ADD COLUMN IF NOT EXISTS git_url text, + ADD COLUMN IF NOT EXISTS sync_status text NOT NULL DEFAULT 'pending', + ADD COLUMN IF NOT EXISTS commit_id text, + ADD COLUMN IF NOT EXISTS last_synced_at timestamptz, + ADD COLUMN IF NOT EXISTS sync_error text; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_constraint + WHERE conname = 'chk_skills_sync_status' + ) THEN + ALTER TABLE skills + ADD CONSTRAINT chk_skills_sync_status + CHECK (sync_status IN ('pending', 'syncing', 'ready', 'failed')); + END IF; +END $$; +"#, + ) + .await?; + + manager + .create_index( + Index::create() + .name("idx_skills_project_id_repository_name_unique") + .table(Skills::Table) + .col(Skills::ProjectId) + .col(Skills::RepositoryName) + .unique() + .if_not_exists() + .to_owned(), + ) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .get_connection() + .execute_unprepared( + r#" +DROP INDEX IF EXISTS idx_skills_project_id_repository_name_unique; + +ALTER TABLE skills + DROP CONSTRAINT IF EXISTS chk_skills_sync_status, + DROP COLUMN IF EXISTS sync_error, + DROP COLUMN IF EXISTS last_synced_at, + DROP COLUMN IF EXISTS commit_id, + DROP COLUMN IF EXISTS sync_status, + DROP COLUMN IF EXISTS git_url, + ADD COLUMN IF NOT EXISTS resource_id uuid; + +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_name = 'skills' AND column_name = 'repository_name' + ) AND NOT EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_name = 'skills' AND column_name = 'skill_name' + ) THEN + ALTER TABLE skills RENAME COLUMN repository_name TO skill_name; + END IF; +END $$; +"#, + ) + .await?; + + manager + .create_index( + Index::create() + .name("idx_skills_project_id_name_unique") + .table(Skills::Table) + .col(Skills::ProjectId) + .col(Skills::SkillName) + .unique() + .if_not_exists() + .to_owned(), + ) + .await?; + + manager + .create_index( + Index::create() + .name("idx_skills_resource_id") + .table(Skills::Table) + .col(Skills::ResourceId) + .if_not_exists() + .to_owned(), + ) + .await?; + + Ok(()) + } +} + +#[derive(DeriveIden)] +enum Skills { + Table, + ProjectId, + SkillName, + RepositoryName, + ResourceId, +} diff --git a/kagent-db/CHANGELOG.md b/kagent-db/CHANGELOG.md index 5bf9773..251b5b8 100644 --- a/kagent-db/CHANGELOG.md +++ b/kagent-db/CHANGELOG.md @@ -1,5 +1,12 @@ # CHANGELOG +## 2026-04-16 +- `skills` 实体从资源绑定模型调整为 Git 仓库模型: + - `skill_name` 更名为 `repository_name`。 + - 移除 `resource_id` 字段。 + - 新增 `git_url`、`sync_status`、`commit_id`、`last_synced_at`、`sync_error` 字段。 + - 新增 `SkillSyncStatus` 枚举,支持 `pending/syncing/ready/failed`。 + ## 2026-02-11 - `kanban_cards` 实体新增 `new_branch_name` 字段(可空),用于持久化卡片建议分支名。 - `kanban_cards` 实体新增 `code_source_id` 字段(可空),用于持久化卡片关联代码源。 diff --git a/kagent-db/src/entities/skills.rs b/kagent-db/src/entities/skills.rs index 8f9b1d0..188271c 100644 --- a/kagent-db/src/entities/skills.rs +++ b/kagent-db/src/entities/skills.rs @@ -1,15 +1,41 @@ use sea_orm::entity::prelude::*; -/// skills 表:项目级技能包。 +#[derive( + Clone, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, serde::Serialize, serde::Deserialize, +)] +#[sea_orm(rs_type = "String", db_type = "Text")] +#[serde(rename_all = "snake_case")] +pub enum SkillSyncStatus { + #[sea_orm(string_value = "pending")] + Pending, + #[sea_orm(string_value = "syncing")] + Syncing, + #[sea_orm(string_value = "ready")] + Ready, + #[sea_orm(string_value = "failed")] + Failed, +} + +impl Default for SkillSyncStatus { + fn default() -> Self { + Self::Pending + } +} + +/// skills 表:项目级技能仓库。 #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] #[sea_orm(table_name = "skills")] pub struct Model { #[sea_orm(primary_key)] pub id: Uuid, pub project_id: Uuid, - pub skill_name: String, + pub repository_name: String, pub description: String, - pub resource_id: Option, + pub git_url: Option, + pub sync_status: SkillSyncStatus, + pub commit_id: Option, + pub last_synced_at: Option, + pub sync_error: Option, pub created_at: DateTimeWithTimeZone, pub updated_at: DateTimeWithTimeZone, } diff --git a/kagent-git/Cargo.toml b/kagent-git/Cargo.toml index d95ce7e..797f418 100644 --- a/kagent-git/Cargo.toml +++ b/kagent-git/Cargo.toml @@ -5,18 +5,18 @@ version.workspace = true license.workspace = true [dependencies] -chrono = { workspace = true } +chrono = { workspace = true, features = ["serde"] } dirs = { workspace = true } git2 = { workspace = true } -serde = { workspace = true } +serde = { workspace = true, features = ["derive"] } tempfile = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } -ts-rs = { workspace = true } +ts-rs = { workspace = true, features = ["uuid-impl", "chrono-impl"] } shellexpand = "3.1.1" similar = { workspace = true } -uuid = { workspace = true } -tokio = { workspace = true } +uuid = { workspace = true, features = ["serde"] } +tokio = { workspace = true, features = ["process", "rt", "rt-multi-thread", "time"] } shlex = { workspace = true } which = { workspace = true } diff --git a/kagent-git/src/lib.rs b/kagent-git/src/lib.rs index 1c16def..9c2b774 100644 --- a/kagent-git/src/lib.rs +++ b/kagent-git/src/lib.rs @@ -2961,6 +2961,42 @@ impl GitService { Ok(repo) } + /// 将当前仓库同步到默认分支的远端最新 HEAD。 + /// + /// 该方法假定调用方管理该工作副本,允许用远端最新提交覆盖本地内容。 + /// 对于 skill 仓库缓存目录,这种“远端为准”的同步语义更合适。 + pub fn sync_repository_to_default_branch_head( + &self, + repo_path: &Path, + ) -> Result { + let repo = self.open_repo(repo_path)?; + let current_head = self.get_head_info(repo_path)?; + let remote = self.resolve_remote_for_branch(repo_path, ¤t_head.branch)?; + self.fetch_all_from_remote(&repo, &repo.find_remote(&remote.name)?)?; + + let remote_ref_name = format!("refs/remotes/{}/{}", remote.name, current_head.branch); + let remote_oid = repo + .find_reference(&remote_ref_name)? + .target() + .ok_or_else(|| { + GitServiceError::InvalidRepository(format!( + "Remote branch '{remote_ref_name}' has no target commit" + )) + })?; + + let local_ref_name = format!("refs/heads/{}", current_head.branch); + repo.reference( + &local_ref_name, + remote_oid, + true, + "sync local branch to remote head", + )?; + repo.set_head(&local_ref_name)?; + repo.checkout_head(Some(git2::build::CheckoutBuilder::new().force()))?; + + self.get_head_info(repo_path) + } + // ============================== // 文件统计方法 // ============================== @@ -3071,3 +3107,69 @@ impl GitService { Ok(stats) } } + +#[cfg(test)] +mod tests { + use super::*; + use git2::{Repository, Signature}; + use tempfile::TempDir; + + fn create_commit(repo: &Repository, relative_path: &str, contents: &str, message: &str) -> String { + let workdir = repo.workdir().expect("repo workdir"); + let file_path = workdir.join(relative_path); + if let Some(parent) = file_path.parent() { + std::fs::create_dir_all(parent).expect("create parent dirs"); + } + std::fs::write(&file_path, contents).expect("write file"); + + let mut index = repo.index().expect("open index"); + index + .add_path(Path::new(relative_path)) + .expect("add file to index"); + index.write().expect("write index"); + + let tree_id = index.write_tree().expect("write tree"); + let tree = repo.find_tree(tree_id).expect("find tree"); + let sig = Signature::now("Kagent Test", "test@kagent.dev").expect("signature"); + + let parent_commit = repo + .head() + .ok() + .and_then(|head| head.target()) + .map(|oid| repo.find_commit(oid).expect("find parent commit")); + + let parents = parent_commit.iter().collect::>(); + repo.commit(Some("HEAD"), &sig, &sig, message, &tree, &parents) + .expect("create commit") + .to_string() + } + + #[test] + fn sync_repository_to_default_branch_head_fast_forwards_clone() { + let remote_dir = TempDir::new().expect("remote tempdir"); + let remote_repo = Repository::init(remote_dir.path()).expect("init remote repo"); + create_commit(&remote_repo, "README.md", "v1\n", "initial commit"); + + let clone_dir = TempDir::new().expect("clone tempdir"); + let clone_path = clone_dir.path().join("skill-repo"); + GitService::clone_repository( + remote_dir.path().to_str().expect("remote path str"), + &clone_path, + None, + ) + .expect("clone repo"); + + let updated_commit = create_commit(&remote_repo, "README.md", "v2\n", "update readme"); + let service = GitService::new(); + + let sync_head = service + .sync_repository_to_default_branch_head(&clone_path) + .expect("sync repository"); + + assert_eq!(sync_head.oid, updated_commit); + assert_eq!( + std::fs::read_to_string(clone_path.join("README.md")).expect("read cloned file"), + "v2\n" + ); + } +} diff --git a/kagent-ui/CHANGELOG.md b/kagent-ui/CHANGELOG.md index 933dc29..6af450d 100644 --- a/kagent-ui/CHANGELOG.md +++ b/kagent-ui/CHANGELOG.md @@ -2,6 +2,10 @@ ## 2026-04-16 +- 技能管理前端切换到技能仓库模型: + - `settings.ts` 的技能 DTO、请求体与映射逻辑改为 `repositoryName/gitUrl/syncStatus/commitId/lastSyncedAt/syncError`,并新增手动同步接口封装。 + - 项目设置中的技能管理页改为维护 Git 仓库地址,展示同步状态、最近 commit、最后同步时间,并支持手动触发同步。 + - 创建工作区弹窗中的技能选择列表改为展示仓库名称,避免继续读取旧的 `skillName` 字段。 - 修复聊天界面 assistant 回复重复显示: - 为 assistant UI 消息补充 `turnId` 元信息,并在前端状态层归并同一 turn 中相邻的 assistant 气泡,避免 `item/started`、`item/agentMessage/delta`、`item/completed` 之间的消息 ID 不一致时留下重复回复。 - `thread/resume` 历史恢复沿用同样的相邻 assistant 归并规则,避免切换或重开线程后再次出现重复欢迎语。 diff --git a/kagent-ui/app/assistant.tsx b/kagent-ui/app/assistant.tsx index fa95cad..8fc48e2 100644 --- a/kagent-ui/app/assistant.tsx +++ b/kagent-ui/app/assistant.tsx @@ -154,6 +154,8 @@ export const Assistant = ({ // 当工作区切换时自动打开会话面板 useEffect(() => { if (workspace.activeWorkspaceId && activeTopTab === "workspace") { + // 当前标签与工作区选择来自外部路由/状态,需要在同步后统一控制面板显隐。 + // eslint-disable-next-line react-hooks/set-state-in-effect setSessionPanelOpen(true); } else { setSessionPanelOpen(false); @@ -171,8 +173,6 @@ export const Assistant = ({ useEffect(() => { if (!aboutOpen) return; let mounted = true; - setBackendVersionLoading(true); - setBackendVersionError(null); fetchJson<{ version: string; git_version: string }>("/api/version") .then((data) => { if (!mounted) return; @@ -239,9 +239,14 @@ export const Assistant = ({ return (
setAboutOpen(true)} + onLogoClick={() => { + setBackendVersionLoading(true); + setBackendVersionError(null); + setAboutOpen(true); + }} overviewActive={activeTopTab === "overview"} leaderboardActive={activeTopTab === "efficiency"} resourceActive={activeTopTab === "resource"} diff --git a/kagent-ui/app/login/page.tsx b/kagent-ui/app/login/page.tsx index 93adf2b..663bd59 100644 --- a/kagent-ui/app/login/page.tsx +++ b/kagent-ui/app/login/page.tsx @@ -1,6 +1,7 @@ 'use client'; import { useState } from 'react'; +import { isAxiosError } from 'axios'; import { useRouter } from 'next/navigation'; import { useAuthStore } from '@/app/stores/auth-store'; import { api } from '@/lib/api'; @@ -26,8 +27,13 @@ export default function LoginPage() { login(res.data.token, res.data.user); toast.success('Logged in successfully'); router.push('/'); - } catch (error: any) { - toast.error(error.response?.data || 'Failed to login'); + } catch (error: unknown) { + const message = isAxiosError(error) + ? error.response?.data || error.message + : error instanceof Error + ? error.message + : 'Failed to login'; + toast.error(message); } finally { setLoading(false); } diff --git a/kagent-ui/app/register/page.tsx b/kagent-ui/app/register/page.tsx index bd8ea1c..747e0d5 100644 --- a/kagent-ui/app/register/page.tsx +++ b/kagent-ui/app/register/page.tsx @@ -1,6 +1,7 @@ 'use client'; import { useState } from 'react'; +import { isAxiosError } from 'axios'; import { useRouter } from 'next/navigation'; import { useAuthStore } from '@/app/stores/auth-store'; import { api } from '@/lib/api'; @@ -27,8 +28,13 @@ export default function RegisterPage() { login(res.data.token, res.data.user); toast.success('Registered successfully'); router.push('/'); - } catch (error: any) { - toast.error(error.response?.data || 'Failed to register'); + } catch (error: unknown) { + const message = isAxiosError(error) + ? error.response?.data || error.message + : error instanceof Error + ? error.message + : 'Failed to register'; + toast.error(message); } finally { setLoading(false); } diff --git a/kagent-ui/app/stores/auth-store.ts b/kagent-ui/app/stores/auth-store.ts index 168ee27..bbf3d34 100644 --- a/kagent-ui/app/stores/auth-store.ts +++ b/kagent-ui/app/stores/auth-store.ts @@ -30,10 +30,10 @@ export const useAuthStore = create()( const token = get().token; if (!token) return true; try { - const decoded: any = jwtDecode(token); + const decoded = jwtDecode<{ exp?: number }>(token); const currentTime = Date.now() / 1000; - return decoded.exp < currentTime; - } catch (error) { + return typeof decoded.exp !== 'number' || decoded.exp < currentTime; + } catch { return true; } }, diff --git a/kagent-ui/bun.lockb b/kagent-ui/bun.lockb index 4fa654cd4dc34127fbc12a187bbd8813a972ec62..6f3b433a9f0ff151800c62dcf3d2c80a2e0ee080 100755 GIT binary patch delta 85074 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
NHU>3T*lq&@%mSL&huW!sq|A%de5vI>ZC+v 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

nx=Ho^v25Ar0W zAvA_{+&95oi@65salHz2B{br?0mu`Q72Gd}Z{TZK3QJ%yEP{ow0OU!^e9U=L{$jZd zBmjwo+zTRPU5op7uo=FE9k3m?!WIyFx!(r6U?)gzmmNV5a_?8}&%zmyiO3<$gRnn} zKl@-WSj|IB>BoP-?tp2(h9;~?%ogV_B7qDzRn z%8j&BX--$Tz6=Anzl8ZIT!cm3TY=x;{yNBHMV~2g{fz5drk%{7ZgMS?7|YKc?)k6Z z7v!Pm-neYX_zc{^9yHynqK_4O;iPmVxOWri=&H049Ue zU)VhaDXQo244%Rhcm$!yWRw11VkCiwLss~M`z*Snp;!hPkrAaL_t?!{+$1&PmJKXj zl8`^anr^x4wDgItFv9<6pdyr*B1xHyibXL)Ap|mVFIg)Qc?n`#*=-HiudtH{%8(u9 z?m7OLEmJB{AmhGdvs}LcQTR7V_Dj}^-CK|%5;=*OyPtR5iyyIIOwsQN`t~t)wLl;L zMNou9;5|nX0NIb;1LcTZF5tgM7W{}?Zp>V;k89ZnccLRbh@b5s{&qtSWP%|GWSbcg z{i%-hKe{4`jYJ|l0$D+nXERN?79M~o$!Xe)Us0R~?s6@P#ZDq+MNsVJ3{+mYZ_0=+ zb2;XtD}IE9a8VBA;Cxvq z2_+x`q$cnNj{c&Jyns^#3PU7_z1)jg3d(>SxGxW)uPVq%{%TMiYJeQdmjnE@K@RRW zNJ2H0#v_eQVk;;48<;f5YzCh}3up;#K~m8Mvo*8>$%RfJ5tE$i3avnLLQ>xwNU6Tq z?};f1w2s<#iQ+~C#j+m^1Sx{?Fa}1$U=TYg9+6!Nqqr8oBVZT|hoLY8EIF|kJGmDm zfnq1uBOw~1zKv$moO8oJrCInku8lcu$uuAcns!rm~Lfcl^P*iHV-kSpdP?|xCh;_zl$kB+<_%f zNY(ky;jB`g5N>h%8{CAeh{|J;>u?@!V0Q@Qamfv_=lUA_#`Q|f<8YDdU)7fH90jy% zT$WRJzjM@UE}Fl=ReXu`1z5)YWlRZ0G_K%&6-bEZOgl@n$XKxvyEVv(&aIdi)$E-P zXAQZPz~#)KT#K5G=Cu{1wYi!D>6$>IVm;qf5tQ=gUZNW+)Xa5l{c*$u51HRJKcHt2BP<+yfLn@42hv zyD4qSb$4s4bR>}=)bPM+2hES$?8d7Auhd%^dwTZUqT^ucaNiRY7vnPD2G)^l|PnubqlE~v5s7! z#c)aPFkEFuBb`5S#S)UWDmwnsU8VMeKEi^EsE! zdmY(r1=WDgJ;%eA?SKtxEv z5Vl@rI>@3ZdTjj#w$ZPj&9d8CgyB*ok`GGk#jEWXbrB=*F>-;(^&YycV84!CzC*5H zNN8wC5f0NRuLBN0zC-w~tmbdaqt-9MD8ZMYNLy7h&mV7H=6J6Uu6{2W-asV^09`ILBs9!pjf%PN@GTM=p+8v2giJ}_o0I)~a(n4f zMJ7^z_Tq!fihQFc)_dm6sk;=EoO!0oF4ufBlL0C1`tjuGwDqUvuDoteu<09_tg7`7 z#H5%Sis7t{oMhF|;TKPK^ggsok4abvMb=KO7lEM&NRpaN?fIma$DBI|golu4jNt0# z4~`nPWh(TbqhrJ&e_9)u}O}Ntna13>s;NM^4By^D@zchga6mm z@qkMI-6U?^w4~=&tQHXgsdliBvM2D ziofnWu6_qEo8|IIr8|o5*XoZSsQ}4%@+P`Bx9xb@GjqTGdb0H@l*Ln}`a<)w=k-*j z4imRNo@)9Lho72w*iq9y!c$#1?5J*8+IM-X=vfGxLiUtN)zofr_hB|8P{stneu z%Lzw-WoLQ`+)8w%$}a!J*?i;KCh1%;IgnhnI!aaiRXsdG%mjJ=2-!SyJ4Kug&vDL{wV59T@e{tytZ*hC7gN5!*Q^T! zW@h;n5!LCx(sGOd z9%TCEt@C0=FvH>D1DBLG^y6b-c>63 z>y_g!7b9;1=a>RH?~M)I7UY@IC9u&)MW2qVHD+|o3hz9QH|eXt?%nRw)KPpgGAS3Rc_so5V|KR)XSbJk)%NvdFlKJ~LKYg~Mk-Ij-;NB&*s z6Lk*%$mDdsK?t1vwC&>|L+AEzrS214a^SMvJ8*5rfE5*7E;aqt^Du|xwM5Wv+pVdKGl{j7%wJMvH-SJwC(TuE` zZ?;IQ-kR;f@*1bg=occ#Q$4bf4{yi6`(f(Net)_0VL2{RCrkYKbNPFV!Zer5?m$(O zKE-(f2^k;@j9;;L-q$6SOX86ld4W#B?7PjrN(FV*0_y<_cRXa&Xa%nJbm%m z)qX}_xY6Vj^pvrhVux9fcK{@s}Kh?0K`OUM)1M%Qci;%?^_uW z%3OAIi0n`Glkt1P$aRA%&na<>7)vk76k#MTGUhLz@UVT(*10R0F6<&t4N{jx`#dBh zwMT5}F4S6m=QR?cA>koNtO`TBM|w<6!`W`(U;ktKPuwVogy1gr}28xd(_$Am;V0PrMpWu^_F-AMrJqS74h1) zN87Me*Ii9{Q&tsr&C#OKX__koaP(zNY4pBhB9ko+PSVA03k}L2BopWlQ}DYf+b%qR zzs}v;dkl{B+Bsv)q6T)`AEwJob^Ds5hI4yPL+SNyInI4kb?^~GDM|DAQ`Ndo;*XQf zGHw2B%7HaWg1+lV3S?GH?wnI64=^^}Q1h=l@&@a(CgeVxDp@YoKT!{^lj5eI7#b(n zYxMP3>u2wAY2;IXK{x2Ib zFhwPodXt>?>B{oujd-d$wuWjGH z9-p;7apXbuWFzgiA|ZuB!pWI+>d%{waOZXeeGy#HxY#fD{cSTlY@sr33N1**D*GGl z?S^WG5%>l<8ILQE^{AR}dE+l!ay}ty`fm=u$b2FCsf_69k5?S4IN(@zmqdA7#BcTU zD+gDqYj+EyMTokB-@pM#Fk^|{{mr*OR9?HevbQv*2TaNPC7b8Ke6DT^% z-3w0d<-uX zLOvD_Q-9+(uo@E5GmboI&sFbp`*4>;%P>{&cUo#+B&5ykdztU$`X3)IaY>9fU3x7m zwSCmk`wLwz^VRU*sjS~3AUR&^>5@GiLi%TP2^>}%5#Xb{M==7QBPSjB%AKw5EgjWi zfJ-h__^jKcFqme=7#khvd^PD-c-miF0wr*f4F99?1t{Tvn@xcZmOC1TrHqXV>a0XJ2ixy992l zleZZ*v_fjp9da=kb##Hz?JtJ}l-N9_giEk|A(a`4$aYB3`9!y>U)S$J{hc*k5{bK* zem|n!44)4VOLRUGUn4=0iKjU6Tr%^~KCgsoc$YSswv?K9*OA8_P)cpR>j-l0DrNMC zeJ50YyZM{oVbs1*V=O$Vp4@eW2Yk4w9AC!hDpc)(T7YVN&yhZR3|8sMpZNWix6N`{ zs;1m?_!mlA)(CE1aGy3Q$F~f$+uoYm0&tNb?#0s@-TOvRHsniM@*gU;TpD>8Ib-eCcu_<}^)>c=wb#L* z(Ma`v?C@7!PaWx#Tb>T8(1!#PtDblvaZ3I12Y0`zjj6e-t&S(fc%f{`G=F>Q3aX5Q z)naf3)#@GN#m~z2h{jQ(f(m@Ztr3R3Q3X|-TYKLMDkwS9Gb<=BjTwy*HlOaTlUB=H z$_hR4!~|C~+FIR4Ng}KDuGdY*27TYeqkKgb^alYN`4X29<2Hq*IT0x{fkxb|IFGKR zHe}HJoL^TmTJOdFOUu9eF=8~sG94_PG@BHXb7t&=tuPXgy{R$bIL1ZU%-{Ev$JL9( zm4Bo(6Dr{Z)B1jS{OdsNu55M7kbmJlUSXMaCqZrhqH>g}W{e~U<`(?n<)~yi^r0|Z zrW%c^sd>-nzPeRYKRshfFbqiCo+Txpl?AemLC^bl3> z3pRo#)l_9(kcPQ6jgY3zuGHzNb5#LCGAH$4tG+KB;eiEt_$^cAS_AVej5xLatr?Xf zJgKdvb{}M8>4NC1Q%n8zf^}??+Qw9B?(8E0y}R?vhtien6Nv>X>opqua#iOl#%6W> zq~@=tzr=6yI%>^JM|FN;QY7p%>!{cZv~H|JUJ=E_RnAwfU(s^b@EfP7+^KpV#5k_j zi>0CLBc9FB#@16k&eMm?=f^Vfru+7F&@K&kHR81+AhL3OWBTbmXoNF((Y1MY+X!=j zs@lNFo_Ym(_4gmVc)Hyt8#iGL)lC|x6R#QAf2JdrVX4`MWR2T*9!+Nx8d6lcc#qBv zROZ!Mo)l5&5DC+VJykqmEV38ZOi%%wJ^nBQ>M3%`smm%L5YJete|*7hRj#zrNRt|BE5w z^A>86ohB61VwR2V!DTIsNwoi%#^o|;EzVHR=JujF?%H;#8aB<}c?Ln5B>%lVX5}wA zy3TbCQVDkS3HR9lB7X1pPjTa_8EvaKv%0P1t<)pC7FT=i^;@ZHi&;R@(Y9};dL<#D zrfJR_P~bL#{EPUEZLLx<{V=B})@&p&@$8t@TGdR02QypNtC4Bwdd$J|usS7k8Z(gz z&r<$H*&BU&yaC;cn-%hV_{}1!lf05PyOEJAyE`dwi8Ef`k`Tr~%Ie-6n z8MYXwnXFhn!ut(A!I}2Ii1GiLq4xi#as<95RWi23I&258_3v=b?7kS6;#HLYx{|+_ zdsZ7vICAkO{{QFZpKy@xS2QcE|Fzy5E0?%*nSuP<;lON^38(a5^u9)w^OpYH!>x)3 zw&YO0Y}X&!RkGcgjh%jxWtzS?jw?>rIO5jd|BGtbRh{};^HwdrHO(As;<{P4RgS%X zId#oj3*+}(GWcjcor5|W^ZnB~s&u@jZuZuvOZvv?=W4zW!{<~}!2YVl#yyW`#+m|b zP0d!P5a8#)?u*>F$fYA;5Au55IN#zq(@B{m7iEocKxOe|=rCr0KAD*%o<&wBhwGc1 z-d*I(4__l&n2k(aZmaId^21Eye6_r`X4}5?)uz~NcT`zFG%z3k%#UT~lTOB7NB09u zN0h$8Pq-RGKdYHHYMKaoc2=1KnIoj@Y$UG6?A?WLrkK3W6eOI$&gu%P_{B6yLtrih zW%k*nS%IeCpLSR$3J?tMJj+RtrA>jk!}4jj_P=2No7sjujrXXgTKa4L5zP>gV``OTrQU@d+BnW*DG-`=hYj8 zi!hU8*JC@!)qF>ZBO&2V=>q+JOXTq+e|h)B+LRa`CY?|$DER~3yl|T02eri~dCQHS`K@=BO^H$!F981h3q^Kxt34!&cb5@kBUQ$ zTHfM7o-}Q>pJj0sX%u!NH7$(OIRw-R8EM>ppVu2U+;dJnWQwqqBgI41AKA&M$!d5Z zifw@@cW3bZ<2N?gCYtL{0^Q-%m&|TQ$U<+`#%9oR-8Zlgz$*cLuqMsfrClP6l zA9?B!IAGl`HB;wh7eim~BQVf(v0onfMW2lsctnJY-nmUtYw_w_Y6?6kxhJq-|N5nM z0Ui=@NL^h=fL|+oBXYNp%SaH@Zmb&{bY?z-2_>c<`S@EE$VG}Wj4;+>n|q$yyft;r zXje#iaq-9NXU=hwdR+)Dj-h+?RYOCxFjXs$p_8YAeyT@dEg-()pH&kbsPf83 zYyVy8zNr;0@{za}ss_qz{Zvdon!6#Sher$dGcrcsC;D9_7^{Lc$+zkGSy1VDn3yPS zL(h*6Dl~-9-16HkiweJ=r+T)<*(uMs9Mc2Q6?%K>Rq6f68x5b63PvwAt00m4T}ghZ z%ms}+rC7Z=@y6>GpD0%fCaUfQsV!=Z(WCVVc^cOCUBeC378#e=c+&G=q**9F4#L(m zQ?CnFF}c*~ZsBIiRjRPK;`Gt*X~s@94AY7*Iisu0n>}ot{xQuD9_@J9ESJaXau`ue zq#L6`>p3g+jzW%$7q?d7Wt6auT4g4H4l{uY`jH@v&N}d<*#le~7xe4wLTkPBhO0L! zt7^rIgjofQ3TIDP$j#b{SApPj6O8fl!|i;_#VW?=NXps_M-RR7do4Mx4iz@4`bV3M z-Wa6)OPo>_(fa{1^?i*WW^}xKcfYl2q`FX4v(5*^H9(`_oI&KO4_#X0d%dQfUD+$W z-c>Q6Mo{@$7t^+wmCw&grQV?Exr#I3{jO{!+&ZQxRkk#3&5-caOXhtEXR#?pVCMp( z(|vi(=eF6y(*MU*BNoTIjz z$EuyPryDb)nHgS0#HKC2hDb3}kVw6E;M%zwIWMZk^I`tKrPw#mc!J7ahJ3@N#;5cM zOUoET`TpyZXLX+zZp?V~;s2N#i2%pT=8D{X6v@M!3_Xtibmxm4%k}!j?3|Zn)ky>* zlf)QLHin=7baK`6C6*#UCdt{451+E)qMr}&icy)%G1bh3U#YyiCX5}weu6!_t~^Yp z4Ph~=ZaG#Z=Iqj5Ax162hjE%ZTsu%%$X+N<@f0rD&|OmM7Dg z(UvH$b+8Ul?Z zXW6}1;+5tO z%kqaMe=ghAtbU{@LT9LJNJJJ#LJsUaUG~||nI(PY__CR*%D6}ZTKOgSdU+)0Q3-^y zpn@4^$NikmOusiGQkNRHVL)|}FlJtzRO?D)MPK!0C2b^4C}U-_p11#E|L;!?5;|GUQ4B|r!nS-6l|`tRHbmb z;!TR6SEUvs{$4n}teJQ+E{cfAQ@m@Z(>`tbtf?&8&1vgk)g7JH1(4??&gk9Wf4_}m`|Cp-}#VFj7+H?PNZqC_=gF~ro&m&IR0HK`h5nDHh1 z2CF?-IJ?f$KmHv({rj)`?EdS}Zda@fJ^X5W>gsr0qjFW(I@-+8eZMkNS$V?T)Hn8* zxTO0+sVp}nbbMW|PB~st>1t4`$T5jVCf30JAzi$cA<+taO#o7d=53lZIW(1KoV3?F z#MHCZx*8089GZ;YKuygu+o;mNZK(9sw|htHm0SFG;j7RGDkDl$gZ3LsA)5XKlRX^_X`?&CxrMuT-7dc*wa( zbwe=FbFoopeKY)CJ@@#^5(01O$kW91i`60|>>1{(U3*a38c8oy-tRJWZ1Q4}3k}I4 zOeA}&hp4oTRay41)(EOgCI3QYtwSiDi&cp_Owuo@p4>)!y1+P1@q6tdNoq8hz0IW} z@n{%YJ>}IQGbW~1KlcFR?0TrOd!==Sd5lp_>e5Jaf32q0)%+q#e{H<9^D15Yu5Sw+ zpH0}I91{#JI^ba3IGxSuhuXV5&S*|wJr+90t&<0ecX3LG5^0|MNjdRqon_vy zYz>h+t71g_u39a|J5{S8)xP;kV}aYWt1UPzY*Z_JnJeJ_YQFd~!lbdt3pw`C)#^b* zT9kRl+gW+F(J1`We%T^bmx*$6!8|}+S9R?|T#XlB;$F-#)x}F-gcqZd)bD%WEQ(xn zv6TMQSs$4_TCX*h*{SDM+rK1fP*-`{Y`6?jCCMu1Oa!F6{o&l|xs$p+rD<3KOH^+J zY(|#8Z&=|wqt+Q+a`w*GJyX9*!M+NuK3q-}n%>T+TutaQepAhwkeZ_F)$}GbBFnVO z$rLs+$vJz2p>+J#z!e=UU#RHH6rNi(r4svXR27@DftKh&ef_Y$%yyy~iVU@-xO}0xr_j?u7mti>lHX5T* z^r(@AhTeXj3SShqOq!pl<85e9X?LhQZRk$S_ucdg^?u^~Y+Y3E_B4YLYJOXz?~@|< z$XjUdMZq>(Wg&MX)_iAlj*mY}`RL(Y+Yhd^C*B~;L?oWCMz)#pRi2kPMP<~8>ej1< zM)1B4X1+!;tk&gRvC|lSpMAIV$1O>o$a_+9l#?-C-|x<>F1OcwWLnFvm~85$?m!-l zQMo$A?VI?hNgcFI#-V^enrI1W;tA~?MNhz(Z+7RnQ5D@-gK04TYmvk z&d9ax6xX24MrV3a4G%j4)Z0!}zOkxKOkCGz)k|Z%3uiJiw=<5$uxHz-PIBiwvd3H! zWpKn?A2WI+(J}|3ODZ*kX1vDqSk+;4vYMjxVra2gwWtfTsOhoB+*3>1d-~3jt6Ix5 z&oDWB=uvjBTEh#KR_k!fnE3Dhj8#3mQePAFV2$7^MK^lW_g^(~2JSPO>4ZB$0p&k? zcAAu!O-&!NIS*#Owra-O*@wMCc@~)S`rm2)$#a7}YO6$yAv~}L8l4DCKX;_rv`kNO zxdhCY1)UR+plgmU8@4+8mR&ycToQ9sOm{NsYt7k{!$COc1duV>a=@06kx=NsqS}yJzY6`Z&eXa>Fd#CkoMbyZeP<||$uRLMTZB3Iv6oY;qE7EcQ+ zw7C8HxOD5Amge5SF5l6LW1NV-!R|_lpGw(J%VJ7dyUCJdjfsIr2i3A470M9L9LaZO z4jaXjPbF*j>8@6b*{LmRY^;n`Yx}v1M-gSRp21_RD$qYpg@y|E`oNAN0r;vf`fHg! z8q+r}0ryBMJ;6rRp&ue7Up0JyrtkDi&et!zD58+I#k zk#Tm!fYsquYP|Xe7qcre+Jv$04*c-7 z3E4Q|U7&&iW!0I1tO^W`rT(+V58sRsRbh-meU7VMgIHuuIBujQD*9NPIj1*#XQqVe zuW*qzHLBpk*q2KVKEkE2(WVxu?+4N7Rw5uX&`r0JRQ+*KzcJdV?!uzdED3K@#>EWqE+&im9(N8&6A zJ+}V#F(=e81c`j;Fly>XwSFj*kiDmjUe>Qn&Q~SE&hgH*8EKE7O_z!pJRcknfXk^m1dBe5u zY!M&6eo4oy|BLz6sADe=S1*u!_Jd>OvgtBMO^PC*uMv0_)Ts z1R{1IAUl7jb6)%|r1EbyT>=M8m%d*m|LyT7124E-PO8+=q~)S|HwxpnD$AWcHmmw9 znu33s!)Oq`nHu>Xe|C#Kph9Mpo~dckbgu~J8>uybR4Ifd0?>f|@CUx)Oie0d;*Ot)a0hK(y?fZ>c&; z3-eCs2#T6MN((Z)S@H3EKRK38GmUx(aiY$PWQ|NGCND`bulJ1QWjU2-z7NaWX`ge* z>AZULxz-@?!&h7xYV^UW{8iO$G`Y3NY~uS5MQ&;}tISb$ZOyNh>{AOzQ$FRd8YR3V zN%wa71Fq$!Mwu!;=Jg?7BU4;Ab`0h?`VM=w{kvG#`@I=Wmke|JG``aO49mhiW|jtD zS0%LLxHqBX18IeqHZ7TiVN9SG&dUiL6F52`M~1 zqT0&(?OfVx60S6$t#v#a6wA4r%?8VF^Xn?rSQe7z+spQD*HszrBZitjVqSOL*+|>) z+U4WJm!~aV57d;gLQR+oB8cpjpZeV2Ux-O-2)J42HS0RuNfi3O(lsR;D zCR>A+*PSZuICLLFKswjE*IQhD6q|CqOW=y>a%j!x?eqTKhF63wua8x8yaw8D7*p$C zz3=Ya^`cxEmw?X=HEcY2ko$&OGY&IFHnlKzjrWuC4>mB7cf$7rlJF5BwwsQVbvnOaH?X~Zzz=>M< zlGZbX7v+s%@Yj`DZhl{AH3diwXQR^iICDmlQz>cpJZ*|)Ic2&Ok*|%+pGep9T5X%i zu5zjeMjv)Ix!2@d$-{cO+DOI+DrgcJl;eRaHc4v{nAnK##jBSxv-VX-CTS^*1*1QE zxPMI2JGkVNIgZ6bQ-3GrFEwd0w(nHBsZ6%$vr|1%v6HEZzK@KiI-~ZAKUdxDPkXZ# z8BK7J*3|ZRmWh)ZL>Dz(Liy^XN(q#wsJX8iVIf2yj}wDQ@lcfsP% z>%451%N4beB?1G@Q4D9|8=>9QyJ=d-1O%+fWivH$IzvOI7&UJ?Q;#8P%XB7|FP|qo zvGlT8F00f+S!*{^nPSL5W91!x9jD8L+k;-HGcjbgu>ub)^up*TF0Q%0E^W3HyhUUc zPK6h$*FxHC;+q9^k(fcfYL5=796k44J@zF1is>#L{cw>f=H2I|%B-!E{+7#SxcYtu zc{LdUsXgO5Hdwtpz&6Vz@RfRk!24SeY(>vjWxgQxOVr}8aJ81v@ph=43tt*lu1dh; z{Fes!?nJFQeP656&SZ=adNpf4&F+&|s>MvIXth_yWH2n%o167B{W8rZ-$BL9Bxj@5 zYK)J6-9_K3eMT)A7gCWv1xmm8I#g>an*!` zTa~%ZyI-Z7OEvlbx~=tQZd_YdmFKauY{VdN)LWy;ju`jyT7_g*2CHbE6{x?C@UpELmBo0K zK2)2rwm{1~8ApE@mB0lY@_vVyEF|*{UEg3+-Dbw_jXuq^cI}9Hw*|Coqpq7(#@NI* zgIe!l(~l4MW<37+_S02mF={qx%de67cC+7ducn3y^~*vnpL2-aroWl@VQ)S}Wm=?V zWtF;j5zQ@m64i7OF*>ZKaO<3y#HJ^qMe1xhYyGgJvYtu$6G>e@+%{1?U&N|5!8Zg$ zW#7?!vrc`sm>l^qbiTAFXEO@&j^2#L#vYE0=H@$kaWCe@y`Wc9Ra&CuO}19E>5tfr zHBZpHznETy{e#RaugT9p*XCldEG&UZkhBiW2I(X}I2i~^jD3zJ%hgqPz=wBD) z<>t_Dw5(QSJS8&Ln~DFEV$Bssa%+C~vud`S0v}yTO<&HSYiy_madL}K%ElPu$cIO9 z%oii~t4YhWyeVRn**sE^rc}vQ>@r3~XL2Kr{vA#}j#*lU88Ykea5><4ipgXKBwLT( zUu}7xmcDvDVtY@fjH!u+Gj#>yIP-!u-A;|Kx^sq0K>rq{e91zh8<_Z6DKjAnd?-Pm zx00-U>fK7VG%qcuwv zBdqVza87%LmN&UIOM0h#R%&_Oz9H#$nZgp{RqYWgskJrKMud}F!RfDX`rC|%GcWRA zMVyT?aHb*xGW|&YtZuIY-`4N#9k-}3N{4rl53SOQ2kuX06w>_Mf491yx#Qo)t9r7h zVZP;Uk7}UrUi0pH+tu_037rP8UM)Ahl(b$sAJVg(zCz6(dGmgy@=<%eYK=G zn|?mMd(Twa(&wxm;fmgKm358uG-*|dHB7WJrM2mAX??$VU)aj}tDd<8OQco3kceoF zL?9_W)9Avc6VY|nyCjC=;)BbAm3M5xLqw{Sz`?u({feTi!1!jKssdPGZ3GL3NNMyzDunU#_>8zAcq4zJ2!@(nK1v~E1kc{xwvo)>W` z91n!Y-IRhVIE%B-hhdj8R9oqA|B>y&fY*4aIQI#hVO@eXDnE(5l?1 zF}gQY^2;)6JpvKmA&?n?VxiUQ2aLSB1_2px5jbat`0PS9d!Y8yy^d3MVA08i4z&Yc~+yT%vGpW#WqwnfE$snTtsogdpkziz&)V^5huMQ&t# zab{3m#LSsNEx-&6&0y0%e%LUe@~JFOzCG-URKpDF!A1(c4H7b$&tkvPqRyj7y^xT1 z9Ee=k3@T`ozFsN1iF}S#-NiVf7H=X~POF`AcUk=*cer1-YL`Z4FPZdJ6+PB^3B6QZx6^m0%3;$VS;ne`+u1@m z$E~!p4(Cg6YLLGOYe5dG>3hmga0$ZY$73h%1=Ma4;&MrsQ$5dS%oysyaJ(VALjq>a~NeWD`zO$HM#V^giXA>lG1{O>7Rj z?^3IGl1o3TTDvi>si^NU-l-=$s0~56RI2YZzkvS`R5wQK9|^#Z0Aemyv28llY8(NW zNqUEi^kV5xAGp!7Sr+LzOqayfChlU^E;G8T)${MP@Q9S37_G5Es|W91PjCGfLC7O! zq7~mowrqoj4GU^f=(uW5y6xG0)u5e}vylyUtMwY?6p>he80Jwwk4-=N7_)8v{?6G# z$GghO$U=MXJnHQ(9tF4BrKQjQpUdo=`f?Yw^u1!RblA0vXmNW`mdD* z2Af@U-(3w>ud}CpqUWyuVHPuumdZx|IhpaN2KKnA!Nhz<*Rc6j<_puK7i=dK4pqtv z0s0Y3|C(PSkeYgwy3OpxZ34ITL4Yc(PfckFGB#OXWVhRvAnS!q`@Y>e_vq*0v7>(U z(Dhy`UV7O!;li{tS}oec1uVS)p@v9Ve- zZ~1bVQ% zF8!zf0iGGtjyoOTF#fr1d`z>jO@E{ID77o-ATI)&-zYNQe>J{eWH&yfRGrm>yc}zN z|45ZOq>ZvZg0$_zA>M|vzH~Hf8*lYywVSHLOdny^N0oj$$^_Z`kP*YiPW8tz(!N|R z#!y9$u_QOb|H#LYbm1CASNCcfhA;Udl8-uhOw&}u;}i@p{;VTA*19ARaRhnDy00DoW{4Q%8KM;u`?s73(Bk0W&#?Y zadV63FZK-Q<4u3t(wx=C+U$IXYSu5>9Lv^BjB58QwZ=%1ioLF7)YzrRqh#T%bEF_a zrw*3&Pa>ihTY%EeQwhu>EOMT>nXigV$odzs3^&uOC3)t&5mUX)%$%W+OI@Ib8y?I& zv2InFi&QW}Qcba7%Emr-N#V@Au}S{=Qjbu}%F5X0kQzu>AXWIZWr_ifEDNu>FmFe}^4$n>~I zz*b|JcZ>a}UDxn$y`68I|94)SW31Dcb{Wur@Y`mY=qDNIWf7{s8Q`xPUw6gdJ^!RC zBv>t^qOQBC=d*koZQJx49HEyq)HYoT5mEi4X+~$Ge1Ah&|A3mFR9$SuZ*g6O**AUc zBW${&k?x7@^nPZT6@-&6%tMWDx~XozYon~3Pj*}T$;x!II;XmW)dO|;4$p_AKfSMo zZ5wn~8Yv5|-|z9z%XpDORliRLwaKszi3c_;8$6^3c1ter3x^bS6_KP|1wSHrn^coW zOfH9Rd;Un9VCT!E+oGRnfi_uREO^SX`IMh*^ZZk*ZL=*>Z~oH!xAl6)i2?b5>AJso zW4Xm&)HUO#x*<`+c2L!MMgQ97h1SVl`fGLU1-oF*ux-D;5G}^b6{bw`bFVag)m=pO zeWg`kE++rl_8kz`rEkw38`72-ezw8FFJmsMBd@f~sK5J4%Yu>oHAkDU&GQ=Db*c)t z7@ZAcf*6a`N>k>@Yps<3E0*%I!Wvy9;+SnvnzxI^6R8a1u-rFj_(OxA{ zi&}dY*IKZsEuzXeJ`R&=nQA!6)2Wi@PMR-3KLH-nuj3FFWy}x?8Z$mw?`%7kENM(;)gxEZ+-g;> zq%D^>$(^)T)t(L8*X`4}eBTWNlj^y)-855_=pR|AR8WzizMY4(@7W%rb delta 84962 zcmeFae|%T-|Ns9y+n)72B^uH~YDgucl~l-@)lNStDHX}8)mE)qTUwEANQE#*9>pu{ zMO0qVkP0EhiZGNy7@{Ifq7dKf?VQJY=k@k-`Fua$_viEdr*rAP+qqvKKhERVd7Q^N zot^!&rs@9gn%;5d$wQ(I;zKk3FL6#kbx6)y<#WI%L)v-`;V-OGBc$ zQ_K3bEsn4q8;O)mpHfhmHDOdF1OEv8kp%^X(^CHARxA=Z4xK(Vw{RqJM!F&Y4NZc7hbm7GJrNyWKN4w<{)irpUWPV8PeW7D zBT=QhiA1X7X{1X=k4ELU4$Dwn@&^Y#22g{ll0$7_mmd{o(3;Yn0&k9zwxl945;-1i zi>knPqbHzaP!-@>8=i^kfKAZGXasG7#)zZ}d_yTtj1)&oHnF1!&s!}=)oP=%M@$(* zYvvX3gY8EOpjNz@$SOz%1y#D&sOD+t!>A582MyArE%3Xc&CwIlR%i#(siLhZMN`H= zNh5YRX30KO0sEJbk zlWr%G^6O5i)G~`u8nxsct1Z!U1oBS~BPQ8^-DIGIuJwPl{;2jTBPWiZIQf(2{)qU~ zirZ5=MFSV+=4D6FtjUwJrtN|2_(@jb*;xhIk@szR)>}QZWjMs1!`IMSj;ev1(n{*t zV^9t6;510dEl6w~#;b%A@A#5k03B!zfZ)uq0I^33}>*H)b)bCEBPuj

r=5sdjjv?MNsJiHWl(VyB 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%0A4fs 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 { useEffect(() => { if (!file) { + // 文件被移除时需要立刻清掉本地 object URL 预览状态。 + // eslint-disable-next-line react-hooks/set-state-in-effect setSrc(undefined); return; } diff --git a/kagent-ui/components/assistant-ui/create-thread-dialog.tsx b/kagent-ui/components/assistant-ui/create-thread-dialog.tsx index cf04a4a..b677495 100644 --- a/kagent-ui/components/assistant-ui/create-thread-dialog.tsx +++ b/kagent-ui/components/assistant-ui/create-thread-dialog.tsx @@ -726,7 +726,7 @@ export function CreateThreadDialog({ }} disabled={submitting} /> - {item.skillName} + {item.repositoryName} ))}

diff --git a/kagent-ui/components/assistant-ui/project-overview.tsx b/kagent-ui/components/assistant-ui/project-overview.tsx index 58b270b..1f9de46 100644 --- a/kagent-ui/components/assistant-ui/project-overview.tsx +++ b/kagent-ui/components/assistant-ui/project-overview.tsx @@ -213,10 +213,6 @@ function Carousel({ images }: { images: OneDayImage[] }) { const [activeIndex, setActiveIndex] = useState(0); const total = images.length; - useEffect(() => { - setActiveIndex(0); - }, [images]); - if (total === 0) { return
暂无图片
; } @@ -281,7 +277,10 @@ function OneDayCard({ data }: { data: OneDayImage[] }) {
{dateText} {weekdayText}
- + item.id).join("|")} + images={data.slice(0, 5)} + /> ); } diff --git a/kagent-ui/components/assistant-ui/resource-tree-dialog.tsx b/kagent-ui/components/assistant-ui/resource-tree-dialog.tsx index aeaf2b7..0dec309 100644 --- a/kagent-ui/components/assistant-ui/resource-tree-dialog.tsx +++ b/kagent-ui/components/assistant-ui/resource-tree-dialog.tsx @@ -68,7 +68,7 @@ export const ResourceTreeDialog = ({ ) : (
- 选择 S3 凭证后点击"加载目录",可在树形结构中选择文件或目录以填充资源引用。 + 选择 S3 凭证后点击"加载目录",可在树形结构中选择文件或目录以填充资源引用。
)}
diff --git a/kagent-ui/components/assistant-ui/thread.tsx b/kagent-ui/components/assistant-ui/thread.tsx index 9304297..6043962 100644 --- a/kagent-ui/components/assistant-ui/thread.tsx +++ b/kagent-ui/components/assistant-ui/thread.tsx @@ -95,7 +95,9 @@ export const Thread: FC<{ selectedResourceCount = 0, }) => { const viewportRef = useRef(null); - const [selectedModel, setSelectedModel] = useState(""); + const [selectedModelOverride, setSelectedModelOverride] = useState( + null, + ); const normalizedModelOptions = useMemo(() => { const list = (modelOptions ?? []) @@ -104,20 +106,21 @@ export const Thread: FC<{ return Array.from(new Set(list)); }, [modelOptions]); - useEffect(() => { - setSelectedModel((current) => { - if (normalizedModelOptions.length === 0) { - return ""; - } - if (current && normalizedModelOptions.includes(current)) { - return current; - } - if (defaultModel && normalizedModelOptions.includes(defaultModel)) { - return defaultModel; - } - return normalizedModelOptions[0]; - }); - }, [defaultModel, normalizedModelOptions]); + const selectedModel = useMemo(() => { + if (normalizedModelOptions.length === 0) { + return ""; + } + if ( + selectedModelOverride && + normalizedModelOptions.includes(selectedModelOverride) + ) { + return selectedModelOverride; + } + if (defaultModel && normalizedModelOptions.includes(defaultModel)) { + return defaultModel; + } + return normalizedModelOptions[0]; + }, [defaultModel, normalizedModelOptions, selectedModelOverride]); const waitingAssistantReply = useMemo(() => { if (!running || messages.length === 0) return false; @@ -158,7 +161,7 @@ export const Thread: FC<{ disabled={disabled} modelOptions={normalizedModelOptions} selectedModel={selectedModel} - onSelectedModelChange={setSelectedModel} + onSelectedModelChange={(value) => setSelectedModelOverride(value)} modelProviderName={modelProviderName} modelProviderType={modelProviderType} onAssociateResource={onAssociateResource} @@ -184,7 +187,7 @@ export const Thread: FC<{ disabled={disabled} modelOptions={normalizedModelOptions} selectedModel={selectedModel} - onSelectedModelChange={setSelectedModel} + onSelectedModelChange={(value) => setSelectedModelOverride(value)} modelProviderName={modelProviderName} modelProviderType={modelProviderType} onAssociateResource={onAssociateResource} diff --git a/kagent-ui/components/auth/auth-guard.tsx b/kagent-ui/components/auth/auth-guard.tsx index df87a31..fdfb9b8 100644 --- a/kagent-ui/components/auth/auth-guard.tsx +++ b/kagent-ui/components/auth/auth-guard.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useEffect, useState } from 'react'; +import { useEffect, useSyncExternalStore } from 'react'; import { usePathname, useRouter } from 'next/navigation'; import { motion, AnimatePresence } from 'framer-motion'; import { Loader2, LogIn, Home } from 'lucide-react'; @@ -108,18 +108,17 @@ function TransitionLoader({ export function AuthGuard({ children }: AuthGuardProps) { const router = useRouter(); const pathname = usePathname(); - const [isHydrated, setIsHydrated] = useState(false); + const isHydrated = useSyncExternalStore( + () => () => undefined, + () => true, + () => false, + ); const token = useAuthStore((state) => state.token); const isAuthenticated = useAuthStore((state) => state.isAuthenticated); const isTokenExpired = useAuthStore((state) => state.isTokenExpired); const logout = useAuthStore((state) => state.logout); - // 等待 zustand persist 完成 hydration - useEffect(() => { - setIsHydrated(true); - }, []); - useEffect(() => { // 等待 hydration 完成 if (!isHydrated) return; diff --git a/kagent-ui/components/form/json-schema-form.tsx b/kagent-ui/components/form/json-schema-form.tsx index bc84abb..84a12bb 100644 --- a/kagent-ui/components/form/json-schema-form.tsx +++ b/kagent-ui/components/form/json-schema-form.tsx @@ -80,6 +80,8 @@ function KvField({ useEffect(() => { if (serialized === lastSerializedRef.current) return; lastSerializedRef.current = serialized; + // 该字段支持外部重置表单值,这里需要同步回本地编辑行状态。 + // eslint-disable-next-line react-hooks/set-state-in-effect setRows(normalizeKvRows(value)); }, [serialized, value]); diff --git a/kagent-ui/components/layout/app-top-bar.tsx b/kagent-ui/components/layout/app-top-bar.tsx index 63ad723..7b4d847 100644 --- a/kagent-ui/components/layout/app-top-bar.tsx +++ b/kagent-ui/components/layout/app-top-bar.tsx @@ -112,7 +112,6 @@ export function AppTopBar({ "flex min-w-0 items-center gap-2 rounded-md px-2 py-1 text-left transition-colors hover:bg-muted/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"; const [progressActive, setProgressActive] = useState(false); const [notifications, setNotifications] = useState([]); - const [notificationOpen, setNotificationOpen] = useState(false); const readIdsRef = useRef>(new Set()); const minShowMs = 1000; const showSinceRef = useRef(null); @@ -165,16 +164,9 @@ export function AppTopBar({ useEffect(() => { if (!projectId || typeof window === "undefined") { readIdsRef.current = new Set(); - setNotifications([]); return; } readIdsRef.current = parseReadIds(window.localStorage.getItem(readStorageKey(projectId))); - setNotifications((prev) => - prev.map((item) => ({ - ...item, - unread: !readIdsRef.current.has(item.id), - })), - ); }, [projectId]); useEffect(() => { @@ -234,20 +226,6 @@ export function AppTopBar({ }; }, [projectId, authToken]); - useEffect(() => { - if (!notificationOpen || !projectId || typeof window === "undefined") return; - setNotifications((prev) => { - if (prev.every((item) => !item.unread)) return prev; - const ids = prev.map((item) => item.id); - readIdsRef.current = new Set([...readIdsRef.current, ...ids]); - window.localStorage.setItem( - readStorageKey(projectId), - JSON.stringify(Array.from(readIdsRef.current)), - ); - return prev.map((item) => ({ ...item, unread: false })); - }); - }, [notificationOpen, projectId]); - const unreadCount = notifications.filter((item) => item.unread).length; return ( @@ -319,7 +297,25 @@ export function AppTopBar({ 效率 - + { + if (!open || !projectId || typeof window === "undefined") { + return; + } + setNotifications((prev) => { + if (prev.every((item) => !item.unread)) { + return prev; + } + const ids = prev.map((item) => item.id); + readIdsRef.current = new Set([...readIdsRef.current, ...ids]); + window.localStorage.setItem( + readStorageKey(projectId), + JSON.stringify(Array.from(readIdsRef.current)), + ); + return prev.map((item) => ({ ...item, unread: false })); + }); + }} + > ); @@ -239,6 +247,33 @@ export function SkillMgmt({ )}
+ {selectedSkill ? ( +
+
+
+
{selectedSkill.repositoryName}
+
{selectedSkill.gitUrl ?? "-"}
+
+
+ {getSkillSyncStatusMeta(selectedSkill.syncStatus).label} +
+
+
+
最新 commit:{selectedSkill.commitId ?? "暂无"}
+
最后同步:{formatTimestamp(selectedSkill.lastSyncedAt)}
+
更新时间:{formatTimestamp(selectedSkill.updatedAt)}
+ {selectedSkill.syncError ? ( +
错误信息:{selectedSkill.syncError}
+ ) : null} +
+
+ ) : null} +
+
(!open ? setDialogOpen(false) : null)}> - {editingId ? "修改技能" : "新增技能"} + {editingId ? "修改技能仓库" : "新增技能仓库"} - 维护项目技能列表,支持关联项目资源。 + 维护项目技能仓库列表,创建后会立即触发一次同步。
-
技能名称
+
仓库名称
- setSkillName(e.target.value)} /> + setRepositoryName(e.target.value)} + placeholder="例如:team-skills" + /> @@ -289,34 +332,21 @@ export function SkillMgmt({
-
技能描述
-